Skip to content

Feat/non interactive #31

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,3 @@ indent_size = 2

[*.md]
trim_trailing_whitespace = false

[test/fixtures/**/*.expected.*]
trim_trailing_whitespace = false
insert_final_newline = false
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,34 @@ Create a Vite-powered Preact app in seconds
<img src="https://github.com/preactjs/create-preact/blob/master/media/demo.gif?raw=true">
</p>

## 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 [<project-name>]

$ yarn create preact
$ yarn create preact [<project-name>]

$ pnpm create preact
$ pnpm create preact [<project-name>]
```

`<project-name>` 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)
2 changes: 1 addition & 1 deletion jsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"allowJs": true,
"checkJs": true,
Expand Down
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"dependencies": {
"@clack/prompts": "^0.9.0",
"kolorist": "^1.8.0",
"mri": "^1.2.0",
"tinyexec": "^0.3.1"
},
"devDependencies": {
Expand Down
176 changes: 122 additions & 54 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,45 @@
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';

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(
Expand All @@ -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} */
Expand All @@ -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`)}
Expand Down Expand Up @@ -264,3 +297,38 @@ function getPkgManager() {
if (userAgent.startsWith('pnpm')) return 'pnpm';
return 'npm';
}

/**
* @param {Record<string, any>} 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 };
}