create-yerd 0.0.0-stage → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +84 -2
- package/bin/create-yerd.js +17 -0
- package/package.json +42 -4
- package/src/args.js +140 -0
- package/src/cli.js +180 -0
- package/src/pm.js +33 -0
- package/src/prompt.js +284 -0
- package/src/render.js +159 -0
- package/src/style.js +33 -0
- package/src/target.js +68 -0
- package/src/versions.js +14 -0
- package/templates/_shared/_env.example +9 -0
- package/templates/_shared/_gitignore +6 -0
- package/templates/_shared/vitest.config.ts +8 -0
- package/templates/api/README.md +39 -0
- package/templates/api/src/app.ts +9 -0
- package/templates/api/src/main.ts +5 -0
- package/templates/api/src/notes.ts +65 -0
- package/templates/api/src/routes/notes.ts +49 -0
- package/templates/api/test/notes.test.ts +68 -0
- package/templates/api/tsconfig.json +16 -0
- package/templates/api/vite.config.ts +7 -0
- package/templates/full/README.md +46 -0
- package/templates/full/e2e/shop.spec.ts +18 -0
- package/templates/full/playwright.config.ts +16 -0
- package/templates/full/public/favicon.svg +1 -0
- package/templates/full/src/app.ts +8 -0
- package/templates/full/src/cart.ts +50 -0
- package/templates/full/src/catalog.ts +59 -0
- package/templates/full/src/components.tsx +31 -0
- package/templates/full/src/layouts.tsx +67 -0
- package/templates/full/src/main.ts +5 -0
- package/templates/full/src/orders.ts +52 -0
- package/templates/full/src/routes/api.ts +34 -0
- package/templates/full/src/routes/auth.tsx +71 -0
- package/templates/full/src/routes/cart.tsx +66 -0
- package/templates/full/src/routes/checkout.tsx +101 -0
- package/templates/full/src/routes/products.tsx +79 -0
- package/templates/full/src/styles.css +72 -0
- package/templates/full/src/users.ts +41 -0
- package/templates/full/test/shop.test.ts +130 -0
- package/templates/full/tsconfig.json +18 -0
- package/templates/full/vite.config.ts +7 -0
- package/templates/minimal/README.md +26 -0
- package/templates/minimal/public/favicon.svg +1 -0
- package/templates/minimal/src/app.ts +7 -0
- package/templates/minimal/src/layouts.tsx +32 -0
- package/templates/minimal/src/main.ts +5 -0
- package/templates/minimal/src/routes/home.tsx +44 -0
- package/templates/minimal/src/styles.css +32 -0
- package/templates/minimal/test/app.test.ts +35 -0
- package/templates/minimal/tsconfig.json +18 -0
- package/templates/minimal/vite.config.ts +7 -0
- package/templates/tailwind/_shared/vite.config.ts +8 -0
- package/templates/tailwind/full/src/styles.css +49 -0
- package/templates/tailwind/minimal/src/layouts.tsx +33 -0
- package/templates/tailwind/minimal/src/routes/home.tsx +48 -0
- package/templates/tailwind/minimal/src/styles.css +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 toyz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,85 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-yerd
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Scaffold a [yerd](https://toyz.github.io/yerd/) app.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm create yerd@latest my-app
|
|
7
|
+
pnpm create yerd my-app
|
|
8
|
+
yarn create yerd my-app
|
|
9
|
+
bun create yerd my-app
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
It asks for anything the flags leave out: the directory, a template,
|
|
13
|
+
Tailwind, the package manager, git, and whether to install. Then it prints
|
|
14
|
+
what to run next. Node 24 or newer; no dependencies.
|
|
15
|
+
|
|
16
|
+
## Templates
|
|
17
|
+
|
|
18
|
+
| Template | What you get |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `full` | A small shop: a catalog with search, product pages with a 404, a session cart updated by actions that answer with `<Region>`s, sign-in with `createAuth`, a validated checkout form, a JSON API, a health check. Unit tests with `app.request()` / `app.client()` and one Playwright test. |
|
|
21
|
+
| `minimal` | One page and a layout, a session counter updated by an action, a stylesheet, a test. |
|
|
22
|
+
| `api` | JSON routes for notes: DTO-validated bodies, `NotFound` / `Conflict` errors, `@status(HttpStatus.Created)`, `HttpStatus.NoContent` for deletes. No pages, no client runtime. Tests. |
|
|
23
|
+
|
|
24
|
+
Every template runs with Vite in dev (`vite`) and builds to a plain Node
|
|
25
|
+
server (`vite build`, then `node dist/server/server.js`), with
|
|
26
|
+
`tsconfig.json` set up for TC39 decorators and yerd's JSX, vitest, and a
|
|
27
|
+
`.env.example` for `YERD_SECRET`.
|
|
28
|
+
|
|
29
|
+
`--tailwind` adds Tailwind CSS v4 to `full` or `minimal`: `@tailwindcss/vite`
|
|
30
|
+
in `vite.config.ts` and a stylesheet that starts with `@import "tailwindcss"`,
|
|
31
|
+
linked with `<Stylesheet>` like any other.
|
|
32
|
+
|
|
33
|
+
## Options
|
|
34
|
+
|
|
35
|
+
| Flag | Meaning |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `[directory]` | where to create the app; `.` for the current folder |
|
|
38
|
+
| `-t, --template <name>` | `full`, `minimal` or `api` |
|
|
39
|
+
| `--tailwind`, `--no-tailwind` | add Tailwind CSS (`full` and `minimal` only) |
|
|
40
|
+
| `--pm <name>` | `npm`, `pnpm`, `yarn` or `bun`; default: the one running create-yerd |
|
|
41
|
+
| `--git`, `--no-git` | `git init` and a first commit (default: yes; skipped inside a repository) |
|
|
42
|
+
| `--install`, `--no-install` | install dependencies (default: yes) |
|
|
43
|
+
| `-y, --yes` | take the defaults for everything not given: `yerd-app`, `minimal`, no Tailwind, git, install |
|
|
44
|
+
| `-f, --force` | write into a directory that isn't empty, overwriting files with the same names |
|
|
45
|
+
| `-h, --help` | show the options |
|
|
46
|
+
| `-v, --version` | show the version |
|
|
47
|
+
|
|
48
|
+
With npm, flags go after `--`:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
npm create yerd@latest my-app -- --template full --tailwind
|
|
52
|
+
pnpm create yerd my-api --template api --yes
|
|
53
|
+
npm create yerd@latest . -- --yes --no-install
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Without a terminal (a pipe, CI), each question is one line on stdin, and
|
|
57
|
+
every question takes its default once input runs out. `NO_COLOR` turns off
|
|
58
|
+
colors.
|
|
59
|
+
|
|
60
|
+
## Versions
|
|
61
|
+
|
|
62
|
+
create-yerd is released together with yerd, at the same version:
|
|
63
|
+
`create-yerd@X.Y.Z` writes `"@toyz/yerd": "^X.Y.Z"`.
|
|
64
|
+
|
|
65
|
+
## Working on it
|
|
66
|
+
|
|
67
|
+
From the repository root:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
npm ci && npm run build # the integration test installs this build
|
|
71
|
+
npm test --prefix packages/create-yerd # unit tests
|
|
72
|
+
npm run test:integration --prefix packages/create-yerd # every template: install, tsc, test, build, serve
|
|
73
|
+
npm run check --prefix packages/create-yerd # type-check the JS
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Templates live in `templates/`: `_shared` goes into every app, then the
|
|
77
|
+
template's folder, then for Tailwind `tailwind/_shared` and
|
|
78
|
+
`tailwind/<template>`; later files replace earlier ones. `{{name}}`,
|
|
79
|
+
`{{run_dev}}` and the other placeholders in `src/render.js` are filled in,
|
|
80
|
+
and `_gitignore` becomes `.gitignore`. `package.json` is written by
|
|
81
|
+
`src/render.js`.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { main } from "../src/cli.js";
|
|
3
|
+
|
|
4
|
+
try {
|
|
5
|
+
process.exitCode = await main({
|
|
6
|
+
argv: process.argv.slice(2),
|
|
7
|
+
env: process.env,
|
|
8
|
+
cwd: process.cwd(),
|
|
9
|
+
stdin: process.stdin,
|
|
10
|
+
stdout: process.stdout,
|
|
11
|
+
stderr: process.stderr,
|
|
12
|
+
});
|
|
13
|
+
} catch (error) {
|
|
14
|
+
// Anything unexpected (a folder it can't write to...): the message, not a stack trace.
|
|
15
|
+
process.stderr.write(`error ${error instanceof Error ? error.message : String(error)}\n`);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-yerd",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Scaffold a yerd app: npm create yerd@latest",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"yerd",
|
|
7
|
+
"create",
|
|
8
|
+
"scaffold",
|
|
9
|
+
"template",
|
|
10
|
+
"typescript",
|
|
11
|
+
"tsx",
|
|
12
|
+
"ssr"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "toyz",
|
|
17
|
+
"homepage": "https://github.com/Toyz/yerd/tree/main/packages/create-yerd#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/Toyz/yerd/issues"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/Toyz/yerd.git",
|
|
24
|
+
"directory": "packages/create-yerd"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=24"
|
|
28
|
+
},
|
|
29
|
+
"bin": {
|
|
30
|
+
"create-yerd": "bin/create-yerd.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"bin",
|
|
34
|
+
"src",
|
|
35
|
+
"templates",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"check": "tsc -p .",
|
|
41
|
+
"test": "node --test test/*.test.js",
|
|
42
|
+
"test:integration": "CREATE_YERD_INTEGRATION=1 node --test test/integration.test.js"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/args.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { parseArgs as parseNodeArgs } from "node:util";
|
|
3
|
+
|
|
4
|
+
export const TEMPLATES = /** @type {const} */ (["full", "minimal", "api"]);
|
|
5
|
+
export const PACKAGE_MANAGERS = /** @type {const} */ (["npm", "pnpm", "yarn", "bun"]);
|
|
6
|
+
|
|
7
|
+
/** @typedef {(typeof TEMPLATES)[number]} Template */
|
|
8
|
+
/** @typedef {(typeof PACKAGE_MANAGERS)[number]} PackageManager */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What the command line said. `undefined` means "ask, or use the default".
|
|
12
|
+
* @typedef {object} Args
|
|
13
|
+
* @property {string} [dir]
|
|
14
|
+
* @property {Template} [template]
|
|
15
|
+
* @property {boolean} [tailwind]
|
|
16
|
+
* @property {PackageManager} [pm]
|
|
17
|
+
* @property {boolean} [git]
|
|
18
|
+
* @property {boolean} [install]
|
|
19
|
+
* @property {boolean} yes
|
|
20
|
+
* @property {boolean} force
|
|
21
|
+
* @property {boolean} help
|
|
22
|
+
* @property {boolean} version
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** A bad command line: printed without a stack trace, exit code 1. */
|
|
26
|
+
export class UsageError extends Error {
|
|
27
|
+
name = "UsageError";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Templates that render pages, so Tailwind applies. */
|
|
31
|
+
export const PAGE_TEMPLATES = /** @type {readonly Template[]} */ (["full", "minimal"]);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string[]} argv the arguments after the script
|
|
35
|
+
* @returns {Args}
|
|
36
|
+
*/
|
|
37
|
+
export function parseArgs(argv) {
|
|
38
|
+
/** @type {ReturnType<typeof parse>} */
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = parse(argv);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new UsageError(cleanMessage(/** @type {Error} */ (error).message));
|
|
44
|
+
}
|
|
45
|
+
const { values, positionals } = parsed;
|
|
46
|
+
if (positionals.length > 1) throw new UsageError(`Expected one project directory, got ${positionals.length}: ${positionals.join(" ")}`);
|
|
47
|
+
|
|
48
|
+
const template = values.template;
|
|
49
|
+
if (template !== undefined && !includes(TEMPLATES, template)) {
|
|
50
|
+
throw new UsageError(`Unknown template "${template}". Use one of: ${TEMPLATES.join(", ")}`);
|
|
51
|
+
}
|
|
52
|
+
const pm = values.pm;
|
|
53
|
+
if (pm !== undefined && !includes(PACKAGE_MANAGERS, pm)) {
|
|
54
|
+
throw new UsageError(`Unknown package manager "${pm}". Use one of: ${PACKAGE_MANAGERS.join(", ")}`);
|
|
55
|
+
}
|
|
56
|
+
if (template === "api" && values.tailwind === true) {
|
|
57
|
+
throw new UsageError("--tailwind needs a template with pages (full or minimal); api has none");
|
|
58
|
+
}
|
|
59
|
+
const dir = positionals[0];
|
|
60
|
+
if (dir !== undefined && dir.trim() === "") throw new UsageError("The project directory can't be empty");
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
dir,
|
|
64
|
+
template,
|
|
65
|
+
tailwind: values.tailwind,
|
|
66
|
+
pm,
|
|
67
|
+
git: values.git,
|
|
68
|
+
install: values.install,
|
|
69
|
+
yes: values.yes ?? false,
|
|
70
|
+
force: values.force ?? false,
|
|
71
|
+
help: values.help ?? false,
|
|
72
|
+
version: values.version ?? false,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @param {string[]} args */
|
|
77
|
+
function parse(args) {
|
|
78
|
+
return parseNodeArgs({
|
|
79
|
+
args,
|
|
80
|
+
allowPositionals: true,
|
|
81
|
+
allowNegative: true,
|
|
82
|
+
strict: true,
|
|
83
|
+
options: {
|
|
84
|
+
template: { type: "string", short: "t" },
|
|
85
|
+
tailwind: { type: "boolean" },
|
|
86
|
+
pm: { type: "string" },
|
|
87
|
+
git: { type: "boolean" },
|
|
88
|
+
install: { type: "boolean" },
|
|
89
|
+
yes: { type: "boolean", short: "y" },
|
|
90
|
+
force: { type: "boolean", short: "f" },
|
|
91
|
+
help: { type: "boolean", short: "h" },
|
|
92
|
+
version: { type: "boolean", short: "v" },
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @template {string} T
|
|
99
|
+
* @param {readonly T[]} list
|
|
100
|
+
* @param {string} value
|
|
101
|
+
* @returns {value is T}
|
|
102
|
+
*/
|
|
103
|
+
export function includes(list, value) {
|
|
104
|
+
return /** @type {readonly string[]} */ (list).includes(value);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** node:util's messages end with advice about "--" that doesn't fit here. @param {string} message */
|
|
108
|
+
function cleanMessage(message) {
|
|
109
|
+
return message.split(". To specify a positional")[0]?.replace(/\.$/, "") ?? message;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const HELP = `Usage: create-yerd [directory] [options]
|
|
113
|
+
|
|
114
|
+
Scaffold a yerd app. Asks for anything the flags leave out.
|
|
115
|
+
|
|
116
|
+
Options:
|
|
117
|
+
-t, --template <name> full | minimal | api
|
|
118
|
+
--tailwind add Tailwind CSS (full and minimal)
|
|
119
|
+
--no-tailwind plain CSS
|
|
120
|
+
--pm <name> npm | pnpm | yarn | bun (default: the one running this)
|
|
121
|
+
--git, --no-git initialize a git repository (default: yes)
|
|
122
|
+
--install, --no-install
|
|
123
|
+
install dependencies (default: yes)
|
|
124
|
+
-y, --yes accept the defaults for everything not given
|
|
125
|
+
-f, --force write into a directory that isn't empty
|
|
126
|
+
-h, --help show this help
|
|
127
|
+
-v, --version show the version
|
|
128
|
+
|
|
129
|
+
Templates:
|
|
130
|
+
full catalog, product pages, a session cart, sign-in, a validated form,
|
|
131
|
+
a JSON API, unit tests and a Playwright test
|
|
132
|
+
minimal one page, a layout, one action, a test
|
|
133
|
+
api JSON routes, DTO validation, errors, tests; no pages
|
|
134
|
+
|
|
135
|
+
Examples:
|
|
136
|
+
npm create yerd@latest my-app
|
|
137
|
+
npm create yerd@latest my-app -- --template full --tailwind
|
|
138
|
+
pnpm create yerd my-api --template api --yes
|
|
139
|
+
npm create yerd@latest . -- --yes --no-install
|
|
140
|
+
`;
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { relative } from "node:path";
|
|
4
|
+
import { HELP, PAGE_TEMPLATES, UsageError, parseArgs } from "./args.js";
|
|
5
|
+
import { commands, detectPackageManager } from "./pm.js";
|
|
6
|
+
import { CancelError, createPrompter } from "./prompt.js";
|
|
7
|
+
import { VERSION, writeProject, yerdSpec } from "./render.js";
|
|
8
|
+
import { createStyle, useColor } from "./style.js";
|
|
9
|
+
import { inspectTarget, packageName, resolveTarget, validateDir } from "./target.js";
|
|
10
|
+
|
|
11
|
+
/** @typedef {import("./args.js").Template} Template */
|
|
12
|
+
/** @typedef {import("./args.js").PackageManager} PackageManager */
|
|
13
|
+
|
|
14
|
+
/** Defaults for --yes and for questions nobody answers. */
|
|
15
|
+
export const DEFAULTS = { dir: "yerd-app", template: /** @type {Template} */ ("minimal"), tailwind: false, git: true, install: true };
|
|
16
|
+
|
|
17
|
+
/** @type {{ value: Template, label: string, hint: string }[]} */
|
|
18
|
+
const TEMPLATE_CHOICES = [
|
|
19
|
+
{ value: "full", label: "Full app", hint: "catalog, cart, sign-in, forms, JSON API, tests" },
|
|
20
|
+
{ value: "minimal", label: "Minimal", hint: "one page and a layout" },
|
|
21
|
+
{ value: "api", label: "API only", hint: "JSON routes and validation, no pages" },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {object} Io
|
|
26
|
+
* @property {string[]} argv
|
|
27
|
+
* @property {NodeJS.ProcessEnv} env
|
|
28
|
+
* @property {string} cwd
|
|
29
|
+
* @property {NodeJS.ReadableStream & { isTTY?: boolean, setRawMode?: (on: boolean) => unknown }} stdin
|
|
30
|
+
* @property {NodeJS.WritableStream & { isTTY?: boolean }} stdout
|
|
31
|
+
* @property {NodeJS.WritableStream & { isTTY?: boolean }} stderr
|
|
32
|
+
* @property {(cmd: string, args: string[], cwd: string, quiet?: boolean) => boolean} [exec] runs a command; true on success
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The whole command. Returns the exit code.
|
|
37
|
+
* @param {Io} io
|
|
38
|
+
*/
|
|
39
|
+
export async function main(io) {
|
|
40
|
+
const style = createStyle(useColor(io.stdout, io.env));
|
|
41
|
+
const err = createStyle(useColor(io.stderr, io.env));
|
|
42
|
+
const exec = io.exec ?? run;
|
|
43
|
+
const say = (/** @type {string} */ text = "") => io.stdout.write(`${text}\n`);
|
|
44
|
+
|
|
45
|
+
let args;
|
|
46
|
+
try {
|
|
47
|
+
args = parseArgs(io.argv);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (!(error instanceof UsageError)) throw error;
|
|
50
|
+
io.stderr.write(`${err.red("error")} ${error.message}\nRun create-yerd --help for the options.\n`);
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
if (args.help) {
|
|
54
|
+
io.stdout.write(HELP);
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
if (args.version) {
|
|
58
|
+
say(VERSION);
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const prompter = createPrompter({ input: io.stdin, output: io.stdout, style });
|
|
63
|
+
try {
|
|
64
|
+
say(`${style.bold("create-yerd")} ${style.dim(`v${VERSION}`)}\n`);
|
|
65
|
+
const ask = !args.yes;
|
|
66
|
+
|
|
67
|
+
const dir = args.dir ?? (ask ? await prompter.text({ message: "Project directory:", initial: DEFAULTS.dir, validate: validateDir }) : DEFAULTS.dir);
|
|
68
|
+
const target = resolveTarget(dir, io.cwd);
|
|
69
|
+
const state = inspectTarget(target);
|
|
70
|
+
if (state === "file") throw new UsageError(`${dir} is a file`);
|
|
71
|
+
if (state === "not-empty" && !args.force) {
|
|
72
|
+
if (!ask) throw new UsageError(`${dir === "." ? "This directory" : dir} is not empty. Use --force to write into it anyway`);
|
|
73
|
+
const proceed = await prompter.confirm({
|
|
74
|
+
message: `${dir === "." ? "This directory" : dir} is not empty. Write into it, overwriting files with the same names?`,
|
|
75
|
+
initial: false,
|
|
76
|
+
});
|
|
77
|
+
if (!proceed) throw new CancelError();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const template =
|
|
81
|
+
args.template ?? (ask ? await prompter.select({ message: "Template:", choices: TEMPLATE_CHOICES, initial: DEFAULTS.template }) : DEFAULTS.template);
|
|
82
|
+
const pages = PAGE_TEMPLATES.includes(template);
|
|
83
|
+
if (!pages && args.tailwind) throw new UsageError("--tailwind needs a template with pages (full or minimal); api has none");
|
|
84
|
+
const tailwind = pages && (args.tailwind ?? (ask ? await prompter.confirm({ message: "Add Tailwind CSS?", initial: DEFAULTS.tailwind }) : DEFAULTS.tailwind));
|
|
85
|
+
|
|
86
|
+
const detected = detectPackageManager(io.env.npm_config_user_agent);
|
|
87
|
+
/** @type {PackageManager} */
|
|
88
|
+
const pm =
|
|
89
|
+
args.pm ??
|
|
90
|
+
(ask
|
|
91
|
+
? await prompter.select({
|
|
92
|
+
message: "Package manager:",
|
|
93
|
+
choices: /** @type {PackageManager[]} */ ([detected, ...["npm", "pnpm", "yarn", "bun"].filter((p) => p !== detected)]).map((p) => ({
|
|
94
|
+
value: p,
|
|
95
|
+
label: p,
|
|
96
|
+
})),
|
|
97
|
+
initial: detected,
|
|
98
|
+
})
|
|
99
|
+
: detected);
|
|
100
|
+
const git = args.git ?? (ask ? await prompter.confirm({ message: "Initialize a git repository?", initial: DEFAULTS.git }) : DEFAULTS.git);
|
|
101
|
+
const install = args.install ?? (ask ? await prompter.confirm({ message: "Install dependencies?", initial: DEFAULTS.install }) : DEFAULTS.install);
|
|
102
|
+
prompter.close();
|
|
103
|
+
|
|
104
|
+
const name = packageName(target, dir);
|
|
105
|
+
const shown = relative(io.cwd, target) || ".";
|
|
106
|
+
const asked = ask && (state === "not-empty" || [args.dir, args.template, args.pm, args.git, args.install, pages ? args.tailwind : false].includes(undefined));
|
|
107
|
+
if (asked) say();
|
|
108
|
+
say(`Scaffolding ${style.bold(name)} ${style.dim(`(${template}${tailwind ? ", tailwind" : ""})`)} in ${style.cyan(target)}`);
|
|
109
|
+
writeProject(target, { name, template, tailwind, pm, yerd: yerdSpec(io.env) });
|
|
110
|
+
|
|
111
|
+
const cmd = commands(pm);
|
|
112
|
+
let installed = false;
|
|
113
|
+
if (install) {
|
|
114
|
+
say(`Installing dependencies with ${pm}...`);
|
|
115
|
+
installed = exec(/** @type {string} */ (cmd.install[0]), cmd.install.slice(1), target);
|
|
116
|
+
if (!installed) io.stderr.write(`${err.yellow("warning")} ${cmd.install.join(" ")} failed; run it yourself in ${shown}\n`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (git) {
|
|
120
|
+
const result = initGit(target, exec);
|
|
121
|
+
if (result === "missing") io.stderr.write(`${err.yellow("warning")} git isn't installed; skipped the repository\n`);
|
|
122
|
+
else if (result === "inside") say(style.dim("Already inside a git repository; skipped git init."));
|
|
123
|
+
else if (result === "no-commit") io.stderr.write(`${err.yellow("warning")} git init ran, but the first commit failed (is user.name set?)\n`);
|
|
124
|
+
else say("Initialized a git repository.");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
say();
|
|
128
|
+
say(style.bold("Next steps:"));
|
|
129
|
+
say();
|
|
130
|
+
if (shown !== ".") say(` cd ${shown.includes(" ") ? JSON.stringify(shown) : shown}`);
|
|
131
|
+
if (!installed) say(` ${cmd.install.join(" ")}`);
|
|
132
|
+
say(` ${cmd.run("dev")}`.padEnd(24) + style.dim(`# http://localhost:5173${pages ? "" : "/api/notes"}`));
|
|
133
|
+
say();
|
|
134
|
+
say(` ${cmd.run("test")}`.padEnd(24) + style.dim("# unit tests"));
|
|
135
|
+
say(` ${cmd.run("build")}`.padEnd(24) + style.dim("# production build"));
|
|
136
|
+
say(` ${cmd.run("start")}`.padEnd(24) + style.dim("# serve it, on PORT or http://localhost:3000"));
|
|
137
|
+
say();
|
|
138
|
+
say(`Docs: ${style.cyan("https://toyz.github.io/yerd/")}`);
|
|
139
|
+
return install && !installed ? 1 : 0;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
prompter.close();
|
|
142
|
+
if (error instanceof CancelError) {
|
|
143
|
+
io.stderr.write(`\n${err.red("Cancelled.")} Nothing was written.\n`);
|
|
144
|
+
return 130;
|
|
145
|
+
}
|
|
146
|
+
if (error instanceof UsageError) {
|
|
147
|
+
io.stderr.write(`${err.red("error")} ${error.message}\n`);
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* git init and a first commit, unless the folder is already in a repository.
|
|
156
|
+
* @param {string} dir
|
|
157
|
+
* @param {NonNullable<Io["exec"]>} exec
|
|
158
|
+
* @returns {"done" | "missing" | "inside" | "no-commit"}
|
|
159
|
+
*/
|
|
160
|
+
function initGit(dir, exec) {
|
|
161
|
+
if (!exec("git", ["--version"], dir, true)) return "missing";
|
|
162
|
+
if (exec("git", ["rev-parse", "--is-inside-work-tree"], dir, true)) return "inside";
|
|
163
|
+
if (!exec("git", ["init", "-q"], dir, true)) return "missing";
|
|
164
|
+
if (!exec("git", ["add", "-A"], dir, true)) return "no-commit";
|
|
165
|
+
return exec("git", ["commit", "-q", "-m", "Initial commit from create-yerd"], dir, true) ? "done" : "no-commit";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Runs a command and waits. `quiet` swallows its output.
|
|
170
|
+
* @type {NonNullable<Io["exec"]>}
|
|
171
|
+
*/
|
|
172
|
+
function run(cmd, args, cwd, quiet = false) {
|
|
173
|
+
const result = spawnSync(cmd, args, {
|
|
174
|
+
cwd,
|
|
175
|
+
stdio: quiet ? "ignore" : "inherit",
|
|
176
|
+
// npm, pnpm and yarn are .cmd shims on Windows.
|
|
177
|
+
shell: process.platform === "win32",
|
|
178
|
+
});
|
|
179
|
+
return result.status === 0;
|
|
180
|
+
}
|
package/src/pm.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { includes, PACKAGE_MANAGERS } from "./args.js";
|
|
3
|
+
|
|
4
|
+
/** @typedef {import("./args.js").PackageManager} PackageManager */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The package manager that ran us, from npm_config_user_agent
|
|
8
|
+
* ("pnpm/10.4.1 npm/? node/v24.0.0 linux x64"). npm when there's none.
|
|
9
|
+
* @param {string | undefined} userAgent
|
|
10
|
+
* @returns {PackageManager}
|
|
11
|
+
*/
|
|
12
|
+
export function detectPackageManager(userAgent) {
|
|
13
|
+
const name = userAgent?.split(" ")[0]?.split("/")[0];
|
|
14
|
+
return name && includes(PACKAGE_MANAGERS, name) ? name : "npm";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The commands to print and run for one package manager.
|
|
19
|
+
* @param {PackageManager} pm
|
|
20
|
+
*/
|
|
21
|
+
export function commands(pm) {
|
|
22
|
+
return {
|
|
23
|
+
/** argv for installing dependencies */
|
|
24
|
+
install: pm === "yarn" ? ["yarn"] : [pm, "install"],
|
|
25
|
+
/** `npm run dev`, `pnpm dev`... for a script. Not `bun test`: that's bun's own runner. */
|
|
26
|
+
run: (/** @type {string} */ script) => {
|
|
27
|
+
if (pm === "npm") return script === "test" || script === "start" ? `npm ${script}` : `npm run ${script}`;
|
|
28
|
+
return pm === "bun" ? `bun run ${script}` : `${pm} ${script}`;
|
|
29
|
+
},
|
|
30
|
+
/** `npx`, `pnpm exec`... for a binary from node_modules */
|
|
31
|
+
exec: pm === "npm" ? "npx" : pm === "bun" ? "bunx" : pm === "pnpm" ? "pnpm exec" : "yarn",
|
|
32
|
+
};
|
|
33
|
+
}
|