@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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm run build:*)",
5
+ "Bash(npmglobalize)"
6
+ ]
7
+ }
8
+ }
package/.hintrc ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": [
3
+ "development"
4
+ ],
5
+ "browserslist": [
6
+ "defaults",
7
+ "not ie 11",
8
+ "not ios_saf <= 15.8"
9
+ ]
10
+ }
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
@@ -0,0 +1,7 @@
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
+ export {};
7
+ //# sourceMappingURL=index.d.ts.map
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
+ }