@gjsify/rolldown-plugin-gjsify 0.4.21 → 0.4.23

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.
@@ -44,6 +44,60 @@ const METHOD_MARKERS = {
44
44
  // @gjsify/node-globals/register/url in GJS_GLOBALS_MAP) already pulls in
45
45
  // the correct register module.
46
46
  };
47
+ /**
48
+ * wasm-bindgen-generated function-name patterns that imply a global
49
+ * identifier should be injected. wasm-bindgen emits its host-API
50
+ * import bindings as top-level functions named
51
+ * `__wbg_<jsName>_<hash>` where `<jsName>` is the property name of the
52
+ * JS API the WASM module wants to call. The body looks like:
53
+ *
54
+ * function __wbg_crypto_574e78ad8b13b65f(arg0) {
55
+ * const ret = getObject(arg0).crypto;
56
+ * return addHeapObject(ret);
57
+ * }
58
+ *
59
+ * `getObject(arg0)` is a runtime heap dereference (the object is one
60
+ * of the host bridges registered by wasm-bindgen at init time —
61
+ * typically `globalThis`, `window`, or `self`). The MemberExpression
62
+ * visitor can't follow that — `node.object` is a CallExpression, not
63
+ * an Identifier — so the underlying `.crypto` access is invisible to
64
+ * the static scan.
65
+ *
66
+ * Matching on the FUNCTION-NAME pattern instead is high precision
67
+ * (no false positives — `__wbg_` is wasm-bindgen-reserved) and high
68
+ * recall (the names are extremely stable across wasm-bindgen
69
+ * versions). Add an entry whenever a new wasm-bindgen-built npm
70
+ * package surfaces a needed global that the static scan misses.
71
+ *
72
+ * Keyed by the `<jsName>` extracted from the `__wbg_<jsName>_<hash>`
73
+ * function name; value is the gjsify global to inject.
74
+ */
75
+ const WASM_BINDGEN_MARKERS = {
76
+ // crypto.getRandomValues chain — wasm-bindgen's canonical
77
+ // crypto-import binding pattern. Used by ed25519-dalek, ring, rand
78
+ // (when targeting wasm), and most Rust crates that touch
79
+ // randomness or hashing. Loro is the driving real-world consumer:
80
+ // its CRDT operations need crypto.getRandomValues for peer-id
81
+ // generation and ChangeID nonces.
82
+ crypto: 'crypto',
83
+ getRandomValues: 'crypto',
84
+ // Legacy IE prefix path — wasm-bindgen probes for `msCrypto` as a
85
+ // fallback when `crypto` isn't available. Doesn't apply to GJS
86
+ // (we ship a real `crypto`), but flagging the marker keeps the
87
+ // detector self-documenting.
88
+ msCrypto: 'crypto',
89
+ };
90
+ /**
91
+ * Match the wasm-bindgen function-name shape — return the `<jsName>`
92
+ * part of `__wbg_<jsName>_<hash>` or `null`. wasm-bindgen hashes are
93
+ * 8–16 hex chars in practice, but we accept any alphanumeric trailer
94
+ * so the pattern survives future format tweaks.
95
+ */
96
+ const WBG_NAME_RE = /^__wbg_([A-Za-z][A-Za-z0-9]*)_[A-Za-z0-9]+$/;
97
+ function wbgJsNameFor(fnName) {
98
+ const match = fnName.match(WBG_NAME_RE);
99
+ return match?.[1] ?? null;
100
+ }
47
101
  /**
48
102
  * Extract all bound names from a binding pattern
49
103
  * (Identifier, ObjectPattern, ArrayPattern, AssignmentPattern, RestElement).
@@ -192,6 +246,24 @@ export function detectFreeGlobals(code) {
192
246
  freeGlobals.add(markerTarget);
193
247
  }
194
248
  },
249
+ FunctionDeclaration(node) {
250
+ // Pattern C: wasm-bindgen marker hook. The function name
251
+ // matches `__wbg_<jsName>_<hash>` and `<jsName>` is a known
252
+ // host API. The body would be `getObject(arg0).<jsName>` —
253
+ // an unfollowable runtime heap dereference — but the
254
+ // function NAME tells us exactly which global is needed.
255
+ // See WASM_BINDGEN_MARKERS for the why + which jsNames are
256
+ // mapped.
257
+ if (!node.id)
258
+ return;
259
+ const jsName = wbgJsNameFor(node.id.name);
260
+ if (!jsName)
261
+ return;
262
+ const target = WASM_BINDGEN_MARKERS[jsName];
263
+ if (target && KNOWN_GLOBALS.has(target)) {
264
+ freeGlobals.add(target);
265
+ }
266
+ },
195
267
  Identifier(node, ancestors) {
196
268
  const name = node.name;
197
269
  // Quick filter: only check known globals
@@ -151,6 +151,12 @@ function tryInlineCall(node, ctx, src) {
151
151
  }
152
152
  }
153
153
  if (calleeName === 'readdirSync') {
154
+ // We inline as a plain string[] — refuse if the caller asks for
155
+ // Dirent[] via { withFileTypes: true }. Otherwise the consumer's
156
+ // child.isFile() call would throw at runtime ("isFile is not a
157
+ // function" on a string).
158
+ if (hasWithFileTypes(node.arguments[1]))
159
+ return undefined;
154
160
  const path = evalPathExpr(node.arguments[0], ctx);
155
161
  if (path && existsSyncSafe(path) && isDirectorySafe(path)) {
156
162
  try {
@@ -476,6 +482,30 @@ function evalEncodingExpr(node) {
476
482
  }
477
483
  return undefined;
478
484
  }
485
+ /**
486
+ * Detect `{ withFileTypes: true }` in a readdirSync options argument.
487
+ * Any non-literal or absence returns `false` (safe — we only abort
488
+ * inlining for an unambiguous `true`).
489
+ */
490
+ function hasWithFileTypes(node) {
491
+ if (!node || node.type !== 'ObjectExpression')
492
+ return false;
493
+ for (const p of node.properties) {
494
+ if (p.type !== 'Property' || p.computed)
495
+ continue;
496
+ const key = p.key.type === 'Identifier'
497
+ ? p.key.name
498
+ : p.key.type === 'Literal'
499
+ ? String(p.key.value)
500
+ : undefined;
501
+ if (key !== 'withFileTypes')
502
+ continue;
503
+ if (p.value.type === 'Literal' && p.value.value === true)
504
+ return true;
505
+ return false;
506
+ }
507
+ return false;
508
+ }
479
509
  function canonicalEncoding(v) {
480
510
  const lc = v.toLowerCase();
481
511
  if (lc === 'utf8' || lc === 'utf-8')
@@ -488,12 +518,20 @@ function canonicalEncoding(v) {
488
518
  }
489
519
  /**
490
520
  * Get the leaf identifier name of a callee. Recognises:
491
- * `foo` → "foo"
492
- * `path.foo` → "foo"
493
- * `node:path.foo` → "foo" (rare)
494
- * `fs.foo` / `fs.promises.foo` → "foo"
521
+ * `foo` → "foo" (assumed named import)
522
+ * `path.foo` → "foo" (path module namespace/default import)
523
+ * `fs.foo` → "foo" (fs module — caller validates context)
524
+ *
525
+ * For MemberExpression callees, the object identifier is restricted to a
526
+ * known module namespace name (`path`, `fs`, `JSON`). Otherwise `arr.join(',')`
527
+ * (Array.prototype.join) would resolve to `path.join`, and our static
528
+ * evaluator would happily treat a free `dir.join('/')` array call as
529
+ * `path.join('/')` → `'/'` → catastrophic root-directory scan. See PR for
530
+ * the TypeDoc bundling incident this prevents.
531
+ *
495
532
  * Returns `undefined` for computed/dynamic callees.
496
533
  */
534
+ const PATH_NAMESPACE_OBJECTS = new Set(['path', 'fs']);
497
535
  function identifierName(node) {
498
536
  if (!node)
499
537
  return undefined;
@@ -501,8 +539,17 @@ function identifierName(node) {
501
539
  return node.name;
502
540
  if (node.type === 'MemberExpression' && !node.computed) {
503
541
  const me = node;
504
- if (me.property.type === 'Identifier')
505
- return me.property.name;
542
+ if (me.property.type !== 'Identifier')
543
+ return undefined;
544
+ // The object must be a known namespace identifier — otherwise we
545
+ // misidentify Array.prototype methods (`arr.join`, `arr.includes`)
546
+ // and userland method calls as path/fs functions.
547
+ if (me.object.type !== 'Identifier')
548
+ return undefined;
549
+ const obj = me.object.name;
550
+ if (!PATH_NAMESPACE_OBJECTS.has(obj))
551
+ return undefined;
552
+ return me.property.name;
506
553
  }
507
554
  return undefined;
508
555
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/rolldown-plugin-gjsify",
3
- "version": "0.4.21",
3
+ "version": "0.4.23",
4
4
  "description": "Rolldown / Rollup / Vite plugin orchestrator for GJS, Node, and Browser targets",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -45,11 +45,11 @@
45
45
  ],
46
46
  "license": "MIT",
47
47
  "dependencies": {
48
- "@gjsify/console": "^0.4.21",
49
- "@gjsify/resolve-npm": "^0.4.21",
50
- "@gjsify/rolldown-plugin-deepkit": "^0.4.21",
51
- "@gjsify/rolldown-plugin-pnp": "^0.4.21",
52
- "@gjsify/vite-plugin-blueprint": "^0.4.21",
48
+ "@gjsify/console": "^0.4.23",
49
+ "@gjsify/resolve-npm": "^0.4.23",
50
+ "@gjsify/rolldown-plugin-deepkit": "^0.4.23",
51
+ "@gjsify/rolldown-plugin-pnp": "^0.4.23",
52
+ "@gjsify/vite-plugin-blueprint": "^0.4.23",
53
53
  "@rollup/pluginutils": "^5.3.0",
54
54
  "acorn": "^8.16.0",
55
55
  "acorn-walk": "^8.3.5",
@@ -57,7 +57,7 @@
57
57
  "lightningcss": "^1.32.0"
58
58
  },
59
59
  "peerDependencies": {
60
- "@gjsify/lightningcss-native": "^0.4.21",
60
+ "@gjsify/lightningcss-native": "^0.4.23",
61
61
  "rolldown": "^1.0.0-rc.18"
62
62
  },
63
63
  "peerDependenciesMeta": {
@@ -69,7 +69,7 @@
69
69
  }
70
70
  },
71
71
  "devDependencies": {
72
- "@types/node": "^25.6.2",
72
+ "@types/node": "^25.9.1",
73
73
  "rolldown": "^1.0.0",
74
74
  "typescript": "^6.0.3"
75
75
  }