@gjsify/cli 0.3.8 → 0.3.9

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.
@@ -6,6 +6,20 @@ import { getPnpPlugin } from "@gjsify/resolve-npm/pnp-relay";
6
6
  import { dirname, extname } from "node:path";
7
7
  import { chmod, readFile, writeFile } from "node:fs/promises";
8
8
  const GJS_SHEBANG = "#!/usr/bin/env -S gjs -m\n";
9
+ /**
10
+ * `true` when `path` points at a location that's unsafe to use as a build
11
+ * outfile (would overwrite source). Currently catches:
12
+ * - any TypeScript extension (`.ts`, `.tsx`, `.mts`, `.cts`, `.mtsx`, `.ctsx`)
13
+ * - paths that live under a `src/` segment (relative or absolute)
14
+ */
15
+ function isUnsafeDefaultOutput(path) {
16
+ if (/\.[cm]?tsx?$/i.test(path))
17
+ return true;
18
+ const norm = path.replace(/\\/g, "/");
19
+ if (/(?:^|\/)src\//.test(norm))
20
+ return true;
21
+ return false;
22
+ }
9
23
  /**
10
24
  * Resolve the gjsify-flavoured PnP plugin. Anchors the relay on this file's
11
25
  * URL so transitive `@gjsify/*` polyfills (reached via @gjsify/cli's deps on
@@ -190,10 +204,20 @@ export class BuildAction {
190
204
  !esbuild?.outfile &&
191
205
  !esbuild?.outdir &&
192
206
  (pgk?.main || pgk?.module)) {
193
- esbuild.outfile =
194
- esbuild?.format === "cjs"
195
- ? pgk.main || pgk.module
196
- : pgk.module || pgk.main;
207
+ const candidate = esbuild?.format === "cjs"
208
+ ? pgk.main || pgk.module
209
+ : pgk.module || pgk.main;
210
+ if (candidate && isUnsafeDefaultOutput(candidate)) {
211
+ // `package.json#main`/`module` commonly points at a TypeScript
212
+ // source (e.g. `src/index.ts` for TS-direct workflows). Falling
213
+ // back to that value would have esbuild OVERWRITE the source.
214
+ // Surface a clear error and require an explicit outfile/outdir
215
+ // instead of silently destroying the user's code.
216
+ throw new Error(`gjsify build: refusing to default --outfile to ${candidate} ` +
217
+ `(would overwrite a TypeScript source file). Pass --outfile/--outdir ` +
218
+ `explicitly, or set "gjsify.esbuild.outfile" in package.json.`);
219
+ }
220
+ esbuild.outfile = candidate;
197
221
  }
198
222
  const { consoleShim, globals } = this.configData;
199
223
  const pluginOpts = {
package/lib/config.js CHANGED
@@ -1,5 +1,23 @@
1
1
  import { APP_NAME } from './constants.js';
2
2
  import { cosmiconfig } from 'cosmiconfig';
3
+ /** Default cosmiconfig search places for a given module name (matches cosmiconfig defaults). */
4
+ function defaultSearchPlaces(name) {
5
+ return [
6
+ 'package.json',
7
+ `.${name}rc`,
8
+ `.${name}rc.json`,
9
+ `.${name}rc.yaml`,
10
+ `.${name}rc.yml`,
11
+ `.${name}rc.js`,
12
+ `.${name}rc.ts`,
13
+ `.${name}rc.mjs`,
14
+ `.${name}rc.cjs`,
15
+ `${name}.config.js`,
16
+ `${name}.config.ts`,
17
+ `${name}.config.mjs`,
18
+ `${name}.config.cjs`,
19
+ ];
20
+ }
3
21
  import { readPackageJSON, resolvePackageJSON } from 'pkg-types';
4
22
  import { getTsconfig } from 'get-tsconfig';
5
23
  /** Deep merge objects (replaces lodash.merge) */
@@ -34,17 +52,46 @@ export class Config {
34
52
  }
35
53
  /** Loads gjsify config file, e.g `.gjsifyrc.js` */
36
54
  async load(searchFrom) {
37
- let configFile = await cosmiconfig(APP_NAME, this.loadOptions).search(searchFrom);
38
- configFile ||= {
39
- config: {},
40
- filepath: '',
41
- isEmpty: true,
55
+ // cosmiconfig's default first-match-wins behaviour silently drops one
56
+ // source when both `package.json#gjsify` and an explicit config file
57
+ // (`.gjsifyrc.js`, `gjsify.config.mjs`, ...) are present. Project hits
58
+ // this footgun: adding `gjsify.bin` to package.json (so `gjsify dlx`
59
+ // resolves the GJS bundle) silently disables `.gjsifyrc.js`. We
60
+ // explicitly load both sources and merge — package.json is the lower
61
+ // layer, the explicit file wins on key collisions.
62
+ //
63
+ // Run two searches:
64
+ // 1. Default (includes package.json) — for projects that only use
65
+ // package.json#gjsify and no separate file.
66
+ // 2. Explicit-file only (package.json excluded) — to find the
67
+ // `.gjsifyrc.*` / `gjsify.config.*` regardless of whether
68
+ // package.json#gjsify exists.
69
+ const fileExplorer = cosmiconfig(APP_NAME, {
70
+ ...this.loadOptions,
71
+ searchPlaces: (this.loadOptions.searchPlaces ?? defaultSearchPlaces(APP_NAME))
72
+ .filter((p) => p !== 'package.json'),
73
+ });
74
+ const fileResult = await fileExplorer.search(searchFrom);
75
+ const merged = {};
76
+ try {
77
+ const pkg = await this.readPackageJSON(searchFrom);
78
+ if (isPlainObject(pkg?.gjsify))
79
+ merge(merged, pkg.gjsify);
80
+ }
81
+ catch {
82
+ // Missing or unreadable package.json — skip.
83
+ }
84
+ if (fileResult?.config && isPlainObject(fileResult.config)) {
85
+ merge(merged, fileResult.config);
86
+ }
87
+ merged.esbuild ||= {};
88
+ merged.library ||= {};
89
+ merged.typescript ||= {};
90
+ return {
91
+ config: merged,
92
+ filepath: fileResult?.filepath ?? '',
93
+ isEmpty: !fileResult && Object.keys(merged).length === 3, // only the three default-empty objects
42
94
  };
43
- configFile.config ||= {};
44
- configFile.config.esbuild ||= {};
45
- configFile.config.library ||= {};
46
- configFile.config.typescript ||= {};
47
- return configFile;
48
95
  }
49
96
  /** Loads package.json of the current project */
50
97
  async readPackageJSON(dirPath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -23,14 +23,14 @@
23
23
  "cli"
24
24
  ],
25
25
  "dependencies": {
26
- "@gjsify/create-app": "^0.3.8",
27
- "@gjsify/esbuild-plugin-gjsify": "^0.3.8",
28
- "@gjsify/node-polyfills": "^0.3.8",
29
- "@gjsify/npm-registry": "^0.3.8",
30
- "@gjsify/resolve-npm": "^0.3.8",
31
- "@gjsify/semver": "^0.3.8",
32
- "@gjsify/tar": "^0.3.8",
33
- "@gjsify/web-polyfills": "^0.3.8",
26
+ "@gjsify/create-app": "^0.3.9",
27
+ "@gjsify/esbuild-plugin-gjsify": "^0.3.9",
28
+ "@gjsify/node-polyfills": "^0.3.9",
29
+ "@gjsify/npm-registry": "^0.3.9",
30
+ "@gjsify/resolve-npm": "^0.3.9",
31
+ "@gjsify/semver": "^0.3.9",
32
+ "@gjsify/tar": "^0.3.9",
33
+ "@gjsify/web-polyfills": "^0.3.9",
34
34
  "@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.15",
35
35
  "cosmiconfig": "^9.0.1",
36
36
  "esbuild": "^0.28.0",
@@ -14,6 +14,19 @@ import { chmod, readFile, writeFile } from "node:fs/promises";
14
14
 
15
15
  const GJS_SHEBANG = "#!/usr/bin/env -S gjs -m\n";
16
16
 
17
+ /**
18
+ * `true` when `path` points at a location that's unsafe to use as a build
19
+ * outfile (would overwrite source). Currently catches:
20
+ * - any TypeScript extension (`.ts`, `.tsx`, `.mts`, `.cts`, `.mtsx`, `.ctsx`)
21
+ * - paths that live under a `src/` segment (relative or absolute)
22
+ */
23
+ function isUnsafeDefaultOutput(path: string): boolean {
24
+ if (/\.[cm]?tsx?$/i.test(path)) return true;
25
+ const norm = path.replace(/\\/g, "/");
26
+ if (/(?:^|\/)src\//.test(norm)) return true;
27
+ return false;
28
+ }
29
+
17
30
  /**
18
31
  * Resolve the gjsify-flavoured PnP plugin. Anchors the relay on this file's
19
32
  * URL so transitive `@gjsify/*` polyfills (reached via @gjsify/cli's deps on
@@ -253,10 +266,23 @@ export class BuildAction {
253
266
  !esbuild?.outdir &&
254
267
  (pgk?.main || pgk?.module)
255
268
  ) {
256
- esbuild.outfile =
269
+ const candidate =
257
270
  esbuild?.format === "cjs"
258
271
  ? pgk.main || pgk.module
259
272
  : pgk.module || pgk.main;
273
+ if (candidate && isUnsafeDefaultOutput(candidate)) {
274
+ // `package.json#main`/`module` commonly points at a TypeScript
275
+ // source (e.g. `src/index.ts` for TS-direct workflows). Falling
276
+ // back to that value would have esbuild OVERWRITE the source.
277
+ // Surface a clear error and require an explicit outfile/outdir
278
+ // instead of silently destroying the user's code.
279
+ throw new Error(
280
+ `gjsify build: refusing to default --outfile to ${candidate} ` +
281
+ `(would overwrite a TypeScript source file). Pass --outfile/--outdir ` +
282
+ `explicitly, or set "gjsify.esbuild.outfile" in package.json.`,
283
+ );
284
+ }
285
+ esbuild.outfile = candidate;
260
286
  }
261
287
 
262
288
  const { consoleShim, globals } = this.configData;
package/src/config.ts CHANGED
@@ -1,5 +1,24 @@
1
1
  import { APP_NAME } from './constants.js';
2
2
  import { cosmiconfig, type Options as LoadOptions } from 'cosmiconfig';
3
+
4
+ /** Default cosmiconfig search places for a given module name (matches cosmiconfig defaults). */
5
+ function defaultSearchPlaces(name: string): string[] {
6
+ return [
7
+ 'package.json',
8
+ `.${name}rc`,
9
+ `.${name}rc.json`,
10
+ `.${name}rc.yaml`,
11
+ `.${name}rc.yml`,
12
+ `.${name}rc.js`,
13
+ `.${name}rc.ts`,
14
+ `.${name}rc.mjs`,
15
+ `.${name}rc.cjs`,
16
+ `${name}.config.js`,
17
+ `${name}.config.ts`,
18
+ `${name}.config.mjs`,
19
+ `${name}.config.cjs`,
20
+ ];
21
+ }
3
22
  import { readPackageJSON, resolvePackageJSON } from 'pkg-types';
4
23
  import { getTsconfig } from 'get-tsconfig';
5
24
 
@@ -40,20 +59,48 @@ export class Config {
40
59
  }
41
60
 
42
61
  /** Loads gjsify config file, e.g `.gjsifyrc.js` */
43
- private async load(searchFrom?: string) {
44
- let configFile = await cosmiconfig(APP_NAME, this.loadOptions).search(searchFrom) as CosmiconfigResult<ConfigData> | null;
45
-
46
- configFile ||= {
47
- config: {},
48
- filepath: '',
49
- isEmpty: true,
62
+ private async load(searchFrom?: string) {
63
+ // cosmiconfig's default first-match-wins behaviour silently drops one
64
+ // source when both `package.json#gjsify` and an explicit config file
65
+ // (`.gjsifyrc.js`, `gjsify.config.mjs`, ...) are present. Project hits
66
+ // this footgun: adding `gjsify.bin` to package.json (so `gjsify dlx`
67
+ // resolves the GJS bundle) silently disables `.gjsifyrc.js`. We
68
+ // explicitly load both sources and merge — package.json is the lower
69
+ // layer, the explicit file wins on key collisions.
70
+ //
71
+ // Run two searches:
72
+ // 1. Default (includes package.json) — for projects that only use
73
+ // package.json#gjsify and no separate file.
74
+ // 2. Explicit-file only (package.json excluded) — to find the
75
+ // `.gjsifyrc.*` / `gjsify.config.*` regardless of whether
76
+ // package.json#gjsify exists.
77
+ const fileExplorer = cosmiconfig(APP_NAME, {
78
+ ...this.loadOptions,
79
+ searchPlaces: (this.loadOptions.searchPlaces ?? defaultSearchPlaces(APP_NAME))
80
+ .filter((p) => p !== 'package.json'),
81
+ });
82
+ const fileResult = await fileExplorer.search(searchFrom) as CosmiconfigResult<ConfigData> | null;
83
+
84
+ const merged: ConfigData = {};
85
+ try {
86
+ const pkg = await this.readPackageJSON(searchFrom) as { gjsify?: ConfigData };
87
+ if (isPlainObject(pkg?.gjsify)) merge(merged, pkg.gjsify);
88
+ } catch {
89
+ // Missing or unreadable package.json — skip.
50
90
  }
91
+ if (fileResult?.config && isPlainObject(fileResult.config)) {
92
+ merge(merged, fileResult.config);
93
+ }
94
+
95
+ merged.esbuild ||= {};
96
+ merged.library ||= {};
97
+ merged.typescript ||= {};
51
98
 
52
- configFile.config ||= {};
53
- configFile.config.esbuild ||= {};
54
- configFile.config.library ||= {};
55
- configFile.config.typescript ||= {};
56
- return configFile;
99
+ return {
100
+ config: merged,
101
+ filepath: fileResult?.filepath ?? '',
102
+ isEmpty: !fileResult && Object.keys(merged).length === 3, // only the three default-empty objects
103
+ };
57
104
  }
58
105
 
59
106
  /** Loads package.json of the current project */