@bobfrankston/importgen 0.1.35 → 0.1.37
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/index.js +202 -55
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -37,49 +37,122 @@ function resolveDependencyPath(packageDir, depName, depVersion) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
* Resolve entry point from package.json
|
|
40
|
+
* Resolve entry point from package.json. Handles modern conditional
|
|
41
|
+
* exports (`{ ".": { "import": { "browser": "...", "default": "..." } } }`)
|
|
42
|
+
* by recursively walking the conditions until a string-valued leaf is
|
|
43
|
+
* found, preferring `import` / `browser` / `default` for our use case.
|
|
41
44
|
*/
|
|
42
45
|
function resolveEntryPoint(pkg, packageDir) {
|
|
46
|
+
void packageDir;
|
|
43
47
|
// 1. Check exports field (modern)
|
|
44
48
|
if (pkg.exports) {
|
|
45
49
|
if (typeof pkg.exports === 'string') {
|
|
46
50
|
return pkg.exports;
|
|
47
51
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
else if (dotExport.import) {
|
|
54
|
-
return dotExport.import;
|
|
55
|
-
}
|
|
56
|
-
else if (dotExport.default) {
|
|
57
|
-
return dotExport.default;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
52
|
+
const dotExport = pkg.exports['.'] ?? pkg.exports;
|
|
53
|
+
const found = pickConditionalExport(dotExport);
|
|
54
|
+
if (found)
|
|
55
|
+
return found;
|
|
60
56
|
}
|
|
61
57
|
// 2. Check module field (ESM)
|
|
62
|
-
if (pkg.module)
|
|
58
|
+
if (typeof pkg.module === 'string')
|
|
63
59
|
return pkg.module;
|
|
64
|
-
}
|
|
65
60
|
// 3. Fall back to main field
|
|
66
|
-
if (pkg.main)
|
|
61
|
+
if (typeof pkg.main === 'string')
|
|
67
62
|
return pkg.main;
|
|
68
|
-
}
|
|
69
63
|
// 4. Default to index.js
|
|
70
64
|
return './index.js';
|
|
71
65
|
}
|
|
66
|
+
/** Walk a conditional-exports node looking for a string entry point.
|
|
67
|
+
* Conditions traversed in priority order: `import`, `browser`, `module`,
|
|
68
|
+
* `default`, then any other keys. Recurses through nested condition
|
|
69
|
+
* objects. Returns null if no string leaf is found. */
|
|
70
|
+
function pickConditionalExport(node) {
|
|
71
|
+
if (typeof node === 'string')
|
|
72
|
+
return node;
|
|
73
|
+
if (!node || typeof node !== 'object')
|
|
74
|
+
return null;
|
|
75
|
+
const conditions = ['import', 'browser', 'module', 'default'];
|
|
76
|
+
for (const cond of conditions) {
|
|
77
|
+
if (cond in node) {
|
|
78
|
+
const found = pickConditionalExport(node[cond]);
|
|
79
|
+
if (found)
|
|
80
|
+
return found;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Fall back to any other key (rarely hit; covers exotic shapes).
|
|
84
|
+
for (const key of Object.keys(node)) {
|
|
85
|
+
if (conditions.includes(key))
|
|
86
|
+
continue;
|
|
87
|
+
const found = pickConditionalExport(node[key]);
|
|
88
|
+
if (found)
|
|
89
|
+
return found;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
72
93
|
/**
|
|
73
94
|
* Recursively collect all dependencies, avoiding circular references.
|
|
74
95
|
* nodeModulesPrefix tracks the import map path prefix for nested deps
|
|
75
96
|
* (e.g., './node_modules' for root, './node_modules/@scope/pkg/node_modules' for nested).
|
|
76
97
|
*/
|
|
98
|
+
/** Expand a single npm `workspaces` glob pattern under `rootDir` to a list
|
|
99
|
+
* of absolute workspace member directories. Supports literal paths and a
|
|
100
|
+
* trailing single `*` segment (the common npm-workspaces shapes:
|
|
101
|
+
* `packages/*`, `apps/*`, `client`). Other glob shapes are not handled. */
|
|
102
|
+
function expandWorkspacePattern(rootDir, pattern) {
|
|
103
|
+
const segs = pattern.split(/[\\/]/).filter(Boolean);
|
|
104
|
+
const globIdx = segs.findIndex(s => /[*?[]/.test(s));
|
|
105
|
+
if (globIdx === -1) {
|
|
106
|
+
const full = path.join(rootDir, ...segs);
|
|
107
|
+
return fs.existsSync(full) ? [full] : [];
|
|
108
|
+
}
|
|
109
|
+
if (globIdx === segs.length - 1 && segs[globIdx] === '*') {
|
|
110
|
+
const parentDir = path.join(rootDir, ...segs.slice(0, globIdx));
|
|
111
|
+
if (!fs.existsSync(parentDir))
|
|
112
|
+
return [];
|
|
113
|
+
return fs.readdirSync(parentDir, { withFileTypes: true })
|
|
114
|
+
.filter(e => e.isDirectory() || e.isSymbolicLink())
|
|
115
|
+
.map(e => path.join(parentDir, e.name));
|
|
116
|
+
}
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
77
119
|
function collectDependencies(packageJsonPath, visited, dependencies, htmlDir, nodeModulesPrefix, warnings, depPaths) {
|
|
78
120
|
try {
|
|
79
121
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
80
122
|
const packageDir = path.dirname(packageJsonPath);
|
|
81
|
-
const
|
|
123
|
+
const declaredDeps = packageJson.dependencies || {};
|
|
82
124
|
const dotDeps = packageJson['.dependencies'] || {};
|
|
125
|
+
// Bug 4: synthesize deps from the `workspaces` field. npm/pnpm
|
|
126
|
+
// workspaces convention is that sibling packages are reachable by
|
|
127
|
+
// package name without re-declaring them as deps. Each member's
|
|
128
|
+
// package.json `name` is added as a `file:`-protocol dep so the
|
|
129
|
+
// existing resolver finds it (via the junction npm puts in
|
|
130
|
+
// node_modules, or via the workspace path directly).
|
|
131
|
+
const workspaces = packageJson.workspaces;
|
|
132
|
+
let workspacePatterns = [];
|
|
133
|
+
if (Array.isArray(workspaces)) {
|
|
134
|
+
workspacePatterns = workspaces;
|
|
135
|
+
}
|
|
136
|
+
else if (workspaces && typeof workspaces === 'object' && Array.isArray(workspaces.packages)) {
|
|
137
|
+
workspacePatterns = workspaces.packages;
|
|
138
|
+
}
|
|
139
|
+
const syntheticDeps = {};
|
|
140
|
+
for (const pattern of workspacePatterns) {
|
|
141
|
+
for (const wsDir of expandWorkspacePattern(packageDir, pattern)) {
|
|
142
|
+
const wsPkgPath = path.join(wsDir, 'package.json');
|
|
143
|
+
if (!fs.existsSync(wsPkgPath))
|
|
144
|
+
continue;
|
|
145
|
+
try {
|
|
146
|
+
const wsPkg = JSON.parse(fs.readFileSync(wsPkgPath, 'utf-8'));
|
|
147
|
+
if (typeof wsPkg.name === 'string' && !(wsPkg.name in declaredDeps)) {
|
|
148
|
+
const rel = path.relative(packageDir, wsDir).split(path.sep).join('/');
|
|
149
|
+
syntheticDeps[wsPkg.name] = `file:${rel}`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch { /* malformed workspace pkg — skip */ }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const deps = { ...syntheticDeps, ...declaredDeps };
|
|
83
156
|
for (const [depName, depVersion] of Object.entries(deps)) {
|
|
84
157
|
// Skip if already processed (circular dependency protection)
|
|
85
158
|
if (visited.has(depName)) {
|
|
@@ -138,10 +211,40 @@ function collectDependencies(packageJsonPath, visited, dependencies, htmlDir, no
|
|
|
138
211
|
searchDir = parent;
|
|
139
212
|
}
|
|
140
213
|
if (!depPrefix) {
|
|
141
|
-
//
|
|
142
|
-
|
|
214
|
+
// Transitive dep that npm placed in a NESTED node_modules
|
|
215
|
+
// (not hoisted to an ancestor of htmlDir). nodeModulesPrefix
|
|
216
|
+
// tracks that nested location through the recursion. Use it
|
|
217
|
+
// when the package physically resolves there — this is a
|
|
218
|
+
// path under the app's node_modules tree that the browser
|
|
219
|
+
// can actually fetch (junctions traverse transparently).
|
|
220
|
+
const nestedCandidate = `${nodeModulesPrefix}/${depName}`;
|
|
221
|
+
if (fs.existsSync(path.join(htmlDir, nestedCandidate))) {
|
|
222
|
+
depPrefix = nestedCandidate;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!depPrefix) {
|
|
226
|
+
// Last resort: not found in any node_modules at all (e.g. a
|
|
227
|
+
// file: dep with no junction). Use the resolved real path —
|
|
228
|
+
// which may escape the app dir; the RED FLAG below catches that.
|
|
229
|
+
const rel = path.relative(htmlDir, depPath).split(path.sep).join('/');
|
|
230
|
+
depPrefix = rel.startsWith('.') ? rel : `./${rel}`;
|
|
231
|
+
}
|
|
232
|
+
// RED FLAG: a `../`-escaping path leaves the HTML file's
|
|
233
|
+
// directory. The dev server's served root rarely extends
|
|
234
|
+
// above the app dir, so the browser will almost certainly
|
|
235
|
+
// 404 on it and the page breaks. We still emit the entry
|
|
236
|
+
// (it's the only real location we found), but flag it in
|
|
237
|
+
// the generated HTML so the breakage is not silent.
|
|
238
|
+
if (depPrefix.startsWith('../')) {
|
|
239
|
+
const warning = `${depName} -> ${depPrefix}/${entryFile} escapes the app directory; the browser will likely fail to load it unless the dev server serves a high-enough root. Add ${depName} to the app's node_modules.`;
|
|
240
|
+
console.warn(`${ts()} [generate-importmap] Warning: ${warning}`);
|
|
241
|
+
warnings.push(warning);
|
|
143
242
|
}
|
|
144
243
|
dependencies.set(depName, `${depPrefix}/${entryFile}`);
|
|
244
|
+
// Bug 3: trailing-slash variant for deep imports
|
|
245
|
+
// (`import "pkg/sub.js"` requires `"pkg/": "<root>/"`).
|
|
246
|
+
// Harmless when unused — browsers only fetch what's imported.
|
|
247
|
+
dependencies.set(`${depName}/`, `${depPrefix}/`);
|
|
145
248
|
// Recursively process this dependency's dependencies
|
|
146
249
|
collectDependencies(depPackageJsonPath, visited, dependencies, htmlDir, `${depPrefix}/node_modules`, warnings, depPaths);
|
|
147
250
|
}
|
|
@@ -241,71 +344,113 @@ if (import.meta.main) {
|
|
|
241
344
|
const unknownArgs = args.filter(a => a.startsWith('-') && !knownFlags.has(a));
|
|
242
345
|
if (unknownArgs.length > 0) {
|
|
243
346
|
console.error(`${ts()} [importgen] Error: unrecognized argument(s): ${unknownArgs.join(', ')}`);
|
|
244
|
-
console.error(` Usage: importgen [htmlfile] [-w|--watch] [-v|--version]`);
|
|
347
|
+
console.error(` Usage: importgen [htmlfile...] [-w|--watch] [-v|--version]`);
|
|
245
348
|
process.exit(1);
|
|
246
349
|
}
|
|
247
350
|
const watchMode = args.includes('-w') || args.includes('--watch');
|
|
248
351
|
const positionalArgs = args.filter(a => !a.startsWith('-'));
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
const htmlArg = positionalArgs[0];
|
|
255
|
-
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
352
|
+
// Bug 5: accept multiple HTML targets. Each gets its own import map
|
|
353
|
+
// with paths rewritten relative to that file's directory. Useful for
|
|
354
|
+
// multi-page apps where each html has its own document/module context
|
|
355
|
+
// (e.g. mailx: index.html, compose/compose.html, …).
|
|
256
356
|
const possibleHtmlFiles = ['index.html', 'default.html', 'default.htm'];
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
357
|
+
const htmlFileNames = positionalArgs.length > 0
|
|
358
|
+
? positionalArgs
|
|
359
|
+
: (() => {
|
|
360
|
+
const found = possibleHtmlFiles.find(f => fs.existsSync(path.join(process.cwd(), f)));
|
|
361
|
+
return found ? [found] : [];
|
|
362
|
+
})();
|
|
363
|
+
const htmlFilePaths = htmlFileNames.map(n => path.resolve(process.cwd(), n));
|
|
364
|
+
// Find the right package.json for resolving deps. Walking up to the
|
|
365
|
+
// FIRST one is wrong when the html lives in a workspace child (e.g.
|
|
366
|
+
// `app/client/index.html` — `client/package.json` declares only the
|
|
367
|
+
// local workspace's direct deps; the runtime deps the html actually
|
|
368
|
+
// uses are declared in the workspace ROOT's package.json one level up).
|
|
369
|
+
//
|
|
370
|
+
// Strategy: walk up; prefer the OUTERMOST package.json that has a
|
|
371
|
+
// `workspaces` field (= monorepo root). Fall back to the nearest one
|
|
372
|
+
// when no workspace ancestor exists. Both cases pick correctly.
|
|
373
|
+
function findEffectivePackageJson(startDir) {
|
|
374
|
+
let nearest = null;
|
|
375
|
+
let workspaceRoot = null;
|
|
376
|
+
let dir = startDir;
|
|
377
|
+
while (true) {
|
|
378
|
+
const candidate = path.join(dir, 'package.json');
|
|
379
|
+
if (fs.existsSync(candidate)) {
|
|
380
|
+
if (!nearest)
|
|
381
|
+
nearest = candidate;
|
|
382
|
+
try {
|
|
383
|
+
const pkg = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
|
|
384
|
+
if (pkg.workspaces)
|
|
385
|
+
workspaceRoot = candidate;
|
|
386
|
+
}
|
|
387
|
+
catch { /* malformed — skip */ }
|
|
388
|
+
}
|
|
389
|
+
const parent = path.dirname(dir);
|
|
390
|
+
if (parent === dir)
|
|
391
|
+
break;
|
|
392
|
+
dir = parent;
|
|
393
|
+
}
|
|
394
|
+
return workspaceRoot ?? nearest;
|
|
395
|
+
}
|
|
396
|
+
if (htmlFilePaths.length === 0) {
|
|
397
|
+
console.error(`${ts()} [generate-importmap] Error: No HTML file found. Looking for: ${possibleHtmlFiles.join(', ')}`);
|
|
261
398
|
process.exit(1);
|
|
262
399
|
}
|
|
263
|
-
|
|
264
|
-
if (
|
|
265
|
-
console.error(`${ts()} [generate-importmap] Error: ${
|
|
400
|
+
for (const p of htmlFilePaths) {
|
|
401
|
+
if (!fs.existsSync(p)) {
|
|
402
|
+
console.error(`${ts()} [generate-importmap] Error: ${p} not found`);
|
|
403
|
+
process.exit(1);
|
|
266
404
|
}
|
|
267
|
-
else {
|
|
268
|
-
console.error(`${ts()} [generate-importmap] Error: No HTML file found. Looking for: ${possibleHtmlFiles.join(', ')}`);
|
|
269
|
-
}
|
|
270
|
-
process.exit(1);
|
|
271
405
|
}
|
|
406
|
+
// Resolve effective package.json per html file (typically the same
|
|
407
|
+
// for all targets in a single project, but compute per-file to handle
|
|
408
|
+
// mixed cases).
|
|
409
|
+
const targets = htmlFilePaths.map(htmlPath => {
|
|
410
|
+
const pkgPath = findEffectivePackageJson(path.dirname(htmlPath)) ?? path.join(process.cwd(), 'package.json');
|
|
411
|
+
if (!fs.existsSync(pkgPath)) {
|
|
412
|
+
console.error(`${ts()} [generate-importmap] Error: no package.json found from ${path.dirname(htmlPath)} or any ancestor`);
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
return { htmlPath, pkgPath };
|
|
416
|
+
});
|
|
417
|
+
const uniquePkgPaths = Array.from(new Set(targets.map(t => t.pkgPath)));
|
|
272
418
|
if (watchMode) {
|
|
273
|
-
let result = generateImportMap(packageJsonPath, htmlFilePath);
|
|
274
419
|
let watchedDepDirs = new Set();
|
|
275
|
-
const watcher = chokidar.watch(
|
|
420
|
+
const watcher = chokidar.watch(uniquePkgPaths, {
|
|
276
421
|
persistent: true,
|
|
277
422
|
ignoreInitial: true
|
|
278
423
|
});
|
|
279
|
-
function
|
|
280
|
-
const newDirs = new Set(
|
|
424
|
+
function regenerateAll() {
|
|
425
|
+
const newDirs = new Set();
|
|
426
|
+
for (const t of targets) {
|
|
427
|
+
const r = generateImportMap(t.pkgPath, t.htmlPath);
|
|
428
|
+
for (const d of r.depDirs)
|
|
429
|
+
newDirs.add(d);
|
|
430
|
+
}
|
|
281
431
|
for (const dir of watchedDepDirs) {
|
|
282
|
-
if (!newDirs.has(dir))
|
|
432
|
+
if (!newDirs.has(dir))
|
|
283
433
|
watcher.unwatch(dir);
|
|
284
|
-
}
|
|
285
434
|
}
|
|
286
435
|
for (const dir of newDirs) {
|
|
287
|
-
if (!watchedDepDirs.has(dir))
|
|
436
|
+
if (!watchedDepDirs.has(dir))
|
|
288
437
|
watcher.add(dir);
|
|
289
|
-
}
|
|
290
438
|
}
|
|
291
439
|
watchedDepDirs = newDirs;
|
|
292
440
|
}
|
|
293
|
-
|
|
441
|
+
regenerateAll();
|
|
294
442
|
let debounceTimer = null;
|
|
295
443
|
const onChange = () => {
|
|
296
444
|
if (debounceTimer)
|
|
297
445
|
clearTimeout(debounceTimer);
|
|
298
|
-
debounceTimer = setTimeout(
|
|
299
|
-
result = generateImportMap(packageJsonPath, htmlFilePath);
|
|
300
|
-
syncWatchedDeps(result.depDirs);
|
|
301
|
-
}, 100);
|
|
446
|
+
debounceTimer = setTimeout(regenerateAll, 100);
|
|
302
447
|
};
|
|
303
448
|
watcher.on('change', onChange);
|
|
304
449
|
watcher.on('add', onChange);
|
|
305
450
|
watcher.on('unlink', onChange);
|
|
306
451
|
watcher.on('ready', () => {
|
|
307
|
-
const total =
|
|
308
|
-
console.log(`${ts()} [generate-importmap] Watching ${total} directories for changes...`);
|
|
452
|
+
const total = uniquePkgPaths.length + watchedDepDirs.size;
|
|
453
|
+
console.log(`${ts()} [generate-importmap] Watching ${total} directories for changes (${targets.length} HTML target${targets.length === 1 ? '' : 's'})...`);
|
|
309
454
|
if (watchedDepDirs.size > 0) {
|
|
310
455
|
for (const dir of watchedDepDirs) {
|
|
311
456
|
console.log(` ${dir}`);
|
|
@@ -317,7 +462,9 @@ if (import.meta.main) {
|
|
|
317
462
|
});
|
|
318
463
|
}
|
|
319
464
|
else {
|
|
320
|
-
|
|
465
|
+
for (const t of targets) {
|
|
466
|
+
generateImportMap(t.pkgPath, t.htmlPath);
|
|
467
|
+
}
|
|
321
468
|
}
|
|
322
469
|
}
|
|
323
470
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED