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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -2
  3. package/bin/create-yerd.js +17 -0
  4. package/package.json +42 -4
  5. package/src/args.js +140 -0
  6. package/src/cli.js +180 -0
  7. package/src/pm.js +33 -0
  8. package/src/prompt.js +284 -0
  9. package/src/render.js +159 -0
  10. package/src/style.js +33 -0
  11. package/src/target.js +68 -0
  12. package/src/versions.js +14 -0
  13. package/templates/_shared/_env.example +9 -0
  14. package/templates/_shared/_gitignore +6 -0
  15. package/templates/_shared/vitest.config.ts +8 -0
  16. package/templates/api/README.md +39 -0
  17. package/templates/api/src/app.ts +9 -0
  18. package/templates/api/src/main.ts +5 -0
  19. package/templates/api/src/notes.ts +65 -0
  20. package/templates/api/src/routes/notes.ts +49 -0
  21. package/templates/api/test/notes.test.ts +68 -0
  22. package/templates/api/tsconfig.json +16 -0
  23. package/templates/api/vite.config.ts +7 -0
  24. package/templates/full/README.md +46 -0
  25. package/templates/full/e2e/shop.spec.ts +18 -0
  26. package/templates/full/playwright.config.ts +16 -0
  27. package/templates/full/public/favicon.svg +1 -0
  28. package/templates/full/src/app.ts +8 -0
  29. package/templates/full/src/cart.ts +50 -0
  30. package/templates/full/src/catalog.ts +59 -0
  31. package/templates/full/src/components.tsx +31 -0
  32. package/templates/full/src/layouts.tsx +67 -0
  33. package/templates/full/src/main.ts +5 -0
  34. package/templates/full/src/orders.ts +52 -0
  35. package/templates/full/src/routes/api.ts +34 -0
  36. package/templates/full/src/routes/auth.tsx +71 -0
  37. package/templates/full/src/routes/cart.tsx +66 -0
  38. package/templates/full/src/routes/checkout.tsx +101 -0
  39. package/templates/full/src/routes/products.tsx +79 -0
  40. package/templates/full/src/styles.css +72 -0
  41. package/templates/full/src/users.ts +41 -0
  42. package/templates/full/test/shop.test.ts +130 -0
  43. package/templates/full/tsconfig.json +18 -0
  44. package/templates/full/vite.config.ts +7 -0
  45. package/templates/minimal/README.md +26 -0
  46. package/templates/minimal/public/favicon.svg +1 -0
  47. package/templates/minimal/src/app.ts +7 -0
  48. package/templates/minimal/src/layouts.tsx +32 -0
  49. package/templates/minimal/src/main.ts +5 -0
  50. package/templates/minimal/src/routes/home.tsx +44 -0
  51. package/templates/minimal/src/styles.css +32 -0
  52. package/templates/minimal/test/app.test.ts +35 -0
  53. package/templates/minimal/tsconfig.json +18 -0
  54. package/templates/minimal/vite.config.ts +7 -0
  55. package/templates/tailwind/_shared/vite.config.ts +8 -0
  56. package/templates/tailwind/full/src/styles.css +49 -0
  57. package/templates/tailwind/minimal/src/layouts.tsx +33 -0
  58. package/templates/tailwind/minimal/src/routes/home.tsx +48 -0
  59. 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
- # Temporary Holding Version
1
+ # create-yerd
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }