@pwrs/mappa 0.0.4

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 ADDED
@@ -0,0 +1,284 @@
1
+ # Mappa
2
+
3
+ The high-performance import map generator for the modern web, available as a CLI tool and Go library.
4
+
5
+ **[Try the web demo](https://bennypowers.github.io/mappa/)** - Generate import maps directly in your browser.
6
+
7
+ ![mappa - import map generator](./docs/images/mappa.png)
8
+
9
+ > *Mappa* (מַפָּה) is Hebrew for "map".
10
+
11
+ Modern web applications use ES modules with bare specifiers like `import { html } from 'lit'`. Browsers need [import maps][importmaps] to resolve these specifiers to actual URLs.
12
+
13
+ Mappa generates import maps from your `package.json` dependencies, pointing to your local `node_modules` paths or to a path of your choosing (like `/assets/packages/...`. It's designed to be fast, with parallel dependency resolution.
14
+
15
+ ## Features
16
+
17
+ - **Local resolution**: Generate import maps pointing to your install node modules paths
18
+ - **Custom URL templates**: Use any asset path with `{package}` and `{path}` variables
19
+ - **Export maps**: Full support for package.json `exports` field including subpaths and wildcards
20
+ - **Scopes**: Automatic scope generation for transitive dependencies
21
+ - **Merge input maps**: Combine generated maps with manual overrides
22
+ - **Parallel resolution**: Fast transitive dependency resolution
23
+
24
+ ## Installation
25
+
26
+ ### npm
27
+
28
+ ```bash
29
+ npm install @pwrs/mappa
30
+ ```
31
+
32
+ Provides both a CLI (`npx mappa generate`) and a JavaScript API:
33
+
34
+ ```typescript
35
+ import { resolve } from '@pwrs/mappa';
36
+
37
+ const importMap = await resolve('.', {
38
+ template: '/assets/packages/{package}/{path}',
39
+ });
40
+ ```
41
+
42
+ ### Gentoo Linux
43
+
44
+ Enable the `bennypowers` overlay, then install:
45
+
46
+ ```bash
47
+ eselect repository enable bennypowers
48
+ emaint sync -r bennypowers
49
+ emerge dev-util/mappa
50
+ ```
51
+
52
+ ### From Source
53
+
54
+ ```bash
55
+ go install bennypowers.dev/mappa@latest
56
+ ```
57
+
58
+ ## Quick Start
59
+
60
+ Generate an import map for your project:
61
+
62
+ ```bash
63
+ # Local paths (default)
64
+ mappa generate
65
+
66
+ # Output as HTML script tag
67
+ mappa generate --format html
68
+
69
+ # Custom asset path
70
+ mappa generate --template "/assets/packages/{package}/{path}"
71
+ ```
72
+
73
+ ## CLI Reference
74
+
75
+ ### `mappa generate`
76
+
77
+ Generate an import map from `package.json` dependencies.
78
+
79
+ ```
80
+ Flags:
81
+ -f, --format string Output format: json, html (default "json")
82
+ --include-package Additional packages to include (repeatable)
83
+ --input-map string Import map file to merge with generated output
84
+ --template string URL template (default: /node_modules/{package}/{path})
85
+ -p, --package string Package directory (default ".")
86
+ -o, --output string Output file (default: stdout)
87
+ ```
88
+
89
+ **Examples:**
90
+
91
+ ```bash
92
+ # Include devDependencies
93
+ mappa generate --include-package fuse.js --include-package vitest
94
+
95
+ # Merge with manual overrides
96
+ mappa generate --input-map manual-imports.json
97
+
98
+ # Custom asset path
99
+ mappa generate --template "/assets/packages/{package}/{path}"
100
+ ```
101
+
102
+ ### `mappa trace`
103
+
104
+ Trace HTML files to discover ES module imports and generate minimal import maps containing only the specifiers actually used.
105
+
106
+ ```
107
+ Flags:
108
+ -f, --format string Output format: json, html, specifiers (default "json")
109
+ --template string URL template (default: /node_modules/{package}/{path})
110
+ --conditions string Export condition priority (e.g., production,browser,import,default)
111
+ --glob string Glob pattern to match HTML files (e.g., "_site/**/*.html")
112
+ -j, --jobs int Number of parallel workers (default: number of CPUs)
113
+ -p, --package string Package directory (default ".")
114
+ -o, --output string Output file (default: stdout)
115
+ ```
116
+
117
+ **Examples:**
118
+
119
+ ```bash
120
+ # Trace a single HTML file
121
+ mappa trace index.html
122
+
123
+ # Trace with custom template
124
+ mappa trace index.html --template "/assets/packages/{package}/{path}"
125
+
126
+ # Batch mode with glob pattern (outputs NDJSON)
127
+ mappa trace --glob "_site/**/*.html" -j 8
128
+
129
+ # Output raw traced specifiers for debugging
130
+ mappa trace index.html --format specifiers
131
+ ```
132
+
133
+ **How it works:**
134
+
135
+ 1. Parses HTML to find `<script type="module">` tags
136
+ 2. Uses tree-sitter to extract all `import` statements from JS modules
137
+ 3. Follows transitive dependencies through local and node_modules files
138
+ 4. Generates an import map with only the bare specifiers actually imported
139
+
140
+ **Static Analysis Limitations:**
141
+
142
+ The trace command uses static analysis to find imports. This means:
143
+
144
+ - **Dynamic imports with variable specifiers cannot be detected.** For example:
145
+ ```javascript
146
+ // This WILL be traced
147
+ import '@rhds/icons/standard/web-icon.js';
148
+
149
+ // This will NOT be traced (specifier is computed at runtime)
150
+ const iconName = 'web-icon';
151
+ import(`@rhds/icons/standard/${iconName}.js`);
152
+ ```
153
+
154
+ - To handle dynamic imports, mappa includes **trailing-slash import map keys** for all direct dependencies that have wildcard exports. For example, if your package.json lists `@rhds/icons` as a dependency and that package exports `./*`, the import map will include `"@rhds/icons/": "/assets/packages/@rhds/icons/"` to cover any dynamic imports.
155
+
156
+ - The same applies to scopes: transitive dependencies with wildcard exports get trailing-slash keys so their dynamic imports work correctly.
157
+
158
+ ## URL Templates
159
+
160
+ Templates use `{variable}` syntax for dynamic URL generation:
161
+
162
+ | Variable | Description | Example |
163
+ | ----------- | -------------------------- | -------------------------- |
164
+ | `{package}` | Full package name | `@scope/name` or `name` |
165
+ | `{name}` | Package name without scope | `name` |
166
+ | `{scope}` | Scope without @ prefix | `scope` |
167
+ | `{path}` | File path within package | `index.js` |
168
+
169
+ **Examples:**
170
+
171
+ ```bash
172
+ # Default (node_modules)
173
+ --template "/node_modules/{package}/{path}"
174
+
175
+ # Custom assets directory
176
+ --template "/assets/packages/{package}/{path}"
177
+
178
+ # Scoped package handling
179
+ --template "/libs/{scope}/{name}/{path}"
180
+ ```
181
+
182
+ ## Performance
183
+
184
+ Mappa is written in Go for speed. Benchmarked against [@jspm/generator][jspm] on a real-world project ([Red Hat Design System][rhds]):
185
+
186
+ | Tool | Time |
187
+ | --------------- | ------------- |
188
+ | mappa | 3.2ms ± 0.2ms |
189
+ | @jspm/generator | 230ms ± 6ms |
190
+
191
+ **mappa is ~72x faster** for local import map generation.
192
+
193
+ [jspm]: https://jspm.org/
194
+ [rhds]: https://github.com/RedHat-UX/red-hat-design-system
195
+
196
+ ## JavaScript API
197
+
198
+ The npm package includes a WASM-powered JavaScript API alongside the CLI.
199
+
200
+ ### `resolve(rootDir, options?)`
201
+
202
+ Generate an import map from a local directory's `node_modules`:
203
+
204
+ ```typescript
205
+ import { resolve } from '@pwrs/mappa';
206
+
207
+ const importMap = await resolve('.', {
208
+ template: '/assets/packages/{package}/{path}',
209
+ conditions: ['browser', 'import', 'default'],
210
+ exclude: ['lodash'],
211
+ });
212
+ ```
213
+
214
+ **Options:**
215
+
216
+ | Option | Type | Description |
217
+ |--------|------|-------------|
218
+ | `template` | `string` | URL template (default: `/node_modules/{package}/{path}`) |
219
+ | `conditions` | `string[]` | Export condition priority |
220
+ | `includePackages` | `string[]` | Additional packages beyond dependencies |
221
+ | `exclude` | `string[]` | Packages to exclude |
222
+ | `inputMap` | `ImportMap` | Import map to merge (takes precedence) |
223
+ | `optimize` | `0 \| 1` | 0=none, 1=simplify+dedup (default: 1) |
224
+
225
+ ### `generate(packageJson, options?)`
226
+
227
+ Generate an import map from package.json contents using a CDN:
228
+
229
+ ```typescript
230
+ import { generate } from '@pwrs/mappa';
231
+
232
+ const importMap = await generate(
233
+ { dependencies: { lit: '^3.0.0' } },
234
+ { cdn: 'esm.sh' }
235
+ );
236
+ ```
237
+
238
+ **Options:**
239
+
240
+ | Option | Type | Description |
241
+ |--------|------|-------------|
242
+ | `cdn` | `"esm.sh" \| "unpkg" \| "jsdelivr"` | CDN provider |
243
+ | `template` | `string` | Custom CDN URL template |
244
+ | `conditions` | `string[]` | Export conditions |
245
+
246
+ ### `version()`
247
+
248
+ Returns the WASM engine version string.
249
+
250
+ ## Integration Examples
251
+
252
+ ### 11ty / Eleventy
253
+
254
+ ```typescript
255
+ import { resolve } from '@pwrs/mappa';
256
+
257
+ export default async function(eleventyConfig) {
258
+ const importMap = await resolve('.', {
259
+ template: '/assets/packages/{package}/{path}',
260
+ });
261
+
262
+ for (const [, path] of Object.entries(importMap.imports)) {
263
+ const match = path.match(/^\/assets\/packages\/(@[^/]+\/[^/]+|[^/]+)/);
264
+ if (match) {
265
+ eleventyConfig.addPassthroughCopy({
266
+ [`node_modules/${match[1]}`]: `/assets/packages/${match[1]}`,
267
+ });
268
+ }
269
+ }
270
+
271
+ eleventyConfig.addTransform('importmap', (content, outputPath) => {
272
+ if (!outputPath?.endsWith('.html')) return content;
273
+
274
+ const script = `<script type="importmap">\n${JSON.stringify(importMap, null, 2)}\n</script>`;
275
+ return content.replace('</head>', `${script}\n</head>`);
276
+ });
277
+ }
278
+ ```
279
+
280
+ ## License
281
+
282
+ GPLv3
283
+
284
+ [importmaps]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap
package/dist/cli.js ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ import { platform, arch } from "node:os";
3
+ import { createRequire } from "node:module";
4
+ import { chmodSync, accessSync, constants } from "node:fs";
5
+ import { spawn } from "node:child_process";
6
+ const require = createRequire(import.meta.url);
7
+ const supportedTargets = new Set([
8
+ "linux-x64",
9
+ "linux-arm64",
10
+ "darwin-x64",
11
+ "darwin-arm64",
12
+ "win32-x64",
13
+ "win32-arm64",
14
+ ]);
15
+ const target = `${platform()}-${arch()}`;
16
+ if (!supportedTargets.has(target)) {
17
+ console.error(`mappa: Unsupported platform/arch: ${target}`);
18
+ process.exit(1);
19
+ }
20
+ let binPath;
21
+ try {
22
+ binPath = require.resolve(`@pwrs/mappa-${target}/mappa${platform() === "win32" ? ".exe" : ""}`);
23
+ }
24
+ catch {
25
+ console.error(`mappa: Platform binary package @pwrs/mappa-${target} not installed. Was there an install error?`);
26
+ process.exit(1);
27
+ }
28
+ if (platform() !== "win32") {
29
+ try {
30
+ accessSync(binPath, constants.X_OK);
31
+ }
32
+ catch {
33
+ chmodSync(binPath, 0o755);
34
+ }
35
+ }
36
+ const child = spawn(binPath, process.argv.slice(2), { stdio: "inherit" });
37
+ const signals = ["SIGTERM", "SIGINT", "SIGHUP"];
38
+ signals.forEach((signal) => {
39
+ process.on(signal, () => {
40
+ child.kill(signal);
41
+ });
42
+ });
43
+ child.on("error", (err) => {
44
+ console.error(`mappa: Failed to spawn binary: ${err.message}`);
45
+ process.exit(1);
46
+ });
47
+ const signalNumbers = {
48
+ SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGTERM: 15,
49
+ };
50
+ child.on("exit", (code, signal) => {
51
+ if (signal) {
52
+ process.exit(128 + (signalNumbers[signal] ?? 1));
53
+ }
54
+ process.exit(code ?? 1);
55
+ });
@@ -0,0 +1,56 @@
1
+ export interface GenerateOptions {
2
+ /** CDN provider name */
3
+ cdn?: "esm.sh" | "unpkg" | "jsdelivr";
4
+ /** Custom CDN URL template */
5
+ template?: string;
6
+ /** Export conditions to resolve */
7
+ conditions?: string[];
8
+ /** Packages to exclude from the generated map, including as transitive dependencies */
9
+ exclude?: string[];
10
+ }
11
+ export interface ResolveOptions {
12
+ /** URL template (default: /node_modules/{package}/{path}) */
13
+ template?: string;
14
+ /** Export condition priority (e.g., ["production", "browser", "import", "default"]) */
15
+ conditions?: string[];
16
+ /** Additional packages to include beyond dependencies */
17
+ includePackages?: string[];
18
+ /** Packages to exclude from the generated map */
19
+ exclude?: string[];
20
+ /** Import map to merge with generated output (input map takes precedence) */
21
+ inputMap?: ImportMap;
22
+ /** Optimization level: 0=none, 1=simplify+dedup (default: 1) */
23
+ optimize?: 0 | 1;
24
+ }
25
+ export interface ImportMap {
26
+ imports?: Record<string, string>;
27
+ scopes?: Record<string, Record<string, string>>;
28
+ }
29
+ interface GoConstructor {
30
+ new (): {
31
+ run(instance: WebAssembly.Instance): void;
32
+ importObject: WebAssembly.Imports;
33
+ };
34
+ }
35
+ interface MappaApi {
36
+ generate(packageJson: string, options?: GenerateOptions): Promise<string>;
37
+ resolve(rootDir: string, options?: ResolveOptions): Promise<string>;
38
+ version: string;
39
+ }
40
+ declare global {
41
+ var Go: GoConstructor;
42
+ var mappa: MappaApi;
43
+ }
44
+ /**
45
+ * Generate an import map from package.json contents using a CDN resolver.
46
+ */
47
+ export declare function generate(packageJson: string | Record<string, unknown>, options?: GenerateOptions): Promise<ImportMap>;
48
+ /**
49
+ * Generate an import map from a local directory's node_modules.
50
+ */
51
+ export declare function resolve(rootDir: string, options?: ResolveOptions): Promise<ImportMap>;
52
+ /**
53
+ * Get the mappa WASM version.
54
+ */
55
+ export declare function version(): Promise<string>;
56
+ export {};
package/dist/mappa.js ADDED
@@ -0,0 +1,65 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { pathToFileURL, fileURLToPath } from "node:url";
3
+ import { dirname, join } from "node:path";
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ let initPromise = null;
6
+ async function init() {
7
+ if (initPromise)
8
+ return initPromise;
9
+ initPromise = (async () => {
10
+ await import(pathToFileURL(join(__dirname, "wasm_exec.js")).href);
11
+ const wasmPath = join(__dirname, "mappa.wasm");
12
+ const wasmBytes = await readFile(wasmPath);
13
+ const go = new Go();
14
+ const { instance } = await WebAssembly.instantiate(wasmBytes, go.importObject);
15
+ go.run(instance);
16
+ if (typeof mappa == "undefined") {
17
+ await new Promise((resolve, reject) => {
18
+ let attempts = 0;
19
+ const check = () => {
20
+ if (typeof mappa !== "undefined") {
21
+ resolve();
22
+ }
23
+ else if (++attempts > 100) {
24
+ reject(new Error("mappa WASM failed to initialize"));
25
+ }
26
+ else {
27
+ setTimeout(check, 10);
28
+ }
29
+ };
30
+ check();
31
+ });
32
+ }
33
+ return mappa;
34
+ })().catch((err) => {
35
+ initPromise = null;
36
+ throw err;
37
+ });
38
+ return initPromise;
39
+ }
40
+ /**
41
+ * Generate an import map from package.json contents using a CDN resolver.
42
+ */
43
+ export async function generate(packageJson, options) {
44
+ const mappa = await init();
45
+ const input = typeof packageJson === "string"
46
+ ? packageJson
47
+ : JSON.stringify(packageJson);
48
+ const resultStr = await mappa.generate(input, options);
49
+ return JSON.parse(resultStr);
50
+ }
51
+ /**
52
+ * Generate an import map from a local directory's node_modules.
53
+ */
54
+ export async function resolve(rootDir, options) {
55
+ const mappa = await init();
56
+ const resultStr = await mappa.resolve(rootDir, options);
57
+ return JSON.parse(resultStr);
58
+ }
59
+ /**
60
+ * Get the mappa WASM version.
61
+ */
62
+ export async function version() {
63
+ const mappa = await init();
64
+ return mappa.version;
65
+ }
Binary file
@@ -0,0 +1,21 @@
1
+ import { platform, arch } from "node:process";
2
+ import { spawnSync } from "node:child_process";
3
+ const platformPackages = {
4
+ "darwin-x64": "@pwrs/mappa-darwin-x64",
5
+ "darwin-arm64": "@pwrs/mappa-darwin-arm64",
6
+ "linux-x64": "@pwrs/mappa-linux-x64",
7
+ "linux-arm64": "@pwrs/mappa-linux-arm64",
8
+ "win32-x64": "@pwrs/mappa-win32-x64",
9
+ "win32-arm64": "@pwrs/mappa-win32-arm64",
10
+ };
11
+ const pkg = platformPackages[`${platform}-${arch}`];
12
+ if (!pkg) {
13
+ console.error(`Unsupported platform: ${platform}-${arch}. ` +
14
+ `Please check https://github.com/bennypowers/mappa for supported platforms.`);
15
+ process.exit(1);
16
+ }
17
+ const result = spawnSync("npm", ["install", "--no-save", pkg], { stdio: "inherit", shell: true });
18
+ if (result.status !== 0) {
19
+ console.error(`Failed to install platform binary package: ${pkg}`);
20
+ process.exit(1);
21
+ }