@gjsify/module 0.4.0 → 0.4.4

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/src/index.ts DELETED
@@ -1,453 +0,0 @@
1
- // Reference: Node.js lib/module.js
2
- // Reimplemented for GJS using Gio and GLib
3
- // CJS loading logic adapted from @gjsify/require (c) Andrea Giammarchi - ISC
4
-
5
- import '@girs/gjs';
6
- import Gio from '@girs/gio-2.0';
7
- import GLib from '@girs/glib-2.0';
8
- import { resolve as resolvePath, readJSON } from '@gjsify/utils';
9
- import { findPnpManifest, loadPnpManifest, resolveBareViaPnp } from './pnp.js';
10
-
11
- export const builtinModules = [
12
- 'assert',
13
- 'async_hooks',
14
- 'buffer',
15
- 'child_process',
16
- 'cluster',
17
- 'console',
18
- 'constants',
19
- 'crypto',
20
- 'dgram',
21
- 'diagnostics_channel',
22
- 'dns',
23
- 'domain',
24
- 'events',
25
- 'fs',
26
- 'http',
27
- 'http2',
28
- 'https',
29
- 'inspector',
30
- 'module',
31
- 'net',
32
- 'os',
33
- 'path',
34
- 'perf_hooks',
35
- 'process',
36
- 'punycode',
37
- 'querystring',
38
- 'readline',
39
- 'repl',
40
- 'stream',
41
- 'string_decoder',
42
- 'sys',
43
- 'timers',
44
- 'tls',
45
- 'tty',
46
- 'url',
47
- 'util',
48
- 'v8',
49
- 'vm',
50
- 'wasi',
51
- 'worker_threads',
52
- 'zlib',
53
- ];
54
-
55
- export function isBuiltin(name: string): boolean {
56
- const n = name.startsWith('node:') ? name.slice(5) : name;
57
- const base = n.split('/')[0];
58
- return builtinModules.includes(n) || builtinModules.includes(base);
59
- }
60
-
61
- // --- Private helpers for createRequire ---
62
- // Resolution logic ported from @gjsify/require, cleaned up for ESM-only use
63
-
64
- /** Walk up from startDir to find the nearest node_modules directory. */
65
- function findNodeModulesDir(startDir: string): string | null {
66
- let dir = Gio.File.new_for_path(startDir);
67
- while (dir.has_parent(null)) {
68
- const nodeModules = dir.resolve_relative_path('node_modules');
69
- if (nodeModules.query_exists(null)) {
70
- return nodeModules.get_path();
71
- }
72
- dir = dir.get_parent()!;
73
- }
74
- return null;
75
- }
76
-
77
- /** Resolve symlinks for a Gio.File, returning the real path. */
78
- function resolveSymlink(file: Gio.File): string {
79
- const info = file.query_info('standard::', Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
80
- if (info.get_is_symlink()) {
81
- const target = info.get_symlink_target();
82
- const parent = file.get_parent();
83
- if (target && parent) {
84
- return parent.resolve_relative_path(target).get_path()!;
85
- }
86
- }
87
- return file.get_path()!;
88
- }
89
-
90
- /** Try appending .js to an extensionless path. Returns the file if found, null otherwise. */
91
- function tryJsExtension(filePath: string): Gio.File | null {
92
- const withJs = Gio.File.new_for_path(filePath + '.js');
93
- return withJs.query_exists(null) ? withJs : null;
94
- }
95
-
96
- /** Check if a basename has a file extension. */
97
- function hasExtension(basename: string): boolean {
98
- return basename.includes('.');
99
- }
100
-
101
- /** Resolve package.json main/module entry for a directory. */
102
- function resolvePackageEntry(dirPath: string): string | null {
103
- const pkgJsonFile = resolvePath(dirPath, 'package.json');
104
- if (!pkgJsonFile.query_exists(null)) return null;
105
-
106
- const pkg = readJSON(pkgJsonFile.get_path()!) as Record<string, unknown>;
107
-
108
- // `exports` map first — when present, it MUST be the authoritative
109
- // resolution for the root entry. The legacy `main`/`module` fields are
110
- // only consulted as a fallback (or when no exports map exists).
111
- if (pkg['exports'] !== undefined) {
112
- const resolvedFromExports = resolveExportsMap(pkg['exports'], '.', DEFAULT_EXPORT_CONDITIONS);
113
- if (resolvedFromExports) {
114
- const resolved = resolvePath(dirPath, resolvedFromExports);
115
- if (resolved.query_exists(null)) return resolved.get_path()!;
116
- }
117
- }
118
-
119
- const main = (pkg['main'] as string | undefined) || (pkg['module'] as string | undefined) || 'index.js';
120
- const entryFile = resolvePath(dirPath, main);
121
-
122
- if (entryFile.query_exists(null)) return entryFile.get_path()!;
123
-
124
- // Try .js extension fallback for extensionless main field
125
- if (!hasExtension(main)) {
126
- const withJs = tryJsExtension(entryFile.get_path()!);
127
- if (withJs) return withJs.get_path()!;
128
- }
129
-
130
- return null;
131
- }
132
-
133
- /**
134
- * Resolve a `pkg.exports`-map entry into a relative file path.
135
- *
136
- * Implements a useful subset of the Node ESM Package Exports spec
137
- * (https://nodejs.org/api/packages.html#package-entry-points):
138
- * - shorthand string form (`"exports": "./lib/index.js"`)
139
- * - object form with `.` + `./<subpath>` keys
140
- * - conditional resolution against `conditions` in order (first match wins)
141
- * - `null` value → block (return undefined, caller treats as not-exported)
142
- *
143
- * Pattern wildcards (`./shims/*`) and subpath imports (`#internal`) are
144
- * out of scope for now — the surface we need right now is fixed-key
145
- * subpaths like `./shims/console-gjs`. Each gap surfaces a clear caller
146
- * error rather than silent miss.
147
- *
148
- * @param exportsField Raw value of `pkg.exports`.
149
- * @param subpath The requested subpath, including the leading `./`
150
- * (so `.` for root, `./foo` for `pkg/foo`).
151
- * @param conditions Ordered condition list — first one that has a value
152
- * wins. Standard order: `["node", "import", "default"]` for GJS.
153
- */
154
- function resolveExportsMap(
155
- exportsField: unknown,
156
- subpath: string,
157
- conditions: readonly string[],
158
- ): string | undefined {
159
- if (typeof exportsField === 'string') {
160
- // Shorthand: `"exports": "./lib/index.js"` applies to `.` only.
161
- return subpath === '.' ? exportsField : undefined;
162
- }
163
- if (exportsField === null || typeof exportsField !== 'object') return undefined;
164
- const map = exportsField as Record<string, unknown>;
165
-
166
- // If the map has no `.`-prefixed keys, it's purely a condition map
167
- // applying to root (`.`) — wrap it under `.` for the lookup.
168
- const hasSubpathKeys = Object.keys(map).some((k) => k === '.' || k.startsWith('./'));
169
- const lookup = hasSubpathKeys ? map[subpath] : (subpath === '.' ? map : undefined);
170
- if (lookup === undefined || lookup === null) return undefined;
171
-
172
- return pickConditionalValue(lookup, conditions);
173
- }
174
-
175
- /**
176
- * Walk a conditional-export node, picking the first matching condition.
177
- * `lookup` is either a leaf (string/null) or a nested condition map.
178
- */
179
- function pickConditionalValue(lookup: unknown, conditions: readonly string[]): string | undefined {
180
- if (typeof lookup === 'string') return lookup;
181
- if (lookup === null || typeof lookup !== 'object') return undefined;
182
- const map = lookup as Record<string, unknown>;
183
- // `default` always wins as the final fallback inside the conditions
184
- // we recognize, but other conditions take precedence in the order the
185
- // caller provides them (e.g. `node` before `import`).
186
- for (const cond of conditions) {
187
- if (cond in map) {
188
- const resolved = pickConditionalValue(map[cond], conditions);
189
- if (resolved !== undefined) return resolved;
190
- }
191
- }
192
- if ('default' in map && !conditions.includes('default')) {
193
- return pickConditionalValue(map['default'], conditions);
194
- }
195
- return undefined;
196
- }
197
-
198
- const DEFAULT_EXPORT_CONDITIONS: readonly string[] = ['node', 'import', 'default'];
199
-
200
- /**
201
- * Extract `<pkgName>` and `<subpath>` from a bare specifier:
202
- * `lodash` → { pkg: 'lodash', subpath: '.' }
203
- * `@scope/foo` → { pkg: '@scope/foo', subpath: '.' }
204
- * `@scope/foo/bar` → { pkg: '@scope/foo', subpath: './bar' }
205
- * `lodash/fp` → { pkg: 'lodash', subpath: './fp' }
206
- */
207
- function splitBareSpecifier(id: string): { pkg: string; subpath: string } {
208
- if (id.startsWith('@')) {
209
- const slash1 = id.indexOf('/');
210
- if (slash1 === -1) return { pkg: id, subpath: '.' };
211
- const slash2 = id.indexOf('/', slash1 + 1);
212
- if (slash2 === -1) return { pkg: id, subpath: '.' };
213
- return { pkg: id.slice(0, slash2), subpath: '.' + id.slice(slash2) };
214
- }
215
- const slash = id.indexOf('/');
216
- if (slash === -1) return { pkg: id, subpath: '.' };
217
- return { pkg: id.slice(0, slash), subpath: '.' + id.slice(slash) };
218
- }
219
-
220
- /** Convert a file: URL (string or object) to an absolute path. */
221
- function fileUrlToPath(filenameOrURL: string | URL): string {
222
- // Duck-type URL objects (avoids dependency on global URL which may not exist in GJS)
223
- if (typeof filenameOrURL === 'object' && filenameOrURL !== null && 'href' in filenameOrURL) {
224
- const urlObj = filenameOrURL as { href: string; protocol?: string };
225
- if (urlObj.protocol && urlObj.protocol !== 'file:') {
226
- throw new TypeError('The URL must use the file: protocol');
227
- }
228
- return GLib.filename_from_uri(urlObj.href)[0];
229
- }
230
-
231
- if (typeof filenameOrURL === 'string' && filenameOrURL.startsWith('file:')) {
232
- return GLib.filename_from_uri(filenameOrURL)[0];
233
- }
234
-
235
- return String(filenameOrURL);
236
- }
237
-
238
- /**
239
- * Try resolving a bare specifier through a Yarn PnP manifest (`.pnp.cjs`)
240
- * sitting above `callerDir`. Returns null when no manifest is found, or when
241
- * PnP can't resolve the request (e.g. the dep isn't listed in the caller
242
- * package's `packageDependencies`). Callers fall back to the node_modules walk.
243
- */
244
- function resolveBareViaPnpFromCaller(id: string, callerDir: string): Gio.File | null {
245
- const pnpPath = findPnpManifest(callerDir);
246
- if (!pnpPath) return null;
247
- const manifest = loadPnpManifest(pnpPath);
248
- if (!manifest) return null;
249
- const resolved = resolveBareViaPnp(manifest, id, callerDir);
250
- if (!resolved) return null;
251
- const file = Gio.File.new_for_path(resolved);
252
- return file.query_exists(null) ? file : null;
253
- }
254
-
255
- /**
256
- * Resolve a bare package specifier by walking ALL ancestor node_modules dirs.
257
- * Mirrors Node.js module resolution: try nearest node_modules first, then each
258
- * ancestor — so a package in a parent node_modules is found even when a closer
259
- * node_modules exists but doesn't contain the requested package.
260
- */
261
- function resolveInNodeModules(id: string, callerDir: string): Gio.File {
262
- const { pkg: pkgName, subpath } = splitBareSpecifier(id);
263
- let dir = Gio.File.new_for_path(callerDir);
264
- while (dir.has_parent(null)) {
265
- const nodeModulesFile = dir.resolve_relative_path('node_modules');
266
- if (nodeModulesFile.query_exists(null)) {
267
- const pkgDir = nodeModulesFile.resolve_relative_path(pkgName);
268
- if (pkgDir.query_exists(null)) {
269
- // Try the package's `exports` map for the requested subpath
270
- // (including `.` for the package root). The map is authoritative
271
- // when present — Node would only fall back to literal-path or
272
- // `main`/`module` when no `exports` exists.
273
- const pkgJson = pkgDir.resolve_relative_path('package.json');
274
- if (pkgJson.query_exists(null)) {
275
- const manifest = readJSON(pkgJson.get_path()!) as Record<string, unknown>;
276
- if (manifest['exports'] !== undefined) {
277
- const relative = resolveExportsMap(manifest['exports'], subpath, DEFAULT_EXPORT_CONDITIONS);
278
- if (relative !== undefined) {
279
- const target = pkgDir.resolve_relative_path(relative);
280
- if (target.query_exists(null)) return target;
281
- // fall through to literal-path on miss — keeps the
282
- // behavior conservative when an exports entry points at
283
- // a non-existent file
284
- }
285
- }
286
- }
287
- // Literal-path fallback (subpath used as plain relative path
288
- // under the package dir, or `.` resolved to the dir itself).
289
- const candidate = subpath === '.' ? pkgDir : pkgDir.resolve_relative_path(subpath.slice(2));
290
- if (candidate.query_exists(null)) return candidate;
291
- const bn = candidate.get_basename();
292
- if (bn && !hasExtension(bn)) {
293
- const withJs = tryJsExtension(candidate.get_path()!);
294
- if (withJs) return withJs;
295
- }
296
- } else {
297
- // Original behavior for the no-pkgDir case — literal `node_modules/id`
298
- // walk, handles edge cases like single-file shims at the root.
299
- const candidate = nodeModulesFile.resolve_relative_path(id);
300
- if (candidate.query_exists(null)) return candidate;
301
- const bn = candidate.get_basename();
302
- if (bn && !hasExtension(bn)) {
303
- const withJs = tryJsExtension(candidate.get_path()!);
304
- if (withJs) return withJs;
305
- }
306
- }
307
- }
308
- dir = dir.get_parent()!;
309
- }
310
- throw new Error(`Cannot find module "${id}" - not found in any node_modules directory`);
311
- }
312
-
313
- /** Resolve a module specifier to an absolute file path. */
314
- function resolveModulePath(id: string, callerDir: string): string {
315
- if (isBuiltin(id)) return id;
316
-
317
- let file: Gio.File;
318
-
319
- if (id.startsWith('/')) {
320
- file = resolvePath(id);
321
- } else if (id.startsWith('.')) {
322
- file = resolvePath(callerDir, id);
323
- } else {
324
- // Bare specifier: try Yarn PnP first (for PnP-built workspaces where no
325
- // node_modules/ tree exists on disk), then fall back to the standard
326
- // node_modules walk.
327
- const pnpFile = resolveBareViaPnpFromCaller(id, callerDir);
328
- file = pnpFile ?? resolveInNodeModules(id, callerDir);
329
- }
330
-
331
- // Extension fallback for absolute/relative paths (bare specifiers handled in resolveInNodeModules)
332
- if (id.startsWith('/') || id.startsWith('.')) {
333
- if (!file.query_exists(null)) {
334
- const basename = file.get_basename();
335
- if (basename && !hasExtension(basename)) {
336
- file = tryJsExtension(file.get_path()!) ?? file;
337
- }
338
- }
339
-
340
- if (!file.query_exists(null)) {
341
- throw new Error(`Cannot find module "${id}"`);
342
- }
343
- }
344
-
345
- const resolvedPath = resolveSymlink(file);
346
-
347
- // Directory → resolve via package.json main field
348
- const basename = file.get_basename();
349
- if (basename && !hasExtension(basename)) {
350
- const entry = resolvePackageEntry(resolvedPath);
351
- if (entry) return entry;
352
- }
353
-
354
- return resolvedPath;
355
- }
356
-
357
- // --- CJS file loading via GJS imports system ---
358
- // Ported from @gjsify/require (c) Andrea Giammarchi - ISC
359
-
360
- /** Load a CJS .js/.cjs file using GJS's legacy imports system. */
361
- function requireJsFile(filePath: string, cache: Record<string, unknown>): unknown {
362
- if (filePath in cache) return cache[filePath];
363
-
364
- let file = Gio.File.new_for_path(filePath);
365
- const dir = file.get_parent()!.get_path()!;
366
- let basename = file.get_basename()!;
367
-
368
- if (basename.endsWith('.mjs')) {
369
- throw new Error(`Cannot require .mjs files. Use import instead. Path: "${filePath}"`);
370
- }
371
-
372
- // GJS can't import .cjs files — copy to .js as workaround
373
- if (basename.endsWith('.cjs')) {
374
- const dest = resolvePath(dir, '__gjsify__' + basename.replace(/\.cjs$/, '.js'));
375
- if (dest.query_exists(null)) dest.delete(null);
376
- file.copy(dest, Gio.FileCopyFlags.NONE, null, null);
377
- file = dest;
378
- basename = file.get_basename()!;
379
- }
380
-
381
- // Save and replace global CJS state
382
- const savedExports = globalThis.exports;
383
- const savedModule = globalThis.module;
384
- const moduleObj = { exports: {} };
385
- globalThis.exports = moduleObj.exports;
386
- globalThis.module = moduleObj as NodeModule;
387
-
388
- try {
389
- // Evaluate the file via GJS imports system
390
- const { searchPath } = imports;
391
- searchPath.unshift(dir);
392
- imports[basename.replace(/\.(js|cjs)$/, '')];
393
- searchPath.shift();
394
-
395
- const result = moduleObj.exports;
396
- cache[filePath] = result;
397
- return result;
398
- } finally {
399
- // Always restore global state, even on error
400
- globalThis.exports = savedExports;
401
- globalThis.module = savedModule;
402
- }
403
- }
404
-
405
- export function createRequire(filenameOrURL: string | URL): NodeRequire {
406
- const filename = fileUrlToPath(filenameOrURL);
407
-
408
- if (!filename.startsWith('/')) {
409
- throw new TypeError(
410
- 'The argument must be a file URL object, file URL string, or absolute path string. ' +
411
- `Received "${String(filenameOrURL)}"`
412
- );
413
- }
414
-
415
- const callerDir = GLib.path_get_dirname(filename);
416
- const cache: Record<string, unknown> = Object.create(null);
417
-
418
- const req = function require(id: string): unknown {
419
- const resolved = resolveModulePath(id, callerDir);
420
- if (resolved in cache) return cache[resolved];
421
-
422
- // JSON files
423
- if (resolved.endsWith('.json')) {
424
- const result = readJSON(resolved);
425
- cache[resolved] = result;
426
- return result;
427
- }
428
-
429
- // Builtin modules can't be required synchronously in ESM
430
- if (isBuiltin(id)) {
431
- throw new Error(
432
- `createRequire: Cannot require builtin module "${id}" synchronously in GJS. ` +
433
- 'Use import instead.'
434
- );
435
- }
436
-
437
- // .js/.cjs files via GJS imports system
438
- return requireJsFile(resolved, cache);
439
- } as NodeRequire;
440
-
441
- req.resolve = function resolve(id: string): string {
442
- return resolveModulePath(id, callerDir);
443
- } as NodeRequire['resolve'];
444
-
445
- req.resolve.paths = (_request: string): string[] | null => null;
446
- req.cache = cache as NodeRequire['cache'];
447
- req.extensions = Object.create(null) as NodeRequire['extensions'];
448
- req.main = undefined;
449
-
450
- return req;
451
- }
452
-
453
- export default { builtinModules, isBuiltin, createRequire };