importmapify 1.3.0 → 1.4.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 CHANGED
@@ -43,6 +43,7 @@ npx importmapify --root . --out import_map.json
43
43
  | `--no-config` | | Skip config file discovery | off |
44
44
  | `--check` | | Exit 1 if the output file is stale, without writing it | off |
45
45
  | `--stdout` | | Print the map instead of writing it | off |
46
+ | `--quiet` | `-q` | Suppress the `Wrote <path>` confirmation (written to stderr) | off |
46
47
 
47
48
  Add global and test-scoped dependencies from the CLI:
48
49
 
@@ -61,9 +62,21 @@ order (first match wins): `importmapify.config.{mjs,cjs,js,ts,mts,cts}`, then `.
61
62
 
62
63
  ```js
63
64
  // importmapify.config.mjs
65
+ import { defineConfig } from 'importmapify';
66
+
67
+ export default defineConfig({
68
+ packages: { dreamcli: 'jsr:@kjanat/dreamcli@^3' },
69
+ extensions: ['ts'],
70
+ });
71
+ ```
72
+
73
+ Or annotate a plain object, no import:
74
+
75
+ ```js
76
+ /** @type {import('importmapify').Config} */
64
77
  export default {
65
- packages: { dreamcli: 'jsr:@kjanat/dreamcli@^3' },
66
- extensions: ['ts'],
78
+ packages: { dreamcli: 'jsr:@kjanat/dreamcli@^3' },
79
+ extensions: ['ts'],
67
80
  };
68
81
  ```
69
82
 
@@ -72,9 +85,8 @@ Then `npx importmapify` reads it automatically. Point at a specific file with `-
72
85
 
73
86
  Explicitly-passed flags override the config, which overrides built-in defaults: `--import`/`--package`/`--scope` merge
74
87
  onto the config's records with flag keys winning; `--condition`/`--ext` replace; `--root`/`--manifest`/`--out` win when
75
- passed. Configs are imported natively; a TypeScript config needs a runtime that strips types (Bun, Deno, or Node
76
-
77
- > = 22.6), so on older Node use a `.mjs`/`.cjs`/`.js` config.
88
+ passed. Configs are imported natively; a TypeScript config needs a type-stripping runtime (Bun, Deno, or Node \>= 22.6);
89
+ on older Node use a `.mjs`/`.cjs`/`.js` config.
78
90
 
79
91
  ## Library
80
92
 
@@ -95,13 +107,13 @@ const out = writeImportMap({
95
107
  });
96
108
  ```
97
109
 
98
- | Export | Signature | Purpose |
99
- | ----------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------- |
100
- | `defineConfig` | `(options: WriteImportMapOptions) => WriteImportMapOptions` | Identity helper that types a config object for export and reuse. |
101
- | `createImportMap` | `(options: CreateImportMapOptions) => ImportMapDocument` | Build the import map in memory. |
102
- | `formatImportMap` | `(map: ImportMapDocument) => string` | Serialize to the canonical sorted, tab-indented JSON text. |
103
- | `writeImportMap` | `(options: WriteImportMapOptions) => string` | Build, serialize, and write to disk; returns the written path. |
104
- | `packageEntries` | `(name: string, target: string) => Record<string, string>` | Build the bare and trailing-slash entry pair a package needs to resolve its subpaths. |
110
+ | Export | Signature | Purpose |
111
+ | ----------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------- |
112
+ | `defineConfig` | `<T extends Config>(config: T) => T` | Identity helper that types a config object for export and reuse. |
113
+ | `createImportMap` | `(options: CreateImportMapOptions) => ImportMapDocument` | Build the import map in memory. |
114
+ | `formatImportMap` | `(map: ImportMapDocument) => string` | Serialize to the canonical sorted, tab-indented JSON text. |
115
+ | `writeImportMap` | `(options: WriteImportMapOptions) => string` | Build, serialize, and write to disk; returns the written path. |
116
+ | `packageEntries` | `(name: string, target: string) => Record<string, string>` | Build the bare and trailing-slash entry pair a package needs to resolve its subpaths. |
105
117
 
106
118
  `CreateImportMapOptions`:
107
119
 
@@ -121,9 +133,18 @@ resolved against `root` and accepts a relative path, an absolute path, or a `fil
121
133
  automatically against `out`'s directory, so a nested `out` (for example `.cache/maps/import_map.json`) still produces
122
134
  targets that resolve correctly from the map's own location.
123
135
 
136
+ `Config` extends `Partial<WriteImportMapOptions>` with an optional `hooks` field — the shape a config file's default
137
+ export and `defineConfig` take, with every field optional. An omitted `root` defaults to the config file's own
138
+ directory; the CLI supplies the remaining defaults.
139
+
124
140
  `defineConfig` returns its argument unchanged; it exists only to type a config object for export and reuse without a
125
141
  manual annotation.
126
142
 
143
+ `hooks` are lifecycle functions the CLI runs around generation, modeled on tsdown's hooks. `generate:before` runs before
144
+ the filesystem is scanned, so building generated targets there keeps them out of a stale map; `generate:done` runs after
145
+ the map is emitted. Each receives `{ root, out }` (`generate:done` also gets the finished `map`) and may be async. The
146
+ synchronous library functions ignore `hooks`.
147
+
127
148
  ### Recipes
128
149
 
129
150
  Add dependencies with `packages`. Each entry expands to the bare specifier plus its conformant trailing-slash entry
@@ -170,6 +191,20 @@ const map = createImportMap(config);
170
191
  const written = writeImportMap(config);
171
192
  ```
172
193
 
194
+ Build before scanning with a `generate:before` hook, so a map that includes build output does not drop entries when it
195
+ runs before the build:
196
+
197
+ ```ts
198
+ import { execSync } from 'node:child_process';
199
+ import { defineConfig } from 'importmapify';
200
+
201
+ export default defineConfig({
202
+ hooks: {
203
+ 'generate:before': () => execSync('bun run build', { stdio: 'inherit' }),
204
+ },
205
+ });
206
+ ```
207
+
173
208
  `root`, `relativeTo`, and `out` accept a path or a `file://` URL, so `import.meta.url` needs no `.pathname`:
174
209
 
175
210
  ```ts
@@ -398,11 +433,10 @@ import { writeImportMap } from 'importmapify';
398
433
  const root = '/path/to/source-tree';
399
434
  const importMap = writeImportMap({ root, out: 'import_map.json' });
400
435
 
401
- const documentation = execFileSync(
402
- 'deno',
403
- ['doc', '--import-map', importMap, '--json', 'src/index.ts'],
404
- { cwd: root, encoding: 'utf8' },
405
- );
436
+ const documentation = execFileSync('deno', ['doc', '--import-map', importMap, '--json', 'src/index.ts'], {
437
+ cwd: root,
438
+ encoding: 'utf8',
439
+ });
406
440
  ```
407
441
 
408
442
  ### `@deno/doc`
package/deno.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/denoland/deno/refs/heads/main/cli/schemas/config-file.v1.json",
3
3
  "name": "@kjanat/importmapify",
4
- "version": "1.3.0",
4
+ "version": "1.4.0",
5
5
  "exports": "./src/mod.ts",
6
6
  "publish": {
7
7
  "include": [
package/dist/mod.d.mts CHANGED
@@ -33,6 +33,35 @@ interface WriteImportMapOptions extends CreateImportMapOptions {
33
33
  */
34
34
  readonly out?: string | URL;
35
35
  }
36
+ /** Resolved paths shared by every generation hook. */
37
+ interface HookContext {
38
+ /** Absolute project root that gets scanned. */
39
+ readonly root: string;
40
+ /** Absolute output path the map resolves against. */
41
+ readonly out: string;
42
+ }
43
+ /**
44
+ * Lifecycle hooks the CLI runs around import map generation, modeled on tsdown's hooks.
45
+ *
46
+ * Each hook may be async; the CLI awaits it. The synchronous library functions ignore hooks.
47
+ */
48
+ interface ImportMapHooks {
49
+ /** Runs before the filesystem is scanned. Build pattern targets here so they exist when patterns expand. */
50
+ readonly "generate:before": (context: HookContext) => void | Promise<void>;
51
+ /** Runs after the map is generated and emitted. */
52
+ readonly "generate:done": (context: HookContext & {
53
+ readonly map: ImportMapDocument;
54
+ }) => void | Promise<void>;
55
+ }
56
+ /**
57
+ * An importmapify config file: the shape a config file's default export and {@linkcode defineConfig} take.
58
+ * Every field is optional; the config loader and CLI supply {@linkcode CreateImportMapOptions.root | root}
59
+ * and the remaining defaults.
60
+ */
61
+ interface Config extends Partial<WriteImportMapOptions> {
62
+ /** Lifecycle hooks the CLI runs around generation. Ignored by {@link writeImportMap} and {@link createImportMap}. */
63
+ readonly hooks?: Partial<ImportMapHooks>;
64
+ }
36
65
  /**
37
66
  * Expand a package's exact and patterned `imports` into a Deno import map.
38
67
  *
@@ -129,9 +158,10 @@ declare function packageEntries(name: string, target: string): Record<string, st
129
158
  * writeImportMap(config);
130
159
  * ```
131
160
  *
132
- * @param options Import map configuration, with an optional `out`.
133
- * @returns The same `options` value, typed as {@link WriteImportMapOptions}.
161
+ * @param config Import map configuration; every field is optional.
162
+ * @returns The same `config` value with its exact type preserved, so a config that includes `root` stays
163
+ * assignable to {@link writeImportMap} while one that omits it is still a valid config file.
134
164
  */
135
- declare function defineConfig(options: WriteImportMapOptions): WriteImportMapOptions;
165
+ declare function defineConfig<T extends Config>(config: T): T;
136
166
  //#endregion
137
- export { type CreateImportMapOptions, type ImportMapDocument, type WriteImportMapOptions, createImportMap, defineConfig, formatImportMap, packageEntries, writeImportMap };
167
+ export { type Config, type CreateImportMapOptions, type HookContext, type ImportMapDocument, type ImportMapHooks, type WriteImportMapOptions, createImportMap, defineConfig, formatImportMap, packageEntries, writeImportMap };
package/dist/mod.mjs CHANGED
@@ -1,9 +1,9 @@
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";
6
- import { cwd } from "node:process";
4
+ import fs, { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import path, { dirname, resolve } from "node:path";
6
+ import { argv, cwd } from "node:process";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
8
  //#region src/expand.ts
9
9
  function escapeRegExp(value) {
@@ -191,20 +191,34 @@ function asScopes(value) {
191
191
  }
192
192
  return scopes;
193
193
  }
194
+ function isHook(value) {
195
+ return typeof value === "function";
196
+ }
197
+ function asHooks(value) {
198
+ if (!isRecord(value)) return;
199
+ const hooks = {};
200
+ const before = value["generate:before"];
201
+ if (isHook(before)) hooks["generate:before"] = before;
202
+ const done = value["generate:done"];
203
+ if (isHook(done)) hooks["generate:done"] = done;
204
+ return hooks["generate:before"] === void 0 && hooks["generate:done"] === void 0 ? void 0 : hooks;
205
+ }
194
206
  /**
195
207
  * Parse a raw config object into typed import map options.
196
208
  *
197
- * Only known fields are read; a relative string `root` is resolved against the config
198
- * file's own directory so a config can anchor itself with `root: '.'`.
209
+ * Only known fields are read. An omitted `root` defaults to the config file's own directory; a relative
210
+ * string `root` resolves against it, so a config can anchor itself with `root: '.'`.
199
211
  *
200
212
  * @param config Raw default-exported config object.
201
213
  * @param configDir Directory containing the config file.
202
- * @returns The subset of {@link WriteImportMapOptions} the config declares.
214
+ * @returns The subset of {@link WriteImportMapOptions} the config declares, plus any hooks.
203
215
  */
204
216
  function configToOptions(config, configDir) {
205
217
  const result = {};
206
218
  const root = asPath(config.root);
207
- if (root !== void 0) result.root = typeof root === "string" && !path.isAbsolute(root) && !root.startsWith("file://") ? path.resolve(configDir, root) : root;
219
+ if (root === void 0) result.root = configDir;
220
+ else if (typeof root === "string" && !path.isAbsolute(root) && !root.startsWith("file://")) result.root = path.resolve(configDir, root);
221
+ else result.root = root;
208
222
  const manifest = asString(config.manifest);
209
223
  if (manifest !== void 0) result.manifest = manifest;
210
224
  const out = asPath(config.out);
@@ -221,6 +235,8 @@ function configToOptions(config, configDir) {
221
235
  if (additionalImports !== void 0) result.additionalImports = additionalImports;
222
236
  const scopes = asScopes(config.scopes);
223
237
  if (scopes !== void 0) result.scopes = scopes;
238
+ const hooks = asHooks(config.hooks);
239
+ if (hooks !== void 0) result.hooks = hooks;
224
240
  return result;
225
241
  }
226
242
  /**
@@ -471,16 +487,19 @@ function packageEntries(name, target) {
471
487
  * writeImportMap(config);
472
488
  * ```
473
489
  *
474
- * @param options Import map configuration, with an optional `out`.
475
- * @returns The same `options` value, typed as {@link WriteImportMapOptions}.
490
+ * @param config Import map configuration; every field is optional.
491
+ * @returns The same `config` value with its exact type preserved, so a config that includes `root` stays
492
+ * assignable to {@link writeImportMap} while one that omits it is still a valid config file.
476
493
  */
477
- function defineConfig(options) {
478
- return options;
494
+ function defineConfig(config) {
495
+ return config;
479
496
  }
480
497
  //#endregion
481
498
  //#region src/cli.ts
482
499
  const EXAMPLE_TOKEN = /(?:'[^']*'|"[^"]*"|\S)+/g;
500
+ const JSON_MODE = argv.includes("--json");
483
501
  function highlightExample(example) {
502
+ if (JSON_MODE) return example;
484
503
  const tokens = example.match(EXAMPLE_TOKEN);
485
504
  if (tokens === null) return example;
486
505
  return tokens.map((token, index) => index === 0 ? bold(token) : token.startsWith("-") ? cyan(token) : token).join(" ");
@@ -572,6 +591,7 @@ async function resolveGenerateOptions(flags) {
572
591
  if (base.relativeTo !== void 0) options.relativeTo = base.relativeTo;
573
592
  if (conditions !== void 0) options.conditions = conditions;
574
593
  if (extensions !== void 0) options.extensions = extensions;
594
+ if (base.hooks !== void 0) options.hooks = base.hooks;
575
595
  return options;
576
596
  }
577
597
  /** DreamCLI command that writes, checks, or prints an expanded Deno import map. */
@@ -591,22 +611,28 @@ const generateCommand = command("generate").description("Expand package.json sub
591
611
  ...options,
592
612
  relativeTo: options.relativeTo ?? dirname(outPath)
593
613
  };
594
- if (stdout) {
595
- log(formatImportMap(createImportMap(createOptions)));
596
- return;
597
- }
598
- if (check) {
599
- const expected = formatImportMap(createImportMap(createOptions));
600
- if ((existsSync(outPath) ? readFileSync(outPath, "utf8") : void 0) !== expected) {
614
+ const hookContext = {
615
+ root: resolve(toPath(options.root)),
616
+ out: outPath
617
+ };
618
+ await options.hooks?.["generate:before"]?.(hookContext);
619
+ const map = createImportMap(createOptions);
620
+ const text = formatImportMap(map);
621
+ if (stdout) log(text);
622
+ else if (check) {
623
+ if ((existsSync(outPath) ? readFileSync(outPath, "utf8") : void 0) !== text) {
601
624
  error(`${outPath} is stale`);
602
625
  setExitCode(1);
603
- return;
604
- }
605
- if (!quiet) warn(`${outPath} is up to date`);
606
- return;
626
+ } else if (!quiet) warn(`${outPath} is up to date`);
627
+ } else {
628
+ mkdirSync(dirname(outPath), { recursive: true });
629
+ writeFileSync(outPath, text);
630
+ if (!quiet) warn(`Wrote ${outPath}`);
607
631
  }
608
- const written = writeImportMap(options);
609
- if (!quiet) warn(`Wrote ${written}`);
632
+ await options.hooks?.["generate:done"]?.({
633
+ ...hookContext,
634
+ map
635
+ });
610
636
  });
611
637
  //#endregion
612
638
  //#region src/mod.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "importmapify",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Expand package.json subpath-pattern imports into explicit Deno import map entries.",
5
5
  "keywords": [
6
6
  "deno",