Last active
May 28, 2020 20:51
-
-
Save Zhendryk/4caf9954dd171ddb929e1784942e2e0e to your computer and use it in GitHub Desktop.
React/TypeScript Project Generator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# create-react-typescript-app.py | |
# Author: Jonathan Bailey | |
import os | |
import json | |
import subprocess | |
############################ | |
# OPERATING SYSTEM UTILITIES | |
############################ | |
def touch(filepath: str, create_intermediate_dirs: bool = False): | |
if create_intermediate_dirs: | |
basedir = os.path.dirname(filepath) | |
if not os.path.exists(basedir): | |
os.makedirs(basedir) | |
with open(filepath, 'a'): # Open in append mode to avoid wiping file contents if it already exists | |
os.utime(filepath, None) # Modify the time to the current time if file exists already | |
def mkdir(path: str): | |
if not os.path.exists(path): | |
os.makedirs(path) | |
def overwrite_file_contents(path: str, new_contents: str) -> bool: | |
successful = False | |
if os.path.exists(path): | |
# Wipe out file contents | |
open(path, 'w').close() | |
with open(path, 'w') as file: | |
file.write(new_contents) | |
successful = True | |
return successful | |
def parse_json_file(path: str) -> (bool, dict): | |
successful = False | |
data = {} | |
if os.path.exists(path): | |
with open(path) as json_file: | |
data = json.load(json_file) | |
successful = True | |
return successful, data | |
def run_cmd(cmd: str, precursor_msg: str = None, print_to_console: bool = True, shell: bool = True): | |
if precursor_msg is not None: | |
print(precursor_msg) | |
if print_to_console: | |
print(cmd) | |
subprocess.run(cmd, shell=shell) | |
##################### | |
# NPM PACKAGING CLASS | |
##################### | |
class NPMPkgBundle: | |
def __init__(self, packages: list, development: bool = False): | |
self.packages = packages | |
self.development = development | |
self.install_cmd = self.__create_install_cmd_str() | |
def __create_install_cmd_str(self) -> str: | |
cmd_str = 'npm install --save-dev ' if self.development else 'npm install --save ' | |
packages_str = ' '.join(self.packages) | |
return cmd_str + packages_str | |
########################### | |
# FILE GENERATION UTILITIES | |
########################### | |
def generate_app_tsx() -> str: | |
return """import React from 'react'; | |
export default function App() { | |
return ( | |
<div>Hello, World!</div> | |
); | |
}""" | |
def generate_index_tsx() -> str: | |
return """import React from 'react'; | |
import ReactDOM from 'react-dom'; | |
import App from './App'; | |
ReactDOM.render(<App />, document.getElementById('root'));""" | |
def generate_index_html(project_name: str) -> str: | |
return """<!DOCTYPE html> | |
<html> | |
<head> | |
<meta http-equiv="content-type" content="text/html"> | |
<meta name="description" content="Portfolio"> | |
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width"> | |
<meta charset="UTF-8"> | |
<title>{project_name}</title> | |
</head> | |
<body> | |
<!-- Dependencies --> | |
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> | |
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script> | |
<!-- Application Code --> | |
<div id="root"></div> | |
</body> | |
</html>""".format(project_name=project_name) | |
def generate_template_webpack_config(injections: dict) -> str: | |
return """const path = require('path'); | |
const HtmlWebpackPlugin = require('html-webpack-plugin'); | |
module.exports = {{ | |
mode: 'development', // Defines the built-in optimizations to use. See: https://webpack.js.org/configuration/mode/ | |
entry: './src/index.tsx', // Entry point for the application | |
output: {{ // How and where webpack should output your bundles, assets, etc. See: https://webpack.js.org/configuration/output/ | |
filename: 'bundle.js', | |
path: path.resolve(__dirname, 'dist'), | |
}}, | |
devtool: 'source-map', // Enable sourcemaps for debugging webpack's output. | |
devServer: {{ // Behavior configuration of webpack-dev-server. See: https://webpack.js.org/configuration/dev-server/ | |
port: 8080, | |
}}, | |
resolve: {{ // Module resolution settings. See: https://webpack.js.org/configuration/resolve/ | |
alias: {{ // Allows for importing like this: "import BearIcon from 'Assets/images/bear.png'" instead of "import BearIcon from '../../assets/images.bear.png'", etc. | |
Assets: path.resolve(__dirname, 'src/assets/'), | |
Components: path.resolve(__dirname, 'src/components/'), | |
Themes: path.resolve(__dirname, 'src/themes') | |
}}, | |
extensions: ['.ts', '.tsx', '.js', '.json'] | |
}}, | |
module: {{ // Module handling settings. See: https://webpack.js.org/configuration/module/ | |
rules: [ | |
{{ | |
// All output '.js' files will have any sourcemaps pre-processed by 'source-map-loader'. | |
enforce: 'pre', | |
test: /\.js$/, | |
loader: 'source-map-loader' | |
}}, | |
{{ | |
// Transpile all .jsx? files using 'babel-loader'. NOTE: .tsx? files will be compiled using the Typescript compiler and then transpiled by Babel afterwards. | |
test: /\.jsx?$/, | |
exclude: /node_modules/, | |
loader: 'babel-loader', | |
options: {{ | |
presets: ['es2015', 'react'] | |
}} | |
}}, | |
{{ | |
// All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'. | |
test: /\.tsx?$/, | |
loader: 'awesome-typescript-loader' | |
}}, | |
{{ | |
// Load images with the following extensions with the 'file-loader'. | |
test: /\.(png|jpg|svg|gif)$/, | |
loader: 'file-loader' | |
}} | |
], | |
}}, | |
plugins: [ | |
// Basically, use public/index.html as a template for the output, injecting our bundle tag into the body tag. | |
// See: https://webpack.js.org/plugins/html-webpack-plugin/ | |
new HtmlWebpackPlugin({{ | |
title: '{proj_name}', | |
inject: 'body', | |
template: 'public/index.html' | |
}}) | |
], | |
// Exclude dependencies from the output bundle. Instead, rely on the following | |
// dependencies to be present in the end-user's application environment, which allows | |
// browsers to cache those libraries between builds. | |
externals: {{ | |
'react': "React", | |
'react-dom': "ReactDOM" | |
}} | |
}};""".format(**injections) | |
####################### | |
# PROJECT SETUP METHODS | |
####################### | |
def run_filestructure_setup(project_name: str): | |
print('Creating project directory...') | |
mkdir(project_name) | |
os.chdir(project_name) | |
print('Creating all neccessary files and folders for project...') | |
touch('public/index.html', create_intermediate_dirs=True) | |
overwrite_file_contents('public/index.html', generate_index_html(project_name)) | |
touch('src/index.tsx', create_intermediate_dirs=True) | |
overwrite_file_contents('src/index.tsx', generate_index_tsx()) | |
touch('src/App.tsx', create_intermediate_dirs=True) | |
overwrite_file_contents('src/App.tsx', generate_app_tsx()) | |
mkdir('src/assets') | |
mkdir('src/components') | |
mkdir('src/themes') | |
print('Filestructure setup complete!') | |
def run_npm_setup(): | |
# Initialize npm | |
run_cmd('npm init -y', precursor_msg='Initializing npm...') | |
print("Replacing application scripts...") | |
# Alter the package.json to include the proper start/build scripts | |
_, package_json = parse_json_file('package.json') # We already know this file exists, don't need to check | |
package_json['scripts'] = { | |
'start': 'webpack-dev-server --mode development --open --hot', | |
'build': 'webpack --mode production' | |
} | |
# And point to the correct entrypoint | |
package_json['main'] = 'src/index.tsx' | |
# And make this a private npm package by default | |
package_json['private'] = True | |
overwrite_file_contents('package.json', json.dumps(package_json)) | |
print('NPM setup complete!') | |
def run_react_setup(): | |
# Install our React npm packages | |
react_pkg_bundle = NPMPkgBundle(['react', 'react-dom', '@types/react', '@types/react-dom']) | |
run_cmd(react_pkg_bundle.install_cmd, precursor_msg='Installing required React packages...') | |
print('React setup complete!') | |
def run_typescript_setup(): | |
# Install our TypeScript npm packages | |
typescript_pkg_bundle = NPMPkgBundle(['typescript', 'awesome-typescript-loader', 'source-map-loader'], development=True) | |
run_cmd(typescript_pkg_bundle.install_cmd, precursor_msg='Installing required TypeScript packages...') | |
# Create a custom typings directory (so we can declare types for imports such as image files) | |
print('Creating custom typings directory for TypeScript type declarations...') | |
mkdir('typings') | |
# Add a type declaration file for images | |
print('Creating custom type declaration for image imports with TypeScript...') | |
touch('typings/import-images.d.ts') | |
import_images_content = """declare module "*.png"\ndeclare module "*.jpg"\ndeclare module "*.svg"\ndeclare module "*.gif\"""" | |
overwrite_file_contents('typings/import-images.d.ts', import_images_content) | |
# Create a standard TSConfig.json | |
print('Creating TSConfig.json...') | |
touch('TSConfig.json') | |
typeroots = ['./typings', './node_modules/@types'] | |
include = ['./src/**/*', './typings/**/*'] | |
exclude = ['node_modules'] | |
tsconfig = { | |
"compilerOptions": { | |
"baseUrl": '.', | |
"outDir": './dist/', | |
"sourceMap": True, | |
"noImplicitAny": True, | |
"esModuleInterop": True, | |
"module": 'commonjs', | |
"target": 'es5', | |
"moduleResolution": 'node', | |
"jsx": 'react', | |
"paths": { | |
"Assets/*": ["src/assets/*"], | |
"Components/*": ["src/components/*"], | |
"Themes/*": ["src/themes/*"] | |
}, | |
"typeRoots": typeroots | |
}, | |
"include": include, | |
"exclude": exclude | |
} | |
print("Saving your TypeScript configuration to TSConfig.json...") | |
overwrite_file_contents('TSConfig.json', json.dumps(tsconfig)) | |
print('TypeScript setup complete!') | |
def run_webpack_setup(project_name: str): | |
# Install our npm packages for webpack and file-loader (for loading image assets and more) | |
webpack_pkg_bundle = NPMPkgBundle(['webpack', 'webpack-dev-server', 'webpack-cli', 'html-webpack-plugin'], development=True) | |
fileloader_pkg_bundle = NPMPkgBundle(['file-loader'], development=True) | |
run_cmd(webpack_pkg_bundle.install_cmd, precursor_msg='Installing required Webpack packages...') | |
run_cmd(fileloader_pkg_bundle.install_cmd, precursor_msg='Installing required loader for file loading...') | |
# Create a webpack.config.js and generate content for it | |
print("Creating webpack.config.js...") | |
touch('webpack.config.js') | |
print('Saving your webpack settings to webpack.config.js...') | |
overwrite_file_contents('webpack.config.js', generate_template_webpack_config({'proj_name': project_name})) | |
print('Webpack setup complete!') | |
def run_babel_setup(): | |
# Install our npm packages for babel | |
babel_pkg_bundle = NPMPkgBundle(['babel-core', 'babel-loader', 'babel-preset-env', 'babel-preset-react'], development=True) | |
run_cmd(babel_pkg_bundle.install_cmd, precursor_msg='Installing required Babel packages...') | |
# Create our .babelrc | |
print("Creating .babelrc...") | |
touch('.babelrc') | |
babelrc_contents = { | |
"presets": [ | |
"env", | |
"react" | |
] | |
} | |
overwrite_file_contents('.babelrc', json.dumps(babelrc_contents)) | |
print('Babel setup complete!') | |
####### | |
# MAIN | |
####### | |
if __name__ == "__main__": | |
project_name = input("Enter the name of your project: ") | |
run_filestructure_setup(project_name) | |
run_npm_setup() | |
run_react_setup() | |
run_typescript_setup() | |
run_webpack_setup(project_name) | |
run_babel_setup() | |
print("Project setup complete!\n\nRun 'npm start' from the root directory of the project to launch your sample application!") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment