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 };
+}