importmapify 1.4.0 → 1.5.0
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 +25 -10
- package/deno.json +1 -1
- package/dist/mod.d.mts +304 -32
- package/dist/mod.mjs +83 -19
- package/import_map.json +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,6 +39,8 @@ npx importmapify --root . --out import_map.json
|
|
|
39
39
|
| `--scope prefix::key=value` | `-s` | Scoped import override; repeatable | none |
|
|
40
40
|
| `--condition name` | `-c` | Condition tried when a target is a conditional object; repeatable | `import`, `default` |
|
|
41
41
|
| `--ext name` | `-e` | Restrict pattern matches to these file extensions; repeatable | all files |
|
|
42
|
+
| `--filter regex` | `-f` | Regular expression a pattern target path must match; repeatable | all files |
|
|
43
|
+
| `--indent value` | | Indentation as a number of spaces or the word `tab` | tab |
|
|
42
44
|
| `--config path` | `-C` | Config file path; skips discovery | auto-discovered |
|
|
43
45
|
| `--no-config` | | Skip config file discovery | off |
|
|
44
46
|
| `--check` | | Exit 1 if the output file is stale, without writing it | off |
|
|
@@ -84,9 +86,9 @@ Then `npx importmapify` reads it automatically. Point at a specific file with `-
|
|
|
84
86
|
`--no-config`.
|
|
85
87
|
|
|
86
88
|
Explicitly-passed flags override the config, which overrides built-in defaults: `--import`/`--package`/`--scope` merge
|
|
87
|
-
onto the config's records with flag keys winning; `--condition`/`--ext` replace;
|
|
88
|
-
passed. Configs are imported natively; a TypeScript config needs a type-stripping
|
|
89
|
-
on older Node use a `.mjs`/`.cjs`/`.js` config.
|
|
89
|
+
onto the config's records with flag keys winning; `--condition`/`--ext`/`--filter` replace;
|
|
90
|
+
`--root`/`--manifest`/`--out` win when passed. Configs are imported natively; a TypeScript config needs a type-stripping
|
|
91
|
+
runtime (Bun, Deno, or Node \>= 22.6); on older Node use a `.mjs`/`.cjs`/`.js` config.
|
|
90
92
|
|
|
91
93
|
## Library
|
|
92
94
|
|
|
@@ -126,15 +128,17 @@ const out = writeImportMap({
|
|
|
126
128
|
| `additionalImports` | Extra entries merged in after manifest expansion and packages; these win on key collision. | none |
|
|
127
129
|
| `scopes` | Scope prefixes mapped to scope-specific import overrides. | none |
|
|
128
130
|
| `relativeTo` | Directory the written targets are rebased against. | `root` |
|
|
129
|
-
| `extensions` |
|
|
131
|
+
| `extensions` | Extension whitelist limiting which pattern targets are kept. | all files |
|
|
132
|
+
| `filter` | `RegExp`/predicate matchers a pattern target path must pass; combines with `extensions`. | all files |
|
|
130
133
|
|
|
131
|
-
`WriteImportMapOptions` extends the above with an optional `out`, defaulting to `import_map.json` like the CLI
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
`WriteImportMapOptions` extends the above with an optional `out`, defaulting to `import_map.json` like the CLI, and an
|
|
135
|
+
optional `indent` with `JSON.stringify` space semantics (a number of spaces or a literal string), defaulting to a tab.
|
|
136
|
+
`out` is resolved against `root` and accepts a relative path, an absolute path, or a `file://` URL. `writeImportMap`
|
|
137
|
+
rebases automatically against `out`'s directory, so a nested `out` (for example `.cache/maps/import_map.json`) still
|
|
138
|
+
produces targets that resolve correctly from the map's own location.
|
|
135
139
|
|
|
136
|
-
`Config` extends `Partial<WriteImportMapOptions>` with an optional `hooks` field
|
|
137
|
-
export and `defineConfig` take, with every field optional. An omitted `root` defaults to the config file's own
|
|
140
|
+
`Config` extends `Partial<WriteImportMapOptions>` with an optional `hooks` field. It is the shape a config file's
|
|
141
|
+
default export and `defineConfig` take, with every field optional. An omitted `root` defaults to the config file's own
|
|
138
142
|
directory; the CLI supplies the remaining defaults.
|
|
139
143
|
|
|
140
144
|
`defineConfig` returns its argument unchanged; it exists only to type a config object for export and reuse without a
|
|
@@ -172,6 +176,17 @@ import { createImportMap } from 'importmapify';
|
|
|
172
176
|
createImportMap({ root: import.meta.dirname, extensions: ['ts', 'tsx'] });
|
|
173
177
|
```
|
|
174
178
|
|
|
179
|
+
`filter` takes `RegExp` or predicate matchers tested against each candidate target path; a target is kept only when
|
|
180
|
+
every matcher accepts it. Combined with `extensions`, this drops hashed build chunks a `dist/*` pattern would otherwise
|
|
181
|
+
enumerate:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { createImportMap } from 'importmapify';
|
|
185
|
+
|
|
186
|
+
// Keep .js targets whose path is not an internal chunk like ./dist/internal-qo9O8jzH.js.
|
|
187
|
+
createImportMap({ root: import.meta.dirname, extensions: ['js'], filter: [/^(?!.*internal)/] });
|
|
188
|
+
```
|
|
189
|
+
|
|
175
190
|
Build one conformant pair directly with `packageEntries`:
|
|
176
191
|
|
|
177
192
|
```ts
|
package/deno.json
CHANGED
package/dist/mod.d.mts
CHANGED
|
@@ -1,67 +1,327 @@
|
|
|
1
|
-
//#region src/
|
|
2
|
-
/**
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A deterministic Deno import map generated from package import entries, following the
|
|
4
|
+
* {@link https://html.spec.whatwg.org/multipage/webappapis.html#import-maps | Import Maps Standard}.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import type { ImportMapDocument } from 'jsr:@kjanat/importmapify';
|
|
9
|
+
*
|
|
10
|
+
* const doc: ImportMapDocument = {
|
|
11
|
+
* imports: { '#lib/bytes': './src/lib/bytes.ts' },
|
|
12
|
+
* scopes: { './tests/': { '#lib/bytes': './tests/stub/bytes.ts' } },
|
|
13
|
+
* };
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
3
16
|
interface ImportMapDocument {
|
|
4
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Exact specifier-to-target mappings, sorted by specifier, as in Deno's
|
|
19
|
+
* {@link https://docs.deno.com/runtime/reference/deno_json/#custom-path-mappings | custom path mappings}.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const imports = { '#lib/bytes': './src/lib/bytes.ts' };
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
5
26
|
readonly imports: Readonly<Record<string, string>>;
|
|
6
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Scope prefixes mapped to sorted, scope-specific import overrides, as in Deno's
|
|
29
|
+
* {@link https://docs.deno.com/runtime/reference/deno_json/#scoped-mappings | scoped mappings}.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const scopes = { './tests/': { '#lib/bytes': './tests/stub/bytes.ts' } };
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
7
36
|
readonly scopes?: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
8
37
|
}
|
|
9
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* A filesystem location as a path string or `file://` URL, accepted wherever an option names a
|
|
40
|
+
* directory or file.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* import type { PathOrUrl } from 'jsr:@kjanat/importmapify';
|
|
45
|
+
*
|
|
46
|
+
* const root: PathOrUrl = new URL('..', import.meta.url);
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
type PathOrUrl = string | URL;
|
|
50
|
+
/**
|
|
51
|
+
* A matcher for {@link CreateImportMapOptions.filter | filter}, tested against a candidate target
|
|
52
|
+
* path such as `./dist/internal-qo9O8jzH.js`. A target is kept only when every matcher accepts it.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* import type { TargetFilter } from 'jsr:@kjanat/importmapify';
|
|
57
|
+
*
|
|
58
|
+
* // Drop hashed internal chunks like ./dist/internal-qo9O8jzH.js.
|
|
59
|
+
* const filter: readonly TargetFilter[] = [/^(?!.*internal)/];
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
type TargetFilter = RegExp | ((target: string) => boolean);
|
|
63
|
+
/**
|
|
64
|
+
* Options for creating an import map without writing it to disk.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* import { createImportMap, type CreateImportMapOptions } from 'jsr:@kjanat/importmapify';
|
|
69
|
+
*
|
|
70
|
+
* const options: CreateImportMapOptions = {
|
|
71
|
+
* root: import.meta.dirname,
|
|
72
|
+
* conditions: ['deno', 'import', 'default'],
|
|
73
|
+
* extensions: ['ts', 'tsx'],
|
|
74
|
+
* };
|
|
75
|
+
*
|
|
76
|
+
* createImportMap(options);
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
10
79
|
interface CreateImportMapOptions {
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Project directory containing the package {@linkcode manifest}, as a path or `file://` URL.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* const root = import.meta.dirname;
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
readonly root: PathOrUrl;
|
|
89
|
+
/**
|
|
90
|
+
* Manifest path relative to {@link root}. Defaults to `package.json`.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* const manifest = 'deno.json';
|
|
95
|
+
* ```
|
|
96
|
+
* @defaultValue `'package.json'`
|
|
97
|
+
*/
|
|
14
98
|
readonly manifest?: string;
|
|
15
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Conditional import keys to try in order. Defaults to `import`, then `default`.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* const conditions = ['deno', 'import', 'default'];
|
|
105
|
+
* ```
|
|
106
|
+
* @defaultValue `['import', 'default']`
|
|
107
|
+
*/
|
|
16
108
|
readonly conditions?: readonly string[];
|
|
17
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* Package specifiers mapped to targets, each expanded to a conformant bare and trailing-slash pair.
|
|
111
|
+
* Defaults to none.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* const packages = { dreamcli: 'jsr:@kjanat/dreamcli@^3' };
|
|
116
|
+
* ```
|
|
117
|
+
* @defaultValue `{}`
|
|
118
|
+
*/
|
|
18
119
|
readonly packages?: Readonly<Record<string, string>>;
|
|
19
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* Explicit entries merged after manifest imports and packages, overriding duplicate keys. Defaults to none.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* const additionalImports = { '#config': './src/config.ts' };
|
|
126
|
+
* ```
|
|
127
|
+
* @defaultValue `{}`
|
|
128
|
+
*/
|
|
20
129
|
readonly additionalImports?: Readonly<Record<string, string>>;
|
|
21
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* Scope-specific import overrides keyed by scope prefix, following Deno's
|
|
132
|
+
* {@link https://docs.deno.com/runtime/reference/deno_json/#scoped-mappings | scoped mappings}. Defaults to none.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* const scopes = { './tests/': { '#lib/bytes': './tests/stub/bytes.ts' } };
|
|
137
|
+
* ```
|
|
138
|
+
* @defaultValue `{}`
|
|
139
|
+
*/
|
|
22
140
|
readonly scopes?: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Directory the generated import map will be read from, as a path or `file://` URL.
|
|
143
|
+
*
|
|
144
|
+
* Manifest targets are written relative to {@link root}, but Deno resolves an import map's relative targets
|
|
145
|
+
* from the location of the map file. Each relative target is rewritten to be relative to this directory
|
|
146
|
+
* instead, so it still points at the right file once the map moves. `writeImportMap` and the CLI set
|
|
147
|
+
* this automatically to the output file's directory; set it yourself only when placing an in-memory map
|
|
148
|
+
* somewhere other than {@link root}. Defaults to {@link root}.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```ts
|
|
152
|
+
* // Map root is /proj, target is ./src/bytes.ts, map will live in /proj/.cache.
|
|
153
|
+
* const relativeTo = '/proj/.cache';
|
|
154
|
+
* // The emitted target becomes ../src/bytes.ts, which resolves back to /proj/src/bytes.ts.
|
|
155
|
+
* ```
|
|
156
|
+
* @defaultValue {@link root}
|
|
157
|
+
*/
|
|
158
|
+
readonly relativeTo?: PathOrUrl;
|
|
159
|
+
/**
|
|
160
|
+
* Extension whitelist limiting which pattern targets are kept, with or without leading dots.
|
|
161
|
+
* Combines with {@link filter}. Unset keeps every extension.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* const extensions = ['ts', 'tsx'];
|
|
166
|
+
* ```
|
|
167
|
+
* @defaultValue `[]`
|
|
168
|
+
*/
|
|
26
169
|
readonly extensions?: readonly string[];
|
|
170
|
+
/**
|
|
171
|
+
* Matchers a candidate pattern target must pass, each tested against the target path. A target is kept
|
|
172
|
+
* only when every matcher accepts it. Combines with {@link extensions}. Unset keeps every target.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```ts
|
|
176
|
+
* // Keep .js targets that are not hashed internal chunks.
|
|
177
|
+
* const options = { extensions: ['js'], filter: [/^(?!.*internal)/] };
|
|
178
|
+
* ```
|
|
179
|
+
* @defaultValue `[]`
|
|
180
|
+
*/
|
|
181
|
+
readonly filter?: readonly TargetFilter[];
|
|
27
182
|
}
|
|
28
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* Options for creating and writing an import map.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```ts
|
|
188
|
+
* import { writeImportMap, type WriteImportMapOptions } from 'jsr:@kjanat/importmapify';
|
|
189
|
+
*
|
|
190
|
+
* const options: WriteImportMapOptions = {
|
|
191
|
+
* root: import.meta.dirname,
|
|
192
|
+
* out: '.cache/maps/import_map.json',
|
|
193
|
+
* };
|
|
194
|
+
*
|
|
195
|
+
* writeImportMap(options);
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
29
198
|
interface WriteImportMapOptions extends CreateImportMapOptions {
|
|
30
199
|
/**
|
|
31
200
|
* Output path, resolved against {@link CreateImportMapOptions.root | root}. Accepts a relative path, an
|
|
32
201
|
* absolute path, a `file://` URL string, or a {@link URL}. Defaults to `import_map.json`.
|
|
202
|
+
*
|
|
203
|
+
* Deno loads the written file through the `importMap` field in `deno.json` or the `--import-map` flag; see
|
|
204
|
+
* {@link https://docs.deno.com/runtime/fundamentals/modules/#differentiating-between-imports-or-importmap-in-deno.json-and---import-map-option | Deno: imports vs importMap}.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```ts
|
|
208
|
+
* const out = '.cache/maps/import_map.json';
|
|
209
|
+
* ```
|
|
210
|
+
* @defaultValue `'import_map.json'`
|
|
211
|
+
*/
|
|
212
|
+
readonly out?: PathOrUrl;
|
|
213
|
+
/**
|
|
214
|
+
* Indentation for the serialized map, with the semantics of `JSON.stringify`'s
|
|
215
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#space | space parameter}:
|
|
216
|
+
* a number of spaces per level or a literal indent string. Defaults to a tab.
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```ts
|
|
220
|
+
* const indent = 2;
|
|
221
|
+
* ```
|
|
222
|
+
* @defaultValue `'\t'`
|
|
33
223
|
*/
|
|
34
|
-
readonly
|
|
224
|
+
readonly indent?: string | number;
|
|
35
225
|
}
|
|
36
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* Resolved paths shared by every generation hook.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```ts
|
|
231
|
+
* import type { HookContext } from 'jsr:@kjanat/importmapify';
|
|
232
|
+
*
|
|
233
|
+
* const logRoot = (ctx: HookContext) => console.log(`scanning ${ctx.root}, writing ${ctx.out}`);
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
37
236
|
interface HookContext {
|
|
38
|
-
/**
|
|
237
|
+
/**
|
|
238
|
+
* Absolute project root that gets scanned.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```ts
|
|
242
|
+
* const root = '/home/me/project';
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
39
245
|
readonly root: string;
|
|
40
|
-
/**
|
|
246
|
+
/**
|
|
247
|
+
* Absolute output path the map resolves against.
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```ts
|
|
251
|
+
* const out = '/home/me/project/import_map.json';
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
41
254
|
readonly out: string;
|
|
42
255
|
}
|
|
43
256
|
/**
|
|
44
257
|
* Lifecycle hooks the CLI runs around import map generation, modeled on tsdown's hooks.
|
|
45
258
|
*
|
|
46
259
|
* Each hook may be async; the CLI awaits it. The synchronous library functions ignore hooks.
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* ```ts
|
|
263
|
+
* // Build generated targets before the scan so they land in the map.
|
|
264
|
+
* import { execSync } from 'node:child_process';
|
|
265
|
+
* import { defineConfig } from 'jsr:@kjanat/importmapify';
|
|
266
|
+
*
|
|
267
|
+
* export default defineConfig({
|
|
268
|
+
* hooks: {
|
|
269
|
+
* 'generate:before': () => execSync('deno task build', { stdio: 'inherit' }),
|
|
270
|
+
* },
|
|
271
|
+
* });
|
|
272
|
+
* ```
|
|
47
273
|
*/
|
|
48
274
|
interface ImportMapHooks {
|
|
49
|
-
/**
|
|
275
|
+
/**
|
|
276
|
+
* Runs before the filesystem is scanned. Build pattern targets here so they exist when patterns expand.
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* ```ts
|
|
280
|
+
* const onBefore = (ctx) => console.log('building targets under', ctx.root);
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
50
283
|
readonly "generate:before": (context: HookContext) => void | Promise<void>;
|
|
51
|
-
/**
|
|
284
|
+
/**
|
|
285
|
+
* Runs after the map is generated and emitted.
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```ts
|
|
289
|
+
* const onDone = (ctx) => console.log('mapped', Object.keys(ctx.map.imports).length, 'imports');
|
|
290
|
+
* ```
|
|
291
|
+
*/
|
|
52
292
|
readonly "generate:done": (context: HookContext & {
|
|
53
293
|
readonly map: ImportMapDocument;
|
|
54
294
|
}) => void | Promise<void>;
|
|
55
295
|
}
|
|
56
296
|
/**
|
|
57
|
-
* An importmapify config file: the shape a config file's default export and
|
|
297
|
+
* An importmapify config file: the shape a config file's default export and `defineConfig` take.
|
|
58
298
|
* Every field is optional; the config loader and CLI supply {@linkcode CreateImportMapOptions.root | root}
|
|
59
299
|
* and the remaining defaults.
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* ```ts
|
|
303
|
+
* // importmapify.config.ts; root is omitted, so it defaults to this file's directory.
|
|
304
|
+
* import { defineConfig } from 'jsr:@kjanat/importmapify';
|
|
305
|
+
*
|
|
306
|
+
* export default defineConfig({
|
|
307
|
+
* packages: { dreamcli: 'jsr:@kjanat/dreamcli@^3' },
|
|
308
|
+
* extensions: ['ts'],
|
|
309
|
+
* });
|
|
310
|
+
* ```
|
|
60
311
|
*/
|
|
61
312
|
interface Config extends Partial<WriteImportMapOptions> {
|
|
62
|
-
/**
|
|
313
|
+
/**
|
|
314
|
+
* Lifecycle hooks the CLI runs around generation. Ignored by `writeImportMap` and `createImportMap`.
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```ts
|
|
318
|
+
* const hooks = { 'generate:before': (ctx) => console.log(ctx.root) };
|
|
319
|
+
* ```
|
|
320
|
+
*/
|
|
63
321
|
readonly hooks?: Partial<ImportMapHooks>;
|
|
64
322
|
}
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/map.d.ts
|
|
65
325
|
/**
|
|
66
326
|
* Expand a package's exact and patterned `imports` into a Deno import map.
|
|
67
327
|
*
|
|
@@ -86,24 +346,23 @@ interface Config extends Partial<WriteImportMapOptions> {
|
|
|
86
346
|
*/
|
|
87
347
|
declare function createImportMap(options: CreateImportMapOptions): ImportMapDocument;
|
|
88
348
|
/**
|
|
89
|
-
* Serialize an import map as stable
|
|
349
|
+
* Serialize an import map as stable JSON with a trailing newline.
|
|
90
350
|
*
|
|
91
351
|
* @example
|
|
92
352
|
* ```ts
|
|
93
|
-
* // Format an import map for stdout.
|
|
353
|
+
* // Format an import map for stdout, indented with two spaces.
|
|
94
354
|
* import { formatImportMap } from 'jsr:@kjanat/importmapify';
|
|
95
355
|
*
|
|
96
|
-
* const text = formatImportMap({
|
|
97
|
-
* imports: { '#config': './src/config.ts' },
|
|
98
|
-
* });
|
|
356
|
+
* const text = formatImportMap({ imports: { '#config': './src/config.ts' } }, 2);
|
|
99
357
|
*
|
|
100
358
|
* console.log(text);
|
|
101
359
|
* ```
|
|
102
360
|
*
|
|
103
361
|
* @param map Import map to serialize.
|
|
362
|
+
* @param indent Indentation with `JSON.stringify` space semantics; defaults to a tab.
|
|
104
363
|
* @returns Formatted JSON ready to print or write.
|
|
105
364
|
*/
|
|
106
|
-
declare function formatImportMap(map: ImportMapDocument): string;
|
|
365
|
+
declare function formatImportMap(map: ImportMapDocument, indent?: string | number): string;
|
|
107
366
|
/**
|
|
108
367
|
* Create an import map and write it to disk, creating parent directories as needed.
|
|
109
368
|
*
|
|
@@ -158,10 +417,23 @@ declare function packageEntries(name: string, target: string): Record<string, st
|
|
|
158
417
|
* writeImportMap(config);
|
|
159
418
|
* ```
|
|
160
419
|
*
|
|
420
|
+
* @example
|
|
421
|
+
* ```ts
|
|
422
|
+
* // Config file with a build hook: generate:before runs before the CLI scans.
|
|
423
|
+
* import { execSync } from 'node:child_process';
|
|
424
|
+
* import { defineConfig } from 'jsr:@kjanat/importmapify';
|
|
425
|
+
*
|
|
426
|
+
* export default defineConfig({
|
|
427
|
+
* hooks: {
|
|
428
|
+
* 'generate:before': () => execSync('deno task build', { stdio: 'inherit' }),
|
|
429
|
+
* },
|
|
430
|
+
* });
|
|
431
|
+
* ```
|
|
432
|
+
*
|
|
161
433
|
* @param config Import map configuration; every field is optional.
|
|
162
|
-
* @returns The same
|
|
434
|
+
* @returns The same {@linkcode config} value with its exact type preserved, so a config that includes {@linkcode CreateImportMapOptions.root} stays
|
|
163
435
|
* assignable to {@link writeImportMap} while one that omits it is still a valid config file.
|
|
164
436
|
*/
|
|
165
437
|
declare function defineConfig<T extends Config>(config: T): T;
|
|
166
438
|
//#endregion
|
|
167
|
-
export { type Config, type CreateImportMapOptions, type HookContext, type ImportMapDocument, type ImportMapHooks, type WriteImportMapOptions, createImportMap, defineConfig, formatImportMap, packageEntries, writeImportMap };
|
|
439
|
+
export { type Config, type CreateImportMapOptions, type HookContext, type ImportMapDocument, type ImportMapHooks, type PathOrUrl, type TargetFilter, type WriteImportMapOptions, createImportMap, defineConfig, formatImportMap, packageEntries, writeImportMap };
|
package/dist/mod.mjs
CHANGED
|
@@ -169,9 +169,18 @@ function asString(value) {
|
|
|
169
169
|
function asPath(value) {
|
|
170
170
|
return typeof value === "string" || value instanceof URL ? value : void 0;
|
|
171
171
|
}
|
|
172
|
+
function asIndent(value) {
|
|
173
|
+
return typeof value === "string" || typeof value === "number" ? value : void 0;
|
|
174
|
+
}
|
|
172
175
|
function asStringArray(value) {
|
|
173
176
|
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
|
|
174
177
|
}
|
|
178
|
+
function isTargetFilter(value) {
|
|
179
|
+
return value instanceof RegExp || typeof value === "function";
|
|
180
|
+
}
|
|
181
|
+
function asTargetFilters(value) {
|
|
182
|
+
return Array.isArray(value) && value.every(isTargetFilter) ? value : void 0;
|
|
183
|
+
}
|
|
175
184
|
function asStringRecord(value) {
|
|
176
185
|
if (!isRecord(value)) return;
|
|
177
186
|
const record = {};
|
|
@@ -223,12 +232,16 @@ function configToOptions(config, configDir) {
|
|
|
223
232
|
if (manifest !== void 0) result.manifest = manifest;
|
|
224
233
|
const out = asPath(config.out);
|
|
225
234
|
if (out !== void 0) result.out = out;
|
|
235
|
+
const indent = asIndent(config.indent);
|
|
236
|
+
if (indent !== void 0) result.indent = indent;
|
|
226
237
|
const relativeTo = asPath(config.relativeTo);
|
|
227
238
|
if (relativeTo !== void 0) result.relativeTo = relativeTo;
|
|
228
239
|
const conditions = asStringArray(config.conditions);
|
|
229
240
|
if (conditions !== void 0) result.conditions = conditions;
|
|
230
241
|
const extensions = asStringArray(config.extensions);
|
|
231
242
|
if (extensions !== void 0) result.extensions = extensions;
|
|
243
|
+
const filter = asTargetFilters(config.filter);
|
|
244
|
+
if (filter !== void 0) result.filter = filter;
|
|
232
245
|
const packages = asStringRecord(config.packages);
|
|
233
246
|
if (packages !== void 0) result.packages = packages;
|
|
234
247
|
const additionalImports = asStringRecord(config.additionalImports);
|
|
@@ -293,17 +306,28 @@ function readManifest(manifestPath) {
|
|
|
293
306
|
if (!isRecord(parsed)) throw new Error(`Manifest at ${manifestPath} must be a JSON object`);
|
|
294
307
|
return parsed;
|
|
295
308
|
}
|
|
296
|
-
function
|
|
309
|
+
function matcherKeeps(matcher, target) {
|
|
310
|
+
return matcher instanceof RegExp ? matcher.test(target) : matcher(target);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Build a predicate deciding whether a candidate target survives: its extension must be in the
|
|
314
|
+
* whitelist (if any extensions are given), and every {@link TargetFilter} must also accept it.
|
|
315
|
+
*/
|
|
316
|
+
function targetFilter(extensions, filters) {
|
|
297
317
|
const allowed = new Set(extensions.map((ext) => ext.startsWith(".") ? ext : `.${ext}`));
|
|
298
|
-
return (
|
|
318
|
+
return (target) => (allowed.size === 0 || allowed.has(path.extname(target))) && filters.every((matcher) => matcherKeeps(matcher, target));
|
|
299
319
|
}
|
|
300
|
-
function expandManifestImport(root, key, rawValue, { conditions, extensions }) {
|
|
320
|
+
function expandManifestImport(root, key, rawValue, { conditions, extensions, filter }) {
|
|
301
321
|
const value = typeof rawValue === "string" ? rawValue : resolveCondition(rawValue, conditions);
|
|
302
322
|
if (value === void 0) return {};
|
|
303
323
|
const pattern = parsePattern(key, value);
|
|
304
324
|
if (pattern === void 0) return { [key]: value };
|
|
305
|
-
const
|
|
306
|
-
|
|
325
|
+
const expanded = expandPattern(pattern, filesUnder(path.join(root, pattern.targetDirectory)));
|
|
326
|
+
if (extensions.length === 0 && filter.length === 0) return expanded;
|
|
327
|
+
const keep = targetFilter(extensions, filter);
|
|
328
|
+
const filtered = {};
|
|
329
|
+
for (const [specifier, target] of Object.entries(expanded)) if (keep(target)) filtered[specifier] = target;
|
|
330
|
+
return filtered;
|
|
307
331
|
}
|
|
308
332
|
function keyBaseLength(key) {
|
|
309
333
|
const star = key.indexOf("*");
|
|
@@ -383,7 +407,8 @@ function createImportMap(options) {
|
|
|
383
407
|
const relativeTo = options.relativeTo === void 0 ? root : toPath(options.relativeTo);
|
|
384
408
|
const imports = expandManifest(root, relativeTo, manifestImports, {
|
|
385
409
|
conditions,
|
|
386
|
-
extensions: options.extensions ?? []
|
|
410
|
+
extensions: options.extensions ?? [],
|
|
411
|
+
filter: options.filter ?? []
|
|
387
412
|
});
|
|
388
413
|
for (const [key, value] of Object.entries(collectAdditional(options))) imports[key] = rebaseTarget(root, relativeTo, value);
|
|
389
414
|
const scopes = buildScopes$1(root, relativeTo, options.scopes ?? {});
|
|
@@ -395,25 +420,24 @@ function createImportMap(options) {
|
|
|
395
420
|
};
|
|
396
421
|
}
|
|
397
422
|
/**
|
|
398
|
-
* Serialize an import map as stable
|
|
423
|
+
* Serialize an import map as stable JSON with a trailing newline.
|
|
399
424
|
*
|
|
400
425
|
* @example
|
|
401
426
|
* ```ts
|
|
402
|
-
* // Format an import map for stdout.
|
|
427
|
+
* // Format an import map for stdout, indented with two spaces.
|
|
403
428
|
* import { formatImportMap } from 'jsr:@kjanat/importmapify';
|
|
404
429
|
*
|
|
405
|
-
* const text = formatImportMap({
|
|
406
|
-
* imports: { '#config': './src/config.ts' },
|
|
407
|
-
* });
|
|
430
|
+
* const text = formatImportMap({ imports: { '#config': './src/config.ts' } }, 2);
|
|
408
431
|
*
|
|
409
432
|
* console.log(text);
|
|
410
433
|
* ```
|
|
411
434
|
*
|
|
412
435
|
* @param map Import map to serialize.
|
|
436
|
+
* @param indent Indentation with `JSON.stringify` space semantics; defaults to a tab.
|
|
413
437
|
* @returns Formatted JSON ready to print or write.
|
|
414
438
|
*/
|
|
415
|
-
function formatImportMap(map) {
|
|
416
|
-
return `${JSON.stringify(map, null,
|
|
439
|
+
function formatImportMap(map, indent = " ") {
|
|
440
|
+
return `${JSON.stringify(map, null, indent)}\n`;
|
|
417
441
|
}
|
|
418
442
|
/**
|
|
419
443
|
* Create an import map and write it to disk, creating parent directories as needed.
|
|
@@ -441,7 +465,7 @@ function writeImportMap(options) {
|
|
|
441
465
|
relativeTo
|
|
442
466
|
});
|
|
443
467
|
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
444
|
-
fs.writeFileSync(out, formatImportMap(map));
|
|
468
|
+
fs.writeFileSync(out, formatImportMap(map, options.indent));
|
|
445
469
|
return out;
|
|
446
470
|
}
|
|
447
471
|
function directoryTarget(target) {
|
|
@@ -487,8 +511,21 @@ function packageEntries(name, target) {
|
|
|
487
511
|
* writeImportMap(config);
|
|
488
512
|
* ```
|
|
489
513
|
*
|
|
514
|
+
* @example
|
|
515
|
+
* ```ts
|
|
516
|
+
* // Config file with a build hook: generate:before runs before the CLI scans.
|
|
517
|
+
* import { execSync } from 'node:child_process';
|
|
518
|
+
* import { defineConfig } from 'jsr:@kjanat/importmapify';
|
|
519
|
+
*
|
|
520
|
+
* export default defineConfig({
|
|
521
|
+
* hooks: {
|
|
522
|
+
* 'generate:before': () => execSync('deno task build', { stdio: 'inherit' }),
|
|
523
|
+
* },
|
|
524
|
+
* });
|
|
525
|
+
* ```
|
|
526
|
+
*
|
|
490
527
|
* @param config Import map configuration; every field is optional.
|
|
491
|
-
* @returns The same
|
|
528
|
+
* @returns The same {@linkcode config} value with its exact type preserved, so a config that includes {@linkcode CreateImportMapOptions.root} stays
|
|
492
529
|
* assignable to {@link writeImportMap} while one that omits it is still a valid config file.
|
|
493
530
|
*/
|
|
494
531
|
function defineConfig(config) {
|
|
@@ -497,7 +534,8 @@ function defineConfig(config) {
|
|
|
497
534
|
//#endregion
|
|
498
535
|
//#region src/cli.ts
|
|
499
536
|
const EXAMPLE_TOKEN = /(?:'[^']*'|"[^"]*"|\S)+/g;
|
|
500
|
-
const
|
|
537
|
+
const SPACE_COUNT = /^\d+$/;
|
|
538
|
+
const JSON_MODE = (argv.includes("--") ? argv.slice(0, argv.indexOf("--")) : argv).includes("--json");
|
|
501
539
|
function highlightExample(example) {
|
|
502
540
|
if (JSON_MODE) return example;
|
|
503
541
|
const tokens = example.match(EXAMPLE_TOKEN);
|
|
@@ -569,11 +607,34 @@ function preferExplicit(explicit, flagValue, configValue) {
|
|
|
569
607
|
function preferArray(flagValue, configValue) {
|
|
570
608
|
return flagValue !== void 0 && flagValue.length > 0 ? flagValue : configValue;
|
|
571
609
|
}
|
|
610
|
+
function parseIndent(raw) {
|
|
611
|
+
if (raw === "tab") return " ";
|
|
612
|
+
if (SPACE_COUNT.test(raw)) return Number(raw);
|
|
613
|
+
throw new CLIError(`--indent expects a number of spaces or "tab", got "${raw}"`, {
|
|
614
|
+
code: "invalid-indent-flag",
|
|
615
|
+
exitCode: 2
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function compileFilters(raws) {
|
|
619
|
+
return raws.map((raw) => {
|
|
620
|
+
try {
|
|
621
|
+
return new RegExp(raw);
|
|
622
|
+
} catch (cause) {
|
|
623
|
+
throw new CLIError(`--filter expects a valid regular expression, got "${raw}"`, {
|
|
624
|
+
code: "invalid-filter-flag",
|
|
625
|
+
exitCode: 2,
|
|
626
|
+
cause
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
}
|
|
572
631
|
/** Resolve final import map options by layering explicit flags over a discovered config over defaults. */
|
|
573
632
|
async function resolveGenerateOptions(flags) {
|
|
574
633
|
const base = await loadConfigOptions(flags.root, flags.config, flags["no-config"] ?? false);
|
|
575
634
|
const conditions = preferArray(flags.condition, base.conditions);
|
|
576
635
|
const extensions = preferArray(flags.ext, base.extensions);
|
|
636
|
+
const filter = preferArray(flags.filter === void 0 ? void 0 : compileFilters(flags.filter), base.filter);
|
|
637
|
+
const indent = flags.indent === void 0 ? base.indent : parseIndent(flags.indent);
|
|
577
638
|
const options = {
|
|
578
639
|
root: preferExplicit(flags.root !== cwd(), flags.root, base.root),
|
|
579
640
|
manifest: preferExplicit(flags.manifest !== "package.json", flags.manifest, base.manifest),
|
|
@@ -589,8 +650,10 @@ async function resolveGenerateOptions(flags) {
|
|
|
589
650
|
scopes: mergeScopes(base.scopes, buildScopes(flags.scope ?? []))
|
|
590
651
|
};
|
|
591
652
|
if (base.relativeTo !== void 0) options.relativeTo = base.relativeTo;
|
|
653
|
+
if (indent !== void 0) options.indent = indent;
|
|
592
654
|
if (conditions !== void 0) options.conditions = conditions;
|
|
593
655
|
if (extensions !== void 0) options.extensions = extensions;
|
|
656
|
+
if (filter !== void 0) options.filter = filter;
|
|
594
657
|
if (base.hooks !== void 0) options.hooks = base.hooks;
|
|
595
658
|
return options;
|
|
596
659
|
}
|
|
@@ -598,7 +661,7 @@ async function resolveGenerateOptions(flags) {
|
|
|
598
661
|
const generateCommand = command("generate").description("Expand package.json subpath-pattern imports into a Deno import map.").flag("root", flag.path({
|
|
599
662
|
mustExist: true,
|
|
600
663
|
type: "directory"
|
|
601
|
-
}).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("config", flag.string().describe("Config file path; skips discovery.").alias("C")).flag("no-config", flag.boolean().describe("Skip config file discovery.")).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.")).flag("quiet", flag.boolean().describe("Suppress the confirmation message on stderr.").alias("q")).example(highlightExample("importmapify --stdout"), "Print the generated import map").example(highlightExample(`importmapify --package 'dreamcli=jsr:@kjanat/dreamcli@^3' --scope './tests/::dreamcli/testkit=jsr:@kjanat/dreamcli@^3/testkit'`), "Add global and test-scoped dependencies").example(highlightExample("importmapify --check"), "Fail when the generated file is stale").action(async ({ flags, out }) => {
|
|
664
|
+
}).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("indent", flag.string().describe("Indentation as a number of spaces or the word \"tab\".")).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("filter", flag.array(flag.string()).describe("Regular expression a pattern target path must match. Repeatable.").alias("f")).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("config", flag.string().describe("Config file path; skips discovery.").alias("C")).flag("no-config", flag.boolean().describe("Skip config file discovery.")).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.")).flag("quiet", flag.boolean().describe("Suppress the confirmation message on stderr.").alias("q")).example(highlightExample("importmapify --stdout"), "Print the generated import map").example(highlightExample(`importmapify --package 'dreamcli=jsr:@kjanat/dreamcli@^3' --scope './tests/::dreamcli/testkit=jsr:@kjanat/dreamcli@^3/testkit'`), "Add global and test-scoped dependencies").example(highlightExample("importmapify --check"), "Fail when the generated file is stale").action(async ({ flags, out }) => {
|
|
602
665
|
const { log, warn, error, setExitCode } = out;
|
|
603
666
|
const { check, stdout, quiet } = flags;
|
|
604
667
|
if (check && stdout) throw new CLIError("--check and --stdout are mutually exclusive", {
|
|
@@ -617,7 +680,7 @@ const generateCommand = command("generate").description("Expand package.json sub
|
|
|
617
680
|
};
|
|
618
681
|
await options.hooks?.["generate:before"]?.(hookContext);
|
|
619
682
|
const map = createImportMap(createOptions);
|
|
620
|
-
const text = formatImportMap(map);
|
|
683
|
+
const text = formatImportMap(map, options.indent);
|
|
621
684
|
if (stdout) log(text);
|
|
622
685
|
else if (check) {
|
|
623
686
|
if ((existsSync(outPath) ? readFileSync(outPath, "utf8") : void 0) !== text) {
|
|
@@ -639,7 +702,8 @@ const generateCommand = command("generate").description("Expand package.json sub
|
|
|
639
702
|
/**
|
|
640
703
|
* Expand package import patterns into explicit Deno import map entries.
|
|
641
704
|
*
|
|
642
|
-
* Use the library API to create, format, or write deterministic import maps
|
|
705
|
+
* Use the library API to create, format, or write deterministic import maps that conform to the
|
|
706
|
+
* {@link https://html.spec.whatwg.org/multipage/webappapis.html#import-maps | Import Maps Standard}.
|
|
643
707
|
* This module also runs the `importmapify` CLI when executed directly.
|
|
644
708
|
*
|
|
645
709
|
* @example
|
package/import_map.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"#src/expand": "./src/expand.ts",
|
|
8
8
|
"#src/map": "./src/map.ts",
|
|
9
9
|
"#src/mod": "./src/mod.ts",
|
|
10
|
+
"#src/types": "./src/types.ts",
|
|
10
11
|
"@types/bun": "npm:@types/bun@1.3.14",
|
|
11
12
|
"ansispeck": "jsr:@kjanat/ansispeck@0.2.0",
|
|
12
13
|
"ansispeck/": "jsr:/@kjanat/ansispeck@0.2.0/",
|