importmapify 1.0.1 → 1.1.1
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 +92 -25
- package/deno.json +3 -3
- package/dist/mod.d.mts +62 -16
- package/dist/mod.mjs +159 -49
- package/import_map.json +16 -10
- package/package.json +13 -11
package/README.md
CHANGED
|
@@ -29,22 +29,24 @@ npm install importmapify
|
|
|
29
29
|
npx importmapify --root . --out import_map.json
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
| Flag | Alias | Meaning
|
|
33
|
-
| --------------------------- | ----- |
|
|
34
|
-
| `--root` | `-r` | Project root containing the manifest
|
|
35
|
-
| `--manifest` | `-m` | Manifest path, relative to root
|
|
36
|
-
| `--out` | `-o` | Output path
|
|
37
|
-
| `--import key=value` | `-i` | Extra import entry; repeatable
|
|
38
|
-
| `--
|
|
39
|
-
| `--
|
|
40
|
-
| `--
|
|
41
|
-
| `--
|
|
32
|
+
| Flag | Alias | Meaning | Default |
|
|
33
|
+
| --------------------------- | ----- | ------------------------------------------------------------------------- | ------------------- |
|
|
34
|
+
| `--root` | `-r` | Project root containing the manifest | current directory |
|
|
35
|
+
| `--manifest` | `-m` | Manifest path, relative to root | `package.json` |
|
|
36
|
+
| `--out` | `-o` | Output path resolved against root; relative, absolute, or `file://` URL | `import_map.json` |
|
|
37
|
+
| `--import key=value` | `-i` | Extra import entry; repeatable | none |
|
|
38
|
+
| `--package name=target` | `-p` | Package expanded to a conformant bare and trailing-slash pair; repeatable | none |
|
|
39
|
+
| `--scope prefix::key=value` | `-s` | Scoped import override; repeatable | none |
|
|
40
|
+
| `--condition name` | `-c` | Condition tried when a target is a conditional object; repeatable | `import`, `default` |
|
|
41
|
+
| `--ext name` | `-e` | Restrict pattern matches to these file extensions; repeatable | all files |
|
|
42
|
+
| `--check` | | Exit 1 if the output file is stale, without writing it | off |
|
|
43
|
+
| `--stdout` | | Print the map instead of writing it | off |
|
|
42
44
|
|
|
43
45
|
Add global and test-scoped dependencies from the CLI:
|
|
44
46
|
|
|
45
47
|
```sh
|
|
46
48
|
npx importmapify \
|
|
47
|
-
--
|
|
49
|
+
--package 'dreamcli=jsr:@kjanat/dreamcli@^3' \
|
|
48
50
|
--scope './tests/::dreamcli/testkit=jsr:@kjanat/dreamcli@^3/testkit'
|
|
49
51
|
```
|
|
50
52
|
|
|
@@ -69,11 +71,13 @@ const out = writeImportMap({
|
|
|
69
71
|
});
|
|
70
72
|
```
|
|
71
73
|
|
|
72
|
-
| Export | Signature
|
|
73
|
-
| ----------------- |
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
74
|
+
| Export | Signature | Purpose |
|
|
75
|
+
| ----------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
76
|
+
| `defineConfig` | `(options: WriteImportMapOptions) => WriteImportMapOptions` | Identity helper that types a config object for export and reuse. |
|
|
77
|
+
| `createImportMap` | `(options: CreateImportMapOptions) => ImportMapDocument` | Build the import map in memory. |
|
|
78
|
+
| `formatImportMap` | `(map: ImportMapDocument) => string` | Serialize to the canonical sorted, tab-indented JSON text. |
|
|
79
|
+
| `writeImportMap` | `(options: WriteImportMapOptions) => string` | Build, serialize, and write to disk; returns the written path. |
|
|
80
|
+
| `packageEntries` | `(name: string, target: string) => Record<string, string>` | Build the bare and trailing-slash entry pair a package needs to resolve its subpaths. |
|
|
77
81
|
|
|
78
82
|
`CreateImportMapOptions`:
|
|
79
83
|
|
|
@@ -82,14 +86,77 @@ const out = writeImportMap({
|
|
|
82
86
|
| `root` | Base directory for the manifest, source targets, and rebasing. | required |
|
|
83
87
|
| `manifest` | Manifest path, relative to `root`. | `package.json` |
|
|
84
88
|
| `conditions` | Condition names tried, in order, against conditional targets (`{"import": ..., "default": ...}`). | `['import', 'default']` |
|
|
85
|
-
| `
|
|
89
|
+
| `packages` | Package specifiers mapped to targets, each expanded to a conformant bare and trailing-slash pair. | none |
|
|
90
|
+
| `additionalImports` | Extra entries merged in after manifest expansion and packages; these win on key collision. | none |
|
|
86
91
|
| `scopes` | Scope prefixes mapped to scope-specific import overrides. | none |
|
|
87
92
|
| `relativeTo` | Directory the written targets are rebased against. | `root` |
|
|
93
|
+
| `extensions` | File extensions, with or without a leading dot, that pattern targets may match. | all files |
|
|
88
94
|
|
|
89
|
-
`WriteImportMapOptions` extends the above with `out`,
|
|
95
|
+
`WriteImportMapOptions` extends the above with an optional `out`, defaulting to `import_map.json` like the CLI. It is
|
|
96
|
+
resolved against `root` and accepts a relative path, an absolute path, or a `file://` URL. `writeImportMap` rebases
|
|
90
97
|
automatically against `out`'s directory, so a nested `out` (for example `.cache/maps/import_map.json`) still produces
|
|
91
98
|
targets that resolve correctly from the map's own location.
|
|
92
99
|
|
|
100
|
+
`defineConfig` returns its argument unchanged; it exists only to type a config object for export and reuse without a
|
|
101
|
+
manual annotation.
|
|
102
|
+
|
|
103
|
+
### Recipes
|
|
104
|
+
|
|
105
|
+
Add dependencies with `packages`. Each entry expands to the bare specifier plus its conformant trailing-slash entry
|
|
106
|
+
(`jsr:/` or `npm:/`), which Deno needs to resolve subpaths:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { writeImportMap } from 'importmapify';
|
|
110
|
+
|
|
111
|
+
writeImportMap({
|
|
112
|
+
root: import.meta.dirname,
|
|
113
|
+
packages: {
|
|
114
|
+
dreamcli: 'jsr:@kjanat/dreamcli@^3',
|
|
115
|
+
ansispeck: 'npm:ansispeck@^0.2',
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
// dreamcli, dreamcli/ -> jsr:/@kjanat/dreamcli@^3/, ansispeck, ansispeck/ -> npm:/ansispeck@^0.2/
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Restrict pattern expansion to importable files with `extensions`, so a bare `./src/*` skips `.md`, `.json`, and other
|
|
122
|
+
non-modules:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { createImportMap } from 'importmapify';
|
|
126
|
+
|
|
127
|
+
createImportMap({ root: import.meta.dirname, extensions: ['ts', 'tsx'] });
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Build one conformant pair directly with `packageEntries`:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { packageEntries } from 'importmapify';
|
|
134
|
+
|
|
135
|
+
packageEntries('@std/async', 'jsr:@std/async@^1.0.0');
|
|
136
|
+
// { '@std/async': 'jsr:@std/async@^1.0.0', '@std/async/': 'jsr:/@std/async@^1.0.0/' }
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Type a reusable config with `defineConfig` and share it across calls:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { createImportMap, defineConfig, writeImportMap } from 'importmapify';
|
|
143
|
+
|
|
144
|
+
const config = defineConfig({ root: import.meta.dirname, packages: { ansispeck: 'npm:ansispeck@^0.2' } });
|
|
145
|
+
const map = createImportMap(config);
|
|
146
|
+
const written = writeImportMap(config);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`root`, `relativeTo`, and `out` accept a path or a `file://` URL, so `import.meta.url` needs no `.pathname`:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { writeImportMap } from 'importmapify';
|
|
153
|
+
|
|
154
|
+
writeImportMap({
|
|
155
|
+
root: new URL('..', import.meta.url),
|
|
156
|
+
out: new URL('./import_map.json', import.meta.url),
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
93
160
|
### Project-local generator
|
|
94
161
|
|
|
95
162
|
A checked-in wrapper keeps project-specific dependency and scope mappings in code:
|
|
@@ -102,7 +169,7 @@ import { writeImportMap } from 'importmapify';
|
|
|
102
169
|
const output = writeImportMap({
|
|
103
170
|
root: new URL('..', import.meta.url).pathname,
|
|
104
171
|
out: 'import_map.json',
|
|
105
|
-
|
|
172
|
+
packages: {
|
|
106
173
|
dreamcli: 'jsr:@kjanat/dreamcli@^3',
|
|
107
174
|
},
|
|
108
175
|
scopes: {
|
|
@@ -161,15 +228,13 @@ the generated map is:
|
|
|
161
228
|
"imports": {
|
|
162
229
|
"#config": "./src/config.ts",
|
|
163
230
|
"#lib/bytes": "./src/lib/bytes.ts",
|
|
164
|
-
"#lib/
|
|
165
|
-
"#lib/codecs/hex": "./src/lib/codecs/hex.ts",
|
|
166
|
-
"#lib/codecs/hex.ts": "./src/lib/codecs/hex.ts"
|
|
231
|
+
"#lib/codecs/hex": "./src/lib/codecs/hex.ts"
|
|
167
232
|
}
|
|
168
233
|
}
|
|
169
234
|
```
|
|
170
235
|
|
|
171
|
-
|
|
172
|
-
|
|
236
|
+
Each matched file yields one specifier, the wildcard substituted into the key. A key with its own suffix, such as
|
|
237
|
+
`#lib/*.js` targeting `./src/lib/*.ts`, produces the renamed specifier `#lib/bytes.js` pointing at `./src/lib/bytes.ts`.
|
|
173
238
|
|
|
174
239
|
## Scope and constraints
|
|
175
240
|
|
|
@@ -180,7 +245,9 @@ real filename: `#lib/bytes.js` and `#lib/bytes.ts`, both pointing at `./src/lib/
|
|
|
180
245
|
missing directory produces no entries.
|
|
181
246
|
- Relative targets (`./...`, `../...`) are rebased against `relativeTo`. Bare specifiers, `npm:`/`jsr:`/`node:`
|
|
182
247
|
specifiers, and absolute URLs pass through unchanged.
|
|
183
|
-
-
|
|
248
|
+
- When two manifest entries produce the same key, the more specific one wins, matching Node's subpath resolution: an
|
|
249
|
+
exact key beats a pattern, and a longer prefix before `*` beats a shorter one. Declaration order does not matter.
|
|
250
|
+
`additionalImports` and `packages` still override manifest entries.
|
|
184
251
|
- Relative scope prefixes and targets are rebased against `relativeTo`; trailing scope slashes are preserved.
|
|
185
252
|
- Import entries, scope prefixes, and scoped entries are sorted by UTF-16 code unit for stable diffs.
|
|
186
253
|
|
package/deno.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/denoland/deno/refs/heads/main/cli/schemas/config-file.v1.json",
|
|
3
3
|
"name": "@kjanat/importmapify",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.1",
|
|
5
5
|
"exports": "./src/mod.ts",
|
|
6
6
|
"publish": {
|
|
7
7
|
"include": [
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"tasks": {
|
|
22
22
|
"build": {
|
|
23
23
|
"description": "build for npm distribution",
|
|
24
|
-
"command": "
|
|
24
|
+
"command": "bun run build",
|
|
25
25
|
"files": ["src/**", "package.json", "bun.lock"],
|
|
26
26
|
"output": ["dist/**", "deno.json"]
|
|
27
27
|
},
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"importmapify": {
|
|
34
34
|
"description": "generate importmap for deno",
|
|
35
|
-
"command": "
|
|
35
|
+
"command": "bun scripts/generate-importmap.ts",
|
|
36
36
|
"files": ["package.json", "scripts/generate-importmap.ts", "src/**"],
|
|
37
37
|
"output": ["import_map.json"]
|
|
38
38
|
}
|
package/dist/mod.d.mts
CHANGED
|
@@ -8,23 +8,30 @@ interface ImportMapDocument {
|
|
|
8
8
|
}
|
|
9
9
|
/** Options for creating an import map without writing it to disk. */
|
|
10
10
|
interface CreateImportMapOptions {
|
|
11
|
-
/** Project directory containing the package manifest. */
|
|
12
|
-
readonly root: string;
|
|
11
|
+
/** Project directory containing the package manifest, as a path or `file://` URL. */
|
|
12
|
+
readonly root: string | URL;
|
|
13
13
|
/** Manifest path relative to {@link root}. Defaults to `package.json`. */
|
|
14
14
|
readonly manifest?: string;
|
|
15
15
|
/** Conditional import keys to try in order. Defaults to `import`, then `default`. */
|
|
16
16
|
readonly conditions?: readonly string[];
|
|
17
|
-
/**
|
|
17
|
+
/** Package specifiers mapped to targets, each expanded to a conformant bare and trailing-slash pair. */
|
|
18
|
+
readonly packages?: Readonly<Record<string, string>>;
|
|
19
|
+
/** Explicit entries merged after manifest imports and packages, overriding duplicate keys. */
|
|
18
20
|
readonly additionalImports?: Readonly<Record<string, string>>;
|
|
19
21
|
/** Scope-specific import overrides keyed by scope prefix. */
|
|
20
22
|
readonly scopes?: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
21
|
-
/** Directory against which relative targets are rebased. Defaults to {@link root}. */
|
|
22
|
-
readonly relativeTo?: string;
|
|
23
|
+
/** Directory against which relative targets are rebased, as a path or `file://` URL. Defaults to {@link root}. */
|
|
24
|
+
readonly relativeTo?: string | URL;
|
|
25
|
+
/** File extensions, with or without a leading dot, that pattern targets may match. Unset matches every file. */
|
|
26
|
+
readonly extensions?: readonly string[];
|
|
23
27
|
}
|
|
24
28
|
/** Options for creating and writing an import map. */
|
|
25
29
|
interface WriteImportMapOptions extends CreateImportMapOptions {
|
|
26
|
-
/**
|
|
27
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Output path, resolved against {@link CreateImportMapOptions.root | root}. Accepts a relative path, an
|
|
32
|
+
* absolute path, a `file://` URL string, or a {@link URL}. Defaults to `import_map.json`.
|
|
33
|
+
*/
|
|
34
|
+
readonly out?: string | URL;
|
|
28
35
|
}
|
|
29
36
|
/**
|
|
30
37
|
* Expand a package's exact and patterned `imports` into a Deno import map.
|
|
@@ -32,8 +39,9 @@ interface WriteImportMapOptions extends CreateImportMapOptions {
|
|
|
32
39
|
* Pattern targets are matched against files below {@link CreateImportMapOptions.root | root}.
|
|
33
40
|
* Conditional targets use the configured condition order, and additional imports are applied last.
|
|
34
41
|
*
|
|
35
|
-
* @example
|
|
42
|
+
* @example
|
|
36
43
|
* ```ts
|
|
44
|
+
* // Generate entries for the current project.
|
|
37
45
|
* import { createImportMap } from 'jsr:@kjanat/importmapify';
|
|
38
46
|
*
|
|
39
47
|
* const map = createImportMap({
|
|
@@ -51,8 +59,9 @@ declare function createImportMap(options: CreateImportMapOptions): ImportMapDocu
|
|
|
51
59
|
/**
|
|
52
60
|
* Serialize an import map as stable, tab-indented JSON with a trailing newline.
|
|
53
61
|
*
|
|
54
|
-
* @example
|
|
62
|
+
* @example
|
|
55
63
|
* ```ts
|
|
64
|
+
* // Format an import map for stdout.
|
|
56
65
|
* import { formatImportMap } from 'jsr:@kjanat/importmapify';
|
|
57
66
|
*
|
|
58
67
|
* const text = formatImportMap({
|
|
@@ -71,21 +80,58 @@ declare function formatImportMap(map: ImportMapDocument): string;
|
|
|
71
80
|
*
|
|
72
81
|
* Relative targets are automatically rebased from the project root to the output directory.
|
|
73
82
|
*
|
|
74
|
-
* @example
|
|
83
|
+
* @example
|
|
75
84
|
* ```ts
|
|
85
|
+
* // Write the conventional Deno import map file.
|
|
76
86
|
* import { writeImportMap } from 'jsr:@kjanat/importmapify';
|
|
77
87
|
*
|
|
78
|
-
* const output = writeImportMap({
|
|
79
|
-
* root: Deno.cwd(),
|
|
80
|
-
* out: 'import_map.json',
|
|
81
|
-
* });
|
|
88
|
+
* const output = writeImportMap({ root: Deno.cwd() });
|
|
82
89
|
*
|
|
83
90
|
* console.log(`Wrote ${output}`);
|
|
84
91
|
* ```
|
|
85
92
|
*
|
|
86
|
-
* @param options Creation options plus the output path.
|
|
93
|
+
* @param options Creation options plus the optional output path.
|
|
87
94
|
* @returns The absolute path of the written file.
|
|
88
95
|
*/
|
|
89
96
|
declare function writeImportMap(options: WriteImportMapOptions): string;
|
|
97
|
+
/**
|
|
98
|
+
* Build the two import map entries a package needs to resolve both itself and its subpaths.
|
|
99
|
+
*
|
|
100
|
+
* Deno's `importMap` resolution requires a trailing-slash entry for subpath imports, and a
|
|
101
|
+
* `jsr:` or `npm:` trailing-slash target only resolves in the `jsr:/` or `npm:/` form.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* import { packageEntries } from 'jsr:@kjanat/importmapify';
|
|
106
|
+
*
|
|
107
|
+
* packageEntries('@std/async', 'jsr:@std/async@^1.0.0');
|
|
108
|
+
* // { '@std/async': 'jsr:@std/async@^1.0.0', '@std/async/': 'jsr:/@std/async@^1.0.0/' }
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* @param name Bare package specifier.
|
|
112
|
+
* @param target Exact target for {@link name}.
|
|
113
|
+
* @returns The bare entry and its trailing-slash subpath entry.
|
|
114
|
+
*/
|
|
115
|
+
declare function packageEntries(name: string, target: string): Record<string, string>;
|
|
116
|
+
/**
|
|
117
|
+
* Type an import map configuration for export and reuse, then pass it to {@link createImportMap} or
|
|
118
|
+
* {@link writeImportMap}. Returns its input unchanged; it exists only for inference and autocomplete.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* import { defineConfig, writeImportMap } from 'jsr:@kjanat/importmapify';
|
|
123
|
+
*
|
|
124
|
+
* export const config = defineConfig({
|
|
125
|
+
* root: import.meta.dirname,
|
|
126
|
+
* packages: { dreamcli: 'jsr:@kjanat/dreamcli@^3' },
|
|
127
|
+
* });
|
|
128
|
+
*
|
|
129
|
+
* writeImportMap(config);
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* @param options Import map configuration, with an optional `out`.
|
|
133
|
+
* @returns The same `options` value, typed as {@link WriteImportMapOptions}.
|
|
134
|
+
*/
|
|
135
|
+
declare function defineConfig(options: WriteImportMapOptions): WriteImportMapOptions;
|
|
90
136
|
//#endregion
|
|
91
|
-
export { type CreateImportMapOptions, type ImportMapDocument, type WriteImportMapOptions, createImportMap, formatImportMap, writeImportMap };
|
|
137
|
+
export { type CreateImportMapOptions, type ImportMapDocument, type WriteImportMapOptions, createImportMap, defineConfig, formatImportMap, packageEntries, writeImportMap };
|
package/dist/mod.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { CLIError, cli, command, flag } from "dreamcli";
|
|
3
3
|
import fs, { existsSync, readFileSync } from "node:fs";
|
|
4
|
-
import path, { dirname
|
|
4
|
+
import path, { dirname } from "node:path";
|
|
5
5
|
import { cwd } from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
//#region src/expand.ts
|
|
7
8
|
function escapeRegExp(value) {
|
|
8
9
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -61,10 +62,7 @@ function expandPattern(pattern, files) {
|
|
|
61
62
|
for (const file of files) {
|
|
62
63
|
const target = `${targetBase}${file}`;
|
|
63
64
|
const wildcard = matcher.exec(target)?.groups?.wildcard;
|
|
64
|
-
if (wildcard !== void 0) {
|
|
65
|
-
imports[`${pattern.keyPrefix}${wildcard}${pattern.keySuffix}`] = target;
|
|
66
|
-
imports[`${pattern.keyPrefix}${file}`] = target;
|
|
67
|
-
}
|
|
65
|
+
if (wildcard !== void 0) imports[`${pattern.keyPrefix}${wildcard}${pattern.keySuffix}`] = target;
|
|
68
66
|
}
|
|
69
67
|
return imports;
|
|
70
68
|
}
|
|
@@ -120,6 +118,15 @@ function rebaseTarget(root, relativeTo, target) {
|
|
|
120
118
|
//#endregion
|
|
121
119
|
//#region src/map.ts
|
|
122
120
|
const DEFAULT_CONDITIONS = ["import", "default"];
|
|
121
|
+
const DEFAULT_OUT = "import_map.json";
|
|
122
|
+
const SCHEME_PREFIX = /^(jsr|npm):(?!\/)/;
|
|
123
|
+
function toPath(value) {
|
|
124
|
+
if (typeof value === "string") return value.startsWith("file://") ? fileURLToPath(value) : value;
|
|
125
|
+
return fileURLToPath(value.href);
|
|
126
|
+
}
|
|
127
|
+
function resolveOut(root, out) {
|
|
128
|
+
return path.resolve(toPath(root), toPath(out));
|
|
129
|
+
}
|
|
123
130
|
function filesUnder(dir, prefix = "") {
|
|
124
131
|
if (!fs.existsSync(dir)) return [];
|
|
125
132
|
const files = [];
|
|
@@ -146,12 +153,58 @@ function readManifest(manifestPath) {
|
|
|
146
153
|
if (!isRecord(parsed)) throw new Error(`Manifest at ${manifestPath} must be a JSON object`);
|
|
147
154
|
return parsed;
|
|
148
155
|
}
|
|
149
|
-
function
|
|
156
|
+
function extensionFilter(extensions) {
|
|
157
|
+
const allowed = new Set(extensions.map((ext) => ext.startsWith(".") ? ext : `.${ext}`));
|
|
158
|
+
return (file) => allowed.has(path.extname(file));
|
|
159
|
+
}
|
|
160
|
+
function expandManifestImport(root, key, rawValue, { conditions, extensions }) {
|
|
150
161
|
const value = typeof rawValue === "string" ? rawValue : resolveCondition(rawValue, conditions);
|
|
151
162
|
if (value === void 0) return {};
|
|
152
163
|
const pattern = parsePattern(key, value);
|
|
153
164
|
if (pattern === void 0) return { [key]: value };
|
|
154
|
-
|
|
165
|
+
const files = filesUnder(path.join(root, pattern.targetDirectory));
|
|
166
|
+
return expandPattern(pattern, extensions.length > 0 ? files.filter(extensionFilter(extensions)) : files);
|
|
167
|
+
}
|
|
168
|
+
function keyBaseLength(key) {
|
|
169
|
+
const star = key.indexOf("*");
|
|
170
|
+
return star === -1 ? key.length : star + 1;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Order two manifest import keys by Node's subpath specificity: an exact key beats a pattern, a longer
|
|
174
|
+
* prefix before `*` beats a shorter one, and a longer key breaks a remaining tie. Negative means `a` wins.
|
|
175
|
+
*/
|
|
176
|
+
function keySpecificity(a, b) {
|
|
177
|
+
const aExact = !a.includes("*");
|
|
178
|
+
if (aExact !== !b.includes("*")) return aExact ? -1 : 1;
|
|
179
|
+
const baseDelta = keyBaseLength(b) - keyBaseLength(a);
|
|
180
|
+
return baseDelta === 0 ? b.length - a.length : baseDelta;
|
|
181
|
+
}
|
|
182
|
+
function expandManifest(root, relativeTo, manifestImports, expansion) {
|
|
183
|
+
const imports = {};
|
|
184
|
+
const source = {};
|
|
185
|
+
for (const [key, rawValue] of Object.entries(manifestImports)) for (const [specifier, target] of Object.entries(expandManifestImport(root, key, rawValue, expansion))) {
|
|
186
|
+
const incumbent = source[specifier];
|
|
187
|
+
if (incumbent === void 0 || keySpecificity(key, incumbent) < 0) {
|
|
188
|
+
imports[specifier] = rebaseTarget(root, relativeTo, target);
|
|
189
|
+
source[specifier] = key;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return imports;
|
|
193
|
+
}
|
|
194
|
+
function collectAdditional(options) {
|
|
195
|
+
const entries = {};
|
|
196
|
+
for (const [name, target] of Object.entries(options.packages ?? {})) Object.assign(entries, packageEntries(name, target));
|
|
197
|
+
Object.assign(entries, options.additionalImports ?? {});
|
|
198
|
+
return entries;
|
|
199
|
+
}
|
|
200
|
+
function buildScopes$1(root, relativeTo, rawScopes) {
|
|
201
|
+
const scopes = {};
|
|
202
|
+
for (const [scope, mappings] of Object.entries(rawScopes)) {
|
|
203
|
+
const rebasedMappings = {};
|
|
204
|
+
for (const [key, value] of Object.entries(mappings)) rebasedMappings[key] = rebaseTarget(root, relativeTo, value);
|
|
205
|
+
scopes[rebaseScopePrefix(root, relativeTo, scope)] = sortEntries(rebasedMappings);
|
|
206
|
+
}
|
|
207
|
+
return scopes;
|
|
155
208
|
}
|
|
156
209
|
function sortEntries(entries) {
|
|
157
210
|
return Object.fromEntries(Object.entries(entries).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
@@ -166,8 +219,9 @@ function rebaseScopePrefix(root, relativeTo, scope) {
|
|
|
166
219
|
* Pattern targets are matched against files below {@link CreateImportMapOptions.root | root}.
|
|
167
220
|
* Conditional targets use the configured condition order, and additional imports are applied last.
|
|
168
221
|
*
|
|
169
|
-
* @example
|
|
222
|
+
* @example
|
|
170
223
|
* ```ts
|
|
224
|
+
* // Generate entries for the current project.
|
|
171
225
|
* import { createImportMap } from 'jsr:@kjanat/importmapify';
|
|
172
226
|
*
|
|
173
227
|
* const map = createImportMap({
|
|
@@ -182,22 +236,17 @@ function rebaseScopePrefix(root, relativeTo, scope) {
|
|
|
182
236
|
* @returns A sorted import map document.
|
|
183
237
|
*/
|
|
184
238
|
function createImportMap(options) {
|
|
185
|
-
const
|
|
239
|
+
const root = toPath(options.root);
|
|
240
|
+
const manifest = readManifest(path.join(root, options.manifest ?? "package.json"));
|
|
186
241
|
const manifestImports = isRecord(manifest.imports) ? manifest.imports : {};
|
|
187
242
|
const conditions = options.conditions !== void 0 && options.conditions.length > 0 ? options.conditions : DEFAULT_CONDITIONS;
|
|
188
|
-
const relativeTo = options.relativeTo
|
|
189
|
-
const imports = {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const scopes = {};
|
|
196
|
-
for (const [scope, mappings] of Object.entries(options.scopes ?? {})) {
|
|
197
|
-
const rebasedMappings = {};
|
|
198
|
-
for (const [key, value] of Object.entries(mappings)) rebasedMappings[key] = rebaseTarget(options.root, relativeTo, value);
|
|
199
|
-
scopes[rebaseScopePrefix(options.root, relativeTo, scope)] = sortEntries(rebasedMappings);
|
|
200
|
-
}
|
|
243
|
+
const relativeTo = options.relativeTo === void 0 ? root : toPath(options.relativeTo);
|
|
244
|
+
const imports = expandManifest(root, relativeTo, manifestImports, {
|
|
245
|
+
conditions,
|
|
246
|
+
extensions: options.extensions ?? []
|
|
247
|
+
});
|
|
248
|
+
for (const [key, value] of Object.entries(collectAdditional(options))) imports[key] = rebaseTarget(root, relativeTo, value);
|
|
249
|
+
const scopes = buildScopes$1(root, relativeTo, options.scopes ?? {});
|
|
201
250
|
const sortedImports = sortEntries(imports);
|
|
202
251
|
const sortedScopes = Object.fromEntries(Object.entries(scopes).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
203
252
|
return Object.keys(sortedScopes).length === 0 ? { imports: sortedImports } : {
|
|
@@ -208,8 +257,9 @@ function createImportMap(options) {
|
|
|
208
257
|
/**
|
|
209
258
|
* Serialize an import map as stable, tab-indented JSON with a trailing newline.
|
|
210
259
|
*
|
|
211
|
-
* @example
|
|
260
|
+
* @example
|
|
212
261
|
* ```ts
|
|
262
|
+
* // Format an import map for stdout.
|
|
213
263
|
* import { formatImportMap } from 'jsr:@kjanat/importmapify';
|
|
214
264
|
*
|
|
215
265
|
* const text = formatImportMap({
|
|
@@ -230,23 +280,21 @@ function formatImportMap(map) {
|
|
|
230
280
|
*
|
|
231
281
|
* Relative targets are automatically rebased from the project root to the output directory.
|
|
232
282
|
*
|
|
233
|
-
* @example
|
|
283
|
+
* @example
|
|
234
284
|
* ```ts
|
|
285
|
+
* // Write the conventional Deno import map file.
|
|
235
286
|
* import { writeImportMap } from 'jsr:@kjanat/importmapify';
|
|
236
287
|
*
|
|
237
|
-
* const output = writeImportMap({
|
|
238
|
-
* root: Deno.cwd(),
|
|
239
|
-
* out: 'import_map.json',
|
|
240
|
-
* });
|
|
288
|
+
* const output = writeImportMap({ root: Deno.cwd() });
|
|
241
289
|
*
|
|
242
290
|
* console.log(`Wrote ${output}`);
|
|
243
291
|
* ```
|
|
244
292
|
*
|
|
245
|
-
* @param options Creation options plus the output path.
|
|
293
|
+
* @param options Creation options plus the optional output path.
|
|
246
294
|
* @returns The absolute path of the written file.
|
|
247
295
|
*/
|
|
248
296
|
function writeImportMap(options) {
|
|
249
|
-
const out =
|
|
297
|
+
const out = resolveOut(options.root, options.out ?? "import_map.json");
|
|
250
298
|
const relativeTo = options.relativeTo ?? path.dirname(out);
|
|
251
299
|
const map = createImportMap({
|
|
252
300
|
...options,
|
|
@@ -256,12 +304,61 @@ function writeImportMap(options) {
|
|
|
256
304
|
fs.writeFileSync(out, formatImportMap(map));
|
|
257
305
|
return out;
|
|
258
306
|
}
|
|
307
|
+
function directoryTarget(target) {
|
|
308
|
+
return (target.endsWith("/") ? target : `${target}/`).replace(SCHEME_PREFIX, "$1:/");
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Build the two import map entries a package needs to resolve both itself and its subpaths.
|
|
312
|
+
*
|
|
313
|
+
* Deno's `importMap` resolution requires a trailing-slash entry for subpath imports, and a
|
|
314
|
+
* `jsr:` or `npm:` trailing-slash target only resolves in the `jsr:/` or `npm:/` form.
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```ts
|
|
318
|
+
* import { packageEntries } from 'jsr:@kjanat/importmapify';
|
|
319
|
+
*
|
|
320
|
+
* packageEntries('@std/async', 'jsr:@std/async@^1.0.0');
|
|
321
|
+
* // { '@std/async': 'jsr:@std/async@^1.0.0', '@std/async/': 'jsr:/@std/async@^1.0.0/' }
|
|
322
|
+
* ```
|
|
323
|
+
*
|
|
324
|
+
* @param name Bare package specifier.
|
|
325
|
+
* @param target Exact target for {@link name}.
|
|
326
|
+
* @returns The bare entry and its trailing-slash subpath entry.
|
|
327
|
+
*/
|
|
328
|
+
function packageEntries(name, target) {
|
|
329
|
+
return {
|
|
330
|
+
[name]: target,
|
|
331
|
+
[`${name}/`]: directoryTarget(target)
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Type an import map configuration for export and reuse, then pass it to {@link createImportMap} or
|
|
336
|
+
* {@link writeImportMap}. Returns its input unchanged; it exists only for inference and autocomplete.
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* ```ts
|
|
340
|
+
* import { defineConfig, writeImportMap } from 'jsr:@kjanat/importmapify';
|
|
341
|
+
*
|
|
342
|
+
* export const config = defineConfig({
|
|
343
|
+
* root: import.meta.dirname,
|
|
344
|
+
* packages: { dreamcli: 'jsr:@kjanat/dreamcli@^3' },
|
|
345
|
+
* });
|
|
346
|
+
*
|
|
347
|
+
* writeImportMap(config);
|
|
348
|
+
* ```
|
|
349
|
+
*
|
|
350
|
+
* @param options Import map configuration, with an optional `out`.
|
|
351
|
+
* @returns The same `options` value, typed as {@link WriteImportMapOptions}.
|
|
352
|
+
*/
|
|
353
|
+
function defineConfig(options) {
|
|
354
|
+
return options;
|
|
355
|
+
}
|
|
259
356
|
//#endregion
|
|
260
357
|
//#region src/cli.ts
|
|
261
|
-
function parseKeyValue(raw) {
|
|
358
|
+
function parseKeyValue(raw, flagName, code) {
|
|
262
359
|
const eq = raw.indexOf("=");
|
|
263
|
-
if (eq === -1) throw new CLIError(
|
|
264
|
-
code
|
|
360
|
+
if (eq === -1) throw new CLIError(`--${flagName} expects key=value, got "${raw}"`, {
|
|
361
|
+
code,
|
|
265
362
|
exitCode: 2
|
|
266
363
|
});
|
|
267
364
|
return [raw.slice(0, eq), raw.slice(eq + 1)];
|
|
@@ -279,34 +376,46 @@ function parseScope(raw) {
|
|
|
279
376
|
raw.slice(eq + 1)
|
|
280
377
|
];
|
|
281
378
|
}
|
|
379
|
+
function parseEntries(raws, flagName, code) {
|
|
380
|
+
const entries = {};
|
|
381
|
+
for (const raw of raws) {
|
|
382
|
+
const [key, value] = parseKeyValue(raw, flagName, code);
|
|
383
|
+
entries[key] = value;
|
|
384
|
+
}
|
|
385
|
+
return entries;
|
|
386
|
+
}
|
|
387
|
+
function buildScopes(raws) {
|
|
388
|
+
const scopes = {};
|
|
389
|
+
for (const raw of raws) {
|
|
390
|
+
const [scope, key, value] = parseScope(raw);
|
|
391
|
+
const mappings = scopes[scope];
|
|
392
|
+
if (mappings === void 0) scopes[scope] = { [key]: value };
|
|
393
|
+
else mappings[key] = value;
|
|
394
|
+
}
|
|
395
|
+
return scopes;
|
|
396
|
+
}
|
|
282
397
|
/** DreamCLI command that writes, checks, or prints an expanded Deno import map. */
|
|
283
398
|
const generateCommand = command("generate").description("Expand package.json subpath-pattern imports into a Deno import map.").flag("root", flag.path({
|
|
284
399
|
mustExist: true,
|
|
285
400
|
type: "directory"
|
|
286
|
-
}).default(cwd()).describe("Project root containing the manifest.").alias("r")).flag("manifest", flag.string().default("package.json").describe("Manifest path, relative to root.").alias("m")).flag("out", flag.string().default(
|
|
401
|
+
}).default(cwd()).describe("Project root containing the manifest.").alias("r")).flag("manifest", flag.string().default("package.json").describe("Manifest path, relative to root.").alias("m")).flag("out", flag.string().default(DEFAULT_OUT).describe("Output path resolved against root; relative, absolute, or file:// URL.").alias("o")).flag("import", flag.array(flag.string()).describe("Additional import entry as key=value. Repeatable.").alias("i")).flag("package", flag.array(flag.string()).describe("Package as name=target, expanded to a conformant bare and trailing-slash pair. Repeatable.").alias("p")).flag("ext", flag.array(flag.string()).describe("Restrict pattern matches to these file extensions. Repeatable.").alias("e")).flag("scope", flag.array(flag.string()).describe("Scoped import as prefix::key=value. Repeatable.").alias("s")).flag("condition", flag.array(flag.string()).describe("Condition to try when a target is a conditional object. Repeatable.").alias("c")).flag("check", flag.boolean().describe("Exit 1 if the output file is stale instead of writing it.")).flag("stdout", flag.boolean().describe("Print the import map to stdout instead of writing it.")).example("importmapify --stdout", "Print the generated import map").example(`importmapify --package 'dreamcli=jsr:@kjanat/dreamcli@^3' --scope './tests/::dreamcli/testkit=jsr:@kjanat/dreamcli@^3/testkit'`, "Add global and test-scoped dependencies").example("importmapify --check", "Fail when the generated file is stale").action(({ flags, out }) => {
|
|
287
402
|
const { log, error, setExitCode } = out;
|
|
288
|
-
const { check, stdout, root, manifest, condition: cdts, out: of, import: imf, scope: scf } = flags;
|
|
403
|
+
const { check, stdout, root, manifest, condition: cdts, out: of, import: imf, package: pkf, ext: exf, scope: scf } = flags;
|
|
289
404
|
if (check && stdout) throw new CLIError("--check and --stdout are mutually exclusive", {
|
|
290
405
|
code: "conflicting-flags",
|
|
291
406
|
exitCode: 2
|
|
292
407
|
});
|
|
293
|
-
const outPath =
|
|
408
|
+
const outPath = resolveOut(root, of);
|
|
294
409
|
const relativeTo = dirname(outPath);
|
|
295
|
-
const additionalImports = Object.fromEntries((imf ?? []).map(parseKeyValue));
|
|
296
|
-
const scopes = {};
|
|
297
|
-
for (const raw of scf ?? []) {
|
|
298
|
-
const [scope, key, value] = parseScope(raw);
|
|
299
|
-
const mappings = scopes[scope];
|
|
300
|
-
if (mappings === void 0) scopes[scope] = { [key]: value };
|
|
301
|
-
else mappings[key] = value;
|
|
302
|
-
}
|
|
303
410
|
const options = {
|
|
304
411
|
root,
|
|
305
412
|
manifest,
|
|
306
413
|
conditions: cdts,
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
414
|
+
packages: parseEntries(pkf ?? [], "package", "invalid-package-flag"),
|
|
415
|
+
additionalImports: parseEntries(imf ?? [], "import", "invalid-import-flag"),
|
|
416
|
+
scopes: buildScopes(scf ?? []),
|
|
417
|
+
relativeTo,
|
|
418
|
+
extensions: exf
|
|
310
419
|
};
|
|
311
420
|
if (stdout) {
|
|
312
421
|
log(formatImportMap(createImportMap(options)));
|
|
@@ -335,8 +444,9 @@ const generateCommand = command("generate").description("Expand package.json sub
|
|
|
335
444
|
* Use the library API to create, format, or write deterministic import maps.
|
|
336
445
|
* This module also runs the `importmapify` CLI when executed directly.
|
|
337
446
|
*
|
|
338
|
-
* @example
|
|
447
|
+
* @example
|
|
339
448
|
* ```ts
|
|
449
|
+
* // Create an import map from the current package.
|
|
340
450
|
* import { createImportMap } from 'jsr:@kjanat/importmapify';
|
|
341
451
|
*
|
|
342
452
|
* const importMap = createImportMap({ root: Deno.cwd() });
|
|
@@ -351,4 +461,4 @@ const cli$1 = cli("importmapify").manifest({
|
|
|
351
461
|
}).links().default(generateCommand).completions({ as: "flag" });
|
|
352
462
|
if (import.meta.main) cli$1.run();
|
|
353
463
|
//#endregion
|
|
354
|
-
export { createImportMap, formatImportMap, writeImportMap };
|
|
464
|
+
export { createImportMap, defineConfig, formatImportMap, packageEntries, writeImportMap };
|
package/import_map.json
CHANGED
|
@@ -2,19 +2,25 @@
|
|
|
2
2
|
"imports": {
|
|
3
3
|
"#deno": "./deno.json",
|
|
4
4
|
"#pkg": "./package.json",
|
|
5
|
-
"#src/cli
|
|
6
|
-
"#src/expand
|
|
7
|
-
"#src/map
|
|
8
|
-
"#src/mod
|
|
9
|
-
"@types/bun": "npm
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
5
|
+
"#src/cli": "./src/cli.ts",
|
|
6
|
+
"#src/expand": "./src/expand.ts",
|
|
7
|
+
"#src/map": "./src/map.ts",
|
|
8
|
+
"#src/mod": "./src/mod.ts",
|
|
9
|
+
"@types/bun": "npm:@types/bun@1.3.14",
|
|
10
|
+
"ansispeck": "jsr:@kjanat/ansispeck@0.2.0",
|
|
11
|
+
"ansispeck/": "jsr:/@kjanat/ansispeck@0.2.0/",
|
|
12
|
+
"bun": "npm:bun-types@1.3.14",
|
|
13
|
+
"bun:test": "npm:bun-types@1.3.14/test.d.ts",
|
|
14
|
+
"dreamcli": "jsr:@kjanat/dreamcli@3.0.0-rc.11",
|
|
15
|
+
"dreamcli/": "jsr:/@kjanat/dreamcli@3.0.0-rc.11/",
|
|
16
|
+
"sort-package-json": "npm:sort-package-json@4.0.0",
|
|
17
|
+
"sort-package-json/": "npm:/sort-package-json@4.0.0/",
|
|
18
|
+
"tsdown": "npm:tsdown@0.22.8",
|
|
19
|
+
"tsdown/": "npm:/tsdown@0.22.8/"
|
|
14
20
|
},
|
|
15
21
|
"scopes": {
|
|
16
22
|
"./tests/": {
|
|
17
|
-
"dreamcli/testkit": "jsr:@kjanat/dreamcli
|
|
23
|
+
"dreamcli/testkit": "jsr:@kjanat/dreamcli@3.0.0-rc.11/testkit"
|
|
18
24
|
}
|
|
19
25
|
}
|
|
20
26
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "importmapify",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Expand package.json subpath-pattern imports into explicit Deno import map entries.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deno",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"imports": {
|
|
31
31
|
"#pkg": "./package.json",
|
|
32
32
|
"#deno": "./deno.json",
|
|
33
|
-
"#src/*": "./src
|
|
33
|
+
"#src/*": "./src/*.ts"
|
|
34
34
|
},
|
|
35
35
|
"exports": {
|
|
36
36
|
".": {
|
|
@@ -52,17 +52,17 @@
|
|
|
52
52
|
"fmt": "biome check --fix 2>/dev/null; dprint fmt",
|
|
53
53
|
"jsr:publish": "bunx jsr publish --allow-dirty",
|
|
54
54
|
"lint": "biome lint",
|
|
55
|
-
"prepack": "
|
|
56
|
-
"
|
|
57
|
-
"runbin": "node \"$(jq -r '.bin.
|
|
58
|
-
"test": "bun test
|
|
59
|
-
"test:coverage": "
|
|
60
|
-
"test:smoke": "
|
|
61
|
-
"test:watch": "
|
|
62
|
-
"typecheck": "
|
|
55
|
+
"prepack": "run -q build -l error",
|
|
56
|
+
"prepare": "./scripts/generate-importmap.ts",
|
|
57
|
+
"runbin": "node \"$(jq -r '.bin.importmapify' package.json)\"",
|
|
58
|
+
"test": "bun test",
|
|
59
|
+
"test:coverage": "run -q test --coverage",
|
|
60
|
+
"test:smoke": "./scripts/smoke.mjs",
|
|
61
|
+
"test:watch": "run -q test --watch",
|
|
62
|
+
"typecheck": "run @typescript/native --noEmit",
|
|
63
63
|
"url:jsr": "printf 'url=https://jsr.io/%s@%s\n' \"$(jq -r '.name' deno.json)\" \"$(jq -r '.version' deno.json)\"",
|
|
64
64
|
"url:npm": "printf 'url=https://npm.im/package/%s/v/%s\n' \"$(jq -r '.name' package.json)\" \"$(jq -r '.version' package.json)\"",
|
|
65
|
-
"volta:bump": "volta pin node@latest npm@11; node --version>.node-version;
|
|
65
|
+
"volta:bump": "volta pin node@latest npm@11; node --version>.node-version; run bd"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"dreamcli": "npm:@kjanat/dreamcli@^3.0.0-rc.11"
|
|
@@ -76,7 +76,9 @@
|
|
|
76
76
|
"biome": "npm:@biomejs/biome@^2.5.4",
|
|
77
77
|
"dprint": "^0.55.2",
|
|
78
78
|
"publint": "^0.3.21",
|
|
79
|
+
"runner-run": "^0.20.0",
|
|
79
80
|
"sort-package-json": "^4.0.0",
|
|
81
|
+
"tombi": "^1.2.1",
|
|
80
82
|
"tsdown": "^0.22.7",
|
|
81
83
|
"typescript": ">=6,<7",
|
|
82
84
|
"unplugin-unused": "^0.5.7"
|