@gjsify/cli 0.7.5 → 0.9.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/dist/cli.gjs.mjs +91 -82
- package/lib/actions/build.d.ts +31 -0
- package/lib/actions/build.js +135 -3
- package/lib/bundler-pick.js +6 -9
- package/lib/cli-app.js +4 -2
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/login.js +6 -3
- package/lib/commands/run.js +2 -6
- package/lib/commands/storybook.d.ts +12 -0
- package/lib/commands/storybook.js +172 -0
- package/lib/utils/oxc-resolve.js +3 -4
- package/lib/utils/prompt.d.ts +31 -5
- package/lib/utils/prompt.js +140 -64
- package/lib/utils/resolve-plugin-by-name.d.ts +21 -1
- package/lib/utils/resolve-plugin-by-name.js +5 -2
- package/package.json +17 -17
package/lib/actions/build.d.ts
CHANGED
|
@@ -37,9 +37,40 @@ export declare class BuildAction {
|
|
|
37
37
|
* there is no in-bundle plugin, so this hook is the sole injection point.
|
|
38
38
|
*/
|
|
39
39
|
private applyShebang;
|
|
40
|
+
/**
|
|
41
|
+
* GJS plugin loader — bundle the resolved plugin module to a single
|
|
42
|
+
* self-contained ESM file, then import that. Works around GJS's native
|
|
43
|
+
* ESM loader not following `package.json#exports` subpath maps for bare
|
|
44
|
+
* specifiers: Rolldown resolves those subpaths at bundle time, so the
|
|
45
|
+
* emitted file has no unresolvable bare-specifier imports left. Injected
|
|
46
|
+
* as the `loadModule` strategy in `buildApp` only when running under GJS.
|
|
47
|
+
*/
|
|
48
|
+
private loadPluginViaGjsBundle;
|
|
49
|
+
/**
|
|
50
|
+
* Bundle a single plugin entry for `--app gjs` to a cached temp file and
|
|
51
|
+
* return its path. Reuses the full `--app gjs` pipeline (exports-map-aware
|
|
52
|
+
* resolution, `node:`→`@gjsify` aliases, `--globals auto`, single-file
|
|
53
|
+
* output) via a nested `BuildAction`. The nested config carries no
|
|
54
|
+
* `bundler.plugins`, so it never recurses back into plugin resolution.
|
|
55
|
+
*
|
|
56
|
+
* Cached under `node_modules/.cache/gjsify/plugins/`, keyed by the resolved
|
|
57
|
+
* source path. Invalidated when the plugin entry OR the project lockfile is
|
|
58
|
+
* newer than the cached bundle — a dep bump rewrites the lockfile but not
|
|
59
|
+
* the plugin's own entry mtime. The one window neither mtime catches (a
|
|
60
|
+
* transitive dep edited in place) is covered by the `GJSIFY_NO_PLUGIN_CACHE=1`
|
|
61
|
+
* escape hatch. Failures are rethrown with context naming the plugin.
|
|
62
|
+
*/
|
|
63
|
+
private bundlePluginForGjs;
|
|
64
|
+
/**
|
|
65
|
+
* Fresh when the cached bundle is newer than BOTH the plugin entry AND the
|
|
66
|
+
* project's lockfile (the dep-change signal). Missing bundle/source → not
|
|
67
|
+
* fresh (rebuild).
|
|
68
|
+
*/
|
|
69
|
+
private isPluginBundleFresh;
|
|
40
70
|
/** Application mode */
|
|
41
71
|
buildApp(app?: App, opts?: {
|
|
42
72
|
watch?: boolean;
|
|
73
|
+
preserveDefaultExport?: boolean;
|
|
43
74
|
}): Promise<RolldownOutput[]>;
|
|
44
75
|
/**
|
|
45
76
|
* Drive `rolldown.watch(...)`: rebuild on source change, apply the
|
package/lib/actions/build.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { runBundle, runWatch, bundleToChunks } from '../bundler-pick.js';
|
|
2
2
|
import { gjsifyPlugin, textLoaderPlugin, resolveShebangLine, NODE_SHEBANG } from '@gjsify/rolldown-plugin-gjsify';
|
|
3
|
+
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
3
4
|
import { resolveUserPlugins } from '../utils/resolve-plugin-by-name.js';
|
|
4
5
|
import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals } from '@gjsify/rolldown-plugin-gjsify/globals';
|
|
5
6
|
import { pnpPlugin } from '@gjsify/rolldown-plugin-pnp';
|
|
6
|
-
import { dirname, extname } from 'node:path';
|
|
7
|
-
import {
|
|
7
|
+
import { dirname, extname, join } from 'node:path';
|
|
8
|
+
import { pathToFileURL } from 'node:url';
|
|
9
|
+
import { chmod, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
8
10
|
import { normalizeBundlerOptions, mergeBundlerOptions } from '../utils/normalize-bundler-options.js';
|
|
9
11
|
const DEFAULT_GJS_SHEBANG = '#!/usr/bin/env -S gjs -m';
|
|
10
12
|
/**
|
|
@@ -34,6 +36,43 @@ function isUnsafeDefaultOutput(path) {
|
|
|
34
36
|
async function buildPnpPlugin() {
|
|
35
37
|
return pnpPlugin({ issuerUrl: import.meta.url });
|
|
36
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Stable, filesystem-safe short hash of a string (djb2 → 8 hex chars).
|
|
41
|
+
* Used to key a plugin's cached GJS bundle by its resolved source path.
|
|
42
|
+
* Dependency-free on purpose — avoids pulling crypto into this hot path.
|
|
43
|
+
*/
|
|
44
|
+
function shortHash(value) {
|
|
45
|
+
let h = 5381;
|
|
46
|
+
for (let i = 0; i < value.length; i++) {
|
|
47
|
+
h = ((h << 5) + h + value.charCodeAt(i)) >>> 0;
|
|
48
|
+
}
|
|
49
|
+
return h.toString(16).padStart(8, '0');
|
|
50
|
+
}
|
|
51
|
+
function isTruthyEnv(v) {
|
|
52
|
+
return v === '1' || v === 'true' || v === 'yes';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Lockfiles whose change implies a plugin's *transitive* deps may have moved.
|
|
56
|
+
* A dep bump (`npm/yarn/pnpm/gjsify install`) rewrites one of these but leaves
|
|
57
|
+
* the plugin's own entry-file mtime untouched — so the plugin GJS-bundle cache
|
|
58
|
+
* must invalidate on these too, not only on the entry.
|
|
59
|
+
*/
|
|
60
|
+
const PLUGIN_CACHE_DEP_SIGNALS = ['gjsify-lock.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
|
|
61
|
+
/** Newest mtime (ms) among known lockfiles in `cwd`, or 0 when none exist. */
|
|
62
|
+
async function newestLockfileMtime(cwd) {
|
|
63
|
+
let newest = 0;
|
|
64
|
+
for (const name of PLUGIN_CACHE_DEP_SIGNALS) {
|
|
65
|
+
try {
|
|
66
|
+
const s = await stat(join(cwd, name));
|
|
67
|
+
if (s.mtimeMs > newest)
|
|
68
|
+
newest = s.mtimeMs;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// not present — ignore
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return newest;
|
|
75
|
+
}
|
|
37
76
|
export class BuildAction {
|
|
38
77
|
configData;
|
|
39
78
|
constructor(configData = {}) {
|
|
@@ -179,6 +218,90 @@ export class BuildAction {
|
|
|
179
218
|
if (verbose)
|
|
180
219
|
console.debug(`[gjsify] --shebang: wrote ${line} + chmod 0o755 to ${outfile}`);
|
|
181
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* GJS plugin loader — bundle the resolved plugin module to a single
|
|
223
|
+
* self-contained ESM file, then import that. Works around GJS's native
|
|
224
|
+
* ESM loader not following `package.json#exports` subpath maps for bare
|
|
225
|
+
* specifiers: Rolldown resolves those subpaths at bundle time, so the
|
|
226
|
+
* emitted file has no unresolvable bare-specifier imports left. Injected
|
|
227
|
+
* as the `loadModule` strategy in `buildApp` only when running under GJS.
|
|
228
|
+
*/
|
|
229
|
+
async loadPluginViaGjsBundle(resolvedPath, pluginName, verbose) {
|
|
230
|
+
const outfile = await this.bundlePluginForGjs(resolvedPath, pluginName, verbose);
|
|
231
|
+
try {
|
|
232
|
+
return (await import(pathToFileURL(outfile).href));
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
throw new Error(`gjsify config: failed to import the GJS bundle for plugin "${pluginName}" ` +
|
|
236
|
+
`(bundled to ${outfile}). (${err.message})`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Bundle a single plugin entry for `--app gjs` to a cached temp file and
|
|
241
|
+
* return its path. Reuses the full `--app gjs` pipeline (exports-map-aware
|
|
242
|
+
* resolution, `node:`→`@gjsify` aliases, `--globals auto`, single-file
|
|
243
|
+
* output) via a nested `BuildAction`. The nested config carries no
|
|
244
|
+
* `bundler.plugins`, so it never recurses back into plugin resolution.
|
|
245
|
+
*
|
|
246
|
+
* Cached under `node_modules/.cache/gjsify/plugins/`, keyed by the resolved
|
|
247
|
+
* source path. Invalidated when the plugin entry OR the project lockfile is
|
|
248
|
+
* newer than the cached bundle — a dep bump rewrites the lockfile but not
|
|
249
|
+
* the plugin's own entry mtime. The one window neither mtime catches (a
|
|
250
|
+
* transitive dep edited in place) is covered by the `GJSIFY_NO_PLUGIN_CACHE=1`
|
|
251
|
+
* escape hatch. Failures are rethrown with context naming the plugin.
|
|
252
|
+
*/
|
|
253
|
+
async bundlePluginForGjs(resolvedPath, pluginName, verbose) {
|
|
254
|
+
const cwd = process.cwd();
|
|
255
|
+
const cacheDir = join(cwd, 'node_modules', '.cache', 'gjsify', 'plugins');
|
|
256
|
+
const safeName = pluginName.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
257
|
+
const outfile = join(cacheDir, `${safeName}-${shortHash(resolvedPath)}.mjs`);
|
|
258
|
+
const cacheDisabled = isTruthyEnv(process.env.GJSIFY_NO_PLUGIN_CACHE);
|
|
259
|
+
if (!cacheDisabled && (await this.isPluginBundleFresh(outfile, resolvedPath, cwd))) {
|
|
260
|
+
if (verbose)
|
|
261
|
+
console.debug(`[gjsify] plugin "${pluginName}": reusing cached GJS bundle ${outfile}`);
|
|
262
|
+
return outfile;
|
|
263
|
+
}
|
|
264
|
+
if (verbose)
|
|
265
|
+
console.debug(`[gjsify] plugin "${pluginName}": bundling for GJS → ${outfile}`);
|
|
266
|
+
try {
|
|
267
|
+
await mkdir(cacheDir, { recursive: true });
|
|
268
|
+
// `--globals auto` stays on so the plugin's own runtime globals
|
|
269
|
+
// (the MDX/unified toolchain touches `document`, etc.) are injected
|
|
270
|
+
// — but that injection wraps the entry with `export * from <entry>`,
|
|
271
|
+
// which drops the `default` export the plugin factory lives on.
|
|
272
|
+
// `preserveDefaultExport` makes the wrapper re-export `default` too,
|
|
273
|
+
// so the bundled plugin imports correctly as a library.
|
|
274
|
+
// shebang is intentionally left unset — the artifact is imported,
|
|
275
|
+
// not executed, so it must NOT carry a `#!` line.
|
|
276
|
+
const pluginBuild = new BuildAction({
|
|
277
|
+
verbose,
|
|
278
|
+
bundler: { input: resolvedPath, output: { file: outfile } },
|
|
279
|
+
});
|
|
280
|
+
await pluginBuild.buildApp('gjs', { preserveDefaultExport: true });
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
throw new Error(`gjsify config: failed to bundle plugin "${pluginName}" for GJS ` +
|
|
284
|
+
`(needed because GJS can't import packages that use exports-map subpaths directly; ` +
|
|
285
|
+
`cache dir ${cacheDir}). (${err.message})`);
|
|
286
|
+
}
|
|
287
|
+
return outfile;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Fresh when the cached bundle is newer than BOTH the plugin entry AND the
|
|
291
|
+
* project's lockfile (the dep-change signal). Missing bundle/source → not
|
|
292
|
+
* fresh (rebuild).
|
|
293
|
+
*/
|
|
294
|
+
async isPluginBundleFresh(outfile, resolvedPath, cwd) {
|
|
295
|
+
try {
|
|
296
|
+
const outStat = await stat(outfile);
|
|
297
|
+
const srcStat = await stat(resolvedPath);
|
|
298
|
+
const depMtime = await newestLockfileMtime(cwd);
|
|
299
|
+
return outStat.mtimeMs >= Math.max(srcStat.mtimeMs, depMtime);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
182
305
|
/** Application mode */
|
|
183
306
|
async buildApp(app = 'gjs', opts = {}) {
|
|
184
307
|
const { verbose, typescript, exclude, library: pkg, aliases, excludeGlobals } = this.configData;
|
|
@@ -211,6 +334,7 @@ export class BuildAction {
|
|
|
211
334
|
reflection: typescript?.reflection,
|
|
212
335
|
consoleShim,
|
|
213
336
|
...(aliases ? { aliases } : {}),
|
|
337
|
+
...(opts.preserveDefaultExport ? { preserveDefaultExport: true } : {}),
|
|
214
338
|
};
|
|
215
339
|
const { autoMode, extras } = this.parseGlobalsValue(globals);
|
|
216
340
|
const pnp = await buildPnpPlugin();
|
|
@@ -230,7 +354,15 @@ export class BuildAction {
|
|
|
230
354
|
// auto-globals pre-build to avoid claiming the same files via
|
|
231
355
|
// Rolldown's default classifier.
|
|
232
356
|
if (userBundler.plugins?.length) {
|
|
233
|
-
|
|
357
|
+
// Under GJS, plugin modules can't always be imported directly:
|
|
358
|
+
// GJS's native ESM loader doesn't follow `package.json#exports`
|
|
359
|
+
// subpath maps, so a plugin whose source imports an internal
|
|
360
|
+
// subpath (e.g. `@mdx-js/mdx/internal-…`) throws `Module not
|
|
361
|
+
// found` even when the file is on disk. The injected loader
|
|
362
|
+
// first bundles such a plugin to a single self-contained ESM
|
|
363
|
+
// (Rolldown resolves the exports map at bundle time) and imports
|
|
364
|
+
// that instead. On Node the default direct-import path is used.
|
|
365
|
+
const resolved = await resolveUserPlugins(userBundler.plugins, process.cwd(), isGjs() ? { loadModule: (p, name) => this.loadPluginViaGjsBundle(p, name, verbose) } : {});
|
|
234
366
|
userPlugins.push(...resolved);
|
|
235
367
|
}
|
|
236
368
|
// --- Auto mode (with optional extras): iterative multi-pass build ---
|
package/lib/bundler-pick.js
CHANGED
|
@@ -32,6 +32,7 @@ import * as path from 'node:path';
|
|
|
32
32
|
import { createRequire } from 'node:module';
|
|
33
33
|
import { pathToFileURL } from 'node:url';
|
|
34
34
|
import { resolveNpmPackage } from './utils/resolve-npm-package.js';
|
|
35
|
+
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
35
36
|
// npm `rolldown` is a Rust crate with platform-specific prebuilds; loading
|
|
36
37
|
// it eagerly at module init pulls musl-detection code that does
|
|
37
38
|
// `require('node:fs')` synchronously — fine on Node, but fatal under GJS
|
|
@@ -46,8 +47,7 @@ async function loadNpmRolldown() {
|
|
|
46
47
|
// inside the crate's wrapper (e.g. on a truly-cold workspace where
|
|
47
48
|
// `@gjsify/rolldown-native`'s lib/ is not built yet, issue #497).
|
|
48
49
|
// Fail with the actionable diagnosis instead.
|
|
49
|
-
|
|
50
|
-
if (isGjs) {
|
|
50
|
+
if (isGjs()) {
|
|
51
51
|
throw new Error('gjsify build: no usable bundler engine under GJS — `@gjsify/rolldown-native` is not loadable ' +
|
|
52
52
|
'(its JS facade lib/ is not built, or its prebuild typelib is missing from GI_TYPELIB_PATH), ' +
|
|
53
53
|
'and the npm `rolldown` engine is a Rust napi crate that cannot run under GJS. ' +
|
|
@@ -83,8 +83,7 @@ async function loadNpmRolldown() {
|
|
|
83
83
|
* rolldown` even though the package is present under the install dir.
|
|
84
84
|
*/
|
|
85
85
|
function resolveImportTargetForGjs(specifier) {
|
|
86
|
-
|
|
87
|
-
if (!isGjs)
|
|
86
|
+
if (!isGjs())
|
|
88
87
|
return specifier;
|
|
89
88
|
const resolved = resolveNpmPackage(specifier, { bundleUrl: import.meta.url });
|
|
90
89
|
if (resolved)
|
|
@@ -193,8 +192,7 @@ export async function shouldUseNative() {
|
|
|
193
192
|
// GJS: try @gjsify/rolldown-native — it's the only engine
|
|
194
193
|
// that can actually run here (npm rolldown is a Rust
|
|
195
194
|
// crate). Fall back to npm only on Node.
|
|
196
|
-
|
|
197
|
-
if (!isGjs)
|
|
195
|
+
if (!isGjs())
|
|
198
196
|
return false;
|
|
199
197
|
const native = await tryLoadNative();
|
|
200
198
|
return native !== null;
|
|
@@ -236,8 +234,7 @@ async function tryLoadNative() {
|
|
|
236
234
|
if (_nativeProbe)
|
|
237
235
|
return _nativeProbe;
|
|
238
236
|
_nativeProbe = (async () => {
|
|
239
|
-
|
|
240
|
-
if (!isGjs)
|
|
237
|
+
if (!isGjs())
|
|
241
238
|
return null;
|
|
242
239
|
try {
|
|
243
240
|
// Under GJS the ESM loader has no node_modules resolver — a bare
|
|
@@ -256,7 +253,7 @@ async function tryLoadNative() {
|
|
|
256
253
|
// to npm rolldown (which then throws ImportError for `rolldown`).
|
|
257
254
|
const specifier = '@gjsify/rolldown-native';
|
|
258
255
|
let target = specifier;
|
|
259
|
-
if (isGjs) {
|
|
256
|
+
if (isGjs()) {
|
|
260
257
|
// Same multi-anchor resolution as `loadNpmRolldown` —
|
|
261
258
|
// when the bundle is invoked from a cwd outside the
|
|
262
259
|
// install dir, anchoring solely at `import.meta.url`
|
package/lib/cli-app.js
CHANGED
|
@@ -19,8 +19,9 @@ import { readFileSync } from 'node:fs';
|
|
|
19
19
|
import { dirname, join } from 'node:path';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
import yargs from 'yargs';
|
|
22
|
-
import { buildCommand as build, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, } from './commands/index.js';
|
|
22
|
+
import { buildCommand as build, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, } from './commands/index.js';
|
|
23
23
|
import { APP_NAME } from './constants.js';
|
|
24
|
+
import { isNode } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
24
25
|
// Detect which runtime is executing the CLI (GJS or Node.js).
|
|
25
26
|
// GJS MUST be checked first because @gjsify/process sets
|
|
26
27
|
// `process.versions.node = '20.0.0'` for compatibility — a plain
|
|
@@ -36,7 +37,7 @@ function runtimeLabel() {
|
|
|
36
37
|
catch {
|
|
37
38
|
/* not GJS */
|
|
38
39
|
}
|
|
39
|
-
if (
|
|
40
|
+
if (isNode()) {
|
|
40
41
|
return `Node.js ${process.version}`;
|
|
41
42
|
}
|
|
42
43
|
return 'unknown runtime';
|
|
@@ -106,6 +107,7 @@ export async function runCli(argv) {
|
|
|
106
107
|
.command(barrels.command, barrels.description, barrels.builder, barrels.handler)
|
|
107
108
|
.command(tsc.command, tsc.description, tsc.builder, tsc.handler)
|
|
108
109
|
.command(affected.command, affected.description, affected.builder, affected.handler)
|
|
110
|
+
.command(storybook.command, storybook.description, storybook.builder, storybook.handler)
|
|
109
111
|
.demandCommand(1)
|
|
110
112
|
.epilogue(`Running on ${runtimeLabel()}`)
|
|
111
113
|
.help()
|
package/lib/commands/index.d.ts
CHANGED
package/lib/commands/index.js
CHANGED
package/lib/commands/login.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { DEFAULT_REGISTRY, registryFor, whoami } from '@gjsify/npm-registry';
|
|
22
22
|
import { loadNpmrc } from '../utils/load-npmrc.js';
|
|
23
23
|
import { writeAuthToken } from '../utils/auth-npmrc.js';
|
|
24
|
-
import {
|
|
24
|
+
import { promptCredentials, promptLine } from '../utils/prompt.js';
|
|
25
25
|
export const loginCommand = {
|
|
26
26
|
command: 'login',
|
|
27
27
|
description: "Log in to an npm registry (Node-free `npm login`). Prompts for username + password (hidden) and writes the token to ~/.npmrc. Use --otp for 2FA. Web-OAuth flow is not supported — this is npm's legacy credentials flow.",
|
|
@@ -44,12 +44,15 @@ export const loginCommand = {
|
|
|
44
44
|
(args.scope ? registryFor(args.scope.startsWith('@') ? `${args.scope}/_` : `@${args.scope}/_`, npmrc) : undefined) ??
|
|
45
45
|
DEFAULT_REGISTRY;
|
|
46
46
|
const registryClean = registry.endsWith('/') ? registry : `${registry}/`;
|
|
47
|
-
|
|
47
|
+
// Read username (visible) + password (masked) in a single raw-mode TTY
|
|
48
|
+
// session — robust against cooked line-discipline state + the shared
|
|
49
|
+
// stdin resume/pause race that intermittently broke the old two-prompt
|
|
50
|
+
// sequence (Enter showing `^M`, or an empty username).
|
|
51
|
+
const { username, password } = await promptCredentials(args.username);
|
|
48
52
|
if (!username) {
|
|
49
53
|
console.error('gjsify login: a username is required.');
|
|
50
54
|
process.exit(1);
|
|
51
55
|
}
|
|
52
|
-
const password = await promptHidden(`Password: `);
|
|
53
56
|
if (!password) {
|
|
54
57
|
console.error('gjsify login: a password is required.');
|
|
55
58
|
process.exit(1);
|
package/lib/commands/run.js
CHANGED
|
@@ -19,6 +19,7 @@ import { spawn } from 'node:child_process';
|
|
|
19
19
|
import { runGjsBundle } from '../utils/run-gjs.js';
|
|
20
20
|
import { readPackageJson } from '../utils/pkg-json-edit.js';
|
|
21
21
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
22
|
+
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
22
23
|
export const runCommand = {
|
|
23
24
|
command: 'run <target> [args..]',
|
|
24
25
|
description: 'Run a script from package.json (yarn-run-style) or a GJS bundle file. If <target> resolves to a file on disk (or has a path-like prefix), it is launched via gjs with LD_LIBRARY_PATH + GI_TYPELIB_PATH set for native packages. Otherwise it is looked up in the current package.json `scripts`.',
|
|
@@ -151,7 +152,7 @@ async function runScript(script, extraArgs) {
|
|
|
151
152
|
// Anything fancier (compound `&&`, pipes, a non-gjsify command) falls
|
|
152
153
|
// through to the shell spawn below. Node keeps spawning (cheap there); this
|
|
153
154
|
// only triggers under GJS, where the nesting actually hurts. See cli-app.ts.
|
|
154
|
-
const inProcArgv =
|
|
155
|
+
const inProcArgv = isGjs() ? gjsifyInProcessArgv(literal, extraArgs) : null;
|
|
155
156
|
if (inProcArgv) {
|
|
156
157
|
// The subcommand runs in our process, so surface the script env on
|
|
157
158
|
// `process.env` (PATH with the workspace .bin dirs, npm_* lifecycle
|
|
@@ -203,11 +204,6 @@ function shellEscape(arg) {
|
|
|
203
204
|
return arg;
|
|
204
205
|
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
205
206
|
}
|
|
206
|
-
// Detected at CALL time (a module-load const reads `undefined` in the bundled
|
|
207
|
-
// `--app gjs` form). `bundler-pick.ts` uses the same expression.
|
|
208
|
-
function runningUnderGjs() {
|
|
209
|
-
return typeof globalThis.imports?.gi !== 'undefined';
|
|
210
|
-
}
|
|
211
207
|
/**
|
|
212
208
|
* If `literal` is a SINGLE `gjsify <subcommand> …` command with no shell
|
|
213
209
|
* operators / substitutions / unquoted globs, return the argv to feed `runCli`
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Command } from '../types/index.js';
|
|
2
|
+
interface StorybookCliOptions {
|
|
3
|
+
stories?: string;
|
|
4
|
+
appId?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
globals?: string;
|
|
7
|
+
out?: string;
|
|
8
|
+
watch: boolean;
|
|
9
|
+
buildOnly: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare const storybookCommand: Command<unknown, StorybookCliOptions>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// `gjsify storybook` — discover every *.story.ts in a project, generate a tiny
|
|
2
|
+
// entry that registers them, then build (`--app gjs`) and launch the generic
|
|
3
|
+
// @gjsify/storybook component browser. No per-project storybook app needed.
|
|
4
|
+
//
|
|
5
|
+
// Dispatch is IN-PROCESS (runCli + runGjsBundle), not a re-spawn of the CLI:
|
|
6
|
+
// the showcase-style `spawn(process.execPath, [cliBin, …])` pattern assumes
|
|
7
|
+
// Node, but under the committed GJS bundle `process.execPath` is `gjs` and the
|
|
8
|
+
// `../index.js` path is not a runnable entry. In-process dispatch works
|
|
9
|
+
// identically under Node and GJS. @gjsify/storybook is resolved from the
|
|
10
|
+
// CONSUMER's node_modules by the generated entry, so it stays out of the CLI's
|
|
11
|
+
// own dependencies.
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { join, relative, resolve } from 'node:path';
|
|
15
|
+
function readPackage(cwd) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Derive a stable GApplication id from the package name when none is configured. */
|
|
24
|
+
function deriveAppId(name) {
|
|
25
|
+
if (!name)
|
|
26
|
+
return 'org.gjsify.Storybook';
|
|
27
|
+
const slug = name
|
|
28
|
+
.replace(/^@/, '')
|
|
29
|
+
.replace(/[^a-zA-Z0-9]+/g, '.')
|
|
30
|
+
.replace(/^\.+|\.+$/g, '');
|
|
31
|
+
return `org.gjsify.storybook.${slug}`;
|
|
32
|
+
}
|
|
33
|
+
/** Recursively collect `*.story.{ts,js,mts,mjs}` files under a directory. */
|
|
34
|
+
function findStoryFiles(dir) {
|
|
35
|
+
const out = [];
|
|
36
|
+
if (!existsSync(dir))
|
|
37
|
+
return out;
|
|
38
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
39
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.'))
|
|
40
|
+
continue;
|
|
41
|
+
const full = join(dir, entry.name);
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
out.push(...findStoryFiles(full));
|
|
44
|
+
}
|
|
45
|
+
else if (/\.story\.(ts|js|mts|mjs)$/.test(entry.name)) {
|
|
46
|
+
out.push(full);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
function generateEntry(files, applicationId, title) {
|
|
52
|
+
const imports = files.map((file, i) => `import * as m${i} from ${JSON.stringify(file)};`).join('\n');
|
|
53
|
+
const namespaces = files.map((_, i) => `m${i}`).join(', ');
|
|
54
|
+
const titleLine = title ? `\n title: ${JSON.stringify(title)},` : '';
|
|
55
|
+
return `// Auto-generated by \`gjsify storybook\` — do not edit by hand.
|
|
56
|
+
import { collectStoryModules, runStorybook } from '@gjsify/storybook';
|
|
57
|
+
${imports}
|
|
58
|
+
|
|
59
|
+
await runStorybook({
|
|
60
|
+
applicationId: ${JSON.stringify(applicationId)},${titleLine}
|
|
61
|
+
stories: collectStoryModules([${namespaces}]),
|
|
62
|
+
});
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
export const storybookCommand = {
|
|
66
|
+
command: 'storybook',
|
|
67
|
+
description: 'Discover *.story.ts in this project and launch the GTK storybook browser.',
|
|
68
|
+
builder: (yargs) => yargs
|
|
69
|
+
.option('stories', {
|
|
70
|
+
description: 'Directory scanned recursively for *.story.ts (default: src, or gjsify.storybook.stories)',
|
|
71
|
+
type: 'string',
|
|
72
|
+
})
|
|
73
|
+
.option('app-id', {
|
|
74
|
+
description: 'GApplication id (default: gjsify.storybook.applicationId or derived from the package name)',
|
|
75
|
+
type: 'string',
|
|
76
|
+
})
|
|
77
|
+
.option('title', { description: 'Window title', type: 'string' })
|
|
78
|
+
.option('globals', {
|
|
79
|
+
description: 'Value for `gjsify build --globals` (default: auto; use auto,dom for canvas/DOM stories)',
|
|
80
|
+
type: 'string',
|
|
81
|
+
})
|
|
82
|
+
.option('out', {
|
|
83
|
+
description: 'Output bundle path (default: node_modules/.cache/gjsify-storybook)',
|
|
84
|
+
type: 'string',
|
|
85
|
+
})
|
|
86
|
+
.option('watch', {
|
|
87
|
+
description: 'Rebuild and relaunch when a story file changes',
|
|
88
|
+
type: 'boolean',
|
|
89
|
+
default: false,
|
|
90
|
+
})
|
|
91
|
+
.option('build-only', { description: 'Build the bundle but do not launch it', type: 'boolean', default: false }),
|
|
92
|
+
handler: async (args) => {
|
|
93
|
+
const cwd = process.cwd();
|
|
94
|
+
const pkg = readPackage(cwd);
|
|
95
|
+
const config = pkg.gjsify?.storybook ?? {};
|
|
96
|
+
const storiesDir = resolve(cwd, args.stories ?? config.stories ?? 'src');
|
|
97
|
+
const applicationId = args.appId ?? config.applicationId ?? deriveAppId(pkg.name);
|
|
98
|
+
const title = args.title ?? config.title;
|
|
99
|
+
const globals = args.globals ?? config.globals ?? 'auto';
|
|
100
|
+
const cacheDir = join(cwd, 'node_modules', '.cache', 'gjsify-storybook');
|
|
101
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
102
|
+
const entryPath = join(cacheDir, 'entry.ts');
|
|
103
|
+
const outPath = args.out ? resolve(cwd, args.out) : join(cacheDir, 'storybook.gjs.mjs');
|
|
104
|
+
const writeEntry = () => {
|
|
105
|
+
const files = findStoryFiles(storiesDir).sort();
|
|
106
|
+
writeFileSync(entryPath, generateEntry(files, applicationId, title));
|
|
107
|
+
return files.length;
|
|
108
|
+
};
|
|
109
|
+
const initialCount = writeEntry();
|
|
110
|
+
if (initialCount === 0) {
|
|
111
|
+
console.error(`No *.story.ts files found under ${relative(cwd, storiesDir) || storiesDir}`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
console.log(`Found ${initialCount} story file(s) under ${relative(cwd, storiesDir) || '.'}`);
|
|
115
|
+
// In-process build — runtime-agnostic (Node uses npm rolldown, GJS uses
|
|
116
|
+
// the @gjsify/rolldown-native facade), no re-spawn of the CLI binary.
|
|
117
|
+
const { runCli } = await import('../cli-app.js');
|
|
118
|
+
const buildBundle = () => runCli(['build', entryPath, '--app', 'gjs', '--globals', globals, '--outfile', outPath]);
|
|
119
|
+
if (!args.watch) {
|
|
120
|
+
await buildBundle();
|
|
121
|
+
if (args.buildOnly)
|
|
122
|
+
return;
|
|
123
|
+
const { runGjsBundle } = await import('../utils/run-gjs.js');
|
|
124
|
+
await runGjsBundle(outPath, [], { exitOnSuccess: true });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// Watch mode: rebuild + relaunch on any change under the stories dir.
|
|
128
|
+
// The storybook bundle only needs system Gtk/Adw typelibs at runtime
|
|
129
|
+
// (no @gjsify native bridges), so a plain `gjs -m` child is enough and
|
|
130
|
+
// stays killable for relaunch.
|
|
131
|
+
const { watch } = await import('node:fs');
|
|
132
|
+
let child = null;
|
|
133
|
+
let rebuilding = false;
|
|
134
|
+
let pending = false;
|
|
135
|
+
const rebuild = async () => {
|
|
136
|
+
if (rebuilding) {
|
|
137
|
+
pending = true;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
rebuilding = true;
|
|
141
|
+
try {
|
|
142
|
+
writeEntry();
|
|
143
|
+
if (child) {
|
|
144
|
+
child.kill();
|
|
145
|
+
child = null;
|
|
146
|
+
}
|
|
147
|
+
await buildBundle();
|
|
148
|
+
if (!args.buildOnly) {
|
|
149
|
+
child = spawn('gjs', ['-m', outPath], { stdio: 'inherit' });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
console.error(`[storybook] rebuild failed: ${err.message}`);
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
rebuilding = false;
|
|
157
|
+
if (pending) {
|
|
158
|
+
pending = false;
|
|
159
|
+
void rebuild();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
await rebuild();
|
|
164
|
+
let debounce = null;
|
|
165
|
+
watch(storiesDir, { recursive: true }, () => {
|
|
166
|
+
if (debounce)
|
|
167
|
+
clearTimeout(debounce);
|
|
168
|
+
debounce = setTimeout(() => void rebuild(), 200);
|
|
169
|
+
});
|
|
170
|
+
console.log(`[storybook] watching ${relative(cwd, storiesDir) || '.'} — press Ctrl+C to stop`);
|
|
171
|
+
},
|
|
172
|
+
};
|
package/lib/utils/oxc-resolve.js
CHANGED
|
@@ -53,6 +53,7 @@ import { createRequire } from 'node:module';
|
|
|
53
53
|
import { pathToFileURL } from 'node:url';
|
|
54
54
|
import { findWorkspaceRoot } from './workspace-root.js';
|
|
55
55
|
import { resolveNpmPackage } from './resolve-npm-package.js';
|
|
56
|
+
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
56
57
|
/**
|
|
57
58
|
* Map Node.js `process.platform` + `arch` (+ musl on Linux) to the
|
|
58
59
|
* napi-rs binding-package suffix oxc uses, e.g. `linux-x64-gnu`,
|
|
@@ -210,8 +211,7 @@ async function tryLoadNativeOxfmt() {
|
|
|
210
211
|
if (_nativeOxfmtProbe)
|
|
211
212
|
return _nativeOxfmtProbe;
|
|
212
213
|
_nativeOxfmtProbe = (async () => {
|
|
213
|
-
|
|
214
|
-
if (!isGjs)
|
|
214
|
+
if (!isGjs())
|
|
215
215
|
return null;
|
|
216
216
|
try {
|
|
217
217
|
const specifier = '@gjsify/oxfmt-native';
|
|
@@ -249,8 +249,7 @@ export async function shouldUseNativeOxfmt() {
|
|
|
249
249
|
}
|
|
250
250
|
return true;
|
|
251
251
|
}
|
|
252
|
-
|
|
253
|
-
if (!isGjs)
|
|
252
|
+
if (!isGjs())
|
|
254
253
|
return false;
|
|
255
254
|
return (await tryLoadNativeOxfmt()) !== null;
|
|
256
255
|
}
|
package/lib/utils/prompt.d.ts
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export
|
|
1
|
+
/** Credentials read from the terminal. */
|
|
2
|
+
export interface PromptedCredentials {
|
|
3
|
+
username: string;
|
|
4
|
+
password: string;
|
|
5
|
+
}
|
|
6
|
+
/** Result of feeding one keystroke to the raw-mode line editor. */
|
|
7
|
+
export interface KeyOutcome {
|
|
8
|
+
/** The line buffer after this key. */
|
|
9
|
+
buf: string;
|
|
10
|
+
/** Text to echo to the terminal for this key (`''` = nothing). */
|
|
11
|
+
echo: string;
|
|
12
|
+
/** The line is complete (Enter). */
|
|
13
|
+
done: boolean;
|
|
14
|
+
/** Ctrl-C was pressed (caller should restore + exit). */
|
|
15
|
+
interrupt: boolean;
|
|
16
|
+
}
|
|
3
17
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
18
|
+
* Pure key handler for the raw-mode line editor — the single source of truth
|
|
19
|
+
* for how a keystroke updates the buffer + echo. Kept pure (no I/O) so it is
|
|
20
|
+
* unit-testable: this is what makes Enter (`\r` OR `\n`) reliably submit, masks
|
|
21
|
+
* passwords, and treats Ctrl-C as an interrupt rather than text.
|
|
7
22
|
*/
|
|
23
|
+
export declare function applyKey(buf: string, ch: string, mask: boolean): KeyOutcome;
|
|
24
|
+
/** Print a question and read one visible line. */
|
|
25
|
+
export declare function promptLine(question: string): Promise<string>;
|
|
26
|
+
/** Print a question and read one line WITHOUT echoing it (passwords). */
|
|
8
27
|
export declare function promptHidden(question: string): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Read npm credentials. On a TTY both the visible username and the masked
|
|
30
|
+
* password are read in ONE raw session (no cooked-line dependency, no
|
|
31
|
+
* inter-prompt resume/pause race). `providedUsername` (from `--username`) skips
|
|
32
|
+
* the username prompt.
|
|
33
|
+
*/
|
|
34
|
+
export declare function promptCredentials(providedUsername?: string): Promise<PromptedCredentials>;
|