@gjsify/create-app 0.40.0 → 0.42.0
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/README.md +44 -19
- package/dist-templates/adw-canvas2d/README.md +21 -11
- package/dist-templates/adw-canvas2d/package.json +19 -8
- package/dist-templates/adw-canvas2d/src/index.ts +6 -1
- package/dist-templates/adw-game/README.md +39 -11
- package/dist-templates/adw-game/package.json +22 -9
- package/dist-templates/adw-game/src/index.ts +6 -1
- package/dist-templates/adw-webgl/README.md +34 -11
- package/dist-templates/adw-webgl/package.json +19 -8
- package/dist-templates/adw-webgl/src/index.ts +8 -1
- package/dist-templates/cli/README.md +1 -1
- package/dist-templates/cli/package.json +5 -5
- package/dist-templates/cli/src/index.ts +14 -1
- package/dist-templates/gtk-minimal/README.md +32 -11
- package/dist-templates/gtk-minimal/package.json +17 -7
- package/dist-templates/web-server-express/README.md +1 -1
- package/dist-templates/web-server-express/package.json +5 -5
- package/dist-templates/web-server-hono/README.md +1 -1
- package/dist-templates/web-server-hono/package.json +5 -5
- package/lib/create.d.ts +31 -11
- package/lib/create.js +114 -49
- package/lib/index.d.ts +2 -1
- package/lib/index.js +46 -8
- package/lib/prompt-template.d.ts +55 -1
- package/lib/prompt-template.js +119 -20
- package/lib/runtimes.d.ts +72 -0
- package/lib/runtimes.js +141 -0
- package/lib/select.d.ts +63 -0
- package/lib/select.js +113 -0
- package/package.json +14 -3
package/lib/prompt-template.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { stdin, stdout } from 'node:process';
|
|
2
2
|
import { emitKeypressEvents } from 'node:readline';
|
|
3
|
+
import { RUNTIME_DESCRIPTIONS } from './runtimes.js';
|
|
4
|
+
import { selectPackageManager, selectRuntime } from './select.js';
|
|
3
5
|
const c = {
|
|
4
6
|
reset: '\x1b[0m',
|
|
5
7
|
bold: '\x1b[1m',
|
|
@@ -8,34 +10,54 @@ const c = {
|
|
|
8
10
|
green: '\x1b[32m',
|
|
9
11
|
};
|
|
10
12
|
/**
|
|
11
|
-
*
|
|
13
|
+
* What a picker rejects with when the user hits Ctrl+C.
|
|
14
|
+
*
|
|
15
|
+
* A recognisable sentinel rather than a bare `throw`: the standalone bin hands
|
|
16
|
+
* its handler to yargs, which turns a rejection into a stack trace followed by
|
|
17
|
+
* the entire `--help` — what a user who deliberately backed out saw. It was
|
|
18
|
+
* unreachable while `-t <template>` skipped the only prompt there was; the
|
|
19
|
+
* runtime and package-manager pickers made aborting the normal way to change
|
|
20
|
+
* your mind, so it is now on the common path.
|
|
21
|
+
*/
|
|
22
|
+
const CANCELLED = 'Cancelled by user';
|
|
23
|
+
/** `true` when `error` is a picker the user aborted with Ctrl+C. */
|
|
24
|
+
export function isCancelled(error) {
|
|
25
|
+
return error instanceof Error && error.message === CANCELLED;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Render a choice list with one row highlighted.
|
|
12
29
|
* First call: just draws. Subsequent calls move the cursor back up first.
|
|
13
30
|
*/
|
|
14
|
-
function render(
|
|
31
|
+
function render(choices, selected, firstRender) {
|
|
15
32
|
if (!firstRender) {
|
|
16
33
|
// Move cursor up over the previously drawn rows
|
|
17
|
-
stdout.write(`\x1b[${
|
|
34
|
+
stdout.write(`\x1b[${choices.length}A`);
|
|
18
35
|
}
|
|
19
|
-
for (let i = 0; i <
|
|
20
|
-
const
|
|
36
|
+
for (let i = 0; i < choices.length; i++) {
|
|
37
|
+
const choice = choices[i];
|
|
21
38
|
const marker = i === selected ? `${c.cyan}❯${c.reset}` : ' ';
|
|
22
|
-
const label = i === selected ? `${c.bold}${c.green}${
|
|
39
|
+
const label = i === selected ? `${c.bold}${c.green}${choice.label}${c.reset}` : choice.label;
|
|
40
|
+
const description = choice.description ? ` ${c.dim}${choice.description}${c.reset}` : '';
|
|
23
41
|
// Clear line, then write content
|
|
24
|
-
stdout.write(`\x1b[2K\r ${marker} ${label}
|
|
42
|
+
stdout.write(`\x1b[2K\r ${marker} ${label}${description}\n`);
|
|
25
43
|
}
|
|
26
44
|
}
|
|
27
45
|
/**
|
|
28
|
-
* Interactive
|
|
46
|
+
* Interactive arrow-key picker. Returns the selected value.
|
|
29
47
|
* Throws if stdin is not a TTY — caller should check process.stdin.isTTY first.
|
|
48
|
+
*
|
|
49
|
+
* `initial` is the index the cursor starts on, so a prompt can open on the
|
|
50
|
+
* answer the tool would have picked anyway (the host runtime, say) and Enter
|
|
51
|
+
* alone is a valid response.
|
|
30
52
|
*/
|
|
31
|
-
export function
|
|
32
|
-
if (
|
|
33
|
-
return Promise.reject(new Error(
|
|
53
|
+
export function promptChoice(title, choices, initial = 0) {
|
|
54
|
+
if (choices.length === 0) {
|
|
55
|
+
return Promise.reject(new Error(`Nothing to choose for "${title}".`));
|
|
34
56
|
}
|
|
35
57
|
return new Promise((resolve, reject) => {
|
|
36
|
-
let selected = 0;
|
|
37
|
-
stdout.write(`${c.bold}
|
|
38
|
-
render(
|
|
58
|
+
let selected = initial >= 0 && initial < choices.length ? initial : 0;
|
|
59
|
+
stdout.write(`${c.bold}${title}${c.reset} ${c.dim}(↑/↓ to navigate, Enter to confirm, Ctrl+C to cancel)${c.reset}\n`);
|
|
60
|
+
render(choices, selected, true);
|
|
39
61
|
emitKeypressEvents(stdin);
|
|
40
62
|
if (stdin.isTTY)
|
|
41
63
|
stdin.setRawMode(true);
|
|
@@ -52,22 +74,99 @@ export function promptTemplate(templates) {
|
|
|
52
74
|
if (key.ctrl && key.name === 'c') {
|
|
53
75
|
cleanup();
|
|
54
76
|
stdout.write('\n');
|
|
55
|
-
reject(new Error(
|
|
77
|
+
reject(new Error(CANCELLED));
|
|
56
78
|
return;
|
|
57
79
|
}
|
|
58
80
|
if (key.name === 'up' || key.name === 'k') {
|
|
59
|
-
selected = (selected - 1 +
|
|
60
|
-
render(
|
|
81
|
+
selected = (selected - 1 + choices.length) % choices.length;
|
|
82
|
+
render(choices, selected, false);
|
|
61
83
|
}
|
|
62
84
|
else if (key.name === 'down' || key.name === 'j') {
|
|
63
|
-
selected = (selected + 1) %
|
|
64
|
-
render(
|
|
85
|
+
selected = (selected + 1) % choices.length;
|
|
86
|
+
render(choices, selected, false);
|
|
65
87
|
}
|
|
66
88
|
else if (key.name === 'return' || key.name === 'enter') {
|
|
67
89
|
cleanup();
|
|
68
|
-
resolve(
|
|
90
|
+
resolve(choices[selected].value);
|
|
69
91
|
}
|
|
70
92
|
};
|
|
71
93
|
stdin.on('keypress', onKeypress);
|
|
72
94
|
});
|
|
73
95
|
}
|
|
96
|
+
/** Interactive template picker. Returns the selected TemplateInfo. */
|
|
97
|
+
export function promptTemplate(templates) {
|
|
98
|
+
if (templates.length === 0) {
|
|
99
|
+
return Promise.reject(new Error('No templates available.'));
|
|
100
|
+
}
|
|
101
|
+
return promptChoice('Select a template', templates.map((t) => ({ value: t, label: t.name, description: t.description })));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Interactive runtime picker, offering only what the chosen template declares.
|
|
105
|
+
*
|
|
106
|
+
* `initial` is the runtime `defaultRuntimeFor()` resolved to; it is passed in
|
|
107
|
+
* rather than recomputed so the prompt opens on exactly the value the non-TTY
|
|
108
|
+
* path would have used.
|
|
109
|
+
*/
|
|
110
|
+
export function promptRuntime(runtimes, initial) {
|
|
111
|
+
return promptChoice('Select a runtime', runtimes.map((runtime) => ({
|
|
112
|
+
value: runtime,
|
|
113
|
+
label: runtime,
|
|
114
|
+
description: RUNTIME_DESCRIPTIONS[runtime] ?? '',
|
|
115
|
+
})), runtimes.indexOf(initial));
|
|
116
|
+
}
|
|
117
|
+
/** Interactive package-manager picker for the runtime already chosen. */
|
|
118
|
+
export function promptPackageManager(managers, initial) {
|
|
119
|
+
return promptChoice('Select a package manager', managers.map((manager) => ({ value: manager, label: manager, description: '' })), managers.indexOf(initial));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Carry out a {@link Selection}: print the notice, ask, or refuse.
|
|
123
|
+
*
|
|
124
|
+
* `undefined` means the caller should exit non-zero — the reason is already on
|
|
125
|
+
* stderr. Notices go to stderr too, so a scripted `create-app … | tee` keeps the
|
|
126
|
+
* next-steps block on stdout intact.
|
|
127
|
+
*/
|
|
128
|
+
async function settle(selection, ask) {
|
|
129
|
+
switch (selection.kind) {
|
|
130
|
+
case 'value':
|
|
131
|
+
return selection.value;
|
|
132
|
+
case 'auto':
|
|
133
|
+
console.error(selection.notice);
|
|
134
|
+
return selection.value;
|
|
135
|
+
case 'prompt':
|
|
136
|
+
// The only throw here is the Ctrl+C sentinel `promptChoice` rejects
|
|
137
|
+
// with, and an abort is an outcome, not a failure — report it the way
|
|
138
|
+
// the refusal branch below reports one instead of unwinding.
|
|
139
|
+
try {
|
|
140
|
+
return await ask(selection.choices, selection.initial);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (!isCancelled(error))
|
|
144
|
+
throw error;
|
|
145
|
+
console.error('Cancelled.');
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
case 'error':
|
|
149
|
+
console.error(`Error: ${selection.message}`);
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Settle runtime and package manager: flags, then prompts, then announced
|
|
155
|
+
* defaults — the runtime first, because it decides which managers are legal.
|
|
156
|
+
*
|
|
157
|
+
* `undefined` means "exit non-zero, the reason is printed". Lives here rather
|
|
158
|
+
* than in either bin because BOTH surfaces ask these two questions — `npm create
|
|
159
|
+
* @gjsify/app` and `gjsify create`. A copy in one of them is a second answer to
|
|
160
|
+
* the same question, which is exactly what let `gjsify create --install` reach
|
|
161
|
+
* for npm on its own while the standalone bin refused to guess.
|
|
162
|
+
*/
|
|
163
|
+
export async function promptSetup(question) {
|
|
164
|
+
const { offered, install, interactive } = question;
|
|
165
|
+
const runtime = await settle(selectRuntime({ flag: question.runtime, offered, manager: question.packageManager, interactive }), (choices, initial) => promptRuntime(choices, initial));
|
|
166
|
+
if (runtime === undefined)
|
|
167
|
+
return undefined;
|
|
168
|
+
const packageManager = await settle(selectPackageManager({ flag: question.packageManager, runtime, install, interactive }), (choices, initial) => promptPackageManager(choices, initial));
|
|
169
|
+
if (packageManager === undefined)
|
|
170
|
+
return undefined;
|
|
171
|
+
return { runtime, packageManager };
|
|
172
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package managers a scaffolded project can be installed with. npm/yarn/pnpm/
|
|
3
|
+
* gjsify all work on a Node host — templates declare every dependency explicitly,
|
|
4
|
+
* `@gjsify/rolldown-native` included, which the three would otherwise skip as an
|
|
5
|
+
* optional peer of `@gjsify/cli`. `gjsify` is the only one of the six that works
|
|
6
|
+
* on a host with no Node.js.
|
|
7
|
+
*/
|
|
8
|
+
export declare const PACKAGE_MANAGERS: readonly ["npm", "yarn", "pnpm", "gjsify", "bun", "deno"];
|
|
9
|
+
export type PackageManager = (typeof PACKAGE_MANAGERS)[number];
|
|
10
|
+
/**
|
|
11
|
+
* The `install` argv per manager, each read off the tool's own `--help` rather
|
|
12
|
+
* than assumed: `bun install` and a BARE `deno install` mean "install what
|
|
13
|
+
* package.json lists" — `deno install --global` is the unrelated
|
|
14
|
+
* install-a-script mode.
|
|
15
|
+
*/
|
|
16
|
+
export declare const INSTALL_ARGV: Record<PackageManager, readonly string[]>;
|
|
17
|
+
/** The command prefix that runs a package script under `manager`. */
|
|
18
|
+
export declare function runScriptCommand(manager: PackageManager): string;
|
|
19
|
+
/**
|
|
20
|
+
* Which managers can install a project the user intends to RUN on `<runtime>` —
|
|
21
|
+
* not taste, but the module layout each runtime resolves against. `gjs` is the
|
|
22
|
+
* Node-free column, where npm/yarn/pnpm are not discouraged but ABSENT; bun writes
|
|
23
|
+
* its own layout; `deno install` populates the `node_modules` that
|
|
24
|
+
* `--node-modules-dir=manual` (how `gjsify run --runtime deno` launches) reads
|
|
25
|
+
* without re-resolving. The KEYS double as "runtimes this scaffolder has an
|
|
26
|
+
* installer for", and the spec asserts they equal the CLI's `EXAMPLE_RUNTIMES`, so
|
|
27
|
+
* a runtime added upstream fails loudly instead of vanishing from every prompt.
|
|
28
|
+
*/
|
|
29
|
+
export declare const RUNTIME_PACKAGE_MANAGERS: Readonly<Record<string, readonly PackageManager[]>>;
|
|
30
|
+
/** One-line label for each runtime, shown beside it in the picker. */
|
|
31
|
+
export declare const RUNTIME_DESCRIPTIONS: Readonly<Record<string, string>>;
|
|
32
|
+
/**
|
|
33
|
+
* The managers that install for `runtime`, `undefined` when unmapped — not a
|
|
34
|
+
* permissive fallback: unmapped means the template is newer than the scaffolder,
|
|
35
|
+
* and guessing an installer buries the cause.
|
|
36
|
+
*/
|
|
37
|
+
export declare function packageManagersForRuntime(runtime: string): readonly PackageManager[] | undefined;
|
|
38
|
+
/** `true` when this scaffolder knows how to install for `runtime`. */
|
|
39
|
+
export declare function isKnownRuntime(runtime: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The JS runtime this scaffolder is itself executing on. A LOCAL implementation,
|
|
42
|
+
* kept for the two measured reasons in the file header and held to the canonical
|
|
43
|
+
* one by `runtimes.spec.ts`, which runs on all four runtimes and compares against
|
|
44
|
+
* `@gjsify/rolldown-plugin-gjsify/runtime`.
|
|
45
|
+
*
|
|
46
|
+
* Probe order is load-bearing: **Bun and Deno first**, because both fake
|
|
47
|
+
* `process.versions.node` for npm compatibility and GJS's `@gjsify/process` shim
|
|
48
|
+
* does too, making that read a false Node positive on three of the four hosts.
|
|
49
|
+
*/
|
|
50
|
+
export declare function hostRuntime(): string;
|
|
51
|
+
/**
|
|
52
|
+
* Which runtime to start on: the host when the template supports it — "whatever
|
|
53
|
+
* you are already running" needs no second toolchain — else the template's own
|
|
54
|
+
* first declaration, which is the canonical one.
|
|
55
|
+
*
|
|
56
|
+
* `manager` OVERRIDES that, because a pinned manager already names a runtime.
|
|
57
|
+
* Without it, `create-app x -t cli -p bun` resolved to the host (`node`), found
|
|
58
|
+
* bun illegal there and refused a combination the user had spelled out. "First
|
|
59
|
+
* declared runtime this manager installs for" needs no per-manager table: bun and
|
|
60
|
+
* deno serve one each, npm/yarn/pnpm only node, and `gjsify` — legal on gjs and
|
|
61
|
+
* node — lands on gjs, the column it was written for.
|
|
62
|
+
*
|
|
63
|
+
* `undefined` for an empty list, or when nothing declared installs with
|
|
64
|
+
* `manager`; the caller decides whether that is an error.
|
|
65
|
+
*/
|
|
66
|
+
export declare function defaultRuntimeFor(runtimes: readonly string[], manager?: PackageManager): string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* The script that launches the project on `runtime`. Template convention: `start`
|
|
69
|
+
* is the gjs entry (what `gjsify.main` points at), every other runtime a suffixed
|
|
70
|
+
* sibling — `start:node`, `start:bun`, `start:deno`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function startScriptFor(runtime: string): string;
|
package/lib/runtimes.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Runtime ↔ package-manager model for `gjsify create` / `npm create @gjsify/app`.
|
|
2
|
+
// gjsify targets four runtimes and each has its own installer, so assuming `npm`
|
|
3
|
+
// printed next steps a `deno` user cannot run and, with `--install`, ran the wrong
|
|
4
|
+
// installer for the layout their runtime resolves against.
|
|
5
|
+
//
|
|
6
|
+
// WHY THE RUNTIME LIST IS NOT IMPORTED FROM `@gjsify/cli`, which owns
|
|
7
|
+
// `EXAMPLE_RUNTIMES`: nothing here twins it. The runtimes OFFERED come from the
|
|
8
|
+
// template's own manifest, and the table below is a different fact (which
|
|
9
|
+
// installer serves which runtime) whose key set `runtimes.spec.ts` asserts
|
|
10
|
+
// against `EXAMPLE_RUNTIMES`. A runtime import was measured and rejected twice —
|
|
11
|
+
// `@gjsify/cli` as a `dependency` closes the loop cli → create-app → cli, and
|
|
12
|
+
// `@gjsify/workspace`'s `topologicalSort` throws on a prod cycle ("dependency
|
|
13
|
+
// cycle detected involving @gjsify/cli, @gjsify/create-app"), taking `build:infra`
|
|
14
|
+
// with it; while `@gjsify/rolldown-plugin-gjsify/runtime`, the cycle-free leaf
|
|
15
|
+
// owning `hostRuntime()`, drags typescript + deepkit + sass + lightningcss into a
|
|
16
|
+
// scaffolder whose whole runtime dependency set is `yargs`. Both are
|
|
17
|
+
// devDependencies, and the spec cross-checks against them.
|
|
18
|
+
/**
|
|
19
|
+
* Package managers a scaffolded project can be installed with. npm/yarn/pnpm/
|
|
20
|
+
* gjsify all work on a Node host — templates declare every dependency explicitly,
|
|
21
|
+
* `@gjsify/rolldown-native` included, which the three would otherwise skip as an
|
|
22
|
+
* optional peer of `@gjsify/cli`. `gjsify` is the only one of the six that works
|
|
23
|
+
* on a host with no Node.js.
|
|
24
|
+
*/
|
|
25
|
+
export const PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm', 'gjsify', 'bun', 'deno'];
|
|
26
|
+
/**
|
|
27
|
+
* The `install` argv per manager, each read off the tool's own `--help` rather
|
|
28
|
+
* than assumed: `bun install` and a BARE `deno install` mean "install what
|
|
29
|
+
* package.json lists" — `deno install --global` is the unrelated
|
|
30
|
+
* install-a-script mode.
|
|
31
|
+
*/
|
|
32
|
+
export const INSTALL_ARGV = {
|
|
33
|
+
npm: ['install', '--no-audit', '--no-fund'],
|
|
34
|
+
yarn: ['install'],
|
|
35
|
+
pnpm: ['install'],
|
|
36
|
+
gjsify: ['install'],
|
|
37
|
+
bun: ['install'],
|
|
38
|
+
deno: ['install'],
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* How each manager spells "run the package script `<name>`" — six managers, four
|
|
42
|
+
* spellings. Printing the one the user's manager accepts is why the choice is
|
|
43
|
+
* threaded this far: `npm run dev` is a dead end for someone who installed with
|
|
44
|
+
* deno.
|
|
45
|
+
*/
|
|
46
|
+
const RUN_SCRIPT_PREFIX = {
|
|
47
|
+
npm: 'npm run',
|
|
48
|
+
yarn: 'yarn',
|
|
49
|
+
pnpm: 'pnpm',
|
|
50
|
+
gjsify: 'gjsify run',
|
|
51
|
+
bun: 'bun run',
|
|
52
|
+
deno: 'deno task',
|
|
53
|
+
};
|
|
54
|
+
/** The command prefix that runs a package script under `manager`. */
|
|
55
|
+
export function runScriptCommand(manager) {
|
|
56
|
+
return RUN_SCRIPT_PREFIX[manager];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Which managers can install a project the user intends to RUN on `<runtime>` —
|
|
60
|
+
* not taste, but the module layout each runtime resolves against. `gjs` is the
|
|
61
|
+
* Node-free column, where npm/yarn/pnpm are not discouraged but ABSENT; bun writes
|
|
62
|
+
* its own layout; `deno install` populates the `node_modules` that
|
|
63
|
+
* `--node-modules-dir=manual` (how `gjsify run --runtime deno` launches) reads
|
|
64
|
+
* without re-resolving. The KEYS double as "runtimes this scaffolder has an
|
|
65
|
+
* installer for", and the spec asserts they equal the CLI's `EXAMPLE_RUNTIMES`, so
|
|
66
|
+
* a runtime added upstream fails loudly instead of vanishing from every prompt.
|
|
67
|
+
*/
|
|
68
|
+
export const RUNTIME_PACKAGE_MANAGERS = {
|
|
69
|
+
gjs: ['gjsify'],
|
|
70
|
+
node: ['npm', 'yarn', 'pnpm', 'gjsify'],
|
|
71
|
+
bun: ['bun'],
|
|
72
|
+
deno: ['deno'],
|
|
73
|
+
};
|
|
74
|
+
/** One-line label for each runtime, shown beside it in the picker. */
|
|
75
|
+
export const RUNTIME_DESCRIPTIONS = {
|
|
76
|
+
gjs: 'GNOME JavaScript — no Node.js needed at runtime.',
|
|
77
|
+
node: 'Node.js — gi:// via the @gjsify/node-gi bridge.',
|
|
78
|
+
bun: 'Bun — runs the same --app node bundle.',
|
|
79
|
+
deno: 'Deno — runs the same --app node bundle.',
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* The managers that install for `runtime`, `undefined` when unmapped — not a
|
|
83
|
+
* permissive fallback: unmapped means the template is newer than the scaffolder,
|
|
84
|
+
* and guessing an installer buries the cause.
|
|
85
|
+
*/
|
|
86
|
+
export function packageManagersForRuntime(runtime) {
|
|
87
|
+
return RUNTIME_PACKAGE_MANAGERS[runtime];
|
|
88
|
+
}
|
|
89
|
+
/** `true` when this scaffolder knows how to install for `runtime`. */
|
|
90
|
+
export function isKnownRuntime(runtime) {
|
|
91
|
+
return packageManagersForRuntime(runtime) !== undefined;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The JS runtime this scaffolder is itself executing on. A LOCAL implementation,
|
|
95
|
+
* kept for the two measured reasons in the file header and held to the canonical
|
|
96
|
+
* one by `runtimes.spec.ts`, which runs on all four runtimes and compares against
|
|
97
|
+
* `@gjsify/rolldown-plugin-gjsify/runtime`.
|
|
98
|
+
*
|
|
99
|
+
* Probe order is load-bearing: **Bun and Deno first**, because both fake
|
|
100
|
+
* `process.versions.node` for npm compatibility and GJS's `@gjsify/process` shim
|
|
101
|
+
* does too, making that read a false Node positive on three of the four hosts.
|
|
102
|
+
*/
|
|
103
|
+
export function hostRuntime() {
|
|
104
|
+
const g = globalThis;
|
|
105
|
+
if (typeof g.Bun !== 'undefined')
|
|
106
|
+
return 'bun';
|
|
107
|
+
if (typeof g.Deno !== 'undefined')
|
|
108
|
+
return 'deno';
|
|
109
|
+
if (typeof g.imports?.gi !== 'undefined')
|
|
110
|
+
return 'gjs';
|
|
111
|
+
return 'node';
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Which runtime to start on: the host when the template supports it — "whatever
|
|
115
|
+
* you are already running" needs no second toolchain — else the template's own
|
|
116
|
+
* first declaration, which is the canonical one.
|
|
117
|
+
*
|
|
118
|
+
* `manager` OVERRIDES that, because a pinned manager already names a runtime.
|
|
119
|
+
* Without it, `create-app x -t cli -p bun` resolved to the host (`node`), found
|
|
120
|
+
* bun illegal there and refused a combination the user had spelled out. "First
|
|
121
|
+
* declared runtime this manager installs for" needs no per-manager table: bun and
|
|
122
|
+
* deno serve one each, npm/yarn/pnpm only node, and `gjsify` — legal on gjs and
|
|
123
|
+
* node — lands on gjs, the column it was written for.
|
|
124
|
+
*
|
|
125
|
+
* `undefined` for an empty list, or when nothing declared installs with
|
|
126
|
+
* `manager`; the caller decides whether that is an error.
|
|
127
|
+
*/
|
|
128
|
+
export function defaultRuntimeFor(runtimes, manager) {
|
|
129
|
+
if (manager)
|
|
130
|
+
return runtimes.find((rt) => packageManagersForRuntime(rt)?.includes(manager));
|
|
131
|
+
const host = hostRuntime();
|
|
132
|
+
return runtimes.includes(host) ? host : runtimes[0];
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The script that launches the project on `runtime`. Template convention: `start`
|
|
136
|
+
* is the gjs entry (what `gjsify.main` points at), every other runtime a suffixed
|
|
137
|
+
* sibling — `start:node`, `start:bun`, `start:deno`.
|
|
138
|
+
*/
|
|
139
|
+
export function startScriptFor(runtime) {
|
|
140
|
+
return runtime === 'gjs' ? 'start' : `start:${runtime}`;
|
|
141
|
+
}
|
package/lib/select.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type PackageManager } from './runtimes.js';
|
|
2
|
+
/** What the caller should do about a choice. */
|
|
3
|
+
export type Selection<T> =
|
|
4
|
+
/** Settled, and the user already knows why (they said so on the command line). */
|
|
5
|
+
{
|
|
6
|
+
kind: 'value';
|
|
7
|
+
value: T;
|
|
8
|
+
}
|
|
9
|
+
/** Settled without being asked — print `notice` so it is not a silent default. */
|
|
10
|
+
| {
|
|
11
|
+
kind: 'auto';
|
|
12
|
+
value: T;
|
|
13
|
+
notice: string;
|
|
14
|
+
}
|
|
15
|
+
/** Ask, opening on `initial`. */
|
|
16
|
+
| {
|
|
17
|
+
kind: 'prompt';
|
|
18
|
+
choices: readonly T[];
|
|
19
|
+
initial: T;
|
|
20
|
+
}
|
|
21
|
+
/** Refuse, with a message that says what to pass instead. */
|
|
22
|
+
| {
|
|
23
|
+
kind: 'error';
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
export interface RuntimeQuestion {
|
|
27
|
+
flag?: string;
|
|
28
|
+
/** The runtimes the chosen template declares AND this scaffolder can serve. */
|
|
29
|
+
offered: readonly string[];
|
|
30
|
+
/** `--package-manager`, when given — it narrows the runtime. */
|
|
31
|
+
manager?: PackageManager;
|
|
32
|
+
/** Whether stdin is a TTY, i.e. whether a prompt is possible at all. */
|
|
33
|
+
interactive: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Settle which runtime the project is set up for.
|
|
37
|
+
*
|
|
38
|
+
* `--runtime` is deliberately NOT mandatory on a non-TTY the way `--template` is:
|
|
39
|
+
* a template is a choice of WHAT gets written and has no defensible default, while
|
|
40
|
+
* the runtime changes not one scaffolded byte (every template ships a bundle for
|
|
41
|
+
* every runtime it declares) and only names the start script. So it defaults —
|
|
42
|
+
* announced, not assumed.
|
|
43
|
+
*/
|
|
44
|
+
export declare function selectRuntime(question: RuntimeQuestion): Selection<string>;
|
|
45
|
+
export interface PackageManagerQuestion {
|
|
46
|
+
flag?: PackageManager;
|
|
47
|
+
/** The runtime already settled by {@link selectRuntime}. */
|
|
48
|
+
runtime: string;
|
|
49
|
+
/** Whether `--install` is set — i.e. whether this choice actually runs something. */
|
|
50
|
+
install: boolean;
|
|
51
|
+
interactive: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Settle which package manager installs the project.
|
|
55
|
+
*
|
|
56
|
+
* Two rules differ from the runtime above, both because a manager DOES something.
|
|
57
|
+
* A runtime offering exactly one is not a question — bun and deno install for
|
|
58
|
+
* themselves, the gjs column has no Node to run npm with — so it is taken and
|
|
59
|
+
* named rather than prompted. And with `--install` on a non-TTY, the flag is
|
|
60
|
+
* REQUIRED: this is the one default that would reach the user's disk, picking the
|
|
61
|
+
* installer that writes their node_modules and lockfile.
|
|
62
|
+
*/
|
|
63
|
+
export declare function selectPackageManager(question: PackageManagerQuestion): Selection<PackageManager>;
|
package/lib/select.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// The runtime / package-manager decision, as data. These functions decide;
|
|
2
|
+
// `index.ts` is the shell that prints, prompts and exits. Each answer depends on a
|
|
3
|
+
// flag, on what the template declares, on the other question's answer, and on
|
|
4
|
+
// whether there is a TTY — deciding that inline in the yargs handler made it
|
|
5
|
+
// reachable only by spawning the binary, so the half that matters most, the
|
|
6
|
+
// non-TTY one, was covered by nothing but an end-to-end scaffold.
|
|
7
|
+
import { defaultRuntimeFor, packageManagersForRuntime } from './runtimes.js';
|
|
8
|
+
/**
|
|
9
|
+
* Settle which runtime the project is set up for.
|
|
10
|
+
*
|
|
11
|
+
* `--runtime` is deliberately NOT mandatory on a non-TTY the way `--template` is:
|
|
12
|
+
* a template is a choice of WHAT gets written and has no defensible default, while
|
|
13
|
+
* the runtime changes not one scaffolded byte (every template ships a bundle for
|
|
14
|
+
* every runtime it declares) and only names the start script. So it defaults —
|
|
15
|
+
* announced, not assumed.
|
|
16
|
+
*/
|
|
17
|
+
export function selectRuntime(question) {
|
|
18
|
+
const { flag, offered, manager, interactive } = question;
|
|
19
|
+
if (flag !== undefined) {
|
|
20
|
+
if (offered.length > 0 && !offered.includes(flag)) {
|
|
21
|
+
return {
|
|
22
|
+
kind: 'error',
|
|
23
|
+
message: `this template does not run on "${flag}". Declared runtimes: ${offered.join(', ')}.`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return { kind: 'value', value: flag };
|
|
27
|
+
}
|
|
28
|
+
// One declared runtime is not a choice — take it without asking.
|
|
29
|
+
if (offered.length === 1)
|
|
30
|
+
return { kind: 'value', value: offered[0] };
|
|
31
|
+
// None at all means the template declares runtimes this build has no
|
|
32
|
+
// installer mapping for (`supportedRuntimes` drops the ones it cannot serve),
|
|
33
|
+
// i.e. the template is newer than the scaffolder. Refuse rather than fall
|
|
34
|
+
// back to the host: picking a runtime the template never claimed buries the
|
|
35
|
+
// real cause under whatever fails next. `--runtime` above still overrides,
|
|
36
|
+
// which is the escape hatch for a template that declares nothing at all.
|
|
37
|
+
if (offered.length === 0) {
|
|
38
|
+
return { kind: 'error', message: 'this template declares no runtime this scaffolder can install for.' };
|
|
39
|
+
}
|
|
40
|
+
const fallback = defaultRuntimeFor(offered, manager);
|
|
41
|
+
if (fallback === undefined) {
|
|
42
|
+
return {
|
|
43
|
+
kind: 'error',
|
|
44
|
+
message: `no runtime this template declares installs with "${manager}". ` +
|
|
45
|
+
`Declared runtimes: ${offered.join(', ')}.`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// A pinned manager has ALREADY answered this question — bun installs for bun
|
|
49
|
+
// and nothing else — so prompting would pose a question with one honest
|
|
50
|
+
// answer. It is still reported, because the user did not name a runtime.
|
|
51
|
+
if (manager !== undefined) {
|
|
52
|
+
return {
|
|
53
|
+
kind: 'auto',
|
|
54
|
+
value: fallback,
|
|
55
|
+
notice: `Setting up for ${fallback} — the runtime ${manager} installs for.`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (interactive)
|
|
59
|
+
return { kind: 'prompt', choices: offered, initial: fallback };
|
|
60
|
+
return {
|
|
61
|
+
kind: 'auto',
|
|
62
|
+
value: fallback,
|
|
63
|
+
notice: `--runtime not given, using "${fallback}". Runs on: ${offered.join(', ')}.`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Settle which package manager installs the project.
|
|
68
|
+
*
|
|
69
|
+
* Two rules differ from the runtime above, both because a manager DOES something.
|
|
70
|
+
* A runtime offering exactly one is not a question — bun and deno install for
|
|
71
|
+
* themselves, the gjs column has no Node to run npm with — so it is taken and
|
|
72
|
+
* named rather than prompted. And with `--install` on a non-TTY, the flag is
|
|
73
|
+
* REQUIRED: this is the one default that would reach the user's disk, picking the
|
|
74
|
+
* installer that writes their node_modules and lockfile.
|
|
75
|
+
*/
|
|
76
|
+
export function selectPackageManager(question) {
|
|
77
|
+
const { flag, runtime, install, interactive } = question;
|
|
78
|
+
const managers = packageManagersForRuntime(runtime);
|
|
79
|
+
if (managers === undefined || managers.length === 0) {
|
|
80
|
+
return { kind: 'error', message: `no package manager known for runtime "${runtime}".` };
|
|
81
|
+
}
|
|
82
|
+
if (flag !== undefined) {
|
|
83
|
+
if (!managers.includes(flag)) {
|
|
84
|
+
return {
|
|
85
|
+
kind: 'error',
|
|
86
|
+
message: `"${flag}" cannot install a project you intend to run on ${runtime}. ` +
|
|
87
|
+
`Package managers for ${runtime}: ${managers.join(', ')}.`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return { kind: 'value', value: flag };
|
|
91
|
+
}
|
|
92
|
+
if (managers.length === 1) {
|
|
93
|
+
return {
|
|
94
|
+
kind: 'auto',
|
|
95
|
+
value: managers[0],
|
|
96
|
+
notice: `Using ${managers[0]} — the only package manager that installs for ${runtime}.`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (interactive)
|
|
100
|
+
return { kind: 'prompt', choices: managers, initial: managers[0] };
|
|
101
|
+
if (install) {
|
|
102
|
+
return {
|
|
103
|
+
kind: 'error',
|
|
104
|
+
message: '--package-manager is required with --install in non-interactive mode. ' +
|
|
105
|
+
`Package managers for ${runtime}: ${managers.join(', ')}.`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
kind: 'auto',
|
|
110
|
+
value: managers[0],
|
|
111
|
+
notice: `--package-manager not given, using "${managers[0]}" for ${runtime}.`,
|
|
112
|
+
};
|
|
113
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/create-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "Create a new Gjsify project",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/create.js",
|
|
@@ -18,10 +18,18 @@
|
|
|
18
18
|
},
|
|
19
19
|
"bin": "./lib/index.js",
|
|
20
20
|
"scripts": {
|
|
21
|
-
"clear": "gjsify clear lib tsconfig.tsbuildinfo",
|
|
21
|
+
"clear": "gjsify clear lib tsconfig.tsbuildinfo test.gjs.mjs test.node.mjs",
|
|
22
22
|
"check": "gjsify tsc --noEmit",
|
|
23
23
|
"build": "node scripts/process-template.mjs && gjsify tsc && node scripts/set-bin-mode.mjs",
|
|
24
|
-
"test": "
|
|
24
|
+
"build:test": "gjsify run build:test:gjs && gjsify run build:test:node",
|
|
25
|
+
"build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
|
|
26
|
+
"build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
|
|
27
|
+
"test": "gjsify run build:test && gjsify run test:gjs && gjsify run test:node",
|
|
28
|
+
"test:gjs": "gjsify run test.gjs.mjs",
|
|
29
|
+
"test:node": "node test.node.mjs",
|
|
30
|
+
"test:bun": "bun test.node.mjs",
|
|
31
|
+
"test:deno": "deno run -A --no-lock --node-modules-dir=none test.node.mjs",
|
|
32
|
+
"test:cross-runtime": "gjsify run build:test && gjsify run test:gjs && gjsify run test:node && gjsify run test:bun && gjsify run test:deno"
|
|
25
33
|
},
|
|
26
34
|
"keywords": [
|
|
27
35
|
"gjs",
|
|
@@ -38,6 +46,9 @@
|
|
|
38
46
|
"yargs": "^18.0.0"
|
|
39
47
|
},
|
|
40
48
|
"devDependencies": {
|
|
49
|
+
"@gjsify/cli": "^0.42.0",
|
|
50
|
+
"@gjsify/rolldown-plugin-gjsify": "^0.42.0",
|
|
51
|
+
"@gjsify/unit": "^0.42.0",
|
|
41
52
|
"@types/node": "^25.9.2",
|
|
42
53
|
"@types/yargs": "^17.0.35",
|
|
43
54
|
"typescript": "^6.0.3"
|