importmapify 0.1.0 → 1.0.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 +235 -45
- package/deno.json +56 -0
- package/dist/mod.d.mts +91 -0
- package/dist/mod.mjs +354 -0
- package/import_map.json +20 -0
- package/package.json +24 -17
- package/dist/import-map.mjs +0 -1
- package/dist/importmapify.mjs +0 -2
- package/dist/index.d.mts +0 -19
- package/dist/index.mjs +0 -1
package/README.md
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
# importmapify
|
|
2
2
|
|
|
3
3
|
[][npm]
|
|
4
|
+
[][jsr]
|
|
4
5
|
[][ci]
|
|
5
6
|
[][socket]
|
|
6
7
|
|
|
7
|
-
Expand `package.json` subpath-pattern imports into the explicit entries a
|
|
8
|
-
[Deno import map] needs.
|
|
8
|
+
Expand `package.json` subpath-pattern imports into the explicit entries a [Deno import map] needs.
|
|
9
9
|
|
|
10
|
-
Node resolves subpath patterns like `#lib/*` -> `./src/lib/*.ts` internally.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
one entry per matched source file instead. This package performs that
|
|
14
|
-
expansion and writes a deterministically sorted import map.
|
|
10
|
+
Node resolves subpath patterns like `#lib/*` -> `./src/lib/*.ts` internally. Deno's import maps only support exact keys
|
|
11
|
+
and trailing-slash prefixes, so tools that expect an import map (`deno doc`, `deno check`, the Deno LSP) need one entry
|
|
12
|
+
per matched source file instead. This package performs that expansion and writes a deterministically sorted import map.
|
|
15
13
|
|
|
16
14
|
[Deno import map]: https://docs.deno.com/runtime/fundamentals/modules/#differentiating-between-imports-or-importmap-in-deno.json-and---import-map-option
|
|
17
15
|
[ci]: https://github.com/kjanat/importmapify/actions/workflows/publish.yml
|
|
18
16
|
[npm]: https://npm.im/importmapify
|
|
17
|
+
[jsr]: https://jsr.io/@kjanat/importmapify
|
|
19
18
|
[socket]: https://socket.dev/npm/package/importmapify
|
|
20
19
|
|
|
21
20
|
## Install
|
|
@@ -27,21 +26,29 @@ npm install importmapify
|
|
|
27
26
|
## CLI
|
|
28
27
|
|
|
29
28
|
```sh
|
|
30
|
-
npx importmapify --root . --out
|
|
29
|
+
npx importmapify --root . --out import_map.json
|
|
31
30
|
```
|
|
32
31
|
|
|
33
|
-
| Flag
|
|
34
|
-
|
|
|
35
|
-
| `--root`
|
|
36
|
-
| `--manifest`
|
|
37
|
-
| `--out`
|
|
38
|
-
| `--import key=value`
|
|
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, relative to root | `import_map.json` |
|
|
37
|
+
| `--import key=value` | `-i` | Extra import entry; repeatable | none |
|
|
38
|
+
| `--scope prefix::key=value` | `-s` | Scoped import override; repeatable | none |
|
|
39
|
+
| `--condition name` | `-c` | Condition tried when a target is a conditional object; repeatable | `import`, `default` |
|
|
40
|
+
| `--check` | | Exit 1 if the output file is stale, without writing it | off |
|
|
41
|
+
| `--stdout` | | Print the map instead of writing it | off |
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
Add global and test-scoped dependencies from the CLI:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npx importmapify \
|
|
47
|
+
--import 'dreamcli=jsr:@kjanat/dreamcli@^3' \
|
|
48
|
+
--scope './tests/::dreamcli/testkit=jsr:@kjanat/dreamcli@^3/testkit'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Run `npx importmapify --help` for the full reference, or `npx importmapify --completions bash` for shell completions.
|
|
45
52
|
|
|
46
53
|
## Library
|
|
47
54
|
|
|
@@ -50,10 +57,15 @@ import { writeImportMap } from 'importmapify';
|
|
|
50
57
|
|
|
51
58
|
const out = writeImportMap({
|
|
52
59
|
root: import.meta.dirname,
|
|
53
|
-
out: '
|
|
60
|
+
out: 'import_map.json',
|
|
54
61
|
additionalImports: {
|
|
55
62
|
'bun:test': './node_modules/bun-types/test.d.ts',
|
|
56
63
|
},
|
|
64
|
+
scopes: {
|
|
65
|
+
'./tests/': {
|
|
66
|
+
'dreamcli/testkit': 'jsr:@kjanat/dreamcli@^3/testkit',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
57
69
|
});
|
|
58
70
|
```
|
|
59
71
|
|
|
@@ -71,12 +83,52 @@ const out = writeImportMap({
|
|
|
71
83
|
| `manifest` | Manifest path, relative to `root`. | `package.json` |
|
|
72
84
|
| `conditions` | Condition names tried, in order, against conditional targets (`{"import": ..., "default": ...}`). | `['import', 'default']` |
|
|
73
85
|
| `additionalImports` | Extra entries merged in after manifest expansion; these win on key collision. | none |
|
|
86
|
+
| `scopes` | Scope prefixes mapped to scope-specific import overrides. | none |
|
|
74
87
|
| `relativeTo` | Directory the written targets are rebased against. | `root` |
|
|
75
88
|
|
|
76
|
-
`WriteImportMapOptions` extends the above with `out`, the output path relative
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
89
|
+
`WriteImportMapOptions` extends the above with `out`, the output path relative to `root`. `writeImportMap` rebases
|
|
90
|
+
automatically against `out`'s directory, so a nested `out` (for example `.cache/maps/import_map.json`) still produces
|
|
91
|
+
targets that resolve correctly from the map's own location.
|
|
92
|
+
|
|
93
|
+
### Project-local generator
|
|
94
|
+
|
|
95
|
+
A checked-in wrapper keeps project-specific dependency and scope mappings in code:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
#!/usr/bin/env -S deno run -A --no-config
|
|
99
|
+
|
|
100
|
+
import { writeImportMap } from 'importmapify';
|
|
101
|
+
|
|
102
|
+
const output = writeImportMap({
|
|
103
|
+
root: new URL('..', import.meta.url).pathname,
|
|
104
|
+
out: 'import_map.json',
|
|
105
|
+
additionalImports: {
|
|
106
|
+
dreamcli: 'jsr:@kjanat/dreamcli@^3',
|
|
107
|
+
},
|
|
108
|
+
scopes: {
|
|
109
|
+
'./tests/': {
|
|
110
|
+
'dreamcli/testkit': 'jsr:@kjanat/dreamcli@^3/testkit',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
console.log(`Wrote ${output}`);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Execute the wrapper directly so its `--no-config` shebang can bootstrap a missing map:
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
scripts/generate-importmap.ts
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The equivalent explicit command is:
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
deno run -A --no-config scripts/generate-importmap.ts
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Do not omit `--no-config`. If `deno.json` references a map that does not exist yet, `deno run` otherwise fails while
|
|
131
|
+
loading configuration, before the generator starts.
|
|
80
132
|
|
|
81
133
|
## What it generates
|
|
82
134
|
|
|
@@ -116,43 +168,181 @@ the generated map is:
|
|
|
116
168
|
}
|
|
117
169
|
```
|
|
118
170
|
|
|
119
|
-
A key with its own suffix, such as `#lib/*.js` targeting `./src/lib/*.ts`,
|
|
120
|
-
|
|
121
|
-
and `#lib/bytes.ts`, both pointing at `./src/lib/bytes.ts`.
|
|
171
|
+
A key with its own suffix, such as `#lib/*.js` targeting `./src/lib/*.ts`, produces both the renamed specifier and the
|
|
172
|
+
real filename: `#lib/bytes.js` and `#lib/bytes.ts`, both pointing at `./src/lib/bytes.ts`.
|
|
122
173
|
|
|
123
174
|
## Scope and constraints
|
|
124
175
|
|
|
125
176
|
- Only the manifest's top-level `imports` field is read.
|
|
126
|
-
- Conditional targets resolve via `conditions`, tried in order, recursively.
|
|
127
|
-
|
|
128
|
-
-
|
|
129
|
-
|
|
130
|
-
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
- Relative
|
|
134
|
-
|
|
135
|
-
through unchanged.
|
|
136
|
-
- If exact and expanded entries produce the same key, the later manifest
|
|
137
|
-
entry wins.
|
|
138
|
-
- Output entries are sorted by UTF-16 code unit for stable diffs.
|
|
177
|
+
- Conditional targets resolve via `conditions`, tried in order, recursively. A target matching no condition is skipped.
|
|
178
|
+
- Pattern keys and targets must both contain `*`, or neither should; a mismatch throws.
|
|
179
|
+
- Expandable pattern targets must point to local files beneath `root`. Target directories are scanned recursively; a
|
|
180
|
+
missing directory produces no entries.
|
|
181
|
+
- Relative targets (`./...`, `../...`) are rebased against `relativeTo`. Bare specifiers, `npm:`/`jsr:`/`node:`
|
|
182
|
+
specifiers, and absolute URLs pass through unchanged.
|
|
183
|
+
- If exact and expanded entries produce the same key, the later manifest entry wins.
|
|
184
|
+
- Relative scope prefixes and targets are rebased against `relativeTo`; trailing scope slashes are preserved.
|
|
185
|
+
- Import entries, scope prefixes, and scoped entries are sorted by UTF-16 code unit for stable diffs.
|
|
139
186
|
|
|
140
187
|
## Use the generated map
|
|
141
188
|
|
|
189
|
+
The output is a standard external import map. Deno CLI commands, `deno.json`, editor language servers, and APIs that
|
|
190
|
+
embed Deno tooling can all consume it.
|
|
191
|
+
|
|
192
|
+
### Deno CLI
|
|
193
|
+
|
|
194
|
+
Pass the map to any command that accepts `--import-map`:
|
|
195
|
+
|
|
142
196
|
```sh
|
|
143
|
-
deno doc --import-map=
|
|
144
|
-
deno
|
|
145
|
-
deno
|
|
197
|
+
deno doc --import-map=import_map.json src/index.ts
|
|
198
|
+
deno doc --import-map=import_map.json --json src/index.ts
|
|
199
|
+
deno doc --import-map=import_map.json --html --output=docs/api src/index.ts
|
|
200
|
+
deno check --import-map=import_map.json src/
|
|
201
|
+
deno run --import-map=import_map.json src/main.ts
|
|
202
|
+
deno test --import-map=import_map.json
|
|
203
|
+
deno bench --import-map=import_map.json
|
|
204
|
+
deno compile --import-map=import_map.json src/main.ts
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### `deno.json`
|
|
208
|
+
|
|
209
|
+
Reference the map once instead of repeating `--import-map`:
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{
|
|
213
|
+
"importMap": "./import_map.json",
|
|
214
|
+
"tasks": {
|
|
215
|
+
"importmapify": {
|
|
216
|
+
"command": "npx importmapify",
|
|
217
|
+
"files": ["package.json", "src/**"],
|
|
218
|
+
"output": ["import_map.json"]
|
|
219
|
+
},
|
|
220
|
+
"check": {
|
|
221
|
+
"command": "deno check src tests",
|
|
222
|
+
"dependencies": ["importmapify"]
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Deno applies the map to CLI commands and its language server whenever that configuration is active. `importMap` is an
|
|
229
|
+
alternative to defining `imports` and `scopes` directly in `deno.json`.
|
|
230
|
+
|
|
231
|
+
When `importMap` points to a generated file, keep that file available after checkout. Deno validates the path before it
|
|
232
|
+
runs commands, including `deno task`, so a task in the same `deno.json` cannot bootstrap a missing map. Bootstrap with
|
|
233
|
+
an executable `--no-config` wrapper, then use cached Deno tasks for subsequent regeneration.
|
|
234
|
+
|
|
235
|
+
### Editor LSP
|
|
236
|
+
|
|
237
|
+
The Deno language server uses the selected import map for diagnostics, completions, hover information, and
|
|
238
|
+
go-to-definition. Enable it in Zed with `.zed/settings.json`:
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"languages": {
|
|
243
|
+
"TypeScript": {
|
|
244
|
+
"language_servers": ["deno", "!typescript-language-server", "!vtsls"]
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
"lsp": {
|
|
248
|
+
"deno": {
|
|
249
|
+
"settings": {
|
|
250
|
+
"deno": {
|
|
251
|
+
"enable": true,
|
|
252
|
+
"config": "./deno.json",
|
|
253
|
+
"importMap": "./import_map.json"
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
146
259
|
```
|
|
147
260
|
|
|
148
|
-
|
|
261
|
+
In mixed-runtime repositories, restrict Deno to the relevant paths:
|
|
149
262
|
|
|
150
263
|
```json
|
|
151
264
|
{
|
|
152
|
-
"
|
|
265
|
+
"lsp": {
|
|
266
|
+
"deno": {
|
|
267
|
+
"settings": {
|
|
268
|
+
"deno": {
|
|
269
|
+
"enable": true,
|
|
270
|
+
"enablePaths": ["./src", "./scripts", "./tests"],
|
|
271
|
+
"config": "./deno.json",
|
|
272
|
+
"importMap": "./import_map.json"
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Other LSP clients expose the same Deno initialization settings. When the client discovers `deno.json` itself, its
|
|
281
|
+
`importMap` field is usually sufficient without a duplicate editor setting.
|
|
282
|
+
|
|
283
|
+
### Keep Bun or npm in charge
|
|
284
|
+
|
|
285
|
+
Reading `package.json` does not make Deno modify it. In a mixed-runtime project, explicitly keep `node_modules` under
|
|
286
|
+
Bun or npm control:
|
|
287
|
+
|
|
288
|
+
```json
|
|
289
|
+
{
|
|
290
|
+
"nodeModulesDir": "manual",
|
|
291
|
+
"importMap": "./import_map.json"
|
|
153
292
|
}
|
|
154
293
|
```
|
|
155
294
|
|
|
295
|
+
For full isolation, set `preferPackageJson` to `false` or run with `DENO_NO_PACKAGE_JSON=1`. The generated map must then
|
|
296
|
+
contain every bare dependency and package alias required by the checked files; unresolved entries cannot fall back to
|
|
297
|
+
`package.json`.
|
|
298
|
+
|
|
299
|
+
### Spawn Deno from Node or Bun
|
|
300
|
+
|
|
301
|
+
Use the absolute path returned by `writeImportMap` to avoid working-directory ambiguity:
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
import { execFileSync } from 'node:child_process';
|
|
305
|
+
import { writeImportMap } from 'importmapify';
|
|
306
|
+
|
|
307
|
+
const root = '/path/to/source-tree';
|
|
308
|
+
const importMap = writeImportMap({ root, out: 'import_map.json' });
|
|
309
|
+
|
|
310
|
+
const documentation = execFileSync(
|
|
311
|
+
'deno',
|
|
312
|
+
['doc', '--import-map', importMap, '--json', 'src/index.ts'],
|
|
313
|
+
{ cwd: root, encoding: 'utf8' },
|
|
314
|
+
);
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### `@deno/doc`
|
|
318
|
+
|
|
319
|
+
The documentation API accepts an import-map file URL:
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
import { doc } from 'jsr:@deno/doc';
|
|
323
|
+
|
|
324
|
+
const entries = [new URL('./src/index.ts', import.meta.url).href];
|
|
325
|
+
const importMap = new URL('./import_map.json', import.meta.url).href;
|
|
326
|
+
const nodes = await doc(entries, {
|
|
327
|
+
importMap,
|
|
328
|
+
printImportMapDiagnostics: false,
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Keep it current
|
|
333
|
+
|
|
334
|
+
Pattern entries reflect the files present when generation runs. Regenerate after:
|
|
335
|
+
|
|
336
|
+
- adding, moving, or deleting matching source files;
|
|
337
|
+
- changing `package.json#imports`;
|
|
338
|
+
- changing extra imports or scopes in a wrapper script;
|
|
339
|
+
- updating dependency versions embedded in generated mappings.
|
|
340
|
+
|
|
341
|
+
Useful generation points include install or prepare hooks, documentation/type-check/test tasks, editor tasks, and CI
|
|
342
|
+
before Deno tooling. Commit the generated map whenever `deno.json#importMap` references it or editor support must work
|
|
343
|
+
immediately after checkout. Ignore it only when bootstrap runs outside that Deno configuration, such as an executable
|
|
344
|
+
`--no-config` wrapper or a Bun/Node lifecycle hook.
|
|
345
|
+
|
|
156
346
|
## License
|
|
157
347
|
|
|
158
348
|
MIT
|
package/deno.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/denoland/deno/refs/heads/main/cli/schemas/config-file.v1.json",
|
|
3
|
+
"name": "@kjanat/importmapify",
|
|
4
|
+
"version": "1.0.1",
|
|
5
|
+
"exports": "./src/mod.ts",
|
|
6
|
+
"publish": {
|
|
7
|
+
"include": [
|
|
8
|
+
"./src/**/*.ts",
|
|
9
|
+
"./README.md",
|
|
10
|
+
"./LICENSE",
|
|
11
|
+
"./deno.json",
|
|
12
|
+
"./import_map.json",
|
|
13
|
+
"./package.json"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"compilerOptions": {
|
|
17
|
+
"lib": ["ESNext", "DOM", "deno.ns"]
|
|
18
|
+
},
|
|
19
|
+
"importMap": "./import_map.json",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"tasks": {
|
|
22
|
+
"build": {
|
|
23
|
+
"description": "build for npm distribution",
|
|
24
|
+
"command": "bunx tsdown",
|
|
25
|
+
"files": ["src/**", "package.json", "bun.lock"],
|
|
26
|
+
"output": ["dist/**", "deno.json"]
|
|
27
|
+
},
|
|
28
|
+
"publish": {
|
|
29
|
+
"command": "deno publish --config deno.json --allow-dirty",
|
|
30
|
+
"description": "Publish package to jsr",
|
|
31
|
+
"dependencies": ["importmapify"]
|
|
32
|
+
},
|
|
33
|
+
"importmapify": {
|
|
34
|
+
"description": "generate importmap for deno",
|
|
35
|
+
"command": "deno run --no-config -A scripts/generate-importmap.ts",
|
|
36
|
+
"files": ["package.json", "scripts/generate-importmap.ts", "src/**"],
|
|
37
|
+
"output": ["import_map.json"]
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"lint": {
|
|
41
|
+
"rules": {
|
|
42
|
+
"tags": ["recommended", "jsr"]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"preferPackageJson": false,
|
|
46
|
+
"homepage": "https://github.com/kjanat/importmapify#readme",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/kjanat/importmapify.git"
|
|
50
|
+
},
|
|
51
|
+
"lock": false,
|
|
52
|
+
"jsrDepsInNodeModules": false,
|
|
53
|
+
"minimumDependencyAge": {
|
|
54
|
+
"exclude": ["jsr:@kjanat/*", "npm:@kjanat/*"]
|
|
55
|
+
}
|
|
56
|
+
}
|
package/dist/mod.d.mts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
//#region src/map.d.ts
|
|
2
|
+
/** A deterministic Deno import map generated from package import entries. */
|
|
3
|
+
interface ImportMapDocument {
|
|
4
|
+
/** Exact specifier-to-target mappings, sorted by specifier. */
|
|
5
|
+
readonly imports: Readonly<Record<string, string>>;
|
|
6
|
+
/** Scope prefixes mapped to sorted, scope-specific import overrides. */
|
|
7
|
+
readonly scopes?: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
8
|
+
}
|
|
9
|
+
/** Options for creating an import map without writing it to disk. */
|
|
10
|
+
interface CreateImportMapOptions {
|
|
11
|
+
/** Project directory containing the package manifest. */
|
|
12
|
+
readonly root: string;
|
|
13
|
+
/** Manifest path relative to {@link root}. Defaults to `package.json`. */
|
|
14
|
+
readonly manifest?: string;
|
|
15
|
+
/** Conditional import keys to try in order. Defaults to `import`, then `default`. */
|
|
16
|
+
readonly conditions?: readonly string[];
|
|
17
|
+
/** Explicit entries merged after manifest imports, overriding duplicate keys. */
|
|
18
|
+
readonly additionalImports?: Readonly<Record<string, string>>;
|
|
19
|
+
/** Scope-specific import overrides keyed by scope prefix. */
|
|
20
|
+
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
|
+
}
|
|
24
|
+
/** Options for creating and writing an import map. */
|
|
25
|
+
interface WriteImportMapOptions extends CreateImportMapOptions {
|
|
26
|
+
/** Output path relative to {@link CreateImportMapOptions.root | root}. */
|
|
27
|
+
readonly out: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Expand a package's exact and patterned `imports` into a Deno import map.
|
|
31
|
+
*
|
|
32
|
+
* Pattern targets are matched against files below {@link CreateImportMapOptions.root | root}.
|
|
33
|
+
* Conditional targets use the configured condition order, and additional imports are applied last.
|
|
34
|
+
*
|
|
35
|
+
* @example Generate entries for the current project.
|
|
36
|
+
* ```ts
|
|
37
|
+
* import { createImportMap } from 'jsr:@kjanat/importmapify';
|
|
38
|
+
*
|
|
39
|
+
* const map = createImportMap({
|
|
40
|
+
* root: Deno.cwd(),
|
|
41
|
+
* conditions: ['deno', 'import', 'default'],
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* console.log(map.imports['#lib/bytes']);
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @param options Project, manifest, condition, and rebasing options.
|
|
48
|
+
* @returns A sorted import map document.
|
|
49
|
+
*/
|
|
50
|
+
declare function createImportMap(options: CreateImportMapOptions): ImportMapDocument;
|
|
51
|
+
/**
|
|
52
|
+
* Serialize an import map as stable, tab-indented JSON with a trailing newline.
|
|
53
|
+
*
|
|
54
|
+
* @example Format an import map for stdout.
|
|
55
|
+
* ```ts
|
|
56
|
+
* import { formatImportMap } from 'jsr:@kjanat/importmapify';
|
|
57
|
+
*
|
|
58
|
+
* const text = formatImportMap({
|
|
59
|
+
* imports: { '#config': './src/config.ts' },
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* console.log(text);
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @param map Import map to serialize.
|
|
66
|
+
* @returns Formatted JSON ready to print or write.
|
|
67
|
+
*/
|
|
68
|
+
declare function formatImportMap(map: ImportMapDocument): string;
|
|
69
|
+
/**
|
|
70
|
+
* Create an import map and write it to disk, creating parent directories as needed.
|
|
71
|
+
*
|
|
72
|
+
* Relative targets are automatically rebased from the project root to the output directory.
|
|
73
|
+
*
|
|
74
|
+
* @example Write the conventional Deno import map file.
|
|
75
|
+
* ```ts
|
|
76
|
+
* import { writeImportMap } from 'jsr:@kjanat/importmapify';
|
|
77
|
+
*
|
|
78
|
+
* const output = writeImportMap({
|
|
79
|
+
* root: Deno.cwd(),
|
|
80
|
+
* out: 'import_map.json',
|
|
81
|
+
* });
|
|
82
|
+
*
|
|
83
|
+
* console.log(`Wrote ${output}`);
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* @param options Creation options plus the output path.
|
|
87
|
+
* @returns The absolute path of the written file.
|
|
88
|
+
*/
|
|
89
|
+
declare function writeImportMap(options: WriteImportMapOptions): string;
|
|
90
|
+
//#endregion
|
|
91
|
+
export { type CreateImportMapOptions, type ImportMapDocument, type WriteImportMapOptions, createImportMap, formatImportMap, writeImportMap };
|
package/dist/mod.mjs
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { CLIError, cli, command, flag } from "dreamcli";
|
|
3
|
+
import fs, { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import path, { dirname, join } from "node:path";
|
|
5
|
+
import { cwd } from "node:process";
|
|
6
|
+
//#region src/expand.ts
|
|
7
|
+
function escapeRegExp(value) {
|
|
8
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9
|
+
}
|
|
10
|
+
function targetMatcher(pattern) {
|
|
11
|
+
const suffixParts = pattern.targetSuffix.split("*");
|
|
12
|
+
const firstSuffix = suffixParts[0] ?? "";
|
|
13
|
+
const repeatedSuffixes = suffixParts.slice(1).map((suffix) => `\\k<wildcard>${escapeRegExp(suffix)}`).join("");
|
|
14
|
+
return new RegExp(`^${escapeRegExp(pattern.targetPrefix)}(?<wildcard>.*)${escapeRegExp(firstSuffix)}${repeatedSuffixes}$`);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse matching wildcards from a package import key and target.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* parsePattern('#lib/*', './src/lib/*.ts');
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @param key Package import specifier pattern.
|
|
25
|
+
* @param target Filesystem target pattern.
|
|
26
|
+
* @returns Parsed pattern components, or `undefined` when neither side contains a wildcard.
|
|
27
|
+
* @throws When only one side contains a wildcard.
|
|
28
|
+
*/
|
|
29
|
+
function parsePattern(key, target) {
|
|
30
|
+
const keyStar = key.indexOf("*");
|
|
31
|
+
const targetStar = target.indexOf("*");
|
|
32
|
+
if (keyStar === -1 && targetStar === -1) return;
|
|
33
|
+
if (keyStar === -1 || targetStar === -1) throw new Error(`Import pattern mismatch: "${key}" -> "${target}"; both sides must contain "*", or neither should.`);
|
|
34
|
+
const targetPrefix = target.slice(0, targetStar);
|
|
35
|
+
const targetDirectory = targetPrefix.endsWith("/") ? targetPrefix.slice(0, -1) : path.posix.dirname(targetPrefix);
|
|
36
|
+
return {
|
|
37
|
+
keyPrefix: key.slice(0, keyStar),
|
|
38
|
+
keySuffix: key.slice(keyStar + 1),
|
|
39
|
+
targetDirectory: targetDirectory === "" ? "." : targetDirectory,
|
|
40
|
+
targetPrefix,
|
|
41
|
+
targetSuffix: target.slice(targetStar + 1)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Expand a parsed pattern into exact import entries for matching files.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* const pattern = parsePattern('#lib/*', './src/lib/*.ts');
|
|
50
|
+
* if (pattern) expandPattern(pattern, ['bytes.ts']);
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @param pattern Parsed key and target wildcard components.
|
|
54
|
+
* @param files File paths relative to the pattern's target directory.
|
|
55
|
+
* @returns Exact import specifiers mapped to their source targets.
|
|
56
|
+
*/
|
|
57
|
+
function expandPattern(pattern, files) {
|
|
58
|
+
const imports = {};
|
|
59
|
+
const matcher = targetMatcher(pattern);
|
|
60
|
+
const targetBase = pattern.targetDirectory === "." ? "./" : `${pattern.targetDirectory}/`;
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const target = `${targetBase}${file}`;
|
|
63
|
+
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
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return imports;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Determine whether a value is a non-array object record.
|
|
73
|
+
*
|
|
74
|
+
* @param value Value to inspect.
|
|
75
|
+
* @returns Whether the value can be safely read as a string-keyed record.
|
|
76
|
+
*/
|
|
77
|
+
function isRecord(value) {
|
|
78
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a string target from nested conditional import objects.
|
|
82
|
+
*
|
|
83
|
+
* @param value Exact target or conditional target object.
|
|
84
|
+
* @param conditions Condition names to try in priority order.
|
|
85
|
+
* @returns The first matching string target, or `undefined` when none matches.
|
|
86
|
+
*/
|
|
87
|
+
function resolveCondition(value, conditions) {
|
|
88
|
+
if (typeof value === "string") return value;
|
|
89
|
+
if (!isRecord(value)) return;
|
|
90
|
+
for (const condition of conditions) if (condition in value) {
|
|
91
|
+
const resolved = resolveCondition(value[condition], conditions);
|
|
92
|
+
if (resolved !== void 0) return resolved;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Determine whether a target is relative and can be rebased.
|
|
97
|
+
*
|
|
98
|
+
* @param target Import target to inspect.
|
|
99
|
+
* @returns Whether the target starts with `./` or `../`.
|
|
100
|
+
*/
|
|
101
|
+
function isRebasableTarget(target) {
|
|
102
|
+
return target.startsWith("./") || target.startsWith("../");
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Rebase a relative import target from the project root to another directory.
|
|
106
|
+
*
|
|
107
|
+
* Bare specifiers and URL-like targets are returned unchanged.
|
|
108
|
+
*
|
|
109
|
+
* @param root Directory against which the original target is resolved.
|
|
110
|
+
* @param relativeTo Directory from which the returned target should resolve.
|
|
111
|
+
* @param target Import target to rebase.
|
|
112
|
+
* @returns A portable slash-separated target.
|
|
113
|
+
*/
|
|
114
|
+
function rebaseTarget(root, relativeTo, target) {
|
|
115
|
+
if (!isRebasableTarget(target)) return target;
|
|
116
|
+
const absolute = path.resolve(root, target);
|
|
117
|
+
const relative = path.relative(relativeTo, absolute).split(path.sep).join("/");
|
|
118
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/map.ts
|
|
122
|
+
const DEFAULT_CONDITIONS = ["import", "default"];
|
|
123
|
+
function filesUnder(dir, prefix = "") {
|
|
124
|
+
if (!fs.existsSync(dir)) return [];
|
|
125
|
+
const files = [];
|
|
126
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
127
|
+
const rel = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
128
|
+
if (entry.isDirectory()) files.push(...filesUnder(path.join(dir, entry.name), rel));
|
|
129
|
+
else files.push(rel);
|
|
130
|
+
}
|
|
131
|
+
return files;
|
|
132
|
+
}
|
|
133
|
+
function readManifest(manifestPath) {
|
|
134
|
+
let raw;
|
|
135
|
+
try {
|
|
136
|
+
raw = fs.readFileSync(manifestPath, "utf8");
|
|
137
|
+
} catch (cause) {
|
|
138
|
+
throw new Error(`Cannot read manifest at ${manifestPath}`, { cause });
|
|
139
|
+
}
|
|
140
|
+
let parsed;
|
|
141
|
+
try {
|
|
142
|
+
parsed = JSON.parse(raw);
|
|
143
|
+
} catch (cause) {
|
|
144
|
+
throw new Error(`Cannot parse manifest at ${manifestPath} as JSON`, { cause });
|
|
145
|
+
}
|
|
146
|
+
if (!isRecord(parsed)) throw new Error(`Manifest at ${manifestPath} must be a JSON object`);
|
|
147
|
+
return parsed;
|
|
148
|
+
}
|
|
149
|
+
function expandManifestImport(root, key, rawValue, conditions) {
|
|
150
|
+
const value = typeof rawValue === "string" ? rawValue : resolveCondition(rawValue, conditions);
|
|
151
|
+
if (value === void 0) return {};
|
|
152
|
+
const pattern = parsePattern(key, value);
|
|
153
|
+
if (pattern === void 0) return { [key]: value };
|
|
154
|
+
return expandPattern(pattern, filesUnder(path.join(root, pattern.targetDirectory)));
|
|
155
|
+
}
|
|
156
|
+
function sortEntries(entries) {
|
|
157
|
+
return Object.fromEntries(Object.entries(entries).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
158
|
+
}
|
|
159
|
+
function rebaseScopePrefix(root, relativeTo, scope) {
|
|
160
|
+
const rebased = rebaseTarget(root, relativeTo, scope);
|
|
161
|
+
return scope.endsWith("/") && !rebased.endsWith("/") ? `${rebased}/` : rebased;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Expand a package's exact and patterned `imports` into a Deno import map.
|
|
165
|
+
*
|
|
166
|
+
* Pattern targets are matched against files below {@link CreateImportMapOptions.root | root}.
|
|
167
|
+
* Conditional targets use the configured condition order, and additional imports are applied last.
|
|
168
|
+
*
|
|
169
|
+
* @example Generate entries for the current project.
|
|
170
|
+
* ```ts
|
|
171
|
+
* import { createImportMap } from 'jsr:@kjanat/importmapify';
|
|
172
|
+
*
|
|
173
|
+
* const map = createImportMap({
|
|
174
|
+
* root: Deno.cwd(),
|
|
175
|
+
* conditions: ['deno', 'import', 'default'],
|
|
176
|
+
* });
|
|
177
|
+
*
|
|
178
|
+
* console.log(map.imports['#lib/bytes']);
|
|
179
|
+
* ```
|
|
180
|
+
*
|
|
181
|
+
* @param options Project, manifest, condition, and rebasing options.
|
|
182
|
+
* @returns A sorted import map document.
|
|
183
|
+
*/
|
|
184
|
+
function createImportMap(options) {
|
|
185
|
+
const manifest = readManifest(path.join(options.root, options.manifest ?? "package.json"));
|
|
186
|
+
const manifestImports = isRecord(manifest.imports) ? manifest.imports : {};
|
|
187
|
+
const conditions = options.conditions !== void 0 && options.conditions.length > 0 ? options.conditions : DEFAULT_CONDITIONS;
|
|
188
|
+
const relativeTo = options.relativeTo ?? options.root;
|
|
189
|
+
const imports = {};
|
|
190
|
+
for (const [key, rawValue] of Object.entries(manifestImports)) {
|
|
191
|
+
const expanded = expandManifestImport(options.root, key, rawValue, conditions);
|
|
192
|
+
for (const [specifier, target] of Object.entries(expanded)) imports[specifier] = rebaseTarget(options.root, relativeTo, target);
|
|
193
|
+
}
|
|
194
|
+
for (const [key, value] of Object.entries(options.additionalImports ?? {})) imports[key] = rebaseTarget(options.root, relativeTo, value);
|
|
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
|
+
}
|
|
201
|
+
const sortedImports = sortEntries(imports);
|
|
202
|
+
const sortedScopes = Object.fromEntries(Object.entries(scopes).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
203
|
+
return Object.keys(sortedScopes).length === 0 ? { imports: sortedImports } : {
|
|
204
|
+
imports: sortedImports,
|
|
205
|
+
scopes: sortedScopes
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Serialize an import map as stable, tab-indented JSON with a trailing newline.
|
|
210
|
+
*
|
|
211
|
+
* @example Format an import map for stdout.
|
|
212
|
+
* ```ts
|
|
213
|
+
* import { formatImportMap } from 'jsr:@kjanat/importmapify';
|
|
214
|
+
*
|
|
215
|
+
* const text = formatImportMap({
|
|
216
|
+
* imports: { '#config': './src/config.ts' },
|
|
217
|
+
* });
|
|
218
|
+
*
|
|
219
|
+
* console.log(text);
|
|
220
|
+
* ```
|
|
221
|
+
*
|
|
222
|
+
* @param map Import map to serialize.
|
|
223
|
+
* @returns Formatted JSON ready to print or write.
|
|
224
|
+
*/
|
|
225
|
+
function formatImportMap(map) {
|
|
226
|
+
return `${JSON.stringify(map, null, " ")}\n`;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Create an import map and write it to disk, creating parent directories as needed.
|
|
230
|
+
*
|
|
231
|
+
* Relative targets are automatically rebased from the project root to the output directory.
|
|
232
|
+
*
|
|
233
|
+
* @example Write the conventional Deno import map file.
|
|
234
|
+
* ```ts
|
|
235
|
+
* import { writeImportMap } from 'jsr:@kjanat/importmapify';
|
|
236
|
+
*
|
|
237
|
+
* const output = writeImportMap({
|
|
238
|
+
* root: Deno.cwd(),
|
|
239
|
+
* out: 'import_map.json',
|
|
240
|
+
* });
|
|
241
|
+
*
|
|
242
|
+
* console.log(`Wrote ${output}`);
|
|
243
|
+
* ```
|
|
244
|
+
*
|
|
245
|
+
* @param options Creation options plus the output path.
|
|
246
|
+
* @returns The absolute path of the written file.
|
|
247
|
+
*/
|
|
248
|
+
function writeImportMap(options) {
|
|
249
|
+
const out = path.join(options.root, options.out);
|
|
250
|
+
const relativeTo = options.relativeTo ?? path.dirname(out);
|
|
251
|
+
const map = createImportMap({
|
|
252
|
+
...options,
|
|
253
|
+
relativeTo
|
|
254
|
+
});
|
|
255
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
256
|
+
fs.writeFileSync(out, formatImportMap(map));
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
//#endregion
|
|
260
|
+
//#region src/cli.ts
|
|
261
|
+
function parseKeyValue(raw) {
|
|
262
|
+
const eq = raw.indexOf("=");
|
|
263
|
+
if (eq === -1) throw new CLIError(`--import expects key=value, got "${raw}"`, {
|
|
264
|
+
code: "invalid-import-flag",
|
|
265
|
+
exitCode: 2
|
|
266
|
+
});
|
|
267
|
+
return [raw.slice(0, eq), raw.slice(eq + 1)];
|
|
268
|
+
}
|
|
269
|
+
function parseScope(raw) {
|
|
270
|
+
const separator = raw.indexOf("::");
|
|
271
|
+
const eq = raw.indexOf("=", separator + 2);
|
|
272
|
+
if (separator <= 0 || eq <= separator + 2 || eq === raw.length - 1) throw new CLIError(`--scope expects prefix::key=value, got "${raw}"`, {
|
|
273
|
+
code: "invalid-scope-flag",
|
|
274
|
+
exitCode: 2
|
|
275
|
+
});
|
|
276
|
+
return [
|
|
277
|
+
raw.slice(0, separator),
|
|
278
|
+
raw.slice(separator + 2, eq),
|
|
279
|
+
raw.slice(eq + 1)
|
|
280
|
+
];
|
|
281
|
+
}
|
|
282
|
+
/** DreamCLI command that writes, checks, or prints an expanded Deno import map. */
|
|
283
|
+
const generateCommand = command("generate").description("Expand package.json subpath-pattern imports into a Deno import map.").flag("root", flag.path({
|
|
284
|
+
mustExist: true,
|
|
285
|
+
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("import_map.json").describe("Output path, relative to root.").alias("o")).flag("import", flag.array(flag.string()).describe("Additional import entry as key=value. Repeatable.").alias("i")).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 --import '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
|
+
const { log, error, setExitCode } = out;
|
|
288
|
+
const { check, stdout, root, manifest, condition: cdts, out: of, import: imf, scope: scf } = flags;
|
|
289
|
+
if (check && stdout) throw new CLIError("--check and --stdout are mutually exclusive", {
|
|
290
|
+
code: "conflicting-flags",
|
|
291
|
+
exitCode: 2
|
|
292
|
+
});
|
|
293
|
+
const outPath = join(root, of);
|
|
294
|
+
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
|
+
const options = {
|
|
304
|
+
root,
|
|
305
|
+
manifest,
|
|
306
|
+
conditions: cdts,
|
|
307
|
+
additionalImports,
|
|
308
|
+
scopes,
|
|
309
|
+
relativeTo
|
|
310
|
+
};
|
|
311
|
+
if (stdout) {
|
|
312
|
+
log(formatImportMap(createImportMap(options)));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (check) {
|
|
316
|
+
const expected = formatImportMap(createImportMap(options));
|
|
317
|
+
if ((existsSync(outPath) ? readFileSync(outPath, "utf8") : void 0) !== expected) {
|
|
318
|
+
error(`${outPath} is stale`);
|
|
319
|
+
setExitCode(1);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
log(`${outPath} is up to date`);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
log(`Wrote ${writeImportMap({
|
|
326
|
+
...options,
|
|
327
|
+
out: of
|
|
328
|
+
})}`);
|
|
329
|
+
});
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/mod.ts
|
|
332
|
+
/**
|
|
333
|
+
* Expand package import patterns into explicit Deno import map entries.
|
|
334
|
+
*
|
|
335
|
+
* Use the library API to create, format, or write deterministic import maps.
|
|
336
|
+
* This module also runs the `importmapify` CLI when executed directly.
|
|
337
|
+
*
|
|
338
|
+
* @example Create an import map from the current package.
|
|
339
|
+
* ```ts
|
|
340
|
+
* import { createImportMap } from 'jsr:@kjanat/importmapify';
|
|
341
|
+
*
|
|
342
|
+
* const importMap = createImportMap({ root: Deno.cwd() });
|
|
343
|
+
* console.log(importMap.imports);
|
|
344
|
+
* ```
|
|
345
|
+
*
|
|
346
|
+
* @module importmapify
|
|
347
|
+
*/
|
|
348
|
+
const cli$1 = cli("importmapify").manifest({
|
|
349
|
+
from: import.meta.url,
|
|
350
|
+
files: ["package.json", "deno.json"]
|
|
351
|
+
}).links().default(generateCommand).completions({ as: "flag" });
|
|
352
|
+
if (import.meta.main) cli$1.run();
|
|
353
|
+
//#endregion
|
|
354
|
+
export { createImportMap, formatImportMap, writeImportMap };
|
package/import_map.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"imports": {
|
|
3
|
+
"#deno": "./deno.json",
|
|
4
|
+
"#pkg": "./package.json",
|
|
5
|
+
"#src/cli.ts": "./src/cli.ts",
|
|
6
|
+
"#src/expand.ts": "./src/expand.ts",
|
|
7
|
+
"#src/map.ts": "./src/map.ts",
|
|
8
|
+
"#src/mod.ts": "./src/mod.ts",
|
|
9
|
+
"@types/bun": "npm:bun-types@^1.3.14",
|
|
10
|
+
"bun": "npm:bun-types@^1.3.14",
|
|
11
|
+
"bun:test": "npm:bun-types@^1.3.14/test.d.ts",
|
|
12
|
+
"dreamcli": "jsr:@kjanat/dreamcli@^3.0.0-rc.11",
|
|
13
|
+
"dreamcli/": "jsr:@kjanat/dreamcli@^3.0.0-rc.11/"
|
|
14
|
+
},
|
|
15
|
+
"scopes": {
|
|
16
|
+
"./tests/": {
|
|
17
|
+
"dreamcli/testkit": "jsr:@kjanat/dreamcli@^3.0.0-rc.11/testkit"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "importmapify",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Expand package.json subpath-pattern imports into explicit Deno import map entries.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deno",
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"ansispeck"
|
|
13
13
|
],
|
|
14
14
|
"homepage": "https://github.com/kjanat/importmapify#readme",
|
|
15
|
-
"bugs":
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/kjanat/importmapify/issues"
|
|
17
|
+
},
|
|
16
18
|
"repository": {
|
|
17
19
|
"type": "git",
|
|
18
20
|
"url": "git+https://github.com/kjanat/importmapify.git"
|
|
@@ -26,48 +28,52 @@
|
|
|
26
28
|
"sideEffects": false,
|
|
27
29
|
"type": "module",
|
|
28
30
|
"imports": {
|
|
29
|
-
"#pkg": "./package.json"
|
|
31
|
+
"#pkg": "./package.json",
|
|
32
|
+
"#deno": "./deno.json",
|
|
33
|
+
"#src/*": "./src/*"
|
|
30
34
|
},
|
|
31
35
|
"exports": {
|
|
32
36
|
".": {
|
|
33
|
-
"types": "./dist/
|
|
34
|
-
"default": "./dist/
|
|
37
|
+
"types": "./dist/mod.d.mts",
|
|
38
|
+
"default": "./dist/mod.mjs"
|
|
35
39
|
},
|
|
36
40
|
"./package.json": "./package.json"
|
|
37
41
|
},
|
|
38
|
-
"types": "./dist/index.d.mts",
|
|
39
42
|
"bin": {
|
|
40
|
-
"importmapify": "dist/
|
|
43
|
+
"importmapify": "dist/mod.mjs"
|
|
41
44
|
},
|
|
42
|
-
"files": [
|
|
43
|
-
"dist"
|
|
44
|
-
],
|
|
45
|
+
"files": ["/dist", "/README.md", "/LICENSE", "/package.json", "/deno.json", "/import_map.json"],
|
|
45
46
|
"scripts": {
|
|
46
47
|
"bd": "tsdown",
|
|
47
48
|
"build": "tsdown",
|
|
48
49
|
"build:watch": "tsdown --watch",
|
|
49
50
|
"check": "biome check",
|
|
50
51
|
"check:fix": "biome check --fix",
|
|
51
|
-
"fmt": "dprint fmt",
|
|
52
|
+
"fmt": "biome check --fix 2>/dev/null; dprint fmt",
|
|
53
|
+
"jsr:publish": "bunx jsr publish --allow-dirty",
|
|
52
54
|
"lint": "biome lint",
|
|
53
|
-
"prepack": "bun --bun bd -l error",
|
|
55
|
+
"prepack": "bun --bun bd -l error && bun scripts/generate-importmap.ts",
|
|
54
56
|
"prepublishOnly": "[ -n \"${GITHUB_ACTIONS:-}\" ] || { printf '%s\\n' 'manual npm publish blocked; use release workflow for provenance' >&2; exit 1; }",
|
|
55
|
-
"runbin": "node \"$(jq -r '.bin.[]' package.json)\"
|
|
57
|
+
"runbin": "node \"$(jq -r '.bin.[]' package.json)\"",
|
|
56
58
|
"test": "bun test --coverage",
|
|
57
59
|
"test:coverage": "bun test --coverage",
|
|
58
60
|
"test:smoke": "node scripts/smoke.mjs",
|
|
59
61
|
"test:watch": "bun test --watch",
|
|
60
|
-
"typecheck": "bunx @typescript/native --noEmit"
|
|
62
|
+
"typecheck": "bunx @typescript/native --noEmit",
|
|
63
|
+
"url:jsr": "printf 'url=https://jsr.io/%s@%s\n' \"$(jq -r '.name' deno.json)\" \"$(jq -r '.version' deno.json)\"",
|
|
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; bun bd"
|
|
61
66
|
},
|
|
62
67
|
"dependencies": {
|
|
63
|
-
"dreamcli": "npm:@kjanat/dreamcli@^3.0.0-rc.
|
|
68
|
+
"dreamcli": "npm:@kjanat/dreamcli@^3.0.0-rc.11"
|
|
64
69
|
},
|
|
65
70
|
"devDependencies": {
|
|
66
71
|
"@arethetypeswrong/core": "^0.18.5",
|
|
67
|
-
"@biomejs/biome": "2.5.2 || ^2.5.4",
|
|
68
72
|
"@types/bun": "^1.3.14",
|
|
73
|
+
"@types/deno": "^2.7.0",
|
|
69
74
|
"@types/node": "^26.1.1",
|
|
70
75
|
"@typescript/native": "npm:typescript@^7",
|
|
76
|
+
"biome": "npm:@biomejs/biome@^2.5.4",
|
|
71
77
|
"dprint": "^0.55.2",
|
|
72
78
|
"publint": "^0.3.21",
|
|
73
79
|
"sort-package-json": "^4.0.0",
|
|
@@ -78,7 +84,8 @@
|
|
|
78
84
|
"packageManager": "bun@1.3.14",
|
|
79
85
|
"engines": {
|
|
80
86
|
"bun": ">=1.3",
|
|
81
|
-
"
|
|
87
|
+
"deno": ">=2",
|
|
88
|
+
"node": ">=22.22.2 <23 || >=24.2"
|
|
82
89
|
},
|
|
83
90
|
"volta": {
|
|
84
91
|
"node": "26.5.0",
|
package/dist/import-map.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import e from"node:fs";import t from"node:path";function n(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}function r(e){let t=e.targetSuffix.split(`*`),r=t[0]??``,i=t.slice(1).map(e=>`\\k<wildcard>${n(e)}`).join(``);return RegExp(`^${n(e.targetPrefix)}(?<wildcard>.*)${n(r)}${i}$`)}function i(e,n){let r=e.indexOf(`*`),i=n.indexOf(`*`);if(r===-1&&i===-1)return;if(r===-1||i===-1)throw Error(`Import pattern mismatch: "${e}" -> "${n}"; both sides must contain "*", or neither should.`);let a=n.slice(0,i),o=a.endsWith(`/`)?a.slice(0,-1):t.posix.dirname(a);return{keyPrefix:e.slice(0,r),keySuffix:e.slice(r+1),targetDirectory:o===``?`.`:o,targetPrefix:a,targetSuffix:n.slice(i+1)}}function a(e,t){let n={},i=r(e),a=e.targetDirectory===`.`?`./`:`${e.targetDirectory}/`;for(let r of t){let t=`${a}${r}`,o=i.exec(t)?.groups?.wildcard;o!==void 0&&(n[`${e.keyPrefix}${o}${e.keySuffix}`]=t,n[`${e.keyPrefix}${r}`]=t)}return n}function o(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function s(e,t){if(typeof e==`string`)return e;if(o(e))for(let n of t){if(!(n in e))continue;let r=s(e[n],t);if(r!==void 0)return r}}function c(e){return e.startsWith(`./`)||e.startsWith(`../`)}function l(e,n,r){if(!c(r))return r;let i=t.resolve(e,r),a=t.relative(n,i).split(t.sep).join(`/`);return a.startsWith(`.`)?a:`./${a}`}const u=[`import`,`default`];function d(n,r=``){if(!e.existsSync(n))return[];let i=[];for(let a of e.readdirSync(n,{withFileTypes:!0})){let e=r===``?a.name:`${r}/${a.name}`;a.isDirectory()?i.push(...d(t.join(n,a.name),e)):i.push(e)}return i}function f(t){let n;try{n=e.readFileSync(t,`utf8`)}catch(e){throw Error(`Cannot read manifest at ${t}`,{cause:e})}let r;try{r=JSON.parse(n)}catch(e){throw Error(`Cannot parse manifest at ${t} as JSON`,{cause:e})}if(!o(r))throw Error(`Manifest at ${t} must be a JSON object`);return r}function p(e){let n=f(t.join(e.root,e.manifest??`package.json`)),r=o(n.imports)?n.imports:{},c=e.conditions?.length?e.conditions:u,p=e.relativeTo??e.root,m={},h=(t,n)=>{m[t]=l(e.root,p,n)};for(let[n,o]of Object.entries(r)){let r=typeof o==`string`?o:s(o,c);if(r===void 0)continue;let l=i(n,r);if(l===void 0){h(n,r);continue}let u=t.join(e.root,l.targetDirectory);for(let[e,t]of Object.entries(a(l,d(u))))h(e,t)}for(let[t,n]of Object.entries(e.additionalImports??{}))h(t,n);return{imports:Object.fromEntries(Object.entries(m).sort(([e],[t])=>e<t?-1:+(e>t)))}}function m(e){return`${JSON.stringify(e,null,` `)}\n`}function h(n){let r=t.join(n.root,n.out),i=n.relativeTo??t.dirname(r),a=p({...n,relativeTo:i});return e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,m(a)),r}export{m as n,h as r,p as t};
|
package/dist/importmapify.mjs
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import{n as e,r as t,t as n}from"./import-map.mjs";import r from"node:fs";import i from"node:path";import{CLIError as a,cli as o,command as s,flag as c}from"dreamcli";function l(e){let t=e.indexOf(`=`);if(t===-1)throw new a(`--import expects key=value, got "${e}"`,{code:`invalid-import-flag`,exitCode:2});return[e.slice(0,t),e.slice(t+1)]}const u=s(`generate`).description(`Expand package.json subpath-pattern imports into a Deno import map.`).flag(`root`,c.path({mustExist:!0,type:`directory`}).default(process.cwd()).describe(`Project root containing the manifest.`).alias(`r`)).flag(`manifest`,c.string().default(`package.json`).describe(`Manifest path, relative to root.`).alias(`m`)).flag(`out`,c.string().default(`deno.import_map.json`).describe(`Output path, relative to root.`).alias(`o`)).flag(`import`,c.array(c.string()).describe(`Additional import entry as key=value. Repeatable.`).alias(`i`)).flag(`condition`,c.array(c.string()).describe(`Condition to try when a target is a conditional object. Repeatable.`).alias(`c`)).flag(`check`,c.boolean().describe(`Exit 1 if the output file is stale instead of writing it.`)).flag(`stdout`,c.boolean().describe(`Print the import map to stdout instead of writing it.`)).action(({flags:o,out:s})=>{if(o.check&&o.stdout)throw new a(`--check and --stdout are mutually exclusive`,{code:`conflicting-flags`,exitCode:2});let c=o.root,u=i.join(c,o.out),d=i.dirname(u),f=Object.fromEntries((o.import??[]).map(l)),p={root:c,manifest:o.manifest,conditions:o.condition,additionalImports:f,relativeTo:d};if(o.stdout){s.log(e(n(p)));return}if(o.check){let t=e(n(p));if((r.existsSync(u)?r.readFileSync(u,`utf8`):void 0)!==t){s.error(`${u} is stale`),s.setExitCode(1);return}s.log(`${u} is up to date`);return}let m=t({...p,out:o.out});s.log(`Wrote ${m}`)});o(`importmapify`).manifest({from:import.meta.url}).default(u).completions().run();export{};
|
package/dist/index.d.mts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
//#region src/import-map.d.ts
|
|
2
|
-
interface ImportMapDocument {
|
|
3
|
-
readonly imports: Readonly<Record<string, string>>;
|
|
4
|
-
}
|
|
5
|
-
interface CreateImportMapOptions {
|
|
6
|
-
readonly root: string;
|
|
7
|
-
readonly manifest?: string;
|
|
8
|
-
readonly conditions?: readonly string[];
|
|
9
|
-
readonly additionalImports?: Readonly<Record<string, string>>;
|
|
10
|
-
readonly relativeTo?: string;
|
|
11
|
-
}
|
|
12
|
-
interface WriteImportMapOptions extends CreateImportMapOptions {
|
|
13
|
-
readonly out: string;
|
|
14
|
-
}
|
|
15
|
-
declare function createImportMap(options: CreateImportMapOptions): ImportMapDocument;
|
|
16
|
-
declare function formatImportMap(map: ImportMapDocument): string;
|
|
17
|
-
declare function writeImportMap(options: WriteImportMapOptions): string;
|
|
18
|
-
//#endregion
|
|
19
|
-
export { type CreateImportMapOptions, type ImportMapDocument, type WriteImportMapOptions, createImportMap, formatImportMap, writeImportMap };
|
package/dist/index.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{n as e,r as t,t as n}from"./import-map.mjs";export{n as createImportMap,e as formatImportMap,t as writeImportMap};
|