Skip to content

Instantly share code, notes, and snippets.

@Zhendryk
Last active May 28, 2020 20:51
Show Gist options
  • Save Zhendryk/4caf9954dd171ddb929e1784942e2e0e to your computer and use it in GitHub Desktop.
Save Zhendryk/4caf9954dd171ddb929e1784942e2e0e to your computer and use it in GitHub Desktop.
React/TypeScript Project Generator
# 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