importmapify 1.3.1 → 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
@@ -109,7 +109,7 @@ const out = writeImportMap({
109
109
 
110
110
  | Export | Signature | Purpose |
111
111
  | ----------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------- |
112
- | `defineConfig` | `(config: Config) => Config` | Identity helper that types a config object for export and reuse. |
112
+ | `defineConfig` | `<T extends Config>(config: T) => T` | Identity helper that types a config object for export and reuse. |
113
113
  | `createImportMap` | `(options: CreateImportMapOptions) => ImportMapDocument` | Build the import map in memory. |
114
114
  | `formatImportMap` | `(map: ImportMapDocument) => string` | Serialize to the canonical sorted, tab-indented JSON text. |
115
115
  | `writeImportMap` | `(options: WriteImportMapOptions) => string` | Build, serialize, and write to disk; returns the written path. |
@@ -133,11 +133,18 @@ resolved against `root` and accepts a relative path, an absolute path, or a `fil
133
133
  automatically against `out`'s directory, so a nested `out` (for example `.cache/maps/import_map.json`) still produces
134
134
  targets that resolve correctly from the map's own location.
135
135
 
136
- `Config` is an alias for `WriteImportMapOptions` — the shape a config file's default export and `defineConfig` take.
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.
137
139
 
138
140
  `defineConfig` returns its argument unchanged; it exists only to type a config object for export and reuse without a
139
141
  manual annotation.
140
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
+
141
148
  ### Recipes
142
149
 
143
150
  Add dependencies with `packages`. Each entry expands to the bare specifier plus its conformant trailing-slash entry
@@ -184,6 +191,20 @@ const map = createImportMap(config);
184
191
  const written = writeImportMap(config);
185
192
  ```
186
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
+
187
208
  `root`, `relativeTo`, and `out` accept a path or a `file://` URL, so `import.meta.url` needs no `.pathname`:
188
209
 
189
210
  ```ts
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.1",
4
+ "version": "1.4.0",
5
5
  "exports": "./src/mod.ts",
6
6
  "publish": {
7
7
  "include": [
package/dist/mod.d.mts CHANGED
@@ -33,8 +33,35 @@ interface WriteImportMapOptions extends CreateImportMapOptions {
33
33
  */
34
34
  readonly out?: string | URL;
35
35
  }
36
- /** An importmapify config: the shape a config file's default export and {@linkcode defineConfig} take. */
37
- type Config = WriteImportMapOptions;
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
+ }
38
65
  /**
39
66
  * Expand a package's exact and patterned `imports` into a Deno import map.
40
67
  *
@@ -131,9 +158,10 @@ declare function packageEntries(name: string, target: string): Record<string, st
131
158
  * writeImportMap(config);
132
159
  * ```
133
160
  *
134
- * @param config Import map configuration, with an optional `out`.
135
- * @returns The same `config` value, typed as {@link Config}.
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.
136
164
  */
137
- declare function defineConfig(config: Config): Config;
165
+ declare function defineConfig<T extends Config>(config: T): T;
138
166
  //#endregion
139
- export { type Config, 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,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
@@ -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,8 +487,9 @@ function packageEntries(name, target) {
471
487
  * writeImportMap(config);
472
488
  * ```
473
489
  *
474
- * @param config Import map configuration, with an optional `out`.
475
- * @returns The same `config` value, typed as {@link Config}.
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
494
  function defineConfig(config) {
478
495
  return config;
@@ -574,6 +591,7 @@ async function resolveGenerateOptions(flags) {
574
591
  if (base.relativeTo !== void 0) options.relativeTo = base.relativeTo;
575
592
  if (conditions !== void 0) options.conditions = conditions;
576
593
  if (extensions !== void 0) options.extensions = extensions;
594
+ if (base.hooks !== void 0) options.hooks = base.hooks;
577
595
  return options;
578
596
  }
579
597
  /** DreamCLI command that writes, checks, or prints an expanded Deno import map. */
@@ -593,22 +611,28 @@ const generateCommand = command("generate").description("Expand package.json sub
593
611
  ...options,
594
612
  relativeTo: options.relativeTo ?? dirname(outPath)
595
613
  };
596
- if (stdout) {
597
- log(formatImportMap(createImportMap(createOptions)));
598
- return;
599
- }
600
- if (check) {
601
- const expected = formatImportMap(createImportMap(createOptions));
602
- 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) {
603
624
  error(`${outPath} is stale`);
604
625
  setExitCode(1);
605
- return;
606
- }
607
- if (!quiet) warn(`${outPath} is up to date`);
608
- 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}`);
609
631
  }
610
- const written = writeImportMap(options);
611
- if (!quiet) warn(`Wrote ${written}`);
632
+ await options.hooks?.["generate:done"]?.({
633
+ ...hookContext,
634
+ map
635
+ });
612
636
  });
613
637
  //#endregion
614
638
  //#region src/mod.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "importmapify",
3
- "version": "1.3.1",
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",