@bobfrankston/importgen 0.1.35 → 0.1.36
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 +180 -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,18 @@ function collectDependencies(packageJsonPath, visited, dependencies, htmlDir, no
|
|
|
138
211
|
searchDir = parent;
|
|
139
212
|
}
|
|
140
213
|
if (!depPrefix) {
|
|
141
|
-
//
|
|
142
|
-
|
|
214
|
+
// Not in any ancestor node_modules (e.g. file: dep outside
|
|
215
|
+
// the workspace, no junction yet). Use the resolved real
|
|
216
|
+
// path relative to htmlDir — strictly more correct than
|
|
217
|
+
// the old `nodeModulesPrefix/depName` guess.
|
|
218
|
+
const rel = path.relative(htmlDir, depPath).split(path.sep).join('/');
|
|
219
|
+
depPrefix = rel.startsWith('.') ? rel : `./${rel}`;
|
|
143
220
|
}
|
|
144
221
|
dependencies.set(depName, `${depPrefix}/${entryFile}`);
|
|
222
|
+
// Bug 3: trailing-slash variant for deep imports
|
|
223
|
+
// (`import "pkg/sub.js"` requires `"pkg/": "<root>/"`).
|
|
224
|
+
// Harmless when unused — browsers only fetch what's imported.
|
|
225
|
+
dependencies.set(`${depName}/`, `${depPrefix}/`);
|
|
145
226
|
// Recursively process this dependency's dependencies
|
|
146
227
|
collectDependencies(depPackageJsonPath, visited, dependencies, htmlDir, `${depPrefix}/node_modules`, warnings, depPaths);
|
|
147
228
|
}
|
|
@@ -241,71 +322,113 @@ if (import.meta.main) {
|
|
|
241
322
|
const unknownArgs = args.filter(a => a.startsWith('-') && !knownFlags.has(a));
|
|
242
323
|
if (unknownArgs.length > 0) {
|
|
243
324
|
console.error(`${ts()} [importgen] Error: unrecognized argument(s): ${unknownArgs.join(', ')}`);
|
|
244
|
-
console.error(` Usage: importgen [htmlfile] [-w|--watch] [-v|--version]`);
|
|
325
|
+
console.error(` Usage: importgen [htmlfile...] [-w|--watch] [-v|--version]`);
|
|
245
326
|
process.exit(1);
|
|
246
327
|
}
|
|
247
328
|
const watchMode = args.includes('-w') || args.includes('--watch');
|
|
248
329
|
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');
|
|
330
|
+
// Bug 5: accept multiple HTML targets. Each gets its own import map
|
|
331
|
+
// with paths rewritten relative to that file's directory. Useful for
|
|
332
|
+
// multi-page apps where each html has its own document/module context
|
|
333
|
+
// (e.g. mailx: index.html, compose/compose.html, …).
|
|
256
334
|
const possibleHtmlFiles = ['index.html', 'default.html', 'default.htm'];
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
335
|
+
const htmlFileNames = positionalArgs.length > 0
|
|
336
|
+
? positionalArgs
|
|
337
|
+
: (() => {
|
|
338
|
+
const found = possibleHtmlFiles.find(f => fs.existsSync(path.join(process.cwd(), f)));
|
|
339
|
+
return found ? [found] : [];
|
|
340
|
+
})();
|
|
341
|
+
const htmlFilePaths = htmlFileNames.map(n => path.resolve(process.cwd(), n));
|
|
342
|
+
// Find the right package.json for resolving deps. Walking up to the
|
|
343
|
+
// FIRST one is wrong when the html lives in a workspace child (e.g.
|
|
344
|
+
// `app/client/index.html` — `client/package.json` declares only the
|
|
345
|
+
// local workspace's direct deps; the runtime deps the html actually
|
|
346
|
+
// uses are declared in the workspace ROOT's package.json one level up).
|
|
347
|
+
//
|
|
348
|
+
// Strategy: walk up; prefer the OUTERMOST package.json that has a
|
|
349
|
+
// `workspaces` field (= monorepo root). Fall back to the nearest one
|
|
350
|
+
// when no workspace ancestor exists. Both cases pick correctly.
|
|
351
|
+
function findEffectivePackageJson(startDir) {
|
|
352
|
+
let nearest = null;
|
|
353
|
+
let workspaceRoot = null;
|
|
354
|
+
let dir = startDir;
|
|
355
|
+
while (true) {
|
|
356
|
+
const candidate = path.join(dir, 'package.json');
|
|
357
|
+
if (fs.existsSync(candidate)) {
|
|
358
|
+
if (!nearest)
|
|
359
|
+
nearest = candidate;
|
|
360
|
+
try {
|
|
361
|
+
const pkg = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
|
|
362
|
+
if (pkg.workspaces)
|
|
363
|
+
workspaceRoot = candidate;
|
|
364
|
+
}
|
|
365
|
+
catch { /* malformed — skip */ }
|
|
366
|
+
}
|
|
367
|
+
const parent = path.dirname(dir);
|
|
368
|
+
if (parent === dir)
|
|
369
|
+
break;
|
|
370
|
+
dir = parent;
|
|
371
|
+
}
|
|
372
|
+
return workspaceRoot ?? nearest;
|
|
373
|
+
}
|
|
374
|
+
if (htmlFilePaths.length === 0) {
|
|
375
|
+
console.error(`${ts()} [generate-importmap] Error: No HTML file found. Looking for: ${possibleHtmlFiles.join(', ')}`);
|
|
261
376
|
process.exit(1);
|
|
262
377
|
}
|
|
263
|
-
|
|
264
|
-
if (
|
|
265
|
-
console.error(`${ts()} [generate-importmap] Error: ${
|
|
266
|
-
|
|
267
|
-
else {
|
|
268
|
-
console.error(`${ts()} [generate-importmap] Error: No HTML file found. Looking for: ${possibleHtmlFiles.join(', ')}`);
|
|
378
|
+
for (const p of htmlFilePaths) {
|
|
379
|
+
if (!fs.existsSync(p)) {
|
|
380
|
+
console.error(`${ts()} [generate-importmap] Error: ${p} not found`);
|
|
381
|
+
process.exit(1);
|
|
269
382
|
}
|
|
270
|
-
process.exit(1);
|
|
271
383
|
}
|
|
384
|
+
// Resolve effective package.json per html file (typically the same
|
|
385
|
+
// for all targets in a single project, but compute per-file to handle
|
|
386
|
+
// mixed cases).
|
|
387
|
+
const targets = htmlFilePaths.map(htmlPath => {
|
|
388
|
+
const pkgPath = findEffectivePackageJson(path.dirname(htmlPath)) ?? path.join(process.cwd(), 'package.json');
|
|
389
|
+
if (!fs.existsSync(pkgPath)) {
|
|
390
|
+
console.error(`${ts()} [generate-importmap] Error: no package.json found from ${path.dirname(htmlPath)} or any ancestor`);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
return { htmlPath, pkgPath };
|
|
394
|
+
});
|
|
395
|
+
const uniquePkgPaths = Array.from(new Set(targets.map(t => t.pkgPath)));
|
|
272
396
|
if (watchMode) {
|
|
273
|
-
let result = generateImportMap(packageJsonPath, htmlFilePath);
|
|
274
397
|
let watchedDepDirs = new Set();
|
|
275
|
-
const watcher = chokidar.watch(
|
|
398
|
+
const watcher = chokidar.watch(uniquePkgPaths, {
|
|
276
399
|
persistent: true,
|
|
277
400
|
ignoreInitial: true
|
|
278
401
|
});
|
|
279
|
-
function
|
|
280
|
-
const newDirs = new Set(
|
|
402
|
+
function regenerateAll() {
|
|
403
|
+
const newDirs = new Set();
|
|
404
|
+
for (const t of targets) {
|
|
405
|
+
const r = generateImportMap(t.pkgPath, t.htmlPath);
|
|
406
|
+
for (const d of r.depDirs)
|
|
407
|
+
newDirs.add(d);
|
|
408
|
+
}
|
|
281
409
|
for (const dir of watchedDepDirs) {
|
|
282
|
-
if (!newDirs.has(dir))
|
|
410
|
+
if (!newDirs.has(dir))
|
|
283
411
|
watcher.unwatch(dir);
|
|
284
|
-
}
|
|
285
412
|
}
|
|
286
413
|
for (const dir of newDirs) {
|
|
287
|
-
if (!watchedDepDirs.has(dir))
|
|
414
|
+
if (!watchedDepDirs.has(dir))
|
|
288
415
|
watcher.add(dir);
|
|
289
|
-
}
|
|
290
416
|
}
|
|
291
417
|
watchedDepDirs = newDirs;
|
|
292
418
|
}
|
|
293
|
-
|
|
419
|
+
regenerateAll();
|
|
294
420
|
let debounceTimer = null;
|
|
295
421
|
const onChange = () => {
|
|
296
422
|
if (debounceTimer)
|
|
297
423
|
clearTimeout(debounceTimer);
|
|
298
|
-
debounceTimer = setTimeout(
|
|
299
|
-
result = generateImportMap(packageJsonPath, htmlFilePath);
|
|
300
|
-
syncWatchedDeps(result.depDirs);
|
|
301
|
-
}, 100);
|
|
424
|
+
debounceTimer = setTimeout(regenerateAll, 100);
|
|
302
425
|
};
|
|
303
426
|
watcher.on('change', onChange);
|
|
304
427
|
watcher.on('add', onChange);
|
|
305
428
|
watcher.on('unlink', onChange);
|
|
306
429
|
watcher.on('ready', () => {
|
|
307
|
-
const total =
|
|
308
|
-
console.log(`${ts()} [generate-importmap] Watching ${total} directories for changes...`);
|
|
430
|
+
const total = uniquePkgPaths.length + watchedDepDirs.size;
|
|
431
|
+
console.log(`${ts()} [generate-importmap] Watching ${total} directories for changes (${targets.length} HTML target${targets.length === 1 ? '' : 's'})...`);
|
|
309
432
|
if (watchedDepDirs.size > 0) {
|
|
310
433
|
for (const dir of watchedDepDirs) {
|
|
311
434
|
console.log(` ${dir}`);
|
|
@@ -317,7 +440,9 @@ if (import.meta.main) {
|
|
|
317
440
|
});
|
|
318
441
|
}
|
|
319
442
|
else {
|
|
320
|
-
|
|
443
|
+
for (const t of targets) {
|
|
444
|
+
generateImportMap(t.pkgPath, t.htmlPath);
|
|
445
|
+
}
|
|
321
446
|
}
|
|
322
447
|
}
|
|
323
448
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED