importmapify 0.1.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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+ ===========
3
+
4
+ Copyright (c) 2026 Kaj Kowalski
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to
8
+ deal in the Software without restriction, including without limitation the
9
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10
+ sell copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22
+ IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # importmapify
2
+
3
+ [![NPM](https://img.shields.io/npm/v/importmapify?logo=npm&labelColor=CB3837&color=black)][npm]
4
+ [![CI](https://github.com/kjanat/importmapify/actions/workflows/publish.yml/badge.svg)][ci]
5
+ [![Socket](https://badge.socket.dev/npm/package/importmapify)][socket]
6
+
7
+ Expand `package.json` subpath-pattern imports into the explicit entries a
8
+ [Deno import map] needs.
9
+
10
+ Node resolves subpath patterns like `#lib/*` -> `./src/lib/*.ts` internally.
11
+ Deno's import maps only support exact keys and trailing-slash prefixes, so
12
+ tools that expect an import map (`deno doc`, `deno check`, the Deno LSP) need
13
+ one entry per matched source file instead. This package performs that
14
+ expansion and writes a deterministically sorted import map.
15
+
16
+ [Deno import map]: https://docs.deno.com/runtime/fundamentals/modules/#differentiating-between-imports-or-importmap-in-deno.json-and---import-map-option
17
+ [ci]: https://github.com/kjanat/importmapify/actions/workflows/publish.yml
18
+ [npm]: https://npm.im/importmapify
19
+ [socket]: https://socket.dev/npm/package/importmapify
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ npm install importmapify
25
+ ```
26
+
27
+ ## CLI
28
+
29
+ ```sh
30
+ npx importmapify --root . --out deno.import_map.json
31
+ ```
32
+
33
+ | Flag | Alias | Meaning | Default |
34
+ | -------------------- | ----- | ----------------------------------------------------------------- | ---------------------- |
35
+ | `--root` | `-r` | Project root containing the manifest | current directory |
36
+ | `--manifest` | `-m` | Manifest path, relative to root | `package.json` |
37
+ | `--out` | `-o` | Output path, relative to root | `deno.import_map.json` |
38
+ | `--import key=value` | `-i` | Extra import entry; 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
+
43
+ Run `npx importmapify --help` for the full reference, or `npx importmapify
44
+ completions` for shell completions.
45
+
46
+ ## Library
47
+
48
+ ```ts
49
+ import { writeImportMap } from 'importmapify';
50
+
51
+ const out = writeImportMap({
52
+ root: import.meta.dirname,
53
+ out: 'deno.import_map.json',
54
+ additionalImports: {
55
+ 'bun:test': './node_modules/bun-types/test.d.ts',
56
+ },
57
+ });
58
+ ```
59
+
60
+ | Export | Signature | Purpose |
61
+ | ----------------- | -------------------------------------------------------- | -------------------------------------------------------------- |
62
+ | `createImportMap` | `(options: CreateImportMapOptions) => ImportMapDocument` | Build the import map in memory. |
63
+ | `formatImportMap` | `(map: ImportMapDocument) => string` | Serialize to the canonical sorted, tab-indented JSON text. |
64
+ | `writeImportMap` | `(options: WriteImportMapOptions) => string` | Build, serialize, and write to disk; returns the written path. |
65
+
66
+ `CreateImportMapOptions`:
67
+
68
+ | Option | Meaning | Default |
69
+ | ------------------- | ------------------------------------------------------------------------------------------------- | ----------------------- |
70
+ | `root` | Base directory for the manifest, source targets, and rebasing. | required |
71
+ | `manifest` | Manifest path, relative to `root`. | `package.json` |
72
+ | `conditions` | Condition names tried, in order, against conditional targets (`{"import": ..., "default": ...}`). | `['import', 'default']` |
73
+ | `additionalImports` | Extra entries merged in after manifest expansion; these win on key collision. | none |
74
+ | `relativeTo` | Directory the written targets are rebased against. | `root` |
75
+
76
+ `WriteImportMapOptions` extends the above with `out`, the output path relative
77
+ to `root`. `writeImportMap` rebases automatically against `out`'s directory,
78
+ so a nested `out` (for example `.cache/maps/deno.import_map.json`) still
79
+ produces targets that resolve correctly from the map's own location.
80
+
81
+ ## What it generates
82
+
83
+ Given this manifest:
84
+
85
+ ```json
86
+ {
87
+ "imports": {
88
+ "#config": "./src/config.ts",
89
+ "#lib/*": "./src/lib/*.ts"
90
+ }
91
+ }
92
+ ```
93
+
94
+ and these files:
95
+
96
+ ```text
97
+ src/
98
+ ├── config.ts
99
+ └── lib/
100
+ ├── bytes.ts
101
+ └── codecs/
102
+ └── hex.ts
103
+ ```
104
+
105
+ the generated map is:
106
+
107
+ ```json
108
+ {
109
+ "imports": {
110
+ "#config": "./src/config.ts",
111
+ "#lib/bytes": "./src/lib/bytes.ts",
112
+ "#lib/bytes.ts": "./src/lib/bytes.ts",
113
+ "#lib/codecs/hex": "./src/lib/codecs/hex.ts",
114
+ "#lib/codecs/hex.ts": "./src/lib/codecs/hex.ts"
115
+ }
116
+ }
117
+ ```
118
+
119
+ A key with its own suffix, such as `#lib/*.js` targeting `./src/lib/*.ts`,
120
+ produces both the renamed specifier and the real filename: `#lib/bytes.js`
121
+ and `#lib/bytes.ts`, both pointing at `./src/lib/bytes.ts`.
122
+
123
+ ## Scope and constraints
124
+
125
+ - Only the manifest's top-level `imports` field is read.
126
+ - Conditional targets resolve via `conditions`, tried in order, recursively.
127
+ A target matching no condition is skipped.
128
+ - Pattern keys and targets must both contain `*`, or neither should; a
129
+ mismatch throws.
130
+ - Expandable pattern targets must point to local files beneath `root`.
131
+ Target directories are scanned recursively; a missing directory produces
132
+ no entries.
133
+ - Relative targets (`./...`, `../...`) are rebased against `relativeTo`.
134
+ Bare specifiers, `npm:`/`jsr:`/`node:` specifiers, and absolute URLs pass
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.
139
+
140
+ ## Use the generated map
141
+
142
+ ```sh
143
+ deno doc --import-map=deno.import_map.json src/index.ts
144
+ deno check --import-map=deno.import_map.json src/
145
+ deno run --import-map=deno.import_map.json src/main.ts
146
+ ```
147
+
148
+ or reference it once in `deno.json`:
149
+
150
+ ```json
151
+ {
152
+ "importMap": "./deno.import_map.json"
153
+ }
154
+ ```
155
+
156
+ ## License
157
+
158
+ MIT
@@ -0,0 +1 @@
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};
@@ -0,0 +1,2 @@
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{};
@@ -0,0 +1,19 @@
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 ADDED
@@ -0,0 +1 @@
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};
package/package.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "name": "importmapify",
3
+ "version": "0.1.0",
4
+ "description": "Expand package.json subpath-pattern imports into explicit Deno import map entries.",
5
+ "keywords": [
6
+ "deno",
7
+ "import-map",
8
+ "importmap",
9
+ "subpath-imports",
10
+ "cli",
11
+ "dreamcli",
12
+ "ansispeck"
13
+ ],
14
+ "homepage": "https://github.com/kjanat/importmapify#readme",
15
+ "bugs": "https://github.com/kjanat/importmapify/issues",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/kjanat/importmapify.git"
19
+ },
20
+ "license": "MIT",
21
+ "author": {
22
+ "name": "Kaj Kowalski",
23
+ "email": "info@kajkowalski.nl",
24
+ "url": "https://github.com/kjanat"
25
+ },
26
+ "sideEffects": false,
27
+ "type": "module",
28
+ "imports": {
29
+ "#pkg": "./package.json"
30
+ },
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.mts",
34
+ "default": "./dist/index.mjs"
35
+ },
36
+ "./package.json": "./package.json"
37
+ },
38
+ "types": "./dist/index.d.mts",
39
+ "bin": {
40
+ "importmapify": "dist/importmapify.mjs"
41
+ },
42
+ "files": [
43
+ "dist"
44
+ ],
45
+ "scripts": {
46
+ "bd": "tsdown",
47
+ "build": "tsdown",
48
+ "build:watch": "tsdown --watch",
49
+ "check": "biome check",
50
+ "check:fix": "biome check --fix",
51
+ "fmt": "dprint fmt",
52
+ "lint": "biome lint",
53
+ "prepack": "bun --bun bd -l error",
54
+ "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)\" --help",
56
+ "test": "bun test --coverage",
57
+ "test:coverage": "bun test --coverage",
58
+ "test:smoke": "node scripts/smoke.mjs",
59
+ "test:watch": "bun test --watch",
60
+ "typecheck": "bunx @typescript/native --noEmit"
61
+ },
62
+ "dependencies": {
63
+ "dreamcli": "npm:@kjanat/dreamcli@^3.0.0-rc.9"
64
+ },
65
+ "devDependencies": {
66
+ "@arethetypeswrong/core": "^0.18.5",
67
+ "@biomejs/biome": "2.5.2 || ^2.5.4",
68
+ "@types/bun": "^1.3.14",
69
+ "@types/node": "^26.1.1",
70
+ "@typescript/native": "npm:typescript@^7",
71
+ "dprint": "^0.55.2",
72
+ "publint": "^0.3.21",
73
+ "sort-package-json": "^4.0.0",
74
+ "tsdown": "^0.22.7",
75
+ "typescript": ">=6,<7",
76
+ "unplugin-unused": "^0.5.7"
77
+ },
78
+ "packageManager": "bun@1.3.14",
79
+ "engines": {
80
+ "bun": ">=1.3",
81
+ "node": ">=24"
82
+ },
83
+ "volta": {
84
+ "node": "26.5.0",
85
+ "npm": "11.18.0"
86
+ },
87
+ "publishConfig": {
88
+ "access": "public",
89
+ "provenance": true,
90
+ "registry": "https://registry.npmjs.org/",
91
+ "tag": "latest"
92
+ }
93
+ }