diff --git a/.editorconfig b/.editorconfig index 41a6bd3..59cdc67 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,7 +13,3 @@ indent_size = 2 [*.md] trim_trailing_whitespace = false - -[test/fixtures/**/*.expected.*] -trim_trailing_whitespace = false -insert_final_newline = false diff --git a/README.md b/README.md index febc1dc..fd0545b 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,34 @@ Create a Vite-powered Preact app in seconds

-## Usage +## Interactive Usage + +Interactive usage will walk you through the process of creating a new Preact project, offering options for you to select from. ```sh -$ npm init preact +$ npm init preact [] -$ yarn create preact +$ yarn create preact [] -$ pnpm create preact +$ pnpm create preact [] ``` +`` is an optional argument, mainly for use in other initializers (such as `create-vite`). + +## Non-interactive Usage + +Non-interactive usage will create a new Preact project based upon passed CLI flags. At least one must be specified, even if it matches the default. + +- `--lang` + - Language to use for the project. Defaults to `js` + - Options: `js`, `ts` +- `--use-router` + - Whether to include the Preact Router. Defaults to `false` +- `--use-prerender` + - Whether to initialize your app for prerendering. Defaults to `false` +- `--use-eslint` + - Whether to include ESLint configuration. Defaults to `false` + ## License [MIT](https://github.com/preactjs/create-preact/blob/master/LICENSE) diff --git a/jsconfig.json b/jsconfig.json index 9b25d80..69dd9ee 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "ESNext", - "module": "ESNext", + "module": "NodeNext", "moduleResolution": "NodeNext", "allowJs": true, "checkJs": true, diff --git a/package-lock.json b/package-lock.json index 7fb2eb8..f17b0b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@clack/prompts": "^0.9.0", "kolorist": "^1.8.0", + "mri": "^1.2.0", "tinyexec": "^0.3.1" }, "bin": { @@ -58,6 +59,15 @@ "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", "license": "MIT" }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", diff --git a/package.json b/package.json index ddc45a3..466b737 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "dependencies": { "@clack/prompts": "^0.9.0", "kolorist": "^1.8.0", + "mri": "^1.2.0", "tinyexec": "^0.3.1" }, "devDependencies": { diff --git a/src/index.js b/src/index.js index 9801785..569b428 100755 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ import { promises as fs, existsSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import mri from 'mri'; import * as prompts from '@clack/prompts'; import { x } from 'tinyexec'; import * as kl from 'kolorist'; @@ -9,14 +10,37 @@ import * as kl from 'kolorist'; const s = prompts.spinner(); const brandColor = /** @type {const} */ ([174, 128, 255]); +const flagDefaults = { + 'skip-hints': false, + lang: 'js', + 'use-router': false, + 'use-prerender': false, + 'use-eslint': false, +}; + (async function createPreact() { - const args = process.argv.slice(2); + const argv = mri(process.argv.slice(2), { + string: ['lang'], + boolean: ['skip-hints', 'router', 'prerender', 'eslint'], + }); + + const { argDir, flagPassed } = processArgs(argv); + + if (flagPassed && !argDir) { + console.error( + kl.red( + 'Error: No project directory specified. One must be provided for non-interactive sessions.', + ), + ); + } + + // Assign defaults only after we've determined if the user passed any flags + argv['skip-hints'] ??= flagDefaults['skip-hints']; + argv['lang'] ??= flagDefaults.lang; + argv['use-router'] ??= flagDefaults['use-router']; + argv['use-prerender'] ??= flagDefaults['use-prerender']; + argv['use-eslint'] ??= flagDefaults['use-eslint']; - // Silences the 'Getting Started' info, mainly - // for use in other initializers that may wrap this - // one but provide their own scripts/instructions. - const skipHint = args.includes('--skip-hints'); - const argDir = args.find((arg) => !arg.startsWith('--')); const packageManager = getPkgManager(); prompts.intro( @@ -25,54 +49,63 @@ const brandColor = /** @type {const} */ ([174, 128, 255]); ), ); - const { dir, language, useRouter, usePrerender, useESLint } = await prompts.group( - { - dir: () => - argDir - ? Promise.resolve(argDir) - : prompts.text({ - message: 'Project directory:', - placeholder: 'my-preact-app', - validate(value) { - if (value.length == 0) { - return 'Directory name is required!'; - } else if (existsSync(value)) { - return 'Refusing to overwrite existing directory or file! Please provide a non-clashing name.'; - } - }, - }), - language: () => - prompts.select({ - message: 'Project language:', - initialValue: 'js', - options: [ - { value: 'js', label: 'JavaScript' }, - { value: 'ts', label: 'TypeScript' }, - ], - }), - useRouter: () => - prompts.confirm({ - message: 'Use router?', - initialValue: false, - }), - usePrerender: () => - prompts.confirm({ - message: 'Prerender app (SSG)?', - initialValue: false, - }), - useESLint: () => - prompts.confirm({ - message: 'Use ESLint?', - initialValue: false, - }), - }, - { - onCancel: () => { - prompts.cancel(kl.yellow('Cancelled')); - process.exit(0); + let dir = argDir, + language = argv['lang'], + useRouter = argv['router'], + usePrerender = argv['prerender'], + useESLint = argv['eslint']; + + if (!flagPassed) { + ({ dir, language, useRouter, usePrerender, useESLint } = await prompts.group( + { + dir: () => + argDir + ? Promise.resolve(argDir) + : prompts.text({ + message: 'Project directory:', + placeholder: 'my-preact-app', + validate(value) { + if (value.length == 0) { + return 'Directory name is required!'; + } else if (existsSync(value)) { + return 'Refusing to overwrite existing directory or file! Please provide a non-clashing name.'; + } + }, + }), + language: () => + prompts.select({ + message: 'Project language:', + initialValue: 'js', + options: [ + { value: 'js', label: 'JavaScript' }, + { value: 'ts', label: 'TypeScript' }, + ], + }), + useRouter: () => + prompts.confirm({ + message: 'Use router?', + initialValue: false, + }), + usePrerender: () => + prompts.confirm({ + message: 'Prerender app (SSG)?', + initialValue: false, + }), + useESLint: () => + prompts.confirm({ + message: 'Use ESLint?', + initialValue: false, + }), }, - }, - ); + { + onCancel: () => { + prompts.cancel(kl.yellow('Cancelled')); + process.exit(0); + }, + }, + )); + } + const targetDir = resolve(process.cwd(), dir); const useTS = language === 'ts'; /** @type {ConfigOptions} */ @@ -90,7 +123,7 @@ const brandColor = /** @type {const} */ ([174, 128, 255]); 'Installed project dependencies', ); - if (!skipHint) { + if (!argv['skip-hints']) { const gettingStarted = ` ${kl.dim('$')} ${kl.lightBlue(`cd ${dir}`)} ${kl.dim('$')} ${kl.lightBlue(`${packageManager == 'npm' ? 'npm run' : packageManager} dev`)} @@ -264,3 +297,38 @@ function getPkgManager() { if (userAgent.startsWith('pnpm')) return 'pnpm'; return 'npm'; } + +/** + * @param {Record} argv + * @returns {{ argDir: string | undefined, flagPassed: boolean }} + */ +function processArgs(argv) { + let argDir, + // bails out of an interactive session + flagPassed = false; + + for (const key in argv) { + // `mri` has an `unknown` callback arg, but it only works for flags + // defined in `alias`, which isn't terribly useful + if (!(key in flagDefaults) && key !== '_') { + console.warn(kl.yellow(`WARN: Unknown flag passed: '${key}'. Ignoring it.`)); + continue; + } + + if (key == '_' && argv[key].length > 0) { + if (argv[key].length > 1) { + console.warn( + kl.yellow( + 'WARN: Multiple arguments were passed, only the first will be used.\n', + ), + argv['_'], + ); + } + argDir = argv._[0]; + } else { + flagPassed = true; + } + } + + return { argDir, flagPassed }; +}