mappel 0.0.0-name-check.0 → 0.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/LICENSE +21 -0
- package/README.md +34 -1
- package/bin/mappel.mjs +124 -0
- package/package.json +33 -3
- package/src/index.mjs +206 -0
- package/src/resolve.mjs +101 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ibrahima Toure
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
# mappel
|
|
2
|
+
|
|
3
|
+
This package builds an import map from what a workspace actually imports. It reads package manifests and import statements, and writes JSON. It does not bundle or build anything.
|
|
4
|
+
|
|
5
|
+
The browser resolves a bare specifier only through an import map and never reads package.json. Every transitive subpath has to be listed — including the ones a package reaches through its own dependencies. Written by hand, that list is wrong within a release.
|
|
6
|
+
|
|
7
|
+
It resolves each specifier through the package's own exports map — nested conditions, wildcards, and the form with no "." key — then walks the files it finds for more specifiers. It reports what it could not resolve rather than emitting a map that fails in the browser.
|
|
8
|
+
|
|
9
|
+
A repo declares named layers in importmap.config.mjs, and a layer can exclude what another already resolves, so a page loads two maps and neither repeats the other. Layers can also drop a whole scope by prefix. A css specifier maps to a `.js` sibling that adds the file as a link, because a browser cannot import a stylesheet as a module. Pass `--css link` to get the specifier mapped away and the link tags printed instead.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
mappel # every layer, to the out in the config
|
|
13
|
+
mappel --layer min # one layer, to stdout
|
|
14
|
+
mappel --layer min --html # ready to paste into a page
|
|
15
|
+
mappel --layer min --dist-tag alpha # follow a channel instead of pinning
|
|
16
|
+
mappel --config packages/cdn/mappel.config.mjs --out dist
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
An option given on the command line wins; the config supplies what the command line
|
|
20
|
+
does not. With no arguments it looks for `mappel.config.mjs` from the current
|
|
21
|
+
directory upwards, so a package's build script is just `mappel`.
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
// mappel.config.mjs
|
|
25
|
+
export default {
|
|
26
|
+
out: 'packages/cdn/dist', // relative to this file
|
|
27
|
+
workspaces: ['packages'],
|
|
28
|
+
split: { components: 'components' }, // a file per package in that layer
|
|
29
|
+
layers: {
|
|
30
|
+
base: { packages: ['@scope/core', '@scope/dom'] },
|
|
31
|
+
extra: { packages: ['@scope/extra'], excludes: ['base'] },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
```
|
package/bin/mappel.mjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Write the import maps a workspace declares.
|
|
3
|
+
//
|
|
4
|
+
// mappel every layer in mappel.config.mjs, to its `out`
|
|
5
|
+
// mappel --layer min one layer, to stdout
|
|
6
|
+
// mappel --config packages/cdn/mappel.config.mjs --out dist
|
|
7
|
+
// mappel --layer min --dist-tag alpha --html
|
|
8
|
+
//
|
|
9
|
+
// With no arguments it finds mappel.config.mjs by walking up from the current
|
|
10
|
+
// directory, so a package's build script is just `mappel`.
|
|
11
|
+
|
|
12
|
+
import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
13
|
+
import { dirname, join, resolve as resolvePath } from 'node:path';
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
15
|
+
import { buildLayer, writeAll, subpathsOf } from '../src/index.mjs';
|
|
16
|
+
|
|
17
|
+
const argv = process.argv.slice(2);
|
|
18
|
+
const flag = (name, fallback) => {
|
|
19
|
+
const i = argv.indexOf(`--${name}`);
|
|
20
|
+
return i === -1 ? fallback : argv[i + 1];
|
|
21
|
+
};
|
|
22
|
+
const has = (name) => argv.includes(`--${name}`);
|
|
23
|
+
|
|
24
|
+
const NAMES = ['mappel.config.mjs', 'mappel.config.js', 'importmap.config.mjs'];
|
|
25
|
+
|
|
26
|
+
function findConfig(from) {
|
|
27
|
+
let dir = resolvePath(from);
|
|
28
|
+
while (true) {
|
|
29
|
+
for (const name of NAMES) {
|
|
30
|
+
const candidate = join(dir, name);
|
|
31
|
+
if (existsSync(candidate)) return candidate;
|
|
32
|
+
}
|
|
33
|
+
const parent = dirname(dir);
|
|
34
|
+
if (parent === dir) return null;
|
|
35
|
+
dir = parent;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const explicit = flag('config', null);
|
|
40
|
+
const configPath = explicit ? resolvePath(explicit) : findConfig(process.cwd());
|
|
41
|
+
if (!configPath) {
|
|
42
|
+
process.stderr.write(
|
|
43
|
+
`[mappel] no ${NAMES[0]} here or above ${process.cwd()}\n` +
|
|
44
|
+
` point at one with --config, or see https://github.com/bambiste/mappel\n`,
|
|
45
|
+
);
|
|
46
|
+
process.exit(2);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let config;
|
|
50
|
+
try {
|
|
51
|
+
config = (await import(pathToFileURL(configPath).href)).default;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
process.stderr.write(`[mappel] cannot read ${configPath}\n ${error.message}\n`);
|
|
54
|
+
process.exit(2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The config lives at the root of what it describes unless it says otherwise, so
|
|
58
|
+
// a path in it reads relative to the file rather than to where this was run from.
|
|
59
|
+
const base = dirname(configPath);
|
|
60
|
+
const root = resolvePath(flag('root', config.root ? join(base, config.root) : base));
|
|
61
|
+
|
|
62
|
+
const options = {
|
|
63
|
+
root,
|
|
64
|
+
target: flag('target', config.target ?? 'cdn'),
|
|
65
|
+
cdn: flag('cdn', config.cdn ?? 'https://unpkg.com'),
|
|
66
|
+
css: flag('css', config.css ?? 'loader'),
|
|
67
|
+
distTag: flag('dist-tag', config.distTag ?? null),
|
|
68
|
+
workspaces: config.workspaces ?? ['packages'],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const layerName = flag('layer', null);
|
|
72
|
+
const splitInto = flag('split', null);
|
|
73
|
+
|
|
74
|
+
if (!layerName && !splitInto) {
|
|
75
|
+
const out = resolvePath(flag('out', config.out ? join(base, config.out) : join(base, 'dist')));
|
|
76
|
+
const written = writeAll(config, { ...options, out });
|
|
77
|
+
for (const { file, entries, split } of written) {
|
|
78
|
+
process.stderr.write(` ${file} — ${entries} ${split ? 'files' : 'entries'}\n`);
|
|
79
|
+
}
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (splitInto) {
|
|
84
|
+
const dir = resolvePath(splitInto);
|
|
85
|
+
mkdirSync(dir, { recursive: true });
|
|
86
|
+
const layer = config.layers[layerName];
|
|
87
|
+
const names =
|
|
88
|
+
typeof layer.packages === 'function'
|
|
89
|
+
? layer.packages({ subpathsOf: (n) => subpathsOf(root, n, options.workspaces), root })
|
|
90
|
+
: layer.packages;
|
|
91
|
+
let written = 0;
|
|
92
|
+
for (const name of names) {
|
|
93
|
+
const one = buildLayer(
|
|
94
|
+
layerName,
|
|
95
|
+
{ ...config, layers: { ...config.layers, [layerName]: { ...layer, packages: [name] } } },
|
|
96
|
+
options,
|
|
97
|
+
);
|
|
98
|
+
if (Object.keys(one.imports).length === 0) continue;
|
|
99
|
+
writeFileSync(join(dir, name.split('/').pop() + '.json'), JSON.stringify({ imports: one.imports }, null, 2) + '\n');
|
|
100
|
+
written += 1;
|
|
101
|
+
}
|
|
102
|
+
process.stderr.write(`wrote ${written} maps to ${dir}\n`);
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const { imports, stylesheets, unresolved } = buildLayer(layerName, config, options);
|
|
107
|
+
const json = JSON.stringify({ imports }, null, 2);
|
|
108
|
+
const out = flag('out', null);
|
|
109
|
+
|
|
110
|
+
if (out) {
|
|
111
|
+
writeFileSync(out, json + '\n');
|
|
112
|
+
process.stderr.write(`wrote ${out} — ${Object.keys(imports).length} entries\n`);
|
|
113
|
+
} else if (has('html')) {
|
|
114
|
+
const links =
|
|
115
|
+
options.css === 'link' ? stylesheets.map((h) => `<link rel="stylesheet" href="${h}" />`).join('\n') : '';
|
|
116
|
+
process.stdout.write(`<script type="importmap">\n${json}\n</script>\n${links ? links + '\n' : ''}`);
|
|
117
|
+
} else {
|
|
118
|
+
process.stdout.write(json + '\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (unresolved.length) {
|
|
122
|
+
process.stderr.write('\nnot in the map (check they are installed):\n');
|
|
123
|
+
for (const u of unresolved.sort()) process.stderr.write(` ${u}\n`);
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mappel",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "generate the import map a page needs from a workspace: resolves every transitive subpath through each package exports map, in layers that compose",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mappel": "./bin/mappel.mjs"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.mjs",
|
|
12
|
+
"./resolve": "./src/resolve.mjs"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"src",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test \"test/*.test.mjs\"",
|
|
22
|
+
"version": "changeset version",
|
|
23
|
+
"release": "npm test && changeset publish"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"registry": "https://registry.npmjs.org/",
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/bambiste/mappel.git"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@changesets/cli": "^3.0.1"
|
|
35
|
+
}
|
|
6
36
|
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, normalize, relative } from 'node:path';
|
|
3
|
+
import { manifest, packageDir, resolveExport, splitSpecifier, subpathsOf } from './resolve.mjs';
|
|
4
|
+
|
|
5
|
+
const IMPORT_RE = /(?:from|import)\s*["']([^"']+)["']/g;
|
|
6
|
+
|
|
7
|
+
function scanImports(file) {
|
|
8
|
+
let src;
|
|
9
|
+
try {
|
|
10
|
+
src = readFileSync(file, 'utf8');
|
|
11
|
+
} catch {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
const found = [];
|
|
15
|
+
for (const [, spec] of src.matchAll(IMPORT_RE)) {
|
|
16
|
+
// Doc comments quote specifiers too; a real one has no whitespace or `${}`.
|
|
17
|
+
if (/\s|\$\{/.test(spec)) continue;
|
|
18
|
+
found.push(spec);
|
|
19
|
+
}
|
|
20
|
+
return found;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Walk from a set of specifiers to every bare specifier reachable from them,
|
|
25
|
+
* mapped to a url a browser can fetch.
|
|
26
|
+
*/
|
|
27
|
+
export function collect(specifiers, options) {
|
|
28
|
+
const {
|
|
29
|
+
root,
|
|
30
|
+
target = 'cdn',
|
|
31
|
+
cdn = 'https://unpkg.com',
|
|
32
|
+
css = 'loader',
|
|
33
|
+
workspaces,
|
|
34
|
+
// The version in the url. A manifest version pins the map to exactly what was
|
|
35
|
+
// installed when it was written, which is what a published map wants. A dist
|
|
36
|
+
// tag follows a channel instead, so a page tracks it without being rebuilt —
|
|
37
|
+
// at the cost of the page changing when the channel does.
|
|
38
|
+
distTag = null,
|
|
39
|
+
} = options;
|
|
40
|
+
const imports = {};
|
|
41
|
+
const stylesheets = new Set();
|
|
42
|
+
const unresolved = new Set();
|
|
43
|
+
const visited = new Set();
|
|
44
|
+
|
|
45
|
+
const urlFor = (name, dir, file) => {
|
|
46
|
+
const clean = file.replace(/^\.\//, '');
|
|
47
|
+
if (target === 'local') return '/' + relative(root, join(dir, clean)).split('\\').join('/');
|
|
48
|
+
const version = distTag === 'none' ? '' : '@' + (distTag || manifest(dir).version);
|
|
49
|
+
return `${cdn}/${name}${version}/${clean}`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const add = (spec) => {
|
|
53
|
+
if (visited.has(spec)) return;
|
|
54
|
+
visited.add(spec);
|
|
55
|
+
|
|
56
|
+
const { pkg, sub } = splitSpecifier(spec);
|
|
57
|
+
const dir = packageDir(root, pkg, workspaces);
|
|
58
|
+
if (!dir) return void unresolved.add(spec);
|
|
59
|
+
|
|
60
|
+
const file = resolveExport(dir, sub);
|
|
61
|
+
if (!file) return void unresolved.add(spec);
|
|
62
|
+
|
|
63
|
+
if (spec.endsWith('.css')) {
|
|
64
|
+
stylesheets.add(urlFor(pkg, dir, file));
|
|
65
|
+
// A browser cannot import a stylesheet as a module. The loader sibling adds
|
|
66
|
+
// it as a <link>, so importing a component still brings its styles.
|
|
67
|
+
imports[spec] = css === 'loader' ? urlFor(pkg, dir, file + '.js') : 'data:text/javascript,';
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
imports[spec] = urlFor(pkg, dir, file);
|
|
72
|
+
|
|
73
|
+
const entry = join(dir, file.replace(/^\.\//, ''));
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
const stack = [entry];
|
|
76
|
+
while (stack.length) {
|
|
77
|
+
const current = stack.pop();
|
|
78
|
+
if (seen.has(current) || !existsSync(current)) continue;
|
|
79
|
+
seen.add(current);
|
|
80
|
+
for (const nested of scanImports(current)) {
|
|
81
|
+
if (nested.startsWith('.')) {
|
|
82
|
+
const next = normalize(join(dirname(current), nested));
|
|
83
|
+
stack.push(existsSync(next) ? next : next + '.js');
|
|
84
|
+
} else if (!nested.startsWith('node:')) {
|
|
85
|
+
add(nested);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
for (const spec of specifiers) add(spec);
|
|
92
|
+
return { imports, stylesheets: [...stylesheets], unresolved: [...unresolved] };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sorted = (obj) =>
|
|
96
|
+
Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build one named layer. `excludes` names other layers whose entries this one
|
|
100
|
+
* leaves out, so a page loading both is not handed the same entry twice.
|
|
101
|
+
*/
|
|
102
|
+
export function buildLayer(name, config, options) {
|
|
103
|
+
const layer = config.layers[name];
|
|
104
|
+
if (!layer) throw new Error(`[import-map] no layer named "${name}"`);
|
|
105
|
+
|
|
106
|
+
const resolveList = (list) => (typeof list === 'function' ? list(helpers(options)) : list);
|
|
107
|
+
const mine = collect(resolveList(layer.packages), { ...options, ...config.options });
|
|
108
|
+
|
|
109
|
+
const skip = new Set();
|
|
110
|
+
for (const other of layer.excludes ?? []) {
|
|
111
|
+
const theirs = collect(resolveList(config.layers[other].packages), { ...options, ...config.options });
|
|
112
|
+
for (const key of Object.keys(theirs.imports)) skip.add(key);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const imports = {};
|
|
116
|
+
for (const [key, value] of Object.entries(mine.imports)) {
|
|
117
|
+
if (skip.has(key)) continue;
|
|
118
|
+
if ((layer.excludePrefixes ?? []).some((p) => key.startsWith(p))) continue;
|
|
119
|
+
imports[key] = value;
|
|
120
|
+
}
|
|
121
|
+
return { imports: sorted(imports), stylesheets: mine.stylesheets, unresolved: mine.unresolved };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function helpers(options) {
|
|
125
|
+
return {
|
|
126
|
+
subpathsOf: (name) => subpathsOf(options.root, name, options.workspaces),
|
|
127
|
+
root: options.root,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { subpathsOf, packageDir, resolveExport };
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Write every layer a config declares, plus any per-item split it asks for.
|
|
135
|
+
* This is what `mappel` does with no arguments: the config says where the
|
|
136
|
+
* workspace is and where the files go, so a repo needs no script of its own.
|
|
137
|
+
*/
|
|
138
|
+
export function writeAll(config, options) {
|
|
139
|
+
const out = options.out;
|
|
140
|
+
mkdirSync(out, { recursive: true });
|
|
141
|
+
|
|
142
|
+
const written = [];
|
|
143
|
+
for (const name of Object.keys(config.layers)) {
|
|
144
|
+
const { imports } = buildLayer(name, config, options);
|
|
145
|
+
const file = join(out, `${name}.json`);
|
|
146
|
+
writeFileSync(file, JSON.stringify({ imports }, null, 2) + '\n');
|
|
147
|
+
written.push({ file, entries: Object.keys(imports).length });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// A combined file, for engines that take only one import map. Several maps in
|
|
151
|
+
// one page need Chromium 133 or newer. A layer of the same name means the config
|
|
152
|
+
// already has one, and writing both would silently overwrite the layer's file.
|
|
153
|
+
const combined = config.full === true || config.full === undefined ? 'full' : config.full;
|
|
154
|
+
if (config.full !== false && config.layers[combined]) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`[mappel] a layer is named "${combined}" and the combined file would overwrite it. ` +
|
|
157
|
+
`Set full: false, or full: "<other-name>".`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (config.full !== false) {
|
|
161
|
+
const imports = {};
|
|
162
|
+
for (const name of Object.keys(config.layers)) {
|
|
163
|
+
Object.assign(imports, buildLayer(name, config, options).imports);
|
|
164
|
+
}
|
|
165
|
+
const file = join(out, `${combined}.json`);
|
|
166
|
+
writeFileSync(
|
|
167
|
+
file,
|
|
168
|
+
JSON.stringify(
|
|
169
|
+
{ imports: Object.fromEntries(Object.entries(imports).sort(([a], [b]) => a.localeCompare(b))) },
|
|
170
|
+
null,
|
|
171
|
+
2,
|
|
172
|
+
) + '\n',
|
|
173
|
+
);
|
|
174
|
+
written.push({ file, entries: Object.keys(imports).length });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const [layerName, target] of Object.entries(config.split ?? {})) {
|
|
178
|
+
const dir = join(out, target);
|
|
179
|
+
mkdirSync(dir, { recursive: true });
|
|
180
|
+
const layer = config.layers[layerName];
|
|
181
|
+
const names =
|
|
182
|
+
typeof layer.packages === 'function'
|
|
183
|
+
? layer.packages({
|
|
184
|
+
subpathsOf: (n) => subpathsOf(options.root, n, options.workspaces),
|
|
185
|
+
root: options.root,
|
|
186
|
+
})
|
|
187
|
+
: layer.packages;
|
|
188
|
+
let count = 0;
|
|
189
|
+
for (const name of names) {
|
|
190
|
+
const one = buildLayer(
|
|
191
|
+
layerName,
|
|
192
|
+
{ ...config, layers: { ...config.layers, [layerName]: { ...layer, packages: [name] } } },
|
|
193
|
+
options,
|
|
194
|
+
);
|
|
195
|
+
if (Object.keys(one.imports).length === 0) continue;
|
|
196
|
+
writeFileSync(
|
|
197
|
+
join(dir, name.split('/').pop() + '.json'),
|
|
198
|
+
JSON.stringify({ imports: one.imports }, null, 2) + '\n',
|
|
199
|
+
);
|
|
200
|
+
count += 1;
|
|
201
|
+
}
|
|
202
|
+
written.push({ file: `${dir}/*.json`, entries: count, split: true });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return written;
|
|
206
|
+
}
|
package/src/resolve.mjs
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const manifests = new Map();
|
|
5
|
+
|
|
6
|
+
export function manifest(dir) {
|
|
7
|
+
if (!manifests.has(dir)) {
|
|
8
|
+
manifests.set(dir, JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')));
|
|
9
|
+
}
|
|
10
|
+
return manifests.get(dir);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function splitSpecifier(spec) {
|
|
14
|
+
const parts = spec.split('/');
|
|
15
|
+
const pkg = spec.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
16
|
+
const sub = spec.length > pkg.length ? '.' + spec.slice(pkg.length) : '.';
|
|
17
|
+
return { pkg, sub };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Where a package lives. A workspace folder wins over an installed copy, and
|
|
22
|
+
* pnpm's store is searched last: it keeps the real files under
|
|
23
|
+
* .pnpm/<name>@<version>/node_modules/<name>, and a package only appears at the
|
|
24
|
+
* top level when something depends on it directly.
|
|
25
|
+
*/
|
|
26
|
+
export function packageDir(root, name, workspaces = ['packages']) {
|
|
27
|
+
for (const dir of workspaces) {
|
|
28
|
+
const local = join(root, dir, name.split('/').pop());
|
|
29
|
+
if (existsSync(join(local, 'package.json'))) {
|
|
30
|
+
try {
|
|
31
|
+
if (manifest(local).name === name) return local;
|
|
32
|
+
} catch {
|
|
33
|
+
/* not a manifest we can read */
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const installed = join(root, 'node_modules', name);
|
|
38
|
+
if (existsSync(join(installed, 'package.json'))) return installed;
|
|
39
|
+
|
|
40
|
+
const store = join(root, 'node_modules', '.pnpm');
|
|
41
|
+
if (existsSync(store)) {
|
|
42
|
+
const prefix = name.replace('/', '+') + '@';
|
|
43
|
+
for (const dir of readdirSync(store).filter((d) => d.startsWith(prefix)).sort().reverse()) {
|
|
44
|
+
const real = join(store, dir, 'node_modules', name);
|
|
45
|
+
if (existsSync(join(real, 'package.json'))) return real;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Conditions nest, and `{ import: { types, default } }` is as common as a string. */
|
|
52
|
+
export function pickTarget(entry) {
|
|
53
|
+
if (typeof entry === 'string') return entry;
|
|
54
|
+
if (Array.isArray(entry)) {
|
|
55
|
+
for (const alt of entry) {
|
|
56
|
+
const found = pickTarget(alt);
|
|
57
|
+
if (found) return found;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (entry && typeof entry === 'object') {
|
|
62
|
+
for (const key of ['browser', 'import', 'default', 'require']) {
|
|
63
|
+
if (entry[key] === undefined) continue;
|
|
64
|
+
const found = pickTarget(entry[key]);
|
|
65
|
+
if (found) return found;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function resolveExport(dir, sub) {
|
|
72
|
+
const m = manifest(dir);
|
|
73
|
+
const exp = m.exports;
|
|
74
|
+
if (!exp) return sub === '.' ? m.module || m.main || null : null;
|
|
75
|
+
// No key starting with '.' means `exports` is a bare set of conditions that IS
|
|
76
|
+
// the root entry — `{ import, require, types }` — rather than a subpath map.
|
|
77
|
+
if (!Object.keys(exp).some((k) => k.startsWith('.'))) {
|
|
78
|
+
return sub === '.' ? pickTarget(exp) : null;
|
|
79
|
+
}
|
|
80
|
+
if (exp[sub] !== undefined) return pickTarget(exp[sub]);
|
|
81
|
+
for (const [pattern, entry] of Object.entries(exp)) {
|
|
82
|
+
if (!pattern.includes('*')) continue;
|
|
83
|
+
const [before, after] = pattern.split('*');
|
|
84
|
+
if (sub.startsWith(before) && sub.endsWith(after)) {
|
|
85
|
+
const filled = sub.slice(before.length, sub.length - (after.length || 0));
|
|
86
|
+
const target = pickTarget(entry);
|
|
87
|
+
if (target) return target.replace('*', filled);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A package with no root export still has subpaths worth mapping. */
|
|
94
|
+
export function subpathsOf(root, name, workspaces) {
|
|
95
|
+
const dir = packageDir(root, name, workspaces);
|
|
96
|
+
if (!dir) return [];
|
|
97
|
+
const exp = manifest(dir).exports || {};
|
|
98
|
+
return Object.keys(exp)
|
|
99
|
+
.filter((k) => k.startsWith('./') && k !== './package.json')
|
|
100
|
+
.map((k) => name + k.slice(1));
|
|
101
|
+
}
|