UNPKG

create-web-config

Version:

A CLI to help you get started building modern web applications.

819 lines (710 loc) 19.4 kB
#! /usr/bin/env node import { version as version$1, command, help, on, parse } from 'commander'; import { green } from 'colors'; import { existsSync, outputFileSync, mkdirpSync } from 'fs-extra'; import { resolve, join } from 'path'; import prompts from 'prompts'; import { exec } from 'child_process'; import ora from 'ora'; var name = "create-web-config"; var version = "1.0.10"; var description = "A CLI to help you get started building modern web applications."; var author = "Andreas Mehlsen"; var main = "index.cjs.js"; var module = "index.esm.js"; var bugs = { url: "https://github.com/andreasbm/create-web-config/issues" }; var homepage = "https://github.com/andreasbm/create-web-config#readme"; var repository = { type: "git", url: "git+https://github.com/andreasbm/create-web-config.git" }; var keywords = ["configuration", "webapp", "custom", "elements", "web", "plugins", "lit-element", "component", "rollup", "lit-html", "template", "tslint", "typescript", "postcss", "browserslist", "scss", "minifying", "build", "tsconfig"]; var scripts = { b: "rollup -c rollup.config.ts && npm run postbuild", readme: "readme generate", postbuild: "node post-build.js", postversion: "npm run readme && npm run b", "publish:patch": "np patch --contents=dist --no-cleanup", "publish:minor": "np minor --contents=dist --no-cleanup", "publish:major": "np major --contents=dist --no-cleanup", ncu: "ncu -u -a && npm update && npm install" }; var bin = { "create-web-config": "index.cjs.js", "web-config": "index.cjs.js" }; var dependencies = { colors: "^1.4.0", commander: "^4.0.0", "fs-extra": "^8.1.0", ora: "^4.0.2", path: "^0.12.7", prompts: "^2.2.1" }; var devDependencies = { "@appnest/readme": "^1.2.4", "@types/fs-extra": "^8.0.1", "@types/prompts": "^2.0.2", "@wessberg/rollup-plugin-ts": "^1.1.73", rollup: "^1.26.3", "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-json": "^4.0.0", "rollup-plugin-node-resolve": "^5.2.0" }; var contributors = [{ name: "Andreas Mehlsen", url: "https://twitter.com/andreasmehlsen", img: "https://avatars1.githubusercontent.com/u/6267397?s=460&v=4" }]; var license = "MIT"; var pkg = { name: name, version: version, description: description, author: author, main: main, module: module, bugs: bugs, homepage: homepage, repository: repository, keywords: keywords, scripts: scripts, bin: bin, dependencies: dependencies, devDependencies: devDependencies, contributors: contributors, license: license }; const NPM_ID = `@appnest/web-config`; const LIT_HOME_PAGE_FOLDER_NAME = `pages/home`; const DIST_FOLDER_NAME = `dist`; const SRC_FOLDER_NAME = `src`; const names = { MAIN_TS: `main.ts`, INDEX_HTML: `index.html`, MAIN_SCSS: `main.scss`, ROLLUP_CONFIG_TS: `rollup.config.ts`, ES_LINT_JSON: `.eslintrc.json`, ES_LINT_IGNORE: `.eslintignore`, TS_CONFIG_JSON: `tsconfig.json`, KARMA_CONFIG_JS: `karma.conf.js`, PACKAGE_JSON: `package.json`, TYPINGS_D_TS: `typings.d.ts`, BROWSERSLISTRC: `.browserslistrc`, GITIGNORE: `.gitignore`, ASSETS: `assets`, MANIFEST_JSON: `manifest.json`, ROBOTS_TXT: `robots.txt`, HOME_ELEMENT_TS: `home-element.ts`, HOME_ELEMENT_TEST_TS: `home-element.test.ts`, HOME_ELEMENT_SCSS: `home-element.scss`, README_MD: `README.md`, SERVICE_WORKER_JS: "sw.js" }; const browsersListTemplate = config => `last 2 Chrome versions last 2 Safari versions last 2 Firefox versions`; const eslintIgnoreTemplate = config => `/node_modules/ /dist/`; const gitignoreTemplate = config => `# See http://help.github.com/ignore-files/ for more about ignoring files. .DS_Store ec2-user-key-pair.pem /tmp env.json package-lock.json # compiled output /dist # dependencies /node_modules /functions/node_modules # IDEs and editors /.idea .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # IDE - VSCode .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json # misc /.sass-cache /connect.lock /coverage/* /libpeerconnection.log npm-debug.log testem.log logfile # e2e /e2e/*.js /e2e/*.map #System Files .DS_Store Thumbs.db dump.rdb /compiled/ /.idea/ /.cache/ /.rpt2_cache/ /.vscode/ *.log /logs/ npm-debug.log* /lib-cov/ /coverage/ /.nyc_output/ /.grunt/ *.7z *.dmg *.gz *.iso *.jar *.rar *.tar *.zip .tgz .env .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db *.pem *.p12 *.crt *.csr /node_modules/ /dist/ /documentation/`; const homeElementScssTemplate = config => `:host { color: red; }`; const homeElementTestTemplate = config => `import "./home-element"; import HomeElement from "./home-element"; describe("home-element", () => { let {expect} = chai; let $elem: HomeElement; let $container: HTMLElement; before(() => { $container = document.createElement("div"); document.body.appendChild($container); }); beforeEach(async () => { $container.innerHTML = \`<home-element></home-element>\`; await window.customElements.whenDefined("home-element"); $elem = $container.querySelector<HomeElement>("home-element")!; }); after(() => $container.remove()); it("should be able to be stamped into the DOM", () => { expect($elem).to.exist; }); });`; const homeElementTsTemplate = config => `import { customElement, html, LitElement, unsafeCSS } from "lit-element"; import css from "./home-element.scss"; import "weightless/button"; @customElement("home-element") export default class HomeElement extends LitElement { static styles = [unsafeCSS(css)]; render () { return html\` <wl-button>Welcome</wl-button> \`; } } declare global { interface HTMLElementTagNameMap { "home-element": HomeElement; } }`; const indexTemplate = ({ dir, lit }) => `<!DOCTYPE html> <html lang="en"> <head> <base href="/"> <title>${dir}</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="theme-color" content="#ffffff"> <meta name="description" content="This project was generated using the 'npm init web-config' command"> <link rel="manifest" href="/assets/manifest.json"> </head> <body> <p>${dir}</p> ${lit ? `<router-slot></router-slot>` : ""} <noscript> <p>Please enable Javascript in your browser.</p> </noscript> </body> </html>`; const karmaTemplate = ({ src }) => `const {defaultResolvePlugins, defaultKarmaConfig} = require("@appnest/web-config"); module.exports = (config) => { config.set({ ...defaultKarmaConfig({ rollupPlugins: defaultResolvePlugins() }), basePath: "${src}", logLevel: config.LOG_INFO }); };`; const mainScssTemplate = config => `html { font-size: 14px; }`; const registerSwTemplate = config => `// Register the service worker if ("serviceWorker" in navigator) { navigator.serviceWorker.register("/${names.SERVICE_WORKER_JS}").then(res => { console.log(\`Service worker registered\`, res); }); }`; const mainTsLitTemplate = config => `import "main.scss"; import "@appnest/web-router"; import {RouterSlot} from "@appnest/web-router"; customElements.whenDefined("router-slot").then(async () => { const routerSlot = document.querySelector<RouterSlot>("router-slot")!; await routerSlot.add([ { path: "home", component: () => import("./${LIT_HOME_PAGE_FOLDER_NAME}/home-element") }, { path: "**", redirectTo: "home" } ]); });`; const mainTsDetaulTemplate = config => `import "main.scss";`; const mainTsTemplate = config => { const { lit, sw } = config; return `${lit ? mainTsLitTemplate(config) : mainTsDetaulTemplate(config)}${sw ? ` ${registerSwTemplate(config)}` : ""}`; }; const manifestTemplate = ({ dir }) => `{ "name": "${dir}", "short_name": "${dir}", "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone", "start_url": "/", "lang": "en-US", "icons": [ { "src": "https://raw.githubusercontent.com/andreasbm/weightless/master/assets/www/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "https://raw.githubusercontent.com/andreasbm/weightless/master/assets/www/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ] }`; const packageTemplate = ({ dir }) => `{ ${dir != "" ? `"name": "${dir}",` : ""} "scripts": { "b:dev": "rollup -c rollup.config.ts --environment NODE_ENV:dev", "b:prod": "rollup -c rollup.config.ts --environment NODE_ENV:prod", "s:dev": "rollup -c rollup.config.ts --watch --environment NODE_ENV:dev", "s:prod": "rollup -c rollup.config.ts --watch --environment NODE_ENV:prod", "s": "npm run s:dev", "test": "karma start karma.conf.js" } }`; const readmeTemplate = ({ dir }) => `# ${dir} This project was built using the [create-web-config](https://github.com/andreasbm/create-web-config) CLI. ## Usage * Run \`npm run s\` to serve your project. * Run \`npm run b:dev\` to build your project for development. * Run \`npm run b:prod\` to build your project for production. * Run \`npm run test\` to test the application.`; const robotsTemplate = config => `User-agent: * Allow: /`; const rollupConfigTemplate = ({ sw, dist, src, lit }) => `import {resolve, join} from "path"; import { defaultOutputConfig, defaultPlugins, defaultProdPlugins, defaultServePlugins, isProd, isServe${sw ? `, workbox` : ""} } from "${NPM_ID}"; const folders = { dist: resolve(__dirname, "${dist}"), src: resolve(__dirname, "${src}"), src_assets: resolve(__dirname, "${src}/${names.ASSETS}"), dist_assets: resolve(__dirname, "${dist}/${names.ASSETS}") }; const files = { main: join(folders.src, "${names.MAIN_TS}"), src_index: join(folders.src, "${names.INDEX_HTML}"), src_robots: join(folders.src, "${names.ROBOTS_TXT}"), dist_index: join(folders.dist, "${names.INDEX_HTML}"), dist_robots: join(folders.dist, "${names.ROBOTS_TXT}")${sw ? `, dist_service_worker: join(folders.dist, "${names.SERVICE_WORKER_JS}")` : ""} }; export default { input: { main: files.main }, output: [ defaultOutputConfig({ dir: folders.dist, format: "esm" }) ], plugins: [ ...defaultPlugins({ cleanConfig: { targets: [ folders.dist ] }, copyConfig: { resources: [ [files.src_robots, files.dist_robots], [folders.src_assets, folders.dist_assets] ] }, htmlTemplateConfig: { ${lit ? `polyfillConfig: { features: ["es", "template", "shadow-dom", "custom-elements"] },` : ""} template: files.src_index, target: files.dist_index, include: /main(-.*)?\\.js$/ }, importStylesConfig: { globals: ["${names.MAIN_SCSS}"] } }), // Serve ...(isServe ? defaultServePlugins({ dist: folders.dist }) : []), // Production ...(isProd ? defaultProdPlugins({ dist: folders.dist, budgetConfig: { sizes: { ".js": 1024 * 170, ".jpg": 1024 * 400 } } }) : [])${sw ? `, // Service worker workbox({ mode: "generateSW", workboxConfig: { globDirectory: folders.dist, swDest: files.dist_service_worker, globPatterns: [ \`**/*.{js,png,html,css}\`] } })` : ""} ], treeshake: isProd, context: "window" }`; const tsconfigTemplate = config => `{ "extends": "./node_modules/@appnest/web-config/tsconfig.json" }`; const eslintTemplate = config => `{ "extends": "./node_modules/@appnest/web-config/eslint.js" }`; const typingsTemplate = config => `/// <reference path="node_modules/@appnest/web-config/typings.d.ts" />`; /** * Install dependencies. * @param deps * @param development * @param dir */ async function install(deps, { development = false, dir = "" }) { await run(`cd ${resolve(process.cwd(), dir)} && npm i ${deps.join(" ")} ${development ? `-D` : ""}`); } /** * Writes a file to the correct path. * @param name * @param content * @param config */ function writeFile(name, content, config) { const path = join(resolve(process.cwd(), config.dir), name); // Check if the file exists and if we should then abort if (!config.overwrite && existsSync(path)) { return; } console.log(green(`✔ Creating "${name}"`)); // Check if the command is dry. if (config.dry) { console.log(content); return; } outputFileSync(path, content); } /** * Creates a directory. * @param dir * @param config */ function createDirectory(dir, config) { console.log(green(`✔ Creating directory "${dir}"`)); // Check if the command is dry. if (config.dry) { return; } const path = resolve(process.cwd(), join(config.dir, dir)); mkdirpSync(path); } /** * Runs a command. * @param cmd */ function run(cmd) { return new Promise((res, rej) => { exec(cmd, error => { if (error !== null) { return rej(error); } res(); }); }); } /** * Install dependencies. * @param dry * @param lit * @param dir */ async function installDependencies({ dry, lit, dir }) { const spinner = ora(green(`︎Installing dependencies...`)).start(); function finish() { spinner.succeed(green(`Finished installing dependencies`)); } // Check if the command is dry if (dry) { finish(); return; } // Run the command try { await install(["@appnest/web-config"], { dir, development: true }); // Install lit related dependencies if (lit) { await install(["@appnest/web-router", "lit-element", "weightless"], { dir }); } finish(); } catch (err) { spinner.warn(`Could not install dependencies: ${err.message}`); } } /** * Asks the user for input and returns a configuration object for the command. * @param options */ async function getNewCommandConfig(options) { const { dir } = options; let overwrite = true; if (existsSync(resolve(process.cwd(), dir))) { const input = await prompts({ type: "confirm", name: "overwrite", message: `The directory "${dir}" already exists. Do you want to overwrite existing files?`, initial: true }, { onCancel: () => { process.exit(1); } }); overwrite = input.overwrite; } return { overwrite, ...options }; } /** * Setup rollup.config.ts * @param config */ function setupRollupConfig(config) { const content = rollupConfigTemplate(config); writeFile(names.ROLLUP_CONFIG_TS, content, config); } /** * Setup tslint.json * @param config */ function setupEslint(config) { const content = eslintTemplate(config); writeFile(names.ES_LINT_JSON, content, config); } /** * Setup .eslintignore * @param config */ function setupEslintIgnore(config) { const content = eslintIgnoreTemplate(config); writeFile(names.ES_LINT_IGNORE, content, config); } /** * Setup tsconfig.json * @param config */ function setupTsconfig(config) { const content = tsconfigTemplate(config); writeFile(names.TS_CONFIG_JSON, content, config); } /** * Setup .browserslistrc * @param config */ function setupBrowserslist(config) { const content = browsersListTemplate(config); writeFile(names.BROWSERSLISTRC, content, config); } // Step 6 - Setup karma.conf.js function setupKarma(config) { const content = karmaTemplate(config); writeFile(names.KARMA_CONFIG_JS, content, config); } /** * Add start and build scripts to package.json * @param config */ function setupScripts(config) { const content = packageTemplate(config); writeFile(names.PACKAGE_JSON, content, config); } /** * Setup typings * @param config */ function setupTypings(config) { const content = typingsTemplate(config); writeFile(names.TYPINGS_D_TS, content, config); } /** * Setup gitignore * @param config */ function setupGitIgnore(config) { const content = gitignoreTemplate(config); writeFile(names.GITIGNORE, content, config); } /** * Setup base files. */ function setupBaseFiles(config) { const { lit, src } = config; const mainScssContent = mainScssTemplate(config); const readmeMdContent = readmeTemplate(config); const robotsTxtContent = robotsTemplate(config); const indexContent = indexTemplate(config); const manifestJsonContent = manifestTemplate(config); const mainTsContent = mainTsTemplate(config); createDirectory(join(src, names.ASSETS), config); writeFile(join(src, names.ASSETS, names.MANIFEST_JSON), manifestJsonContent, config); writeFile(join(src, names.MAIN_TS), mainTsContent, config); writeFile(join(src, names.MAIN_SCSS), mainScssContent, config); writeFile(join(src, names.INDEX_HTML), indexContent, config); writeFile(join(src, names.ROBOTS_TXT), robotsTxtContent, config); writeFile(names.README_MD, readmeMdContent, config); // Write the lit specific files if necessary if (lit) { const homeElementTsContent = homeElementTsTemplate(config); const homeElementScssContent = homeElementScssTemplate(config); const homeElementTsTestContent = homeElementTestTemplate(config); writeFile(join(src, names.MAIN_TS), mainTsContent, config); writeFile(join(src, LIT_HOME_PAGE_FOLDER_NAME, names.HOME_ELEMENT_TS), homeElementTsContent, config); writeFile(join(src, LIT_HOME_PAGE_FOLDER_NAME, names.HOME_ELEMENT_SCSS), homeElementScssContent, config); writeFile(join(src, LIT_HOME_PAGE_FOLDER_NAME, names.HOME_ELEMENT_TEST_TS), homeElementTsTestContent, config); } } /** * Executes the new command. * @param options */ async function newCommand(options) { const { dir, install } = options; const config = await getNewCommandConfig(options); setupRollupConfig(config); setupEslint(config); setupEslintIgnore(config); setupTsconfig(config); setupBrowserslist(config); setupKarma(config); setupScripts(config); setupTypings(config); setupGitIgnore(config); setupBaseFiles(config); // Only install if specified if (install) { await installDependencies(config); } // Tell the user that everything worked! console.log(green(`✔ Finished creating project in "${resolve(process.cwd(), dir)}" 🎉`)); console.log(`What's next? → Run "${green("npm run s")}" to serve your project. → Run "${green("npm run b:dev")}" to build your project for development. → Run "${green("npm run b:prod")}" to build your project for production.`); } version$1(pkg.version); command("new <dir>").description("Setup a new project from scratch.").option(`--dry`, `Runs the command without writing any files.`).option(`--lit`, `Adds lit-element and various webapp related libraries to the setup.`).option(`--no-install`, `Doesn't install node_modules.`).option(`--sw`, `Adds a service worker to the setup.`).option(`--src <string>`, `Name of the folder with the transpiled output`, SRC_FOLDER_NAME).option(`--dist <string>`, `Name of the folder with the source code`, DIST_FOLDER_NAME).action((dir, cmd) => { const { dry, lit, install, sw, src, dist } = cmd; newCommand({ dir, dry, lit, install, sw, src, dist }).then(); }); // Do some error handling const userArgs = process.argv.slice(2); if (userArgs.length === 0) { help(); } // Handle unknown commands on("command:*", () => { console.error(`Invalid command: ${userArgs.join(" ")}\nSee --help for a list of available commands.`); process.exit(1); }); // Parse the input parse(process.argv);