@bobfrankston/importgen 0.1.29 → 0.1.30

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.
Files changed (3) hide show
  1. package/index-new.js +305 -0
  2. package/index.js +28 -19
  3. package/package.json +1 -1
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 version - look in node_modules
33
- const nodeModulesPath = path.join(packageDir, 'node_modules', depName);
34
- if (fs.existsSync(nodeModulesPath)) {
35
- return nodeModulesPath;
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 (follows symlinks)
126
- // Check if hoisted to top-level node_modules first
127
- const topLevelDir = path.join(htmlDir, 'node_modules', depName);
128
- let depPrefix;
129
- if (fs.existsSync(topLevelDir)) {
130
- depPrefix = `./node_modules/${depName}`;
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
- else {
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}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/importgen",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "Generate ES Module import maps from package.json dependencies for native browser module loading",
5
5
  "main": "index.js",
6
6
  "exports": {