Skip to content

Instantly share code, notes, and snippets.

@stevejay
Last active September 3, 2022 07:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stevejay/e8067e8ea953aaad979c4408e61f6322 to your computer and use it in GitHub Desktop.
Save stevejay/e8067e8ea953aaad979c4408e61f6322 to your computer and use it in GitHub Desktop.
Script for scaffolding an app or library that is build using React + TypeScript + Vite + Vitest + Storybook + Playwright. The package manager used is Yarn Berry (non-PnP mode). The script has only been tested on macOS with the zsh shell. It was used to scaffold https://github.com/stevejay/ons-explorer
#!zsh
PACKAGE_MANAGER='yarn'
INSTALL_TAILWIND=true
# Run either with no args as an interactive script or with args for a non-interactive script.
HAS_ARGS=$([ ${#} -eq 0 ])
# Parse any command line args:
while [[ "$#" -gt 0 ]]
do case $1 in
--repoUsername) REPOSITORY_USERNAME="$2"
shift;;
--repoName) REPOSITORY_NAME="$2"
shift;;
--packageName) PACKAGE_NAME="$2"
shift;;
--packageType) PACKAGE_TYPE="$2"
shift;;
--copyrightName) COPYRIGHT_NAME="$2"
shift;;
--quiet) QUIET=true
;;
*) echo "Unknown parameter passed: $1"
exit 1;;
esac
shift
done
# Get any required script parameters that were not supplied on the command line:
if [ "$HAS_ARGS" != true ]; then
if [[ -z "$REPOSITORY_USERNAME" ]]; then
vared -p 'Repository user name, e.g., microsoft: ' -c REPOSITORY_USERNAME
fi
if [[ -z "$REPOSITORY_NAME" ]]; then
vared -p 'Repository name, e.g., vscode: ' -c REPOSITORY_NAME
fi
if [[ -z "$PACKAGE_NAME" ]]; then
vared -p 'Package name, e.g., vscode-core: ' -c PACKAGE_NAME
fi
if [[ -z "$PACKAGE_TYPE" ]]; then
vared -p 'Package type (app/lib): ' -c PACKAGE_TYPE
fi
if [[ -z "$COPYRIGHT_NAME" ]]; then
vared -p 'Copyright name, e.g., John Smith: ' -c COPYRIGHT_NAME
fi
fi
# Default values if not explicitly set:
PACKAGE_NAME="${PACKAGE_NAME:=$REPOSITORY_NAME}"
# Check that we have the required script parameters:
: ${REPOSITORY_USERNAME:?Must provide repository username}
: ${REPOSITORY_NAME:?Must provide repository name}
: ${PACKAGE_NAME:?Must provide package name}
: ${PACKAGE_TYPE:?Must provide package type}
: ${COPYRIGHT_NAME:?Must provide copyright name}
: ${PACKAGE_MANAGER:?Must provide package manager}
if [[ "$PACKAGE_TYPE" != "app" && "$PACKAGE_TYPE" != "lib" ]]; then
echo "Unsupported value for package type (must be 'app' or 'lib')"
exit 1
fi
if [[ "$PACKAGE_MANAGER" != "pnpm" && "$PACKAGE_MANAGER" != "yarn" ]]; then
echo "Unsupported value for package manager (must be 'pnpm' or 'yarn')"
exit 1
fi
# Derived variables:
PROJECT_DIR="$PWD/$PACKAGE_NAME"
# Confirm execution:
if [ "$QUIET" != true ]; then
echo "About to create package $PACKAGE_NAME for $COPYRIGHT_NAME in $PROJECT_DIR"
echo " - Repository username: $REPOSITORY_USERNAME"
echo " - Repository name: $REPOSITORY_NAME"
echo " - Package name: $PACKAGE_NAME"
echo " - Package type: $PACKAGE_TYPE"
echo " - Copyright name: $COPYRIGHT_NAME"
read REPLY\?"Are you sure? (y/Y) "
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]
then
exit 1
fi
fi
# Check the version of node in use:
NODE_VERSION=`node --version | cut -c 2-`
NODE_MAJOR_VERSION=`echo $NODE_VERSION | cut -d'.' -f1`
NODE_MINOR_VERSION=`echo $NODE_VERSION | cut -d'.' -f2`
if [[ "$NODE_MAJOR_VERSION" < "16" || ("$NODE_MAJOR_VERSION" == "16" && "$NODE_MINOR_VERSION" < "10") ]]; then
echo "The version of node used must be at least v16.10.0 or v17+ (for corepack support when installing yarn or pnpm)"
exit 1
fi
# Install package managers (yarn and pnpm):
corepack enable
# Prepare the package manager:
# update pnpm
if [[ "$PACKAGE_MANAGER" = "pnpm" ]]; then
pnpm add -g pnpm
fi
# Install vite:
if [[ "$PACKAGE_MANAGER" = "pnpm" ]]; then
pnpm create vite $PACKAGE_NAME -- --template react-ts
fi
if [[ "$PACKAGE_MANAGER" = "yarn" ]]; then
yarn create vite $PACKAGE_NAME --template react-ts
fi
cd $PROJECT_DIR
# Initialise git in the project:
git init
# Package setup:
if [[ "$PACKAGE_MANAGER" = "pnpm" ]]; then
# Run pnpm for the first time in the repo:
pnpm install --shamefully-hoist
fi
if [[ "$PACKAGE_MANAGER" = "yarn" ]]; then
# Use yarn v2+ as the package manager:
yarn set version berry
yarn config set enableTelemetry 0
# Disable PnP. There are too many issues with the Storybook Vite builder.
tee -a .yarnrc.yml <<EOF > /dev/null
nodeLinker: node-modules
EOF
# Add some useful yarn plugins:
yarn plugin import typescript
yarn plugin import interactive-tools
# Run yarn for the first time in the repo:
yarn install
fi
# Add extra content to the .gitignore file:
read -d '' GITIGNORE << EOF
/node_modules/
.DS_Store
/dist/
/dist-ssr/
*.local
# vite
/.vite/
# yarn (without PnP)
.pnp.*
.yarn/*
# linting caches
.stylelintcache
.eslintcache
# TS types generation
*.tsbuildinfo
/types-temp/
# Storybook
/storybook-static/
# Playwright
/e2e-tests-results/
# npm publish
/*.tgz
# coverage
/coverage
# VSCode
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
!.vscode/launch.json
EOF
if [[ "$PACKAGE_MANAGER" = "pnpm" ]]; then
tee .gitignore <<EOF > /dev/null
$GITIGNORE
EOF
fi
if [[ "$PACKAGE_MANAGER" = "yarn" ]]; then
tee .gitignore <<EOF > /dev/null
$GITIGNORE
# yarn (without PnP) - exceptions
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
EOF
fi
# Identify the release and plugin bundles to Git as binary content,
# so Git won't show massive diffs each time they are added or updated:
if [[ "$PACKAGE_MANAGER" = "yarn" ]]; then
tee .gitattributes <<EOF > /dev/null
/.yarn/releases/** binary
/.yarn/plugins/** binary
EOF
fi
# Add the license file:
$PACKAGE_MANAGER dlx dot-json package.json license "ISC"
tee LICENSE <<EOF > /dev/null
ISC License
Copyright (c) `date +%Y` $COPYRIGHT_NAME
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
EOF
# Fix react and react-dom dependencies for creating a library:
if [[ "$PACKAGE_TYPE" = "lib" ]]; then
$PACKAGE_MANAGER dlx dot-json package.json dependencies --delete
$PACKAGE_MANAGER add -D react react-dom
$PACKAGE_MANAGER dlx dot-json package.json peerDependencies.react "^16.8.0 || ^17.0.0"
$PACKAGE_MANAGER dlx dot-json package.json peerDependencies.react-dom "^16.8.0 || ^17.0.0"
fi
# Fix up the project metadata in package.json:
$PACKAGE_MANAGER dlx dot-json package.json author "$COPYRIGHT_NAME"
$PACKAGE_MANAGER dlx dot-json package.json description "TODO"
$PACKAGE_MANAGER dlx dot-json package.json homepage "https://github.com/$REPOSITORY_USERNAME/$REPOSITORY_NAME#readme"
$PACKAGE_MANAGER dlx dot-json package.json bugs.url "https://github.com/$REPOSITORY_USERNAME/$REPOSITORY_NAME/issues"
$PACKAGE_MANAGER dlx dot-json package.json repository "github:$REPOSITORY_USERNAME/$REPOSITORY_NAME"
$PACKAGE_MANAGER dlx dot-json package.json private false
if [[ "$PACKAGE_TYPE" = "lib" ]]; then
$PACKAGE_MANAGER dlx dot-json package.json sideEffects.0 "./dist/style.css" # or false
$PACKAGE_MANAGER dlx dot-json package.json style "./dist/style.css" # or remove this entry
$PACKAGE_MANAGER dlx dot-json package.json keywords.0 "TODO"
$PACKAGE_MANAGER dlx dot-json package.json main "./dist/$PACKAGE_NAME.umd.js"
$PACKAGE_MANAGER dlx dot-json package.json module "./dist/$PACKAGE_NAME.es.js"
$PACKAGE_MANAGER dlx dot-json package.json types "./dist/index.d.ts"
$PACKAGE_MANAGER dlx dot-json package.json files.0 "dist"
$PACKAGE_MANAGER dlx dot-json package.json exports.REPLACE_ME_DOT.import "./dist/$PACKAGE_NAME.es.js"
$PACKAGE_MANAGER dlx dot-json package.json exports.REPLACE_ME_DOT.require "./dist/$PACKAGE_NAME.umd.js"
sed -i '' 's/REPLACE_ME_DOT/./g' package.json
$PACKAGE_MANAGER dlx dot-json package.json exports.REPLACE_ME_CSS_FILE "./dist/style.css"
sed -i '' 's/REPLACE_ME_CSS_FILE/.\/dist\/style.css/g' package.json
fi
# Add to tsconfig.json file:
$PACKAGE_MANAGER dlx dot-json tsconfig.json compilerOptions.noUnusedLocals true
$PACKAGE_MANAGER dlx dot-json tsconfig.json compilerOptions.baseUrl "."
$PACKAGE_MANAGER dlx dot-json tsconfig.json compilerOptions.paths.REPLACE_ME_ALIAS_1.0 "src/*"
sed -i '' 's/REPLACE_ME_ALIAS_1/@\/\*/g' tsconfig.json
$PACKAGE_MANAGER dlx dot-json tsconfig.json exclude.0 "./types-temp"
$PACKAGE_MANAGER dlx dot-json tsconfig.json exclude.1 "**/node_modules"
$PACKAGE_MANAGER dlx dot-json tsconfig.json exclude.2 "**/.*/"
$PACKAGE_MANAGER dlx dot-json tsconfig.json include.0 "./src"
$PACKAGE_MANAGER dlx dot-json tsconfig.json include.1 "./e2e-tests"
$PACKAGE_MANAGER dlx dot-json tsconfig.json compilerOptions.skipLibCheck true
$PACKAGE_MANAGER dlx dot-json tsconfig.json compilerOptions.skipDefaultLibCheck true
# Set up linting:
$PACKAGE_MANAGER add -D @types/node husky lint-staged eslint stylelint stylelint-config-standard prettier pretty-quick markdownlint markdownlint-cli dockerfilelint
$PACKAGE_MANAGER dlx dot-json package.json scripts.prepare "husky install"
$PACKAGE_MANAGER run prepare
$PACKAGE_MANAGER dlx husky add .husky/pre-commit "yarn lint-staged"
# Dockerfile linting:
$PACKAGE_MANAGER dlx dot-json package.json scripts.lint:dockerfile "dockerfilelint Dockerfile*"
# Prettier:
tee .prettierignore <<EOF > /dev/null
#-------------------------------------------------------------------------------------------------------------------
# Keep this section in sync with .gitignore
#-------------------------------------------------------------------------------------------------------------------
$GITIGNORE
#-------------------------------------------------------------------------------------------------------------------
# Prettier-specific overrides
#-------------------------------------------------------------------------------------------------------------------
# Package manager files
pnpm-lock.yaml
yarn.lock
package-lock.json
shrinkwrap.json
# Build outputs
dist
lib
# Prettier reformats code blocks inside Markdown, which affects rendered output
*.md
EOF
tee .prettierrc.cjs <<EOF > /dev/null
// Documentation for this file: https://prettier.io/docs/en/configuration.html
module.exports = {
// Using a larger print width because Prettier's word-wrapping seems to be tuned
// for plain JavaScript without type annotations
printWidth: 120,
// Use .gitattributes to manage newlines
endOfLine: "auto",
// Use single quotes instead of double quotes
singleQuote: true,
// For ES5, trailing commas cannot be used in function parameters; it is counterintuitive
// to use them for arrays only
trailingComma: "none"
};
EOF
# ESLint:
$PACKAGE_MANAGER add -D eslint-plugin-simple-import-sort eslint-plugin-unicorn eslint-plugin-testing-library eslint-config-prettier eslint-plugin-react eslint-plugin-import eslint-import-resolver-node eslint-plugin-storybook eslint-plugin-react-hooks eslint-plugin-eslint-comments eslint-plugin-tsdoc @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-import-resolver-typescript eslint-plugin-html eslint-plugin-jsx-a11y eslint-plugin-prettier
tee .eslintignore <<EOF > /dev/null
#-------------------------------------------------------------------------------------------------------------------
# Keep this section in sync with .gitignore
#-------------------------------------------------------------------------------------------------------------------
$GITIGNORE
#-------------------------------------------------------------------------------------------------------------------
# eslint-specific overrides
#-------------------------------------------------------------------------------------------------------------------
EOF
tee .eslintrc.cjs <<EOF > /dev/null
module.exports = {
env: {
browser: true,
es6: true,
node: true
},
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module'
},
plugins: [
'html',
'import',
'unicorn',
'testing-library',
'jsx-a11y',
'eslint-plugin-tsdoc',
'simple-import-sort',
'@typescript-eslint',
'prettier'
],
extends: [
'eslint:recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
'plugin:testing-library/react',
'plugin:storybook/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:eslint-comments/recommended',
'prettier'
],
rules: {
'no-restricted-globals': ['error', 'innerWidth', 'innerHeight'],
'prettier/prettier': ['error'],
'sort-imports': 'off',
'import/order': 'off',
'import/first': 'error',
'import/newline-after-import': 'error',
'import/no-duplicates': 'error',
'simple-import-sort/exports': 'error',
'simple-import-sort/imports': [
'error',
{
groups: [
// Packages. react related packages come first.
['^react', '^@?\\\\w'],
// Internal packages.
['^(@)(/.*|$)'],
// Side effect imports.
['^\\\\u0000'],
// Parent imports. Put .. last.
['^\\\\.\\\\.(?!/?$)', '^\\\\.\\\\./?$'],
// Other relative imports. Put same-folder imports and . last.
['^\\\\./(?=.*/)(?!/?$)', '^\\\\.(?!/?$)', '^\\\\./?$'],
// Style imports.
['^.+\\\\.s?css$']
]
}
],
'unicorn/filename-case': [
'error',
{
cases: {
camelCase: true,
pascalCase: true
},
// ignore ambient module declaration file names
ignore: ['\\.d\\.ts$']
}
],
'testing-library/no-node-access': 0,
'testing-library/render-result-naming-convention': 0,
// Can't enable this until https://github.com/microsoft/tsdoc/issues/220 is fixed:
'tsdoc/syntax': 0,
'react/display-name': 'off', // forwardRef causing a problem here
'react/prop-types': 'off', // forwardRef causing a problem here,
'no-unused-vars': 'off'
},
settings: {
react: {
version: '17.0' // Would prefer this to be "detect"
},
'import/extensions': ['.js', '.jsx', '.ts', '.tsx'],
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx']
},
'import/resolver': {
typescript: {
// always try to resolve types under <root>@types directory even it doesn't contain any source code, like @types/unist
alwaysTryTypes: true
}
}
}
};
EOF
# Markdownlint
# TODO maybe add a .markdownlintignore file.
tee .markdownlint.json <<EOF > /dev/null
{
"line-length": false
}
EOF
# Stylelink
tee .stylelintignore <<EOF > /dev/null
#-------------------------------------------------------------------------------------------------------------------
# Keep this section in sync with .gitignore
#-------------------------------------------------------------------------------------------------------------------
$GITIGNORE
#-------------------------------------------------------------------------------------------------------------------
# stylelint-specific overrides
#-------------------------------------------------------------------------------------------------------------------
EOF
tee stylelint.config.cjs <<EOF > /dev/null
module.exports = {
extends: "stylelint-config-standard",
rules: {
"value-list-comma-newline-after": null,
"declaration-colon-newline-after": null,
"declaration-empty-line-before": null,
"font-family-name-quotes": null,
"selector-class-pattern": null,
"keyframes-name-pattern": null,
"string-quotes": "single",
"at-rule-no-unknown": [
true,
{
ignoreAtRules: ["extends", "layer", "tailwind", "apply"]
}
]
},
};
EOF
# Add the linting scripts
$PACKAGE_MANAGER dlx dot-json package.json scripts.lint:prettier "prettier --loglevel warn --check ."
$PACKAGE_MANAGER dlx dot-json package.json scripts.lint:eslint "eslint --ext .html,.js,.jsx,.ts,.tsx --max-warnings 0 ."
$PACKAGE_MANAGER dlx dot-json package.json scripts.lint:ts "tsc --noEmit --pretty"
$PACKAGE_MANAGER dlx dot-json package.json scripts.lint:css "stylelint 'src/**/*.css'"
$PACKAGE_MANAGER dlx dot-json package.json scripts.lint:md "markdownlint --ignore-path ./.gitignore ."
$PACKAGE_MANAGER add -D npm-run-all tsc-files
$PACKAGE_MANAGER dlx dot-json package.json scripts.lint "npm-run-all --parallel --continue-on-error 'lint:*'"
# Lint staged:
tee -a .lintstagedrc.cjs <<EOF > /dev/null
module.exports = {
// Add '--max-warnings 0' back to the eslint command below when it is possible
// to do so. The problem is that if a file in the .eslintignore file is
// included in the commit, eslint warns that it will ignore the file. This makes
// it impossible to update the eslint command to fail if there are any warnings.
// See https://github.com/eslint/eslint/issues/15010
"*.{js,cjs,mjs,ts,tsx}": "yarn eslint --cache --fix",
// tsc-files currently adds an empty includes array to the temporary tsconfig
// file that it generates for linting. The problem is that your code may rely on
// ambient declaration files. These will not be included in the linting, so you
// will get 'Could not find a declaration file' errors. As a workaround you can
// explicitly add those declaration files here, e.g. 'tsc-files --noEmit
// src/types/use-debounced-effect.d.ts'. See
// https://github.com/gustavopch/tsc-files/issues/20 for more info.
'**/*.{ts,tsx}': 'tsc-files --noEmit src/types/image.d.ts src/types/viteEnv.d.ts',
"*.css": "yarn stylelint --cache",
"**/*": "yarn pretty-quick --staged",
"*.md": "yarn markdownlint --config ./.markdownlint.json",
"**/Dockerfile*": "yarn dockerfilelint"
};
EOF
# Set up vite config:
if [[ "$PACKAGE_TYPE" = "lib" ]]; then
tee vite.config.ts <<EOF > /dev/null
/// <reference types="vitest" />
import react from "@vitejs/plugin-react";
import { resolve } from "path";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
react({
exclude: [
/\.(stories|spec|test)\.(t|j)sx?$/,
/__tests__/,
],
}),
],
resolve: {
alias: {
"@": resolve(__dirname, "src"),
},
},
build: {
minify: true,
sourcemap: true,
lib: {
entry: resolve(__dirname, "src/index.ts"),
name: "$PACKAGE_NAME",
fileName: (format) => \`$PACKAGE_NAME.\${format}.js\`,
},
rollupOptions: {
// Externalize the deps that shouldn't be bundled into the library
external: ["react", "react-dom"],
output: {
// Provide global variables to use in the UMD build
// for those externalized deps
globals: {
react: "React",
},
},
},
},
test: {
exclude: ['node_modules', 'dist', '.idea', '.git', '.cache', 'e2e-tests'],
environment: 'jsdom',
setupFiles: ['./src/setupTests.ts'],
coverage: {
reporter: ['html']
},
clearMocks: true,
mockReset: true,
restoreMocks: true
}
});
EOF
else
tee vite.config.ts <<EOF > /dev/null
/// <reference types="vitest" />
import react from "@vitejs/plugin-react";
import { resolve } from "path";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
react({
exclude: [
/\.(stories|spec|test)\.(t|j)sx?$/,
/__tests__/,
],
}),
],
resolve: {
alias: {
"@": resolve(__dirname, "src"),
},
},
build: {
minify: true,
sourcemap: true,
},
test: {
exclude: ['node_modules', 'dist', '.idea', '.git', '.cache', 'e2e-tests'],
environment: 'jsdom',
setupFiles: ['./src/setupTests.ts'],
coverage: {
reporter: ['html']
},
clearMocks: true,
mockReset: true,
restoreMocks: true
}
});
EOF
fi
if [[ "$PACKAGE_TYPE" = "lib" ]]; then
tee tsconfig.types.json <<EOF > /dev/null
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"declaration": true,
"outDir": "./types-temp"
},
"exclude": [
"./types-temp",
"./e2e-tests",
"./playwright.config.ts",
"./src/stories/**/*",
"./src/setupTests.ts"
],
test: {
exclude: ['node_modules', 'dist', '.idea', '.git', '.cache', 'e2e-tests'],
environment: 'jsdom',
setupFiles: ['./src/setupTests.ts'],
coverage: {
reporter: ['html']
},
clearMocks: true,
mockReset: true,
restoreMocks: true
}
}
EOF
fi
# Set up TS types generation:
if [[ "$PACKAGE_TYPE" = "lib" ]]; then
$PACKAGE_MANAGER add -D rollup rollup-plugin-dts @rollup/plugin-alias rimraf
tee rollupTypes.config.cjs <<EOF > /dev/null
const path = require("path");
const dts = require("rollup-plugin-dts").default;
const alias = require("@rollup/plugin-alias");
// From https://github.com/vuetifyjs/vuetify/blob/next/packages/vuetify/build/rollup.types.config.js
const externalsPlugin = () => ({
resolveId(source, importer) {
if (importer && source.endsWith(".css")) {
return {
id: source,
external: true,
moduleSideEffects: false,
};
}
},
});
function createTypesConfig(input, output) {
return {
input: "types-temp/" + input,
output: [{ file: output, format: "es" }],
plugins: [
dts(),
externalsPlugin(),
alias({
entries: [
{
find: /^@\/(.*)/,
replacement: path.resolve(__dirname, "./types-temp/\$1"),
},
],
}),
],
};
}
const config = [createTypesConfig("index.d.ts", "dist/index.d.ts")];
module.exports = config;
EOF
fi
if [[ "$PACKAGE_TYPE" = "lib" ]]; then
$PACKAGE_MANAGER dlx dot-json package.json scripts.build "tsc && vite build && yarn build:types"
$PACKAGE_MANAGER dlx dot-json package.json scripts.build:types "rimraf types-temp && tsc --pretty --emitDeclarationOnly -p tsconfig.types.json && rollup --config rollupTypes.config.cjs && rimraf types-temp"
else
$PACKAGE_MANAGER dlx dot-json package.json scripts.build "tsc && vite build"
fi
# Configure TS Doc:
tee ./tsdoc.json <<EOF > /dev/null
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/tsdoc/v0/tsdoc.schema.json",
"tagDefinitions": [
{
"tagName": "@jsxImportSource",
"syntaxKind": "modifier"
}
]
}
EOF
# Fix up the source code:
tee ./src/index.ts <<EOF > /dev/null
export { App } from "@/App";
EOF
# Fix up the types:
mkdir ./src/types
mv ./src/vite-env.d.ts ./src/types/viteEnv.d.ts
tee ./src/types/image.d.ts <<EOF > /dev/null
// https://stackoverflow.com/questions/58726319/typescript-cannot-find-module-when-import-svg-file
declare module '*.svg';
EOF
if [[ "$PACKAGE_TYPE" = "lib" ]]; then
tee ./src/types/index.ts <<EOF > /dev/null
// This file has a .ts rather than a .d.ts extension in order to
// simplify type file generation: https://stackoverflow.com/a/56440335/604006
// TODO delete the following line when you start to add content to this file.
export {}
EOF
fi
# Add Storybook:
$PACKAGE_MANAGER dlx sb@next init --builder @storybook/builder-vite @mdx-js/preact
# Rename the build storybook script:
$PACKAGE_MANAGER dlx dot-json package.json scripts.build:storybook "build-storybook"
$PACKAGE_MANAGER dlx dot-json package.json scripts.build-storybook --delete
$PACKAGE_MANAGER dlx dot-json package.json scripts.preview:storybook "serve ./storybook-static --listen 5000"
# ./.storybook/main.js needs to be fixed up for alias support and emotion lib errors
# if your own code uses emotion too. But this file might have changed in a new version
# of @storybook/builder-vite so the unedited version is backed up as ./.storybook/main.js.bak
# so you can review any changes.
cp ./.storybook/main.js ./.storybook/main.js.bak
tee ./.storybook/main.cjs <<EOF > /dev/null
const { loadConfigFromFile, mergeConfig } = require('vite');
const path = require('path');
module.exports = {
stories: ['../src/**/*.stories.@(ts|tsx|mdx)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'],
framework: '@storybook/react',
core: {
builder: '@storybook/builder-vite'
},
// Workaround for https://github.com/storybookjs/storybook/issues/16099
webpackFinal(config) {
delete config.resolve.alias['emotion-theming'];
delete config.resolve.alias['@emotion/styled'];
delete config.resolve.alias['@emotion/core'];
return config;
},
// Workaround for https://github.com/eirslett/storybook-builder-vite/issues/85
async viteFinal(config /*, { configType }*/) {
const { config: userConfig } = await loadConfigFromFile(path.resolve(__dirname, '../vite.config.ts'));
return mergeConfig(config, {
resolve: userConfig.resolve,
// Hack for https://github.com/eirslett/storybook-builder-vite/issues/173#issuecomment-989264127
optimizeDeps: {
...config.optimizeDeps,
// Entries are specified relative to the root
entries: [\`\${path.relative(config.root, path.resolve(__dirname, '../src'))}/**/*.stories.tsx\`],
include: [
...(config?.optimizeDeps?.include ?? []),
'@storybook/theming',
'@storybook/addon-actions'
]
}
});
}
};
EOF
# Set up VSCode:
mkdir .vscode
tee .vscode/settings.json <<EOF > /dev/null
{
"search.exclude": {
".vite/": true,
".yarn/": true,
"coverage/": true,
"dist/": true
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.markdownlint": true
},
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/node_modules/**": true,
"env-*": true
},
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/node_modules": true,
"env*": true,
"*.tsbuildinfo": true,
".vite/": true
},
"css.validate": false,
"stylelint.enable": true,
"typescript.tsdk": "./node_modules/typescript/lib",
"eslint.validate": ["html", "javascript", "typescript", "javascriptreact", "typescriptreact"],
"eslint.packageManager": "yarn",
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
EOF
tee .vscode/launch.json <<EOF > /dev/null
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:6006",
"webRoot": "${workspaceFolder}"
}
]
}
EOF
tee .vscode/extensions.json <<EOF > /dev/null
{
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
}
EOF
# Add a VSCode development container:
# https://code.visualstudio.com/docs/remote/containers#_quick-start-open-an-existing-folder-in-a-container
# https://code.visualstudio.com/docs/remote/containers#_getting-started
# https://code.visualstudio.com/docs/remote/containers-tutorial
# https://github.com/microsoft/vscode-dev-containers/tree/v0.192.0/containers/typescript-node
mkdir .devcontainer
tee ./.devcontainer/devcontainer.json <<EOF > /dev/null
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/typescript-node
{
"name": "Node.js & TypeScript",
"build": {
"dockerfile": "Dockerfile",
// Update 'VARIANT' to pick a Node version: 16, 14, 12.
// Append -bullseye or -buster to pin to an OS version.
// Use -bullseye variants on local on arm64/Apple Silicon.
"args": {
"VARIANT": "16-bullseye"
}
},
// Set *default* container specific settings.json values on container create.
"settings": {},
// Add the IDs of extensions you want installed when the container is created.
"extensions": ["dbaeumer.vscode-eslint"],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [3000],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "yarn install",
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node"
}
EOF
tee ./.devcontainer/Dockerfile <<EOF > /dev/null
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/typescript-node/.devcontainer/base.Dockerfile
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 16, 14, 12, 16-bullseye, 14-bullseye, 12-bullseye, 16-buster, 14-buster, 12-buster
ARG VARIANT="16-bullseye"
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment if you want to install an additional version of node using nvm
# ARG EXTRA_NODE_VERSION=10
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
# [Optional] Uncomment if you want to install more global node packages
# RUN su node -c "npm install -g <your-package-list -here>"
EOF
# Add Playwright:
$PACKAGE_MANAGER add -D @playwright/test
# install supported browsers: (supports-color could be removed after this line)
$PACKAGE_MANAGER add -D supports-color
$PACKAGE_MANAGER dlx playwright install
tee playwright.config.ts <<EOF > /dev/null
import { devices, PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
testDir: 'e2e-tests',
outputDir: './e2e-tests-results',
forbidOnly: !!process.env.CI,
// Having to run Playwright in single-threaded mode locally
// as some tests will time out without this:
workers: process.env.CI ? 2 : 1,
retries: process.env.CI ? 2 : 0,
use: {
baseURL: 'http://localhost:6006',
trace: 'on-first-retry',
video: 'on-first-retry',
screenshot: 'only-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
}
]
};
export default config;
EOF
$PACKAGE_MANAGER dlx dot-json package.json scripts.test:e2e "playwright test"
mkdir ./e2e-tests
tee ./e2e-tests/example.spec.ts <<EOF > /dev/null
import { expect, test } from '@playwright/test';
test('example test', async ({ page }) => {
await page.goto('http://localhost:6006/?path=/story/example-app--default');
const heading = page.frameLocator('#storybook-preview-iframe').locator('#root').locator('h1');
await expect(heading).toHaveText('Hello Vite + React! (mode=test)');
});
EOF
# Add vitest
$PACKAGE_MANAGER add -D @testing-library/react @testing-library/react-hooks @testing-library/user-event @types/chai @types/chai-dom @types/jsdom @vitejs/plugin-react chai-dom jsdom vitest
tee ./src/testUtils.ts <<EOF > /dev/null
import { ReactElement } from 'react';
import { render } from '@testing-library/react';
const customRender = (ui: ReactElement, options = {}) =>
render(ui, {
// wrap provider(s) here if needed
wrapper: ({ children }) => children,
...options
});
/* eslint-disable import/export */
export * from '@testing-library/react';
export { default as userEvent } from '@testing-library/user-event';
export { customRender as render }; // override render export
/* eslint-enable import/export */
EOF
tee ./src/setupTests.ts <<EOF > /dev/null
import chai from 'chai';
import { afterEach } from 'vitest';
import { cleanup } from '@/testUtils';
// import { server } from './mocks/server.js';
// Extend chai with chai-dom assertions
// eslint-disable-next-line @typescript-eslint/no-var-requires
chai.use(require('chai-dom'));
// React Testing Library cleanup
afterEach(cleanup);
// // Establish API mocking before all tests.
// beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
// // Reset any request handlers that we may add during the tests,
// // so they don't affect other tests.
// afterEach(() => server.resetHandlers());
// // Clean up after the tests are finished.
// afterAll(() => server.close());
EOF
$PACKAGE_MANAGER dlx dot-json package.json scripts.test "vitest --run"
$PACKAGE_MANAGER dlx dot-json package.json scripts.test:coverage "vitest --run --coverage",
$PACKAGE_MANAGER dlx dot-json package.json scripts.test:watch "vitest watch"
# Fix up Storybook files:
rm -r ./src/stories/assets
rm ./src/stories/button.css
rm ./src/stories/Button.stories.tsx
rm ./src/stories/Button.tsx
rm ./src/stories/header.css
rm ./src/stories/Header.stories.tsx
rm ./src/stories/Header.tsx
rm ./src/stories/Introduction.stories.mdx
rm ./src/stories/page.css
rm ./src/stories/Page.stories.tsx
rm ./src/stories/Page.tsx
tee ./src/stories/App.stories.tsx <<EOF > /dev/null
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { App } from '@/App';
export default {
title: 'Example/App',
component: App
} as ComponentMeta<typeof App>;
const Template: ComponentStory<typeof App> = () => <App />;
export const Default = Template.bind({});
Default.args = {};
EOF
# Fix up source code:
rm ./src/App.css
tee ./src/App.test.tsx <<EOF > /dev/null
import { render, screen } from '@testing-library/react';
import { expect, it } from 'vitest';
import { App } from '@/App';
it('should pass', () => {
render(<App />);
expect(screen.getByRole('heading')).to.have.text('Hello Vite + React! (mode=development)');
});
EOF
tee ./src/main.tsx <<EOF > /dev/null
import { StrictMode } from 'react';
import { render } from 'react-dom';
import { App } from '@/App';
import './index.css';
render(
<StrictMode>
<App />
</StrictMode>,
document.getElementById('root')
);
EOF
tee ./src/App.tsx <<EOF > /dev/null
import { useState } from 'react';
import logo from './assets/react.svg';
export function App() {
const [count, setCount] = useState(0);
return (
<div>
<header>
<img src={logo} alt="logo" />
<h1>Hello Vite + React! (mode={import.meta.env.MODE})</h1>
<p>
<button type="button" onClick={() => setCount((count) => count + 1)}>
Count is: {count}
</button>
</p>
</header>
</div>
);
}
EOF
if [[ "$PACKAGE_TYPE" = "app" ]]; then
rm ./src/index.ts
fi
if [[ "$PACKAGE_TYPE" = "app" ]]; then
# Add docker for an app build
tee docker-compose.yml <<EOF > /dev/null
version: '3.7'
services:
$PACKAGE_NAME:
build: .
ports:
- '6008:80'
EOF
# Directory for things like robots.txt
mkdir public
tee Dockerfile <<EOF > /dev/null
# ----- Stage 1: build the Web site -----
# Use a node docker image:
FROM node:16-stretch AS src-build
WORKDIR /usr/src/app
COPY package.json .
COPY ./.yarnrc.yml ./.yarnrc.yml
COPY ./yarn.lock ./yarn.lock
COPY ./.yarn/releases/ ./.yarn/releases/
COPY ./.yarn/plugins/ ./.yarn/plugins/
RUN yarn
COPY ./index.html .
COPY ./tsconfig.json .
COPY ./vite.config.ts .
COPY ./tailwind.config.js .
COPY ./postcss.config.js .
COPY ./src ./src
COPY ./public ./public
RUN yarn build
# ----- Stage 2: create the final nginx docker image -----
FROM nginx:1.20
# Copy the built Web pages into it:
COPY --from=src-build /usr/src/app/dist /usr/share/nginx/html/
# Copy the nginx config files:
COPY nginx /etc/nginx
EXPOSE 80/tcp
EOF
fi
# Add the README file with the Docker build instructions.
if [[ "$PACKAGE_TYPE" = "app" ]]; then
tee -a README.md <<EOF > /dev/null
# Title
## Building the Web site Docker image
The image currently needs to be manually built locally and then pushed to Docker Hub. The image is built and pushed with a tag of \`latest\`.
First log in to Docker:
\`\`\`bash
docker login -u yourusername
[enter your password]
\`\`\`
Then build and push the image:
\`\`\`bash
docker build --tag $REPOSITORY_USERNAME/$PACKAGE_NAME:latest --file Dockerfile .
docker push $REPOSITORY_USERNAME/$PACKAGE_NAME:latest
\`\`\`
### Building and running locally
If you want to test building and running the image locally, then run the following command from the project root: \`docker-compose up --force-recreate --build --detach\`. You should now be able to access the Web site at \`http://localhost:6008/\`.
EOF
fi
# Add tailwind
if [ "$INSTALL_TAILWIND" = true ]; then
$PACKAGE_MANAGER add -D tailwindcss postcss autoprefixer
$PACKAGE_MANAGER run tailwindcss init -p
tee ./tailwind.config.cjs <<EOF > /dev/null
module.exports = {
content: [
"./index.html",
"./src/**/*.{jsx,tsx}",
],
theme: {
fontFamily: {
sans: ['"Readex Pro"', 'sans-serif']
},
extend: {},
},
plugins: [],
}
EOF
tee ./postcss.config.cjs <<EOF > /dev/null
module.exports = {
plugins: {
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {}
}
};
EOF
tee ./src/index.css <<EOF > /dev/null
@tailwind base;
@tailwind components;
@tailwind utilities;
EOF
# Prepend a link to the tailwind css file
echo -e "import '../src/index.css';\n\n$(cat .storybook/preview.js)" > .storybook/preview.cjs
tee ./index.html <<EOF > /dev/null
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Readex+Pro:wght@200;300;400;600&display=swap"
rel="stylesheet"
/>
</head>
<body class="font-sans font-normal text-base">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
EOF
tee ./src/App.tsx <<EOF > /dev/null
import { useState } from 'react';
import logo from './logo.svg';
export function App() {
const [count, setCount] = useState(0);
return (
<div>
<header className="flex flex-col items-center space-y-4">
<img src={logo} alt="logo" className="w-48 h-48" />
<h1 className="text-2xl font-bold">Hello Vite + React! (mode={import.meta.env.MODE})</h1>
<p>
<button type="button" onClick={() => setCount((count) => count + 1)}>
Count is: {count}
</button>
</p>
</header>
</div>
);
}
EOF
tee ./.storybook/preview-head.html <<EOF > /dev/null
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Readex+Pro:wght@200;300;400;600&display=swap"
rel="stylesheet"
/>
<style>
body {
font-family: 'Readex Pro', sans-serif;
}
</style>
<!-- Fix for @storybook/addon-interactions use of jest-mock that relies on the node global variable: -->
<script>
window.global = window;
</script>
EOF
fi
# Add hook test example:
tee ./src/hook.test.tsx <<EOF > /dev/null
import { createContext, ReactNode, useCallback, useContext, useState } from 'react';
import { act, renderHook } from '@testing-library/react-hooks';
import { expect, it } from 'vitest';
const CounterStepContext = createContext(1);
export function CounterStepProvider({ step, children }: { step: number; children: ReactNode }) {
return <CounterStepContext.Provider value={step}>{children}</CounterStepContext.Provider>;
}
export function useCounter(initialValue = 0): {
count: number;
increment: () => void;
reset: () => void;
} {
const [count, setCount] = useState(initialValue);
const step = useContext(CounterStepContext);
const increment = useCallback(() => setCount((x) => x + step), [step]);
const reset = useCallback(() => setCount(initialValue), [initialValue]);
return { count, increment, reset };
}
it('should use custom step when incrementing', () => {
const { result } = renderHook(() => useCounter(), {
wrapper: ({ children }: { children: ReactNode }) => <CounterStepProvider step={2}>{children}</CounterStepProvider>
});
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(2);
});
EOF
# Final linting:
$PACKAGE_MANAGER dlx stylelint --fix 'src/**/*.css'
$PACKAGE_MANAGER dlx eslint --ext .html,.js,.jsx,.ts,.tsx --fix .
$PACKAGE_MANAGER dlx prettier --write .
# Coda:
echo "Done. Now run:"
echo
echo " cd $PACKAGE_NAME"
echo " code ."
echo " yarn dev"
echo
echo "To set up VSCode to launch from the command line (as 'code .'):"
echo " https://code.visualstudio.com/docs/setup/mac#_launching-from-the-command-line"
echo
echo "In VSCode:"
echo " The \`typescript.tsdk\` workspace setting in \`./.vscode/settings.json\` only tells VS Code"
echo " that a workspace version of TypeScript exists. To actually start using the workspace"
echo " version for IntelliSense, you must run the \`TypeScript: Select TypeScript Version\`"
echo " command and select the workspace version."
echo
echo "Yarn:"
echo " To upgrade yarn: \`yarn set version stable\`"
echo " To validate the project dependencies: \`yarn dlx @yarnpkg/doctor .\`"
echo
# VSCode plugins to install:
#
# code --install-extension esbenp.prettier-vscode
# code --install-extension stylelint.vscode-stylelint
# code --install-extension dbaeumer.vscode-eslint
# code --install-extension DavidAnson.vscode-markdownlint
# # Support 'go to definition' for Yarn Berry PnP:
# code --install-extension arcanis.vscode-zipfs
# code --install-extension ryanrosello-og.playwright-vscode-trace-viewer
#
# code --install-extension streetsidesoftware.code-spell-checker
# code --install-extension bierner.color-info
# code --install-extension nextfaze.comment-wrap
# code --install-extension salbert.comment-ts
# code --install-extension christian-kohler.npm-intellisense
# code --install-extension bradlc.vscode-tailwindcss
# code --install-extension heybourn.headwind
# code --install-extension styled-components.vscode-styled-components
# code --install-extension aaron-bond.better-comments
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment