@bobfrankston/importgen 0.1.10
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/.claude/settings.local.json +8 -0
- package/.hintrc +10 -0
- package/README.md +91 -0
- package/index.d.ts +7 -0
- package/index.js +238 -0
- package/package.json +37 -0
- package/tasks-prototype.json +24 -0
package/.hintrc
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @bobfrankston/importgen
|
|
2
|
+
|
|
3
|
+
Generate ES Module import maps from package.json dependencies for native browser module loading.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @bobfrankston/importgen
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
In a directory with `package.json` and `index.html`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Generate once
|
|
17
|
+
importgen
|
|
18
|
+
|
|
19
|
+
# Watch for package.json changes
|
|
20
|
+
importgen -w
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Automatic Generation with VS Code Tasks
|
|
24
|
+
|
|
25
|
+
Add to `.vscode/tasks.json`:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"version": "2.0.0",
|
|
30
|
+
"tasks": [
|
|
31
|
+
{
|
|
32
|
+
"label": "importgen: watch",
|
|
33
|
+
"type": "shell",
|
|
34
|
+
"command": "importgen",
|
|
35
|
+
"args": ["-w"],
|
|
36
|
+
"runOptions": {
|
|
37
|
+
"runOn": "folderOpen"
|
|
38
|
+
},
|
|
39
|
+
"isBackground": true
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## How It Works
|
|
46
|
+
|
|
47
|
+
1. Reads `package.json` dependencies
|
|
48
|
+
2. **Recursively resolves** all transitive dependencies
|
|
49
|
+
3. Handles `file:` protocol dependencies and npm packages
|
|
50
|
+
4. Detects and avoids circular dependencies
|
|
51
|
+
5. Resolves correct entry points using `exports`, `module`, or `main` fields
|
|
52
|
+
6. Generates import map with relative paths from HTML location
|
|
53
|
+
7. Injects or updates `<script type="importmap">` in `index.html`
|
|
54
|
+
8. In watch mode, regenerates when `package.json` changes
|
|
55
|
+
|
|
56
|
+
### Circular Dependency Protection
|
|
57
|
+
|
|
58
|
+
The tool tracks visited packages to prevent infinite loops when packages have circular dependencies (A → B → A). Each package is processed only once.
|
|
59
|
+
|
|
60
|
+
## PWA Compatible
|
|
61
|
+
|
|
62
|
+
Generated import maps are static and can be cached by service workers, making this ideal for Progressive Web Apps.
|
|
63
|
+
|
|
64
|
+
## Example
|
|
65
|
+
|
|
66
|
+
**package.json:**
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"@bobfrankston/lxlan": "^0.1.0",
|
|
71
|
+
"@bobfrankston/colorlib": "file:../colorlib"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
If `lxlan` depends on `lxlan-browser`, and `lxlan-browser` depends on `colorlib`, **all three packages** will be included in the generated import map:
|
|
77
|
+
|
|
78
|
+
**Generated in index.html:**
|
|
79
|
+
```html
|
|
80
|
+
<script type="importmap">
|
|
81
|
+
{
|
|
82
|
+
"imports": {
|
|
83
|
+
"@bobfrankston/lxlan": "./node_modules/@bobfrankston/lxlan/index.js",
|
|
84
|
+
"@bobfrankston/lxlan-browser": "./node_modules/@bobfrankston/lxlan-browser/index.js",
|
|
85
|
+
"@bobfrankston/colorlib": "../colorlib/index.js"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
</script>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Note: The tool automatically resolves transitive dependencies and handles both npm packages and `file:` protocol dependencies.
|
package/index.d.ts
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
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 { fileURLToPath } from 'node:url';
|
|
9
|
+
import chokidar from 'chokidar';
|
|
10
|
+
// Get version from package.json
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
|
|
14
|
+
const VERSION = packageJson.version;
|
|
15
|
+
/**
|
|
16
|
+
* Resolve dependency path based on version specifier
|
|
17
|
+
*/
|
|
18
|
+
function resolveDependencyPath(packageDir, depName, depVersion) {
|
|
19
|
+
if (depVersion.startsWith('file:')) {
|
|
20
|
+
// Handle file: protocol - resolve relative to package directory
|
|
21
|
+
const relativePath = depVersion.slice(5); // Remove 'file:' prefix
|
|
22
|
+
return path.resolve(packageDir, relativePath);
|
|
23
|
+
}
|
|
24
|
+
else if (depVersion.startsWith('workspace:')) {
|
|
25
|
+
// Workspace dependencies - look in node_modules
|
|
26
|
+
const nodeModulesPath = path.join(packageDir, 'node_modules', depName);
|
|
27
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
28
|
+
return nodeModulesPath;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
// npm version - look in node_modules
|
|
34
|
+
const nodeModulesPath = path.join(packageDir, 'node_modules', depName);
|
|
35
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
36
|
+
return nodeModulesPath;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolve entry point from package.json
|
|
43
|
+
*/
|
|
44
|
+
function resolveEntryPoint(pkg, packageDir) {
|
|
45
|
+
// 1. Check exports field (modern)
|
|
46
|
+
if (pkg.exports) {
|
|
47
|
+
if (typeof pkg.exports === 'string') {
|
|
48
|
+
return pkg.exports;
|
|
49
|
+
}
|
|
50
|
+
else if (pkg.exports['.']) {
|
|
51
|
+
const dotExport = pkg.exports['.'];
|
|
52
|
+
if (typeof dotExport === 'string') {
|
|
53
|
+
return dotExport;
|
|
54
|
+
}
|
|
55
|
+
else if (dotExport.import) {
|
|
56
|
+
return dotExport.import;
|
|
57
|
+
}
|
|
58
|
+
else if (dotExport.default) {
|
|
59
|
+
return dotExport.default;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// 2. Check module field (ESM)
|
|
64
|
+
if (pkg.module) {
|
|
65
|
+
return pkg.module;
|
|
66
|
+
}
|
|
67
|
+
// 3. Fall back to main field
|
|
68
|
+
if (pkg.main) {
|
|
69
|
+
return pkg.main;
|
|
70
|
+
}
|
|
71
|
+
// 4. Default to index.js
|
|
72
|
+
return './index.js';
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Recursively collect all dependencies, avoiding circular references
|
|
76
|
+
*/
|
|
77
|
+
function collectDependencies(packageJsonPath, visited, dependencies, htmlDir, currentPath = '') {
|
|
78
|
+
try {
|
|
79
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
80
|
+
const packageDir = path.dirname(packageJsonPath);
|
|
81
|
+
const deps = 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
|
+
// Resolve dependency path
|
|
89
|
+
const depPath = resolveDependencyPath(packageDir, depName, depVersion);
|
|
90
|
+
if (!depPath) {
|
|
91
|
+
console.warn(`[generate-importmap] Warning: Could not resolve ${depName}`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const depPackageJsonPath = path.join(depPath, 'package.json');
|
|
95
|
+
if (!fs.existsSync(depPackageJsonPath)) {
|
|
96
|
+
console.warn(`[generate-importmap] Warning: No package.json found for ${depName} at ${depPath}`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// Read dependency's package.json
|
|
100
|
+
const depPackageJson = JSON.parse(fs.readFileSync(depPackageJsonPath, 'utf-8'));
|
|
101
|
+
// Resolve entry point
|
|
102
|
+
const entryPoint = resolveEntryPoint(depPackageJson, depPath);
|
|
103
|
+
const cleanEntryPoint = entryPoint.startsWith('./') ? entryPoint.slice(2) : entryPoint;
|
|
104
|
+
// Build path relative to node_modules
|
|
105
|
+
let relativePath;
|
|
106
|
+
if (currentPath === '') {
|
|
107
|
+
// Top-level dependency - directly in node_modules
|
|
108
|
+
relativePath = `./node_modules/${depName}/${cleanEntryPoint}`;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Nested dependency - under parent's node_modules
|
|
112
|
+
relativePath = `./node_modules/${currentPath}/node_modules/${depName}/${cleanEntryPoint}`;
|
|
113
|
+
}
|
|
114
|
+
dependencies.set(depName, relativePath);
|
|
115
|
+
// Recursively process this dependency's dependencies
|
|
116
|
+
const nestedPath = currentPath === '' ? depName : `${currentPath}/node_modules/${depName}`;
|
|
117
|
+
collectDependencies(depPackageJsonPath, visited, dependencies, htmlDir, nestedPath);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
console.error(`[generate-importmap] Error processing ${packageJsonPath}:`, e.message);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function generateImportMap(packageJsonPath, htmlFilePath) {
|
|
125
|
+
try {
|
|
126
|
+
const htmlDir = path.dirname(htmlFilePath);
|
|
127
|
+
const visited = new Set();
|
|
128
|
+
const dependencies = new Map();
|
|
129
|
+
// Recursively collect all dependencies
|
|
130
|
+
collectDependencies(packageJsonPath, visited, dependencies, htmlDir);
|
|
131
|
+
// Convert Map to plain object for JSON
|
|
132
|
+
const imports = {};
|
|
133
|
+
for (const [name, importPath] of dependencies) {
|
|
134
|
+
imports[name] = importPath;
|
|
135
|
+
}
|
|
136
|
+
const now = new Date();
|
|
137
|
+
const timestamp = now.toLocaleString('en-US', {
|
|
138
|
+
year: 'numeric',
|
|
139
|
+
month: '2-digit',
|
|
140
|
+
day: '2-digit',
|
|
141
|
+
hour: '2-digit',
|
|
142
|
+
minute: '2-digit',
|
|
143
|
+
second: '2-digit',
|
|
144
|
+
hour12: false
|
|
145
|
+
}).replace(',', '');
|
|
146
|
+
const importMapScript = `<!-- ⚠️⚠️⚠️ WARNING: DO NOT MANUALLY EDIT THE IMPORT MAP BELOW ⚠️⚠️⚠️ -->
|
|
147
|
+
<!-- This import map is AUTO-GENERATED by 'importgen' from package.json dependencies -->
|
|
148
|
+
<!-- To update: modify package.json dependencies, then run: npm run importmap -->
|
|
149
|
+
<!-- NEVER manually edit the import map entries - they will be overwritten -->
|
|
150
|
+
<!-- @ts-expect-error: importmap not supported in older browsers -->
|
|
151
|
+
<!-- Generated by @bobfrankston/importgen v${VERSION} on ${timestamp} -->
|
|
152
|
+
<script type="importmap">
|
|
153
|
+
${JSON.stringify({ imports }, null, 12)}
|
|
154
|
+
</script>`;
|
|
155
|
+
// Read HTML file
|
|
156
|
+
let html = fs.readFileSync(htmlFilePath, 'utf-8');
|
|
157
|
+
// Replace import map section (including any preceding comments about importgen/warning)
|
|
158
|
+
const importMapRegex = /(?:<!--[^>]*?-->\s*)*<script type="importmap">[\s\S]*?<\/script>/;
|
|
159
|
+
if (importMapRegex.test(html)) {
|
|
160
|
+
html = html.replace(importMapRegex, importMapScript);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// Auto-init: insert before </head> if no import map found
|
|
164
|
+
console.log(`[generate-importmap] Initializing import map in ${path.basename(htmlFilePath)}`);
|
|
165
|
+
html = html.replace('</head>', ` ${importMapScript}\n</head>`);
|
|
166
|
+
}
|
|
167
|
+
// Write back to HTML file
|
|
168
|
+
fs.writeFileSync(htmlFilePath, html, 'utf-8');
|
|
169
|
+
console.log(`[generate-importmap v${VERSION}] Updated ${path.basename(htmlFilePath)}`);
|
|
170
|
+
console.log(` Scanned ${visited.size} dependencies, generated ${dependencies.size} entries`);
|
|
171
|
+
if (dependencies.size > 0) {
|
|
172
|
+
console.log(' Packages:', Array.from(dependencies.keys()).join(', '));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
console.error('[generate-importmap] Error:', e.message);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Find HTML file - checks for parameter, then searches for common defaults
|
|
182
|
+
*/
|
|
183
|
+
function findHtmlFile(specifiedFile) {
|
|
184
|
+
if (specifiedFile) {
|
|
185
|
+
const fullPath = path.isAbsolute(specifiedFile)
|
|
186
|
+
? specifiedFile
|
|
187
|
+
: path.join(process.cwd(), specifiedFile);
|
|
188
|
+
if (fs.existsSync(fullPath)) {
|
|
189
|
+
return fullPath;
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
// Search for common HTML file names
|
|
194
|
+
const candidates = ['index.html', 'default.html', 'default.htm'];
|
|
195
|
+
for (const candidate of candidates) {
|
|
196
|
+
const candidatePath = path.join(process.cwd(), candidate);
|
|
197
|
+
if (fs.existsSync(candidatePath)) {
|
|
198
|
+
return candidatePath;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
// Parse CLI arguments
|
|
204
|
+
const args = process.argv.slice(2);
|
|
205
|
+
const watchMode = args.includes('-w') || args.includes('--watch');
|
|
206
|
+
// Find HTML file argument (non-flag argument that ends with .html or .htm)
|
|
207
|
+
const htmlArg = args.find(arg => !arg.startsWith('-') && /\.html?$/i.test(arg));
|
|
208
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
209
|
+
const htmlFilePath = findHtmlFile(htmlArg);
|
|
210
|
+
// Check if files exist
|
|
211
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
212
|
+
console.error('[generate-importmap] Error: package.json not found in current directory');
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
if (!htmlFilePath) {
|
|
216
|
+
const searchedFiles = htmlArg ? htmlArg : 'index.html, default.html, default.htm';
|
|
217
|
+
console.error(`[generate-importmap] Error: HTML file not found (searched: ${searchedFiles})`);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
if (watchMode) {
|
|
221
|
+
generateImportMap(packageJsonPath, htmlFilePath);
|
|
222
|
+
console.log('[generate-importmap] Watching for changes...');
|
|
223
|
+
const watcher = chokidar.watch([packageJsonPath], {
|
|
224
|
+
persistent: true,
|
|
225
|
+
ignoreInitial: true
|
|
226
|
+
});
|
|
227
|
+
watcher.on('change', () => generateImportMap(packageJsonPath, htmlFilePath));
|
|
228
|
+
watcher.on('ready', () => {
|
|
229
|
+
console.log('[generate-importmap] Watcher is ready and running.');
|
|
230
|
+
});
|
|
231
|
+
watcher.on('error', (error) => {
|
|
232
|
+
console.error('[generate-importmap] Watcher error:', error.message);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
generateImportMap(packageJsonPath, htmlFilePath);
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bobfrankston/importgen",
|
|
3
|
+
"version": "0.1.10",
|
|
4
|
+
"description": "Generate ES Module import maps from package.json dependencies for native browser module loading",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"importgen": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"watch": "tsc -w",
|
|
13
|
+
"preversion": "npm run build",
|
|
14
|
+
"postversion": "echo Version updated to $(node -p \"require('./package.json').version\")",
|
|
15
|
+
"release": "git add -A && git diff-index --quiet HEAD || git commit -m 'Build for release' && npm version patch",
|
|
16
|
+
"installer": "npm run release && npm install -g ."
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"import-map",
|
|
20
|
+
"esm",
|
|
21
|
+
"browser",
|
|
22
|
+
"modules",
|
|
23
|
+
"dependencies"
|
|
24
|
+
],
|
|
25
|
+
"author": "Bob Frankston",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/BobFrankston/importgen.git"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"chokidar": "^4.0.3"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^25.0.9"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"label": "importgen: watch",
|
|
3
|
+
"type": "shell",
|
|
4
|
+
"command": "importgen",
|
|
5
|
+
"args": ["--watch"],
|
|
6
|
+
"runOptions": {
|
|
7
|
+
"runOn": "folderOpen"
|
|
8
|
+
},
|
|
9
|
+
"isBackground": true,
|
|
10
|
+
"problemMatcher": {
|
|
11
|
+
"pattern": {
|
|
12
|
+
"regexp": "^$",
|
|
13
|
+
"file": 1,
|
|
14
|
+
"location": 2,
|
|
15
|
+
"message": 3
|
|
16
|
+
},
|
|
17
|
+
"background": {
|
|
18
|
+
"activeOnStart": true,
|
|
19
|
+
"beginsPattern": "^\\[generate-importmap\\].*watching.*$",
|
|
20
|
+
"endsPattern": "^\\[generate-importmap\\].*Updated.*$"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
}
|