importmapify 1.1.0 → 1.2.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 +29 -6
- package/deno.json +1 -1
- package/dist/mod.mjs +184 -20
- 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
|
+
| `--config path` | `-C` | Config file path; skips discovery | auto-discovered |
|
|
43
|
+
| `--no-config` | | Skip config file discovery | off |
|
|
42
44
|
| `--check` | | Exit 1 if the output file is stale, without writing it | off |
|
|
43
45
|
| `--stdout` | | Print the map instead of writing it | off |
|
|
44
46
|
|
|
@@ -52,6 +54,28 @@ npx importmapify \
|
|
|
52
54
|
|
|
53
55
|
Run `npx importmapify --help` for the full reference, or `npx importmapify --completions bash` for shell completions.
|
|
54
56
|
|
|
57
|
+
### Config file
|
|
58
|
+
|
|
59
|
+
The CLI auto-discovers a config file in the project root and uses its default export as the base options. Discovery
|
|
60
|
+
order (first match wins): `importmapify.config.{mjs,cjs,js,ts,mts,cts}`, then `.importmapify.{mjs,cjs,js,ts,mts,cts}`.
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
// importmapify.config.mjs
|
|
64
|
+
export default {
|
|
65
|
+
packages: { dreamcli: 'jsr:@kjanat/dreamcli@^3' },
|
|
66
|
+
extensions: ['ts'],
|
|
67
|
+
};
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Then `npx importmapify` reads it automatically. Point at a specific file with `--config <path>`, or skip discovery with
|
|
71
|
+
`--no-config`.
|
|
72
|
+
|
|
73
|
+
Explicitly-passed flags override the config, which overrides built-in defaults: `--import`/`--package`/`--scope` merge
|
|
74
|
+
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.
|
|
78
|
+
|
|
55
79
|
## Library
|
|
56
80
|
|
|
57
81
|
```ts
|
|
@@ -97,9 +121,8 @@ resolved against `root` and accepts a relative path, an absolute path, or a `fil
|
|
|
97
121
|
automatically against `out`'s directory, so a nested `out` (for example `.cache/maps/import_map.json`) still produces
|
|
98
122
|
targets that resolve correctly from the map's own location.
|
|
99
123
|
|
|
100
|
-
`defineConfig` returns its argument unchanged; it exists only to type
|
|
101
|
-
annotation.
|
|
102
|
-
helps only for a config used locally rather than exported.
|
|
124
|
+
`defineConfig` returns its argument unchanged; it exists only to type a config object for export and reuse without a
|
|
125
|
+
manual annotation.
|
|
103
126
|
|
|
104
127
|
### Recipes
|
|
105
128
|
|
|
@@ -113,10 +136,10 @@ writeImportMap({
|
|
|
113
136
|
root: import.meta.dirname,
|
|
114
137
|
packages: {
|
|
115
138
|
dreamcli: 'jsr:@kjanat/dreamcli@^3',
|
|
116
|
-
|
|
139
|
+
ansispeck: 'npm:ansispeck@^0.2',
|
|
117
140
|
},
|
|
118
141
|
});
|
|
119
|
-
// dreamcli, dreamcli/ -> jsr:/@kjanat/dreamcli@^3/,
|
|
142
|
+
// dreamcli, dreamcli/ -> jsr:/@kjanat/dreamcli@^3/, ansispeck, ansispeck/ -> npm:/ansispeck@^0.2/
|
|
120
143
|
```
|
|
121
144
|
|
|
122
145
|
Restrict pattern expansion to importable files with `extensions`, so a bare `./src/*` skips `.md`, `.json`, and other
|
|
@@ -142,7 +165,7 @@ Type a reusable config with `defineConfig` and share it across calls:
|
|
|
142
165
|
```ts
|
|
143
166
|
import { createImportMap, defineConfig, writeImportMap } from 'importmapify';
|
|
144
167
|
|
|
145
|
-
const config = defineConfig({ root: import.meta.dirname, packages: {
|
|
168
|
+
const config = defineConfig({ root: import.meta.dirname, packages: { ansispeck: 'npm:ansispeck@^0.2' } });
|
|
146
169
|
const map = createImportMap(config);
|
|
147
170
|
const written = writeImportMap(config);
|
|
148
171
|
```
|
package/deno.json
CHANGED
package/dist/mod.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { CLIError, cli, command, flag } from "dreamcli";
|
|
|
3
3
|
import fs, { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import path, { dirname } from "node:path";
|
|
5
5
|
import { cwd } from "node:process";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
//#region src/expand.ts
|
|
8
8
|
function escapeRegExp(value) {
|
|
9
9
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -116,6 +116,129 @@ function rebaseTarget(root, relativeTo, target) {
|
|
|
116
116
|
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
117
117
|
}
|
|
118
118
|
//#endregion
|
|
119
|
+
//#region src/config.ts
|
|
120
|
+
const CONFIG_BASENAMES = ["importmapify.config", ".importmapify"];
|
|
121
|
+
const CONFIG_EXTENSIONS = [
|
|
122
|
+
"mjs",
|
|
123
|
+
"cjs",
|
|
124
|
+
"js",
|
|
125
|
+
"ts",
|
|
126
|
+
"mts",
|
|
127
|
+
"cts"
|
|
128
|
+
];
|
|
129
|
+
/**
|
|
130
|
+
* Find the first config file directly under `root`.
|
|
131
|
+
*
|
|
132
|
+
* Bases are tried in order (`importmapify.config`, then `.importmapify`), each across
|
|
133
|
+
* `.mjs`, `.cjs`, `.js`, then the TypeScript equivalents. The first existing file wins.
|
|
134
|
+
*
|
|
135
|
+
* @param root Directory to search.
|
|
136
|
+
* @returns The config file path, or `undefined` when none exists.
|
|
137
|
+
*/
|
|
138
|
+
function discoverConfig(root) {
|
|
139
|
+
for (const base of CONFIG_BASENAMES) for (const ext of CONFIG_EXTENSIONS) {
|
|
140
|
+
const candidate = path.join(root, `${base}.${ext}`);
|
|
141
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Import a config file and return its default-exported object.
|
|
146
|
+
*
|
|
147
|
+
* Uses the runtime's native module loader, so a `.ts` config requires a runtime that
|
|
148
|
+
* strips types (Bun, Deno, or Node >= 22.6).
|
|
149
|
+
*
|
|
150
|
+
* @param file Config file path.
|
|
151
|
+
* @returns The default export, validated as a plain object.
|
|
152
|
+
* @throws When the file cannot be imported or its default export is not a plain object.
|
|
153
|
+
*/
|
|
154
|
+
async function loadConfig(file) {
|
|
155
|
+
let module;
|
|
156
|
+
try {
|
|
157
|
+
module = await import(pathToFileURL(file).href);
|
|
158
|
+
} catch (cause) {
|
|
159
|
+
throw new Error(`Cannot load config at ${file}; a .ts config needs a runtime that strips types (Bun, Deno, or Node >= 22.6)`, { cause });
|
|
160
|
+
}
|
|
161
|
+
const config = isRecord(module) ? module.default : void 0;
|
|
162
|
+
if (!isRecord(config)) throw new Error(`Config at ${file} must have a default export object`);
|
|
163
|
+
return config;
|
|
164
|
+
}
|
|
165
|
+
function asString(value) {
|
|
166
|
+
return typeof value === "string" ? value : void 0;
|
|
167
|
+
}
|
|
168
|
+
function asPath(value) {
|
|
169
|
+
return typeof value === "string" || value instanceof URL ? value : void 0;
|
|
170
|
+
}
|
|
171
|
+
function asStringArray(value) {
|
|
172
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
|
|
173
|
+
}
|
|
174
|
+
function asStringRecord(value) {
|
|
175
|
+
if (!isRecord(value)) return;
|
|
176
|
+
const record = {};
|
|
177
|
+
for (const [key, item] of Object.entries(value)) {
|
|
178
|
+
if (typeof item !== "string") return;
|
|
179
|
+
record[key] = item;
|
|
180
|
+
}
|
|
181
|
+
return record;
|
|
182
|
+
}
|
|
183
|
+
function asScopes(value) {
|
|
184
|
+
if (!isRecord(value)) return;
|
|
185
|
+
const scopes = {};
|
|
186
|
+
for (const [prefix, entries] of Object.entries(value)) {
|
|
187
|
+
const record = asStringRecord(entries);
|
|
188
|
+
if (record === void 0) return;
|
|
189
|
+
scopes[prefix] = record;
|
|
190
|
+
}
|
|
191
|
+
return scopes;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Parse a raw config object into typed import map options.
|
|
195
|
+
*
|
|
196
|
+
* Only known fields are read; a relative string `root` is resolved against the config
|
|
197
|
+
* file's own directory so a config can anchor itself with `root: '.'`.
|
|
198
|
+
*
|
|
199
|
+
* @param config Raw default-exported config object.
|
|
200
|
+
* @param configDir Directory containing the config file.
|
|
201
|
+
* @returns The subset of {@link WriteImportMapOptions} the config declares.
|
|
202
|
+
*/
|
|
203
|
+
function configToOptions(config, configDir) {
|
|
204
|
+
const result = {};
|
|
205
|
+
const root = asPath(config.root);
|
|
206
|
+
if (root !== void 0) result.root = typeof root === "string" && !path.isAbsolute(root) && !root.startsWith("file://") ? path.resolve(configDir, root) : root;
|
|
207
|
+
const manifest = asString(config.manifest);
|
|
208
|
+
if (manifest !== void 0) result.manifest = manifest;
|
|
209
|
+
const out = asPath(config.out);
|
|
210
|
+
if (out !== void 0) result.out = out;
|
|
211
|
+
const relativeTo = asPath(config.relativeTo);
|
|
212
|
+
if (relativeTo !== void 0) result.relativeTo = relativeTo;
|
|
213
|
+
const conditions = asStringArray(config.conditions);
|
|
214
|
+
if (conditions !== void 0) result.conditions = conditions;
|
|
215
|
+
const extensions = asStringArray(config.extensions);
|
|
216
|
+
if (extensions !== void 0) result.extensions = extensions;
|
|
217
|
+
const packages = asStringRecord(config.packages);
|
|
218
|
+
if (packages !== void 0) result.packages = packages;
|
|
219
|
+
const additionalImports = asStringRecord(config.additionalImports);
|
|
220
|
+
if (additionalImports !== void 0) result.additionalImports = additionalImports;
|
|
221
|
+
const scopes = asScopes(config.scopes);
|
|
222
|
+
if (scopes !== void 0) result.scopes = scopes;
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Merge scope maps, with override entries winning per scope-prefix key.
|
|
227
|
+
*
|
|
228
|
+
* @param base Scopes from the config file.
|
|
229
|
+
* @param overrides Scopes from CLI flags.
|
|
230
|
+
* @returns Combined scopes.
|
|
231
|
+
*/
|
|
232
|
+
function mergeScopes(base, overrides) {
|
|
233
|
+
const result = {};
|
|
234
|
+
for (const [prefix, entries] of Object.entries(base ?? {})) result[prefix] = { ...entries };
|
|
235
|
+
for (const [prefix, entries] of Object.entries(overrides)) result[prefix] = {
|
|
236
|
+
...result[prefix],
|
|
237
|
+
...entries
|
|
238
|
+
};
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
119
242
|
//#region src/map.ts
|
|
120
243
|
const DEFAULT_CONDITIONS = ["import", "default"];
|
|
121
244
|
const DEFAULT_OUT = "import_map.json";
|
|
@@ -394,35 +517,79 @@ function buildScopes(raws) {
|
|
|
394
517
|
}
|
|
395
518
|
return scopes;
|
|
396
519
|
}
|
|
520
|
+
async function loadConfigOptions(searchRoot, configFlag, noConfig) {
|
|
521
|
+
if (noConfig) return {};
|
|
522
|
+
const file = configFlag ?? discoverConfig(searchRoot);
|
|
523
|
+
if (file === void 0) return {};
|
|
524
|
+
if (!existsSync(file)) throw new CLIError(`Config file not found: ${file}`, {
|
|
525
|
+
code: "config-not-found",
|
|
526
|
+
exitCode: 2
|
|
527
|
+
});
|
|
528
|
+
try {
|
|
529
|
+
return configToOptions(await loadConfig(file), dirname(file));
|
|
530
|
+
} catch (cause) {
|
|
531
|
+
throw new CLIError(cause instanceof Error ? cause.message : `Cannot load config at ${file}`, {
|
|
532
|
+
code: "config-load-failed",
|
|
533
|
+
exitCode: 2,
|
|
534
|
+
cause
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/** Explicitly-passed flag value wins; otherwise the config value, otherwise the flag default. */
|
|
539
|
+
function preferExplicit(explicit, flagValue, configValue) {
|
|
540
|
+
return explicit ? flagValue : configValue ?? flagValue;
|
|
541
|
+
}
|
|
542
|
+
/** A non-empty flag array wins over the config value. */
|
|
543
|
+
function preferArray(flagValue, configValue) {
|
|
544
|
+
return flagValue !== void 0 && flagValue.length > 0 ? flagValue : configValue;
|
|
545
|
+
}
|
|
546
|
+
/** Resolve final import map options by layering explicit flags over a discovered config over defaults. */
|
|
547
|
+
async function resolveGenerateOptions(flags) {
|
|
548
|
+
const base = await loadConfigOptions(flags.root, flags.config, flags["no-config"] ?? false);
|
|
549
|
+
const conditions = preferArray(flags.condition, base.conditions);
|
|
550
|
+
const extensions = preferArray(flags.ext, base.extensions);
|
|
551
|
+
const options = {
|
|
552
|
+
root: preferExplicit(flags.root !== cwd(), flags.root, base.root),
|
|
553
|
+
manifest: preferExplicit(flags.manifest !== "package.json", flags.manifest, base.manifest),
|
|
554
|
+
out: preferExplicit(flags.out !== DEFAULT_OUT, flags.out, base.out),
|
|
555
|
+
packages: {
|
|
556
|
+
...base.packages,
|
|
557
|
+
...parseEntries(flags.package ?? [], "package", "invalid-package-flag")
|
|
558
|
+
},
|
|
559
|
+
additionalImports: {
|
|
560
|
+
...base.additionalImports,
|
|
561
|
+
...parseEntries(flags.import ?? [], "import", "invalid-import-flag")
|
|
562
|
+
},
|
|
563
|
+
scopes: mergeScopes(base.scopes, buildScopes(flags.scope ?? []))
|
|
564
|
+
};
|
|
565
|
+
if (base.relativeTo !== void 0) options.relativeTo = base.relativeTo;
|
|
566
|
+
if (conditions !== void 0) options.conditions = conditions;
|
|
567
|
+
if (extensions !== void 0) options.extensions = extensions;
|
|
568
|
+
return options;
|
|
569
|
+
}
|
|
397
570
|
/** DreamCLI command that writes, checks, or prints an expanded Deno import map. */
|
|
398
571
|
const generateCommand = command("generate").description("Expand package.json subpath-pattern imports into a Deno import map.").flag("root", flag.path({
|
|
399
572
|
mustExist: true,
|
|
400
573
|
type: "directory"
|
|
401
|
-
}).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("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 --package '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 }) => {
|
|
574
|
+
}).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.")).example("importmapify --stdout", "Print the generated import map").example(`importmapify --package '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(async ({ flags, out }) => {
|
|
402
575
|
const { log, error, setExitCode } = out;
|
|
403
|
-
const { check, stdout
|
|
576
|
+
const { check, stdout } = flags;
|
|
404
577
|
if (check && stdout) throw new CLIError("--check and --stdout are mutually exclusive", {
|
|
405
578
|
code: "conflicting-flags",
|
|
406
579
|
exitCode: 2
|
|
407
580
|
});
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
conditions: cdts,
|
|
414
|
-
packages: parseEntries(pkf ?? [], "package", "invalid-package-flag"),
|
|
415
|
-
additionalImports: parseEntries(imf ?? [], "import", "invalid-import-flag"),
|
|
416
|
-
scopes: buildScopes(scf ?? []),
|
|
417
|
-
relativeTo,
|
|
418
|
-
extensions: exf
|
|
581
|
+
const options = await resolveGenerateOptions(flags);
|
|
582
|
+
const outPath = resolveOut(options.root, options.out ?? "import_map.json");
|
|
583
|
+
const createOptions = {
|
|
584
|
+
...options,
|
|
585
|
+
relativeTo: options.relativeTo ?? dirname(outPath)
|
|
419
586
|
};
|
|
420
587
|
if (stdout) {
|
|
421
|
-
log(formatImportMap(createImportMap(
|
|
588
|
+
log(formatImportMap(createImportMap(createOptions)));
|
|
422
589
|
return;
|
|
423
590
|
}
|
|
424
591
|
if (check) {
|
|
425
|
-
const expected = formatImportMap(createImportMap(
|
|
592
|
+
const expected = formatImportMap(createImportMap(createOptions));
|
|
426
593
|
if ((existsSync(outPath) ? readFileSync(outPath, "utf8") : void 0) !== expected) {
|
|
427
594
|
error(`${outPath} is stale`);
|
|
428
595
|
setExitCode(1);
|
|
@@ -431,10 +598,7 @@ const generateCommand = command("generate").description("Expand package.json sub
|
|
|
431
598
|
log(`${outPath} is up to date`);
|
|
432
599
|
return;
|
|
433
600
|
}
|
|
434
|
-
log(`Wrote ${writeImportMap(
|
|
435
|
-
...options,
|
|
436
|
-
out: of
|
|
437
|
-
})}`);
|
|
601
|
+
log(`Wrote ${writeImportMap(options)}`);
|
|
438
602
|
});
|
|
439
603
|
//#endregion
|
|
440
604
|
//#region src/mod.ts
|
package/import_map.json
CHANGED