importmapify 1.1.1 → 1.3.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 +24 -0
- package/deno.json +2 -1
- package/dist/mod.mjs +201 -23
- package/import_map.json +1 -0
- package/package.json +2 -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
|
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.
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"exports": "./src/mod.ts",
|
|
6
6
|
"publish": {
|
|
7
7
|
"include": [
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"lock": false,
|
|
52
52
|
"jsrDepsInNodeModules": false,
|
|
53
53
|
"minimumDependencyAge": {
|
|
54
|
+
"age": 0,
|
|
54
55
|
"exclude": ["jsr:@kjanat/*", "npm:@kjanat/*"]
|
|
55
56
|
}
|
|
56
57
|
}
|
package/dist/mod.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { bold, cyan, detectHyperlinkSupport } from "ansispeck";
|
|
2
3
|
import { CLIError, cli, command, flag } from "dreamcli";
|
|
3
4
|
import fs, { existsSync, readFileSync } from "node:fs";
|
|
4
5
|
import path, { dirname } from "node:path";
|
|
5
6
|
import { cwd } from "node:process";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
8
|
//#region src/expand.ts
|
|
8
9
|
function escapeRegExp(value) {
|
|
9
10
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -116,6 +117,129 @@ function rebaseTarget(root, relativeTo, target) {
|
|
|
116
117
|
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
117
118
|
}
|
|
118
119
|
//#endregion
|
|
120
|
+
//#region src/config.ts
|
|
121
|
+
const CONFIG_BASENAMES = ["importmapify.config", ".importmapify"];
|
|
122
|
+
const CONFIG_EXTENSIONS = [
|
|
123
|
+
"mjs",
|
|
124
|
+
"cjs",
|
|
125
|
+
"js",
|
|
126
|
+
"ts",
|
|
127
|
+
"mts",
|
|
128
|
+
"cts"
|
|
129
|
+
];
|
|
130
|
+
/**
|
|
131
|
+
* Find the first config file directly under `root`.
|
|
132
|
+
*
|
|
133
|
+
* Bases are tried in order (`importmapify.config`, then `.importmapify`), each across
|
|
134
|
+
* `.mjs`, `.cjs`, `.js`, then the TypeScript equivalents. The first existing file wins.
|
|
135
|
+
*
|
|
136
|
+
* @param root Directory to search.
|
|
137
|
+
* @returns The config file path, or `undefined` when none exists.
|
|
138
|
+
*/
|
|
139
|
+
function discoverConfig(root) {
|
|
140
|
+
for (const base of CONFIG_BASENAMES) for (const ext of CONFIG_EXTENSIONS) {
|
|
141
|
+
const candidate = path.join(root, `${base}.${ext}`);
|
|
142
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Import a config file and return its default-exported object.
|
|
147
|
+
*
|
|
148
|
+
* Uses the runtime's native module loader, so a `.ts` config requires a runtime that
|
|
149
|
+
* strips types (Bun, Deno, or Node >= 22.6).
|
|
150
|
+
*
|
|
151
|
+
* @param file Config file path.
|
|
152
|
+
* @returns The default export, validated as a plain object.
|
|
153
|
+
* @throws When the file cannot be imported or its default export is not a plain object.
|
|
154
|
+
*/
|
|
155
|
+
async function loadConfig(file) {
|
|
156
|
+
let module;
|
|
157
|
+
try {
|
|
158
|
+
module = await import(pathToFileURL(file).href);
|
|
159
|
+
} catch (cause) {
|
|
160
|
+
throw new Error(`Cannot load config at ${file}; a .ts config needs a runtime that strips types (Bun, Deno, or Node >= 22.6)`, { cause });
|
|
161
|
+
}
|
|
162
|
+
const config = isRecord(module) ? module.default : void 0;
|
|
163
|
+
if (!isRecord(config)) throw new Error(`Config at ${file} must have a default export object`);
|
|
164
|
+
return config;
|
|
165
|
+
}
|
|
166
|
+
function asString(value) {
|
|
167
|
+
return typeof value === "string" ? value : void 0;
|
|
168
|
+
}
|
|
169
|
+
function asPath(value) {
|
|
170
|
+
return typeof value === "string" || value instanceof URL ? value : void 0;
|
|
171
|
+
}
|
|
172
|
+
function asStringArray(value) {
|
|
173
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
|
|
174
|
+
}
|
|
175
|
+
function asStringRecord(value) {
|
|
176
|
+
if (!isRecord(value)) return;
|
|
177
|
+
const record = {};
|
|
178
|
+
for (const [key, item] of Object.entries(value)) {
|
|
179
|
+
if (typeof item !== "string") return;
|
|
180
|
+
record[key] = item;
|
|
181
|
+
}
|
|
182
|
+
return record;
|
|
183
|
+
}
|
|
184
|
+
function asScopes(value) {
|
|
185
|
+
if (!isRecord(value)) return;
|
|
186
|
+
const scopes = {};
|
|
187
|
+
for (const [prefix, entries] of Object.entries(value)) {
|
|
188
|
+
const record = asStringRecord(entries);
|
|
189
|
+
if (record === void 0) return;
|
|
190
|
+
scopes[prefix] = record;
|
|
191
|
+
}
|
|
192
|
+
return scopes;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Parse a raw config object into typed import map options.
|
|
196
|
+
*
|
|
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: '.'`.
|
|
199
|
+
*
|
|
200
|
+
* @param config Raw default-exported config object.
|
|
201
|
+
* @param configDir Directory containing the config file.
|
|
202
|
+
* @returns The subset of {@link WriteImportMapOptions} the config declares.
|
|
203
|
+
*/
|
|
204
|
+
function configToOptions(config, configDir) {
|
|
205
|
+
const result = {};
|
|
206
|
+
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;
|
|
208
|
+
const manifest = asString(config.manifest);
|
|
209
|
+
if (manifest !== void 0) result.manifest = manifest;
|
|
210
|
+
const out = asPath(config.out);
|
|
211
|
+
if (out !== void 0) result.out = out;
|
|
212
|
+
const relativeTo = asPath(config.relativeTo);
|
|
213
|
+
if (relativeTo !== void 0) result.relativeTo = relativeTo;
|
|
214
|
+
const conditions = asStringArray(config.conditions);
|
|
215
|
+
if (conditions !== void 0) result.conditions = conditions;
|
|
216
|
+
const extensions = asStringArray(config.extensions);
|
|
217
|
+
if (extensions !== void 0) result.extensions = extensions;
|
|
218
|
+
const packages = asStringRecord(config.packages);
|
|
219
|
+
if (packages !== void 0) result.packages = packages;
|
|
220
|
+
const additionalImports = asStringRecord(config.additionalImports);
|
|
221
|
+
if (additionalImports !== void 0) result.additionalImports = additionalImports;
|
|
222
|
+
const scopes = asScopes(config.scopes);
|
|
223
|
+
if (scopes !== void 0) result.scopes = scopes;
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Merge scope maps, with override entries winning per scope-prefix key.
|
|
228
|
+
*
|
|
229
|
+
* @param base Scopes from the config file.
|
|
230
|
+
* @param overrides Scopes from CLI flags.
|
|
231
|
+
* @returns Combined scopes.
|
|
232
|
+
*/
|
|
233
|
+
function mergeScopes(base, overrides) {
|
|
234
|
+
const result = {};
|
|
235
|
+
for (const [prefix, entries] of Object.entries(base ?? {})) result[prefix] = { ...entries };
|
|
236
|
+
for (const [prefix, entries] of Object.entries(overrides)) result[prefix] = {
|
|
237
|
+
...result[prefix],
|
|
238
|
+
...entries
|
|
239
|
+
};
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
119
243
|
//#region src/map.ts
|
|
120
244
|
const DEFAULT_CONDITIONS = ["import", "default"];
|
|
121
245
|
const DEFAULT_OUT = "import_map.json";
|
|
@@ -355,6 +479,12 @@ function defineConfig(options) {
|
|
|
355
479
|
}
|
|
356
480
|
//#endregion
|
|
357
481
|
//#region src/cli.ts
|
|
482
|
+
const EXAMPLE_TOKEN = /(?:'[^']*'|"[^"]*"|\S)+/g;
|
|
483
|
+
function highlightExample(example) {
|
|
484
|
+
const tokens = example.match(EXAMPLE_TOKEN);
|
|
485
|
+
if (tokens === null) return example;
|
|
486
|
+
return tokens.map((token, index) => index === 0 ? bold(token) : token.startsWith("-") ? cyan(token) : token).join(" ");
|
|
487
|
+
}
|
|
358
488
|
function parseKeyValue(raw, flagName, code) {
|
|
359
489
|
const eq = raw.indexOf("=");
|
|
360
490
|
if (eq === -1) throw new CLIError(`--${flagName} expects key=value, got "${raw}"`, {
|
|
@@ -394,47 +524,89 @@ function buildScopes(raws) {
|
|
|
394
524
|
}
|
|
395
525
|
return scopes;
|
|
396
526
|
}
|
|
527
|
+
async function loadConfigOptions(searchRoot, configFlag, noConfig) {
|
|
528
|
+
if (noConfig) return {};
|
|
529
|
+
const file = configFlag ?? discoverConfig(searchRoot);
|
|
530
|
+
if (file === void 0) return {};
|
|
531
|
+
if (!existsSync(file)) throw new CLIError(`Config file not found: ${file}`, {
|
|
532
|
+
code: "config-not-found",
|
|
533
|
+
exitCode: 2
|
|
534
|
+
});
|
|
535
|
+
try {
|
|
536
|
+
return configToOptions(await loadConfig(file), dirname(file));
|
|
537
|
+
} catch (cause) {
|
|
538
|
+
throw new CLIError(cause instanceof Error ? cause.message : `Cannot load config at ${file}`, {
|
|
539
|
+
code: "config-load-failed",
|
|
540
|
+
exitCode: 2,
|
|
541
|
+
cause
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
/** Explicitly-passed flag value wins; otherwise the config value, otherwise the flag default. */
|
|
546
|
+
function preferExplicit(explicit, flagValue, configValue) {
|
|
547
|
+
return explicit ? flagValue : configValue ?? flagValue;
|
|
548
|
+
}
|
|
549
|
+
/** A non-empty flag array wins over the config value. */
|
|
550
|
+
function preferArray(flagValue, configValue) {
|
|
551
|
+
return flagValue !== void 0 && flagValue.length > 0 ? flagValue : configValue;
|
|
552
|
+
}
|
|
553
|
+
/** Resolve final import map options by layering explicit flags over a discovered config over defaults. */
|
|
554
|
+
async function resolveGenerateOptions(flags) {
|
|
555
|
+
const base = await loadConfigOptions(flags.root, flags.config, flags["no-config"] ?? false);
|
|
556
|
+
const conditions = preferArray(flags.condition, base.conditions);
|
|
557
|
+
const extensions = preferArray(flags.ext, base.extensions);
|
|
558
|
+
const options = {
|
|
559
|
+
root: preferExplicit(flags.root !== cwd(), flags.root, base.root),
|
|
560
|
+
manifest: preferExplicit(flags.manifest !== "package.json", flags.manifest, base.manifest),
|
|
561
|
+
out: preferExplicit(flags.out !== DEFAULT_OUT, flags.out, base.out),
|
|
562
|
+
packages: {
|
|
563
|
+
...base.packages,
|
|
564
|
+
...parseEntries(flags.package ?? [], "package", "invalid-package-flag")
|
|
565
|
+
},
|
|
566
|
+
additionalImports: {
|
|
567
|
+
...base.additionalImports,
|
|
568
|
+
...parseEntries(flags.import ?? [], "import", "invalid-import-flag")
|
|
569
|
+
},
|
|
570
|
+
scopes: mergeScopes(base.scopes, buildScopes(flags.scope ?? []))
|
|
571
|
+
};
|
|
572
|
+
if (base.relativeTo !== void 0) options.relativeTo = base.relativeTo;
|
|
573
|
+
if (conditions !== void 0) options.conditions = conditions;
|
|
574
|
+
if (extensions !== void 0) options.extensions = extensions;
|
|
575
|
+
return options;
|
|
576
|
+
}
|
|
397
577
|
/** DreamCLI command that writes, checks, or prints an expanded Deno import map. */
|
|
398
578
|
const generateCommand = command("generate").description("Expand package.json subpath-pattern imports into a Deno import map.").flag("root", flag.path({
|
|
399
579
|
mustExist: true,
|
|
400
580
|
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'
|
|
402
|
-
const { log, error, setExitCode } = out;
|
|
403
|
-
const { check, stdout,
|
|
581
|
+
}).default(cwd()).describe("Project root containing the manifest.").alias("r")).flag("manifest", flag.string().default("package.json").describe("Manifest path, relative to root.").alias("m")).flag("out", flag.string().default(DEFAULT_OUT).describe("Output path resolved against root; relative, absolute, or file:// URL.").alias("o")).flag("import", flag.array(flag.string()).describe("Additional import entry as key=value. Repeatable.").alias("i")).flag("package", flag.array(flag.string()).describe("Package as name=target, expanded to a conformant bare and trailing-slash pair. Repeatable.").alias("p")).flag("ext", flag.array(flag.string()).describe("Restrict pattern matches to these file extensions. Repeatable.").alias("e")).flag("scope", flag.array(flag.string()).describe("Scoped import as prefix::key=value. Repeatable.").alias("s")).flag("condition", flag.array(flag.string()).describe("Condition to try when a target is a conditional object. Repeatable.").alias("c")).flag("config", flag.string().describe("Config file path; skips discovery.").alias("C")).flag("no-config", flag.boolean().describe("Skip config file discovery.")).flag("check", flag.boolean().describe("Exit 1 if the output file is stale instead of writing it.")).flag("stdout", flag.boolean().describe("Print the import map to stdout instead of writing it.")).flag("quiet", flag.boolean().describe("Suppress the confirmation message on stderr.").alias("q")).example(highlightExample("importmapify --stdout"), "Print the generated import map").example(highlightExample(`importmapify --package 'dreamcli=jsr:@kjanat/dreamcli@^3' --scope './tests/::dreamcli/testkit=jsr:@kjanat/dreamcli@^3/testkit'`), "Add global and test-scoped dependencies").example(highlightExample("importmapify --check"), "Fail when the generated file is stale").action(async ({ flags, out }) => {
|
|
582
|
+
const { log, warn, error, setExitCode } = out;
|
|
583
|
+
const { check, stdout, quiet } = flags;
|
|
404
584
|
if (check && stdout) throw new CLIError("--check and --stdout are mutually exclusive", {
|
|
405
585
|
code: "conflicting-flags",
|
|
406
586
|
exitCode: 2
|
|
407
587
|
});
|
|
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
|
|
588
|
+
const options = await resolveGenerateOptions(flags);
|
|
589
|
+
const outPath = resolveOut(options.root, options.out ?? "import_map.json");
|
|
590
|
+
const createOptions = {
|
|
591
|
+
...options,
|
|
592
|
+
relativeTo: options.relativeTo ?? dirname(outPath)
|
|
419
593
|
};
|
|
420
594
|
if (stdout) {
|
|
421
|
-
log(formatImportMap(createImportMap(
|
|
595
|
+
log(formatImportMap(createImportMap(createOptions)));
|
|
422
596
|
return;
|
|
423
597
|
}
|
|
424
598
|
if (check) {
|
|
425
|
-
const expected = formatImportMap(createImportMap(
|
|
599
|
+
const expected = formatImportMap(createImportMap(createOptions));
|
|
426
600
|
if ((existsSync(outPath) ? readFileSync(outPath, "utf8") : void 0) !== expected) {
|
|
427
601
|
error(`${outPath} is stale`);
|
|
428
602
|
setExitCode(1);
|
|
429
603
|
return;
|
|
430
604
|
}
|
|
431
|
-
|
|
605
|
+
if (!quiet) warn(`${outPath} is up to date`);
|
|
432
606
|
return;
|
|
433
607
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
out: of
|
|
437
|
-
})}`);
|
|
608
|
+
const written = writeImportMap(options);
|
|
609
|
+
if (!quiet) warn(`Wrote ${written}`);
|
|
438
610
|
});
|
|
439
611
|
//#endregion
|
|
440
612
|
//#region src/mod.ts
|
|
@@ -459,6 +631,12 @@ const cli$1 = cli("importmapify").manifest({
|
|
|
459
631
|
from: import.meta.url,
|
|
460
632
|
files: ["package.json", "deno.json"]
|
|
461
633
|
}).links().default(generateCommand).completions({ as: "flag" });
|
|
462
|
-
if (import.meta.main) cli$1.run(
|
|
634
|
+
if (import.meta.main) cli$1.run({ help: {
|
|
635
|
+
hyperlinks: detectHyperlinkSupport(),
|
|
636
|
+
theme: (c) => ({
|
|
637
|
+
headerName: (input) => c.bold(c.underline(input)),
|
|
638
|
+
headerVersion: (input) => c.dim(c.underline(input))
|
|
639
|
+
})
|
|
640
|
+
} });
|
|
463
641
|
//#endregion
|
|
464
642
|
export { createImportMap, defineConfig, formatImportMap, packageEntries, writeImportMap };
|
package/import_map.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "importmapify",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Expand package.json subpath-pattern imports into explicit Deno import map entries.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deno",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"volta:bump": "volta pin node@latest npm@11; node --version>.node-version; run bd"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
+
"ansispeck": "^0.2.0",
|
|
68
69
|
"dreamcli": "npm:@kjanat/dreamcli@^3.0.0-rc.11"
|
|
69
70
|
},
|
|
70
71
|
"devDependencies": {
|