@bobfrankston/importgen 0.1.29 → 0.1.31
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/README.md +26 -0
- package/index-new.js +305 -0
- package/index.js +28 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -137,6 +137,32 @@ For each dependency, `importgen` reads that package's `package.json` and picks t
|
|
|
137
137
|
- **`file:` paths** (`file:../path/to/package`) -- resolved relative to `package.json`
|
|
138
138
|
- **`workspace:` references** -- resolved from `node_modules/`
|
|
139
139
|
|
|
140
|
+
## Monorepo / npm Workspaces
|
|
141
|
+
|
|
142
|
+
`importgen` works in npm workspaces (monorepos) without any configuration. When a dependency isn't in the local `node_modules/`, it walks up the directory tree checking each ancestor's `node_modules/` -- the same resolution strategy Node uses. The import map path is generated relative to the HTML file.
|
|
143
|
+
|
|
144
|
+
Example layout:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
my-monorepo/ ← root, has node_modules/quill/
|
|
148
|
+
packages/
|
|
149
|
+
client/ ← workspace, package.json lists quill
|
|
150
|
+
compose/
|
|
151
|
+
compose.html ← importgen target
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Running `importgen compose/compose.html` from `client/` finds `quill` in the root `node_modules/` and generates:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"imports": {
|
|
159
|
+
"quill": "../../node_modules/quill/dist/quill.js"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
If the dependency exists in the local `node_modules/` it is found there first (existing behavior preserved).
|
|
165
|
+
|
|
140
166
|
## `.dependencies` Override
|
|
141
167
|
|
|
142
168
|
Add a `.dependencies` field to `package.json` to override resolution paths for specific packages without changing the real `dependencies`:
|
package/index-new.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate ES Module import map from package.json dependencies
|
|
4
|
+
* Injects into HTML files for native browser module loading
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import chokidar from 'chokidar';
|
|
9
|
+
import packageJson from './package.json' with { type: 'json' };
|
|
10
|
+
/** Timestamp prefix for log messages */
|
|
11
|
+
function ts() {
|
|
12
|
+
return `[${new Date().toLocaleTimeString()}]`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolve dependency path based on version specifier
|
|
16
|
+
*/
|
|
17
|
+
function resolveDependencyPath(packageDir, depName, depVersion) {
|
|
18
|
+
if (depVersion.startsWith('file:')) {
|
|
19
|
+
// Handle file: protocol - resolve relative to package directory
|
|
20
|
+
const relativePath = depVersion.slice(5); // Remove 'file:' prefix
|
|
21
|
+
return path.resolve(packageDir, relativePath);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
// npm or workspace dependency - walk up looking for node_modules (monorepo support)
|
|
25
|
+
let dir = packageDir;
|
|
26
|
+
while (true) {
|
|
27
|
+
const candidate = path.join(dir, 'node_modules', depName);
|
|
28
|
+
if (fs.existsSync(candidate)) {
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
const parent = path.dirname(dir);
|
|
32
|
+
if (parent === dir)
|
|
33
|
+
break; // filesystem root
|
|
34
|
+
dir = parent;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve entry point from package.json
|
|
41
|
+
*/
|
|
42
|
+
function resolveEntryPoint(pkg, packageDir) {
|
|
43
|
+
// 1. Check exports field (modern)
|
|
44
|
+
if (pkg.exports) {
|
|
45
|
+
if (typeof pkg.exports === 'string') {
|
|
46
|
+
return pkg.exports;
|
|
47
|
+
}
|
|
48
|
+
else if (pkg.exports['.']) {
|
|
49
|
+
const dotExport = pkg.exports['.'];
|
|
50
|
+
if (typeof dotExport === 'string') {
|
|
51
|
+
return dotExport;
|
|
52
|
+
}
|
|
53
|
+
else if (dotExport.import) {
|
|
54
|
+
return dotExport.import;
|
|
55
|
+
}
|
|
56
|
+
else if (dotExport.default) {
|
|
57
|
+
return dotExport.default;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// 2. Check module field (ESM)
|
|
62
|
+
if (pkg.module) {
|
|
63
|
+
return pkg.module;
|
|
64
|
+
}
|
|
65
|
+
// 3. Fall back to main field
|
|
66
|
+
if (pkg.main) {
|
|
67
|
+
return pkg.main;
|
|
68
|
+
}
|
|
69
|
+
// 4. Default to index.js
|
|
70
|
+
return './index.js';
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Recursively collect all dependencies, avoiding circular references.
|
|
74
|
+
* Import map paths are computed relative to the HTML file's directory.
|
|
75
|
+
*/
|
|
76
|
+
function collectDependencies(packageJsonPath, visited, dependencies, htmlDir, warnings, depPaths) {
|
|
77
|
+
try {
|
|
78
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
79
|
+
const packageDir = path.dirname(packageJsonPath);
|
|
80
|
+
const deps = packageJson.dependencies || {};
|
|
81
|
+
const dotDeps = packageJson['.dependencies'] || {};
|
|
82
|
+
for (const [depName, depVersion] of Object.entries(deps)) {
|
|
83
|
+
// Skip if already processed (circular dependency protection)
|
|
84
|
+
if (visited.has(depName)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
visited.add(depName);
|
|
88
|
+
// Check if .dependencies has an override path for this package
|
|
89
|
+
const overrideVersion = dotDeps[depName];
|
|
90
|
+
const versionToUse = overrideVersion || depVersion;
|
|
91
|
+
// Resolve dependency path (real filesystem location)
|
|
92
|
+
let depPath = resolveDependencyPath(packageDir, depName, versionToUse);
|
|
93
|
+
if (!depPath) {
|
|
94
|
+
const warning = `Could not follow path for ${depName} (${versionToUse})`;
|
|
95
|
+
console.warn(`${ts()} [generate-importmap] Warning: ${warning}`);
|
|
96
|
+
warnings.push(warning);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// Check for .dependency backup directory and use it if it exists
|
|
100
|
+
const dependencyBackupPath = depPath + '.dependency';
|
|
101
|
+
if (fs.existsSync(dependencyBackupPath)) {
|
|
102
|
+
const warning = `Using backup path ${dependencyBackupPath}`;
|
|
103
|
+
console.warn(`${ts()} [generate-importmap] Warning: ${warning}`);
|
|
104
|
+
warnings.push(warning);
|
|
105
|
+
depPath = dependencyBackupPath;
|
|
106
|
+
}
|
|
107
|
+
// Track resolved dependency directory for watch mode
|
|
108
|
+
if (depPaths) {
|
|
109
|
+
depPaths.add(depPath);
|
|
110
|
+
}
|
|
111
|
+
const depPackageJsonPath = path.join(depPath, 'package.json');
|
|
112
|
+
if (!fs.existsSync(depPackageJsonPath)) {
|
|
113
|
+
const warning = `Could not follow path for ${depName} - no package.json found at ${depPath}`;
|
|
114
|
+
console.warn(`${ts()} [generate-importmap] Warning: ${warning}`);
|
|
115
|
+
warnings.push(warning);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// Read dependency's package.json
|
|
119
|
+
const depPackageJson = JSON.parse(fs.readFileSync(depPackageJsonPath, 'utf-8'));
|
|
120
|
+
// Resolve entry point
|
|
121
|
+
const entryPoint = resolveEntryPoint(depPackageJson, depPath);
|
|
122
|
+
const entryFile = entryPoint.startsWith('./') ? entryPoint.slice(2) : entryPoint;
|
|
123
|
+
// Generate import path relative to HTML directory
|
|
124
|
+
const relPath = path.relative(htmlDir, depPath).split(path.sep).join('/');
|
|
125
|
+
const depPrefix = relPath.startsWith('.') ? relPath : `./${relPath}`;
|
|
126
|
+
dependencies.set(depName, `${depPrefix}/${entryFile}`);
|
|
127
|
+
// Recursively process this dependency's dependencies
|
|
128
|
+
collectDependencies(depPackageJsonPath, visited, dependencies, htmlDir, warnings, depPaths);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
console.error(`${ts()} [generate-importmap] Error processing ${packageJsonPath}:`, e.message);
|
|
133
|
+
throw e; // Propagate so callers can avoid overwriting HTML with empty map
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function generateImportMap(packageJsonPath, htmlFilePath) {
|
|
137
|
+
const result = { depDirs: [] };
|
|
138
|
+
try {
|
|
139
|
+
const htmlDir = path.dirname(htmlFilePath);
|
|
140
|
+
const visited = new Set();
|
|
141
|
+
const dependencies = new Map();
|
|
142
|
+
const depPaths = new Set();
|
|
143
|
+
const warnings = [];
|
|
144
|
+
// Recursively collect all dependencies
|
|
145
|
+
try {
|
|
146
|
+
collectDependencies(packageJsonPath, visited, dependencies, htmlDir, warnings, depPaths);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// Root package.json unreadable (e.g., mid-write) — skip update to preserve existing HTML
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
result.depDirs = Array.from(depPaths);
|
|
153
|
+
// Convert Map to plain object for JSON
|
|
154
|
+
const imports = {};
|
|
155
|
+
for (const [name, importPath] of dependencies) {
|
|
156
|
+
imports[name] = importPath;
|
|
157
|
+
}
|
|
158
|
+
// Generate warning comments
|
|
159
|
+
let warningComments = '';
|
|
160
|
+
if (warnings.length > 0) {
|
|
161
|
+
warningComments = warnings.map(w => `<!-- importgen: ${w} -->`).join('\n ') + '\n ';
|
|
162
|
+
}
|
|
163
|
+
const timestamp = new Date().toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'long' });
|
|
164
|
+
const generatedComment = `<!-- Generated by importgen ${packageJson.version} on ${timestamp} -->`;
|
|
165
|
+
const importMapScript = `${warningComments}${generatedComment}\n <script type="importmap">
|
|
166
|
+
${JSON.stringify({ imports }, null, 12)}
|
|
167
|
+
</script>`;
|
|
168
|
+
// Read HTML file
|
|
169
|
+
let html = fs.readFileSync(htmlFilePath, 'utf-8');
|
|
170
|
+
// Replace import map section (including importgen comments and generated-by timestamp)
|
|
171
|
+
const importMapRegex = /(<!--\s*importgen:.*?-->\s*)*(<!--\s*Generated by importgen.*?-->\s*)?<script type="importmap">[\s\S]*?<\/script>/;
|
|
172
|
+
if (importMapRegex.test(html)) {
|
|
173
|
+
html = html.replace(importMapRegex, importMapScript);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// Insert before </head> if not found
|
|
177
|
+
html = html.replace('</head>', ` ${importMapScript}\n</head>`);
|
|
178
|
+
}
|
|
179
|
+
// Write back to HTML file
|
|
180
|
+
fs.writeFileSync(htmlFilePath, html, 'utf-8');
|
|
181
|
+
console.log(`${ts()} [generate-importmap] Updated import map`);
|
|
182
|
+
console.log(` Scanned ${visited.size} dependencies, generated ${dependencies.size} entries`);
|
|
183
|
+
if (dependencies.size > 0) {
|
|
184
|
+
console.log(' Packages:', Array.from(dependencies.keys()).join(', '));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
console.error(`${ts()} [generate-importmap] Error:`, e.message);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* API: run importgen on a directory.
|
|
195
|
+
* @param appDir - directory containing package.json and HTML file (defaults to cwd)
|
|
196
|
+
* @param htmlFile - HTML filename to update (auto-detected if omitted)
|
|
197
|
+
* @returns GenerateResult with resolved dependency directories
|
|
198
|
+
*/
|
|
199
|
+
export function importgen(appDir, htmlFile) {
|
|
200
|
+
const dir = appDir || process.cwd();
|
|
201
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
202
|
+
if (!fs.existsSync(pkgPath)) {
|
|
203
|
+
throw new Error(`package.json not found in ${dir}`);
|
|
204
|
+
}
|
|
205
|
+
const possibleHtmlFiles = ['index.html', 'default.html', 'default.htm'];
|
|
206
|
+
const htmlName = htmlFile || possibleHtmlFiles.find(f => fs.existsSync(path.join(dir, f)));
|
|
207
|
+
const htmlPath = htmlName ? path.join(dir, htmlName) : null;
|
|
208
|
+
if (!htmlPath || !fs.existsSync(htmlPath)) {
|
|
209
|
+
throw new Error(htmlFile
|
|
210
|
+
? `${htmlFile} not found in ${dir}`
|
|
211
|
+
: `No HTML file found in ${dir}. Looking for: ${possibleHtmlFiles.join(', ')}`);
|
|
212
|
+
}
|
|
213
|
+
return generateImportMap(pkgPath, htmlPath);
|
|
214
|
+
}
|
|
215
|
+
// CLI entry point
|
|
216
|
+
if (import.meta.main) {
|
|
217
|
+
const args = process.argv.slice(2);
|
|
218
|
+
if (args.includes('-v') || args.includes('--version')) {
|
|
219
|
+
console.log(`importgen ${packageJson.version}`);
|
|
220
|
+
process.exit(0);
|
|
221
|
+
}
|
|
222
|
+
const knownFlags = new Set(['-w', '--watch', '-v', '--version']);
|
|
223
|
+
const unknownArgs = args.filter(a => a.startsWith('-') && !knownFlags.has(a));
|
|
224
|
+
if (unknownArgs.length > 0) {
|
|
225
|
+
console.error(`${ts()} [importgen] Error: unrecognized argument(s): ${unknownArgs.join(', ')}`);
|
|
226
|
+
console.error(` Usage: importgen [htmlfile] [-w|--watch] [-v|--version]`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
const watchMode = args.includes('-w') || args.includes('--watch');
|
|
230
|
+
const positionalArgs = args.filter(a => !a.startsWith('-'));
|
|
231
|
+
if (positionalArgs.length > 1) {
|
|
232
|
+
console.error(`${ts()} [importgen] Error: too many arguments: ${positionalArgs.join(', ')}`);
|
|
233
|
+
console.error(` Usage: importgen [htmlfile] [-w|--watch] [-v|--version]`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
const htmlArg = positionalArgs[0];
|
|
237
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
238
|
+
const possibleHtmlFiles = ['index.html', 'default.html', 'default.htm'];
|
|
239
|
+
const htmlFileName = htmlArg || possibleHtmlFiles.find(f => fs.existsSync(path.join(process.cwd(), f)));
|
|
240
|
+
const htmlFilePath = htmlFileName ? path.join(process.cwd(), htmlFileName) : null;
|
|
241
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
242
|
+
console.error(`${ts()} [generate-importmap] Error: package.json not found in current directory`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
if (!htmlFilePath || !fs.existsSync(htmlFilePath)) {
|
|
246
|
+
if (htmlArg) {
|
|
247
|
+
console.error(`${ts()} [generate-importmap] Error: ${htmlArg} not found in current directory`);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
console.error(`${ts()} [generate-importmap] Error: No HTML file found. Looking for: ${possibleHtmlFiles.join(', ')}`);
|
|
251
|
+
}
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
if (watchMode) {
|
|
255
|
+
let result = generateImportMap(packageJsonPath, htmlFilePath);
|
|
256
|
+
let watchedDepDirs = new Set();
|
|
257
|
+
const watcher = chokidar.watch([packageJsonPath], {
|
|
258
|
+
persistent: true,
|
|
259
|
+
ignoreInitial: true
|
|
260
|
+
});
|
|
261
|
+
function syncWatchedDeps(depDirs) {
|
|
262
|
+
const newDirs = new Set(depDirs);
|
|
263
|
+
for (const dir of watchedDepDirs) {
|
|
264
|
+
if (!newDirs.has(dir)) {
|
|
265
|
+
watcher.unwatch(dir);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
for (const dir of newDirs) {
|
|
269
|
+
if (!watchedDepDirs.has(dir)) {
|
|
270
|
+
watcher.add(dir);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
watchedDepDirs = newDirs;
|
|
274
|
+
}
|
|
275
|
+
syncWatchedDeps(result.depDirs);
|
|
276
|
+
let debounceTimer = null;
|
|
277
|
+
const onChange = () => {
|
|
278
|
+
if (debounceTimer)
|
|
279
|
+
clearTimeout(debounceTimer);
|
|
280
|
+
debounceTimer = setTimeout(() => {
|
|
281
|
+
result = generateImportMap(packageJsonPath, htmlFilePath);
|
|
282
|
+
syncWatchedDeps(result.depDirs);
|
|
283
|
+
}, 100);
|
|
284
|
+
};
|
|
285
|
+
watcher.on('change', onChange);
|
|
286
|
+
watcher.on('add', onChange);
|
|
287
|
+
watcher.on('unlink', onChange);
|
|
288
|
+
watcher.on('ready', () => {
|
|
289
|
+
const total = 1 + watchedDepDirs.size;
|
|
290
|
+
console.log(`${ts()} [generate-importmap] Watching ${total} directories for changes...`);
|
|
291
|
+
if (watchedDepDirs.size > 0) {
|
|
292
|
+
for (const dir of watchedDepDirs) {
|
|
293
|
+
console.log(` ${dir}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
watcher.on('error', (error) => {
|
|
298
|
+
console.error(`${ts()} [generate-importmap] Watcher error:`, error.message);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
generateImportMap(packageJsonPath, htmlFilePath);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
//# sourceMappingURL=index.js.map
|
package/index.js
CHANGED
|
@@ -20,19 +20,18 @@ function resolveDependencyPath(packageDir, depName, depVersion) {
|
|
|
20
20
|
const relativePath = depVersion.slice(5); // Remove 'file:' prefix
|
|
21
21
|
return path.resolve(packageDir, relativePath);
|
|
22
22
|
}
|
|
23
|
-
else if (depVersion.startsWith('workspace:')) {
|
|
24
|
-
// Workspace dependencies - look in node_modules
|
|
25
|
-
const nodeModulesPath = path.join(packageDir, 'node_modules', depName);
|
|
26
|
-
if (fs.existsSync(nodeModulesPath)) {
|
|
27
|
-
return nodeModulesPath;
|
|
28
|
-
}
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
23
|
else {
|
|
32
|
-
// npm
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
24
|
+
// npm or workspace dependency - walk up looking for node_modules (monorepo support)
|
|
25
|
+
let dir = packageDir;
|
|
26
|
+
while (true) {
|
|
27
|
+
const candidate = path.join(dir, 'node_modules', depName);
|
|
28
|
+
if (fs.existsSync(candidate)) {
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
const parent = path.dirname(dir);
|
|
32
|
+
if (parent === dir)
|
|
33
|
+
break; // filesystem root
|
|
34
|
+
dir = parent;
|
|
36
35
|
}
|
|
37
36
|
return null;
|
|
38
37
|
}
|
|
@@ -122,14 +121,24 @@ function collectDependencies(packageJsonPath, visited, dependencies, htmlDir, no
|
|
|
122
121
|
// Resolve entry point
|
|
123
122
|
const entryPoint = resolveEntryPoint(depPackageJson, depPath);
|
|
124
123
|
const entryFile = entryPoint.startsWith('./') ? entryPoint.slice(2) : entryPoint;
|
|
125
|
-
// Generate import map path through node_modules
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
let
|
|
129
|
-
|
|
130
|
-
|
|
124
|
+
// Generate import map path through node_modules
|
|
125
|
+
// Walk up from htmlDir looking for node_modules/depName (handles monorepo hoisting)
|
|
126
|
+
let depPrefix = null;
|
|
127
|
+
let searchDir = htmlDir;
|
|
128
|
+
while (true) {
|
|
129
|
+
const candidate = path.join(searchDir, 'node_modules', depName);
|
|
130
|
+
if (fs.existsSync(candidate)) {
|
|
131
|
+
const rel = path.relative(htmlDir, candidate).split(path.sep).join('/');
|
|
132
|
+
depPrefix = rel.startsWith('.') ? rel : `./${rel}`;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
const parent = path.dirname(searchDir);
|
|
136
|
+
if (parent === searchDir)
|
|
137
|
+
break; // filesystem root
|
|
138
|
+
searchDir = parent;
|
|
131
139
|
}
|
|
132
|
-
|
|
140
|
+
if (!depPrefix) {
|
|
141
|
+
// Fallback for deps not in any ancestor node_modules
|
|
133
142
|
depPrefix = `${nodeModulesPrefix}/${depName}`;
|
|
134
143
|
}
|
|
135
144
|
dependencies.set(depName, `${depPrefix}/${entryFile}`);
|