importmapify 1.3.1 → 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 +46 -10
- package/deno.json +1 -1
- package/dist/mod.d.mts +329 -29
- package/dist/mod.mjs +126 -38
- 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
|
|
|
@@ -109,7 +111,7 @@ const out = writeImportMap({
|
|
|
109
111
|
|
|
110
112
|
| Export | Signature | Purpose |
|
|
111
113
|
| ----------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
112
|
-
| `defineConfig` |
|
|
114
|
+
| `defineConfig` | `<T extends Config>(config: T) => T` | Identity helper that types a config object for export and reuse. |
|
|
113
115
|
| `createImportMap` | `(options: CreateImportMapOptions) => ImportMapDocument` | Build the import map in memory. |
|
|
114
116
|
| `formatImportMap` | `(map: ImportMapDocument) => string` | Serialize to the canonical sorted, tab-indented JSON text. |
|
|
115
117
|
| `writeImportMap` | `(options: WriteImportMapOptions) => string` | Build, serialize, and write to disk; returns the written path. |
|
|
@@ -126,18 +128,27 @@ 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`
|
|
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
|
|
142
|
+
directory; the CLI supplies the remaining defaults.
|
|
137
143
|
|
|
138
144
|
`defineConfig` returns its argument unchanged; it exists only to type a config object for export and reuse without a
|
|
139
145
|
manual annotation.
|
|
140
146
|
|
|
147
|
+
`hooks` are lifecycle functions the CLI runs around generation, modeled on tsdown's hooks. `generate:before` runs before
|
|
148
|
+
the filesystem is scanned, so building generated targets there keeps them out of a stale map; `generate:done` runs after
|
|
149
|
+
the map is emitted. Each receives `{ root, out }` (`generate:done` also gets the finished `map`) and may be async. The
|
|
150
|
+
synchronous library functions ignore `hooks`.
|
|
151
|
+
|
|
141
152
|
### Recipes
|
|
142
153
|
|
|
143
154
|
Add dependencies with `packages`. Each entry expands to the bare specifier plus its conformant trailing-slash entry
|
|
@@ -165,6 +176,17 @@ import { createImportMap } from 'importmapify';
|
|
|
165
176
|
createImportMap({ root: import.meta.dirname, extensions: ['ts', 'tsx'] });
|
|
166
177
|
```
|
|
167
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
|
+
|
|
168
190
|
Build one conformant pair directly with `packageEntries`:
|
|
169
191
|
|
|
170
192
|
```ts
|
|
@@ -184,6 +206,20 @@ const map = createImportMap(config);
|
|
|
184
206
|
const written = writeImportMap(config);
|
|
185
207
|
```
|
|
186
208
|
|
|
209
|
+
Build before scanning with a `generate:before` hook, so a map that includes build output does not drop entries when it
|
|
210
|
+
runs before the build:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
import { execSync } from 'node:child_process';
|
|
214
|
+
import { defineConfig } from 'importmapify';
|
|
215
|
+
|
|
216
|
+
export default defineConfig({
|
|
217
|
+
hooks: {
|
|
218
|
+
'generate:before': () => execSync('bun run build', { stdio: 'inherit' }),
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
187
223
|
`root`, `relativeTo`, and `out` accept a path or a `file://` URL, so `import.meta.url` needs no `.pathname`:
|
|
188
224
|
|
|
189
225
|
```ts
|
package/deno.json
CHANGED
package/dist/mod.d.mts
CHANGED
|
@@ -1,40 +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'`
|
|
223
|
+
*/
|
|
224
|
+
readonly indent?: string | number;
|
|
225
|
+
}
|
|
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
|
+
*/
|
|
236
|
+
interface HookContext {
|
|
237
|
+
/**
|
|
238
|
+
* Absolute project root that gets scanned.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```ts
|
|
242
|
+
* const root = '/home/me/project';
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
readonly root: string;
|
|
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
|
+
*/
|
|
254
|
+
readonly out: string;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Lifecycle hooks the CLI runs around import map generation, modeled on tsdown's hooks.
|
|
258
|
+
*
|
|
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
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
interface ImportMapHooks {
|
|
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
|
+
*/
|
|
283
|
+
readonly "generate:before": (context: HookContext) => void | Promise<void>;
|
|
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
|
+
*/
|
|
292
|
+
readonly "generate:done": (context: HookContext & {
|
|
293
|
+
readonly map: ImportMapDocument;
|
|
294
|
+
}) => void | Promise<void>;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* An importmapify config file: the shape a config file's default export and `defineConfig` take.
|
|
298
|
+
* Every field is optional; the config loader and CLI supply {@linkcode CreateImportMapOptions.root | root}
|
|
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
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
interface Config extends Partial<WriteImportMapOptions> {
|
|
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
|
+
* ```
|
|
33
320
|
*/
|
|
34
|
-
readonly
|
|
321
|
+
readonly hooks?: Partial<ImportMapHooks>;
|
|
35
322
|
}
|
|
36
|
-
|
|
37
|
-
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/map.d.ts
|
|
38
325
|
/**
|
|
39
326
|
* Expand a package's exact and patterned `imports` into a Deno import map.
|
|
40
327
|
*
|
|
@@ -59,24 +346,23 @@ type Config = WriteImportMapOptions;
|
|
|
59
346
|
*/
|
|
60
347
|
declare function createImportMap(options: CreateImportMapOptions): ImportMapDocument;
|
|
61
348
|
/**
|
|
62
|
-
* Serialize an import map as stable
|
|
349
|
+
* Serialize an import map as stable JSON with a trailing newline.
|
|
63
350
|
*
|
|
64
351
|
* @example
|
|
65
352
|
* ```ts
|
|
66
|
-
* // Format an import map for stdout.
|
|
353
|
+
* // Format an import map for stdout, indented with two spaces.
|
|
67
354
|
* import { formatImportMap } from 'jsr:@kjanat/importmapify';
|
|
68
355
|
*
|
|
69
|
-
* const text = formatImportMap({
|
|
70
|
-
* imports: { '#config': './src/config.ts' },
|
|
71
|
-
* });
|
|
356
|
+
* const text = formatImportMap({ imports: { '#config': './src/config.ts' } }, 2);
|
|
72
357
|
*
|
|
73
358
|
* console.log(text);
|
|
74
359
|
* ```
|
|
75
360
|
*
|
|
76
361
|
* @param map Import map to serialize.
|
|
362
|
+
* @param indent Indentation with `JSON.stringify` space semantics; defaults to a tab.
|
|
77
363
|
* @returns Formatted JSON ready to print or write.
|
|
78
364
|
*/
|
|
79
|
-
declare function formatImportMap(map: ImportMapDocument): string;
|
|
365
|
+
declare function formatImportMap(map: ImportMapDocument, indent?: string | number): string;
|
|
80
366
|
/**
|
|
81
367
|
* Create an import map and write it to disk, creating parent directories as needed.
|
|
82
368
|
*
|
|
@@ -131,9 +417,23 @@ declare function packageEntries(name: string, target: string): Record<string, st
|
|
|
131
417
|
* writeImportMap(config);
|
|
132
418
|
* ```
|
|
133
419
|
*
|
|
134
|
-
* @
|
|
135
|
-
*
|
|
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
|
+
*
|
|
433
|
+
* @param config Import map configuration; every field is optional.
|
|
434
|
+
* @returns The same {@linkcode config} value with its exact type preserved, so a config that includes {@linkcode CreateImportMapOptions.root} stays
|
|
435
|
+
* assignable to {@link writeImportMap} while one that omits it is still a valid config file.
|
|
136
436
|
*/
|
|
137
|
-
declare function defineConfig(config:
|
|
437
|
+
declare function defineConfig<T extends Config>(config: T): T;
|
|
138
438
|
//#endregion
|
|
139
|
-
export { type Config, type CreateImportMapOptions, type ImportMapDocument, 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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { bold, cyan, detectHyperlinkSupport } from "ansispeck";
|
|
3
3
|
import { CLIError, cli, command, flag } from "dreamcli";
|
|
4
|
-
import fs, { existsSync, readFileSync } from "node:fs";
|
|
5
|
-
import path, { dirname } from "node:path";
|
|
4
|
+
import fs, { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import path, { dirname, resolve } from "node:path";
|
|
6
6
|
import { argv, cwd } from "node:process";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
8
|
//#region src/expand.ts
|
|
@@ -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 = {};
|
|
@@ -191,36 +200,56 @@ function asScopes(value) {
|
|
|
191
200
|
}
|
|
192
201
|
return scopes;
|
|
193
202
|
}
|
|
203
|
+
function isHook(value) {
|
|
204
|
+
return typeof value === "function";
|
|
205
|
+
}
|
|
206
|
+
function asHooks(value) {
|
|
207
|
+
if (!isRecord(value)) return;
|
|
208
|
+
const hooks = {};
|
|
209
|
+
const before = value["generate:before"];
|
|
210
|
+
if (isHook(before)) hooks["generate:before"] = before;
|
|
211
|
+
const done = value["generate:done"];
|
|
212
|
+
if (isHook(done)) hooks["generate:done"] = done;
|
|
213
|
+
return hooks["generate:before"] === void 0 && hooks["generate:done"] === void 0 ? void 0 : hooks;
|
|
214
|
+
}
|
|
194
215
|
/**
|
|
195
216
|
* Parse a raw config object into typed import map options.
|
|
196
217
|
*
|
|
197
|
-
* Only known fields are read
|
|
198
|
-
*
|
|
218
|
+
* Only known fields are read. An omitted `root` defaults to the config file's own directory; a relative
|
|
219
|
+
* string `root` resolves against it, so a config can anchor itself with `root: '.'`.
|
|
199
220
|
*
|
|
200
221
|
* @param config Raw default-exported config object.
|
|
201
222
|
* @param configDir Directory containing the config file.
|
|
202
|
-
* @returns The subset of {@link WriteImportMapOptions} the config declares.
|
|
223
|
+
* @returns The subset of {@link WriteImportMapOptions} the config declares, plus any hooks.
|
|
203
224
|
*/
|
|
204
225
|
function configToOptions(config, configDir) {
|
|
205
226
|
const result = {};
|
|
206
227
|
const root = asPath(config.root);
|
|
207
|
-
if (root
|
|
228
|
+
if (root === void 0) result.root = configDir;
|
|
229
|
+
else if (typeof root === "string" && !path.isAbsolute(root) && !root.startsWith("file://")) result.root = path.resolve(configDir, root);
|
|
230
|
+
else result.root = root;
|
|
208
231
|
const manifest = asString(config.manifest);
|
|
209
232
|
if (manifest !== void 0) result.manifest = manifest;
|
|
210
233
|
const out = asPath(config.out);
|
|
211
234
|
if (out !== void 0) result.out = out;
|
|
235
|
+
const indent = asIndent(config.indent);
|
|
236
|
+
if (indent !== void 0) result.indent = indent;
|
|
212
237
|
const relativeTo = asPath(config.relativeTo);
|
|
213
238
|
if (relativeTo !== void 0) result.relativeTo = relativeTo;
|
|
214
239
|
const conditions = asStringArray(config.conditions);
|
|
215
240
|
if (conditions !== void 0) result.conditions = conditions;
|
|
216
241
|
const extensions = asStringArray(config.extensions);
|
|
217
242
|
if (extensions !== void 0) result.extensions = extensions;
|
|
243
|
+
const filter = asTargetFilters(config.filter);
|
|
244
|
+
if (filter !== void 0) result.filter = filter;
|
|
218
245
|
const packages = asStringRecord(config.packages);
|
|
219
246
|
if (packages !== void 0) result.packages = packages;
|
|
220
247
|
const additionalImports = asStringRecord(config.additionalImports);
|
|
221
248
|
if (additionalImports !== void 0) result.additionalImports = additionalImports;
|
|
222
249
|
const scopes = asScopes(config.scopes);
|
|
223
250
|
if (scopes !== void 0) result.scopes = scopes;
|
|
251
|
+
const hooks = asHooks(config.hooks);
|
|
252
|
+
if (hooks !== void 0) result.hooks = hooks;
|
|
224
253
|
return result;
|
|
225
254
|
}
|
|
226
255
|
/**
|
|
@@ -277,17 +306,28 @@ function readManifest(manifestPath) {
|
|
|
277
306
|
if (!isRecord(parsed)) throw new Error(`Manifest at ${manifestPath} must be a JSON object`);
|
|
278
307
|
return parsed;
|
|
279
308
|
}
|
|
280
|
-
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) {
|
|
281
317
|
const allowed = new Set(extensions.map((ext) => ext.startsWith(".") ? ext : `.${ext}`));
|
|
282
|
-
return (
|
|
318
|
+
return (target) => (allowed.size === 0 || allowed.has(path.extname(target))) && filters.every((matcher) => matcherKeeps(matcher, target));
|
|
283
319
|
}
|
|
284
|
-
function expandManifestImport(root, key, rawValue, { conditions, extensions }) {
|
|
320
|
+
function expandManifestImport(root, key, rawValue, { conditions, extensions, filter }) {
|
|
285
321
|
const value = typeof rawValue === "string" ? rawValue : resolveCondition(rawValue, conditions);
|
|
286
322
|
if (value === void 0) return {};
|
|
287
323
|
const pattern = parsePattern(key, value);
|
|
288
324
|
if (pattern === void 0) return { [key]: value };
|
|
289
|
-
const
|
|
290
|
-
|
|
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;
|
|
291
331
|
}
|
|
292
332
|
function keyBaseLength(key) {
|
|
293
333
|
const star = key.indexOf("*");
|
|
@@ -367,7 +407,8 @@ function createImportMap(options) {
|
|
|
367
407
|
const relativeTo = options.relativeTo === void 0 ? root : toPath(options.relativeTo);
|
|
368
408
|
const imports = expandManifest(root, relativeTo, manifestImports, {
|
|
369
409
|
conditions,
|
|
370
|
-
extensions: options.extensions ?? []
|
|
410
|
+
extensions: options.extensions ?? [],
|
|
411
|
+
filter: options.filter ?? []
|
|
371
412
|
});
|
|
372
413
|
for (const [key, value] of Object.entries(collectAdditional(options))) imports[key] = rebaseTarget(root, relativeTo, value);
|
|
373
414
|
const scopes = buildScopes$1(root, relativeTo, options.scopes ?? {});
|
|
@@ -379,25 +420,24 @@ function createImportMap(options) {
|
|
|
379
420
|
};
|
|
380
421
|
}
|
|
381
422
|
/**
|
|
382
|
-
* Serialize an import map as stable
|
|
423
|
+
* Serialize an import map as stable JSON with a trailing newline.
|
|
383
424
|
*
|
|
384
425
|
* @example
|
|
385
426
|
* ```ts
|
|
386
|
-
* // Format an import map for stdout.
|
|
427
|
+
* // Format an import map for stdout, indented with two spaces.
|
|
387
428
|
* import { formatImportMap } from 'jsr:@kjanat/importmapify';
|
|
388
429
|
*
|
|
389
|
-
* const text = formatImportMap({
|
|
390
|
-
* imports: { '#config': './src/config.ts' },
|
|
391
|
-
* });
|
|
430
|
+
* const text = formatImportMap({ imports: { '#config': './src/config.ts' } }, 2);
|
|
392
431
|
*
|
|
393
432
|
* console.log(text);
|
|
394
433
|
* ```
|
|
395
434
|
*
|
|
396
435
|
* @param map Import map to serialize.
|
|
436
|
+
* @param indent Indentation with `JSON.stringify` space semantics; defaults to a tab.
|
|
397
437
|
* @returns Formatted JSON ready to print or write.
|
|
398
438
|
*/
|
|
399
|
-
function formatImportMap(map) {
|
|
400
|
-
return `${JSON.stringify(map, null,
|
|
439
|
+
function formatImportMap(map, indent = " ") {
|
|
440
|
+
return `${JSON.stringify(map, null, indent)}\n`;
|
|
401
441
|
}
|
|
402
442
|
/**
|
|
403
443
|
* Create an import map and write it to disk, creating parent directories as needed.
|
|
@@ -425,7 +465,7 @@ function writeImportMap(options) {
|
|
|
425
465
|
relativeTo
|
|
426
466
|
});
|
|
427
467
|
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
428
|
-
fs.writeFileSync(out, formatImportMap(map));
|
|
468
|
+
fs.writeFileSync(out, formatImportMap(map, options.indent));
|
|
429
469
|
return out;
|
|
430
470
|
}
|
|
431
471
|
function directoryTarget(target) {
|
|
@@ -471,8 +511,22 @@ function packageEntries(name, target) {
|
|
|
471
511
|
* writeImportMap(config);
|
|
472
512
|
* ```
|
|
473
513
|
*
|
|
474
|
-
* @
|
|
475
|
-
*
|
|
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
|
+
*
|
|
527
|
+
* @param config Import map configuration; every field is optional.
|
|
528
|
+
* @returns The same {@linkcode config} value with its exact type preserved, so a config that includes {@linkcode CreateImportMapOptions.root} stays
|
|
529
|
+
* assignable to {@link writeImportMap} while one that omits it is still a valid config file.
|
|
476
530
|
*/
|
|
477
531
|
function defineConfig(config) {
|
|
478
532
|
return config;
|
|
@@ -480,7 +534,8 @@ function defineConfig(config) {
|
|
|
480
534
|
//#endregion
|
|
481
535
|
//#region src/cli.ts
|
|
482
536
|
const EXAMPLE_TOKEN = /(?:'[^']*'|"[^"]*"|\S)+/g;
|
|
483
|
-
const
|
|
537
|
+
const SPACE_COUNT = /^\d+$/;
|
|
538
|
+
const JSON_MODE = (argv.includes("--") ? argv.slice(0, argv.indexOf("--")) : argv).includes("--json");
|
|
484
539
|
function highlightExample(example) {
|
|
485
540
|
if (JSON_MODE) return example;
|
|
486
541
|
const tokens = example.match(EXAMPLE_TOKEN);
|
|
@@ -552,11 +607,34 @@ function preferExplicit(explicit, flagValue, configValue) {
|
|
|
552
607
|
function preferArray(flagValue, configValue) {
|
|
553
608
|
return flagValue !== void 0 && flagValue.length > 0 ? flagValue : configValue;
|
|
554
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
|
+
}
|
|
555
631
|
/** Resolve final import map options by layering explicit flags over a discovered config over defaults. */
|
|
556
632
|
async function resolveGenerateOptions(flags) {
|
|
557
633
|
const base = await loadConfigOptions(flags.root, flags.config, flags["no-config"] ?? false);
|
|
558
634
|
const conditions = preferArray(flags.condition, base.conditions);
|
|
559
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);
|
|
560
638
|
const options = {
|
|
561
639
|
root: preferExplicit(flags.root !== cwd(), flags.root, base.root),
|
|
562
640
|
manifest: preferExplicit(flags.manifest !== "package.json", flags.manifest, base.manifest),
|
|
@@ -572,15 +650,18 @@ async function resolveGenerateOptions(flags) {
|
|
|
572
650
|
scopes: mergeScopes(base.scopes, buildScopes(flags.scope ?? []))
|
|
573
651
|
};
|
|
574
652
|
if (base.relativeTo !== void 0) options.relativeTo = base.relativeTo;
|
|
653
|
+
if (indent !== void 0) options.indent = indent;
|
|
575
654
|
if (conditions !== void 0) options.conditions = conditions;
|
|
576
655
|
if (extensions !== void 0) options.extensions = extensions;
|
|
656
|
+
if (filter !== void 0) options.filter = filter;
|
|
657
|
+
if (base.hooks !== void 0) options.hooks = base.hooks;
|
|
577
658
|
return options;
|
|
578
659
|
}
|
|
579
660
|
/** DreamCLI command that writes, checks, or prints an expanded Deno import map. */
|
|
580
661
|
const generateCommand = command("generate").description("Expand package.json subpath-pattern imports into a Deno import map.").flag("root", flag.path({
|
|
581
662
|
mustExist: true,
|
|
582
663
|
type: "directory"
|
|
583
|
-
}).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 }) => {
|
|
584
665
|
const { log, warn, error, setExitCode } = out;
|
|
585
666
|
const { check, stdout, quiet } = flags;
|
|
586
667
|
if (check && stdout) throw new CLIError("--check and --stdout are mutually exclusive", {
|
|
@@ -593,29 +674,36 @@ const generateCommand = command("generate").description("Expand package.json sub
|
|
|
593
674
|
...options,
|
|
594
675
|
relativeTo: options.relativeTo ?? dirname(outPath)
|
|
595
676
|
};
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
677
|
+
const hookContext = {
|
|
678
|
+
root: resolve(toPath(options.root)),
|
|
679
|
+
out: outPath
|
|
680
|
+
};
|
|
681
|
+
await options.hooks?.["generate:before"]?.(hookContext);
|
|
682
|
+
const map = createImportMap(createOptions);
|
|
683
|
+
const text = formatImportMap(map, options.indent);
|
|
684
|
+
if (stdout) log(text);
|
|
685
|
+
else if (check) {
|
|
686
|
+
if ((existsSync(outPath) ? readFileSync(outPath, "utf8") : void 0) !== text) {
|
|
603
687
|
error(`${outPath} is stale`);
|
|
604
688
|
setExitCode(1);
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
689
|
+
} else if (!quiet) warn(`${outPath} is up to date`);
|
|
690
|
+
} else {
|
|
691
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
692
|
+
writeFileSync(outPath, text);
|
|
693
|
+
if (!quiet) warn(`Wrote ${outPath}`);
|
|
609
694
|
}
|
|
610
|
-
|
|
611
|
-
|
|
695
|
+
await options.hooks?.["generate:done"]?.({
|
|
696
|
+
...hookContext,
|
|
697
|
+
map
|
|
698
|
+
});
|
|
612
699
|
});
|
|
613
700
|
//#endregion
|
|
614
701
|
//#region src/mod.ts
|
|
615
702
|
/**
|
|
616
703
|
* Expand package import patterns into explicit Deno import map entries.
|
|
617
704
|
*
|
|
618
|
-
* 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}.
|
|
619
707
|
* This module also runs the `importmapify` CLI when executed directly.
|
|
620
708
|
*
|
|
621
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/",
|