@rsc-kit/core 0.30.25 → 0.30.27
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/dist/action.d.ts +11 -0
- package/dist/action.js +20 -1
- package/dist/action.js.map +1 -1
- package/dist/native.d.ts +123 -0
- package/dist/native.js +612 -0
- package/dist/native.js.map +1 -0
- package/dist/vite.d.ts +0 -21
- package/dist/vite.js +48 -2
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/dist/native.js
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native packages inside a compiled binary.
|
|
3
|
+
*
|
|
4
|
+
* A package with a native binary - sharp, a database driver - is left external
|
|
5
|
+
* by the build, and Nitro traces it into .output/server/node_modules. A server
|
|
6
|
+
* run from that directory loads it from there. `bun build --compile` cannot:
|
|
7
|
+
* it bundles the package's JavaScript, and the package then looks for its
|
|
8
|
+
* `.node` file beside a module that is no longer on disk, so the binary starts
|
|
9
|
+
* and dies on the first import of it.
|
|
10
|
+
*
|
|
11
|
+
* So a binary carries those packages as files, and writes them to disk the
|
|
12
|
+
* first time it runs: to `<RSC_KIT_RUNTIME_DIR>/<hash>/node_modules`, the
|
|
13
|
+
* binary's own directory when the variable is unset. The hash is of the files,
|
|
14
|
+
* so two versions sharing one directory during a deploy never share a tree,
|
|
15
|
+
* and a second start of the same version writes nothing.
|
|
16
|
+
*
|
|
17
|
+
* Inside a binary Bun resolves no package by name, from anywhere - not from
|
|
18
|
+
* the bundle, and not from a file on disk that the bundle loaded. Neither a
|
|
19
|
+
* `Module._resolveFilename` hook nor a runtime plugin sees an ESM import there.
|
|
20
|
+
* So every name is resolved here, at build time, while the tree is on disk:
|
|
21
|
+
* each import and require of a package in the tree is rewritten to a relative
|
|
22
|
+
* path, and the app's own imports of one go through a shim that loads it from
|
|
23
|
+
* the written tree. The hook stays, for a require whose name is computed.
|
|
24
|
+
*
|
|
25
|
+
* An app with nothing traced gets none of this: its compile entry is the one
|
|
26
|
+
* it always had.
|
|
27
|
+
*/
|
|
28
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
29
|
+
import { createHash } from 'node:crypto';
|
|
30
|
+
import { isBuiltin } from 'node:module';
|
|
31
|
+
import { dirname, join, relative, sep } from 'node:path';
|
|
32
|
+
/** Where the compile tree is written, beside .output/server. */
|
|
33
|
+
export const COMPILE_TREE_DIR = 'compile';
|
|
34
|
+
/** The platform this process runs on. */
|
|
35
|
+
export function hostPlatform() {
|
|
36
|
+
if (process.platform !== 'linux')
|
|
37
|
+
return { os: process.platform, arch: process.arch };
|
|
38
|
+
const header = process.report?.getReport?.()?.header;
|
|
39
|
+
return { os: 'linux', arch: process.arch, libc: header && !header.glibcVersionRuntime ? 'musl' : 'glibc' };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The platform a compile script targets: `--target=bun-linux-x64-musl`, or
|
|
43
|
+
* the build machine's own when it names none. `baseline` and `modern` are CPU
|
|
44
|
+
* levels of the same arch, and change nothing a package declares.
|
|
45
|
+
*/
|
|
46
|
+
export function targetPlatform(script, host = hostPlatform()) {
|
|
47
|
+
const target = script?.match(/--target[=\s]+["']?bun-([a-z0-9]+)-([a-z0-9]+)((?:-[a-z]+)*)/);
|
|
48
|
+
if (!target)
|
|
49
|
+
return host;
|
|
50
|
+
const [, os, arch, modifiers] = target;
|
|
51
|
+
const platform = os === 'windows' ? 'win32' : os;
|
|
52
|
+
if (platform !== 'linux')
|
|
53
|
+
return { os: platform, arch };
|
|
54
|
+
return { os: platform, arch, libc: modifiers.includes('-musl') ? 'musl' : 'glibc' };
|
|
55
|
+
}
|
|
56
|
+
export function describePlatform(platform) {
|
|
57
|
+
return `${platform.os}-${platform.arch}${platform.libc ? ` (${platform.libc})` : ''}`;
|
|
58
|
+
}
|
|
59
|
+
/** npm's rule for os, cpu and libc: a list of allowed values, or of `!`-negated ones. */
|
|
60
|
+
function allows(list, value) {
|
|
61
|
+
if (!list || list.length === 0 || value === undefined)
|
|
62
|
+
return true;
|
|
63
|
+
const denied = list.filter((v) => v.startsWith('!')).map((v) => v.slice(1));
|
|
64
|
+
const allowed = list.filter((v) => !v.startsWith('!'));
|
|
65
|
+
if (denied.includes(value))
|
|
66
|
+
return false;
|
|
67
|
+
return allowed.length === 0 || allowed.includes(value);
|
|
68
|
+
}
|
|
69
|
+
export function runsOn(pkg, platform) {
|
|
70
|
+
return allows(pkg.os, platform.os) && allows(pkg.cpu, platform.arch) && allows(pkg.libc, platform.libc);
|
|
71
|
+
}
|
|
72
|
+
function readJson(path) {
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** A directory, following a symlink: Nitro links each traced package into node_modules from a store beside it. */
|
|
81
|
+
function isDir(path) {
|
|
82
|
+
try {
|
|
83
|
+
return statSync(path).isDirectory();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Every package directory under a node_modules, scoped and nested ones included. */
|
|
90
|
+
function packageDirs(nodeModules) {
|
|
91
|
+
if (!isDir(nodeModules))
|
|
92
|
+
return [];
|
|
93
|
+
const dirs = [];
|
|
94
|
+
for (const name of readdirSync(nodeModules)) {
|
|
95
|
+
const path = join(nodeModules, name);
|
|
96
|
+
if (name.startsWith('.') || !isDir(path))
|
|
97
|
+
continue;
|
|
98
|
+
if (name.startsWith('@')) {
|
|
99
|
+
for (const scoped of readdirSync(path)) {
|
|
100
|
+
if (isDir(join(path, scoped)))
|
|
101
|
+
dirs.push(join(path, scoped), ...packageDirs(join(path, scoped, 'node_modules')));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
dirs.push(path, ...packageDirs(join(path, 'node_modules')));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return dirs;
|
|
109
|
+
}
|
|
110
|
+
/** Every file under a directory, following symlinks, and not into a package's own node_modules when `packageOnly`. */
|
|
111
|
+
function files(dir, packageOnly = false) {
|
|
112
|
+
const found = [];
|
|
113
|
+
const walk = (sub) => {
|
|
114
|
+
for (const name of readdirSync(join(dir, sub))) {
|
|
115
|
+
const rel = sub ? `${sub}/${name}` : name;
|
|
116
|
+
if (packageOnly && name === 'node_modules')
|
|
117
|
+
continue;
|
|
118
|
+
if (isDir(join(dir, rel)))
|
|
119
|
+
walk(rel);
|
|
120
|
+
else if (existsSync(join(dir, rel)))
|
|
121
|
+
found.push(rel);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
walk('');
|
|
125
|
+
return found.sort();
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A platform in a package's name: `darwin-arm64`, `linuxmusl-x64`,
|
|
129
|
+
* `linux-x64-gnu`. Packages that ship one binary per platform name each one
|
|
130
|
+
* this way, which is how the one for another platform is found.
|
|
131
|
+
*/
|
|
132
|
+
const PLATFORM_IN_NAME = /(darwin|linux|linuxmusl|win32|freebsd|android)-(x64|arm64|arm|ia32|ppc64|riscv64|s390x)(-(?:gnu|musl|msvc|gnueabihf|musleabihf))?/;
|
|
133
|
+
function nameFor(name, platform) {
|
|
134
|
+
const found = name.match(PLATFORM_IN_NAME);
|
|
135
|
+
if (!found)
|
|
136
|
+
return undefined;
|
|
137
|
+
const [token, os, , suffix] = found;
|
|
138
|
+
const musl = platform.libc === 'musl';
|
|
139
|
+
let replacement;
|
|
140
|
+
if (suffix) {
|
|
141
|
+
// napi-rs: linux-x64-gnu / linux-x64-musl, darwin-arm64, win32-x64-msvc.
|
|
142
|
+
const libc = platform.os === 'linux' ? (musl ? '-musl' : '-gnu') : platform.os === 'win32' ? '-msvc' : '';
|
|
143
|
+
replacement = `${platform.os}-${platform.arch}${libc}`;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// sharp: linux-x64 / linuxmusl-x64.
|
|
147
|
+
replacement = `${platform.os === 'linux' && musl ? 'linuxmusl' : platform.os}-${platform.arch}`;
|
|
148
|
+
}
|
|
149
|
+
return os === undefined ? undefined : name.replace(token, replacement);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The package as it was installed, rather than as Nitro traced it. The trace
|
|
153
|
+
* copies the files the server's imports reach, and only those: an import's
|
|
154
|
+
* entry and not a require's, and not a file read at a path the tracer cannot
|
|
155
|
+
* work out. A package written to disk whole finds whatever it looks for
|
|
156
|
+
* beside its own code.
|
|
157
|
+
*/
|
|
158
|
+
function installedCopy(pkg, projectRoot) {
|
|
159
|
+
if (!pkg?.name)
|
|
160
|
+
return undefined;
|
|
161
|
+
const candidates = [
|
|
162
|
+
join(projectRoot, 'node_modules', pkg.name),
|
|
163
|
+
// Bun's isolated store.
|
|
164
|
+
join(projectRoot, 'node_modules', '.bun', `${pkg.name.replace('/', '+')}@${pkg.version}`, 'node_modules', pkg.name),
|
|
165
|
+
];
|
|
166
|
+
return candidates.find((dir) => {
|
|
167
|
+
const found = readJson(join(dir, 'package.json'));
|
|
168
|
+
return found !== undefined && found.name === pkg.name && found.version === pkg.version;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The packages a binary for `target` needs: what Nitro traced, with each
|
|
173
|
+
* package built for the build machine swapped for the one built for the
|
|
174
|
+
* target. Nitro traces what is installed and loads on this machine, so a Mac
|
|
175
|
+
* compiling for linux-x64 traced darwin binaries; the linux ones are found
|
|
176
|
+
* beside them in the project's node_modules, or reported as missing.
|
|
177
|
+
*/
|
|
178
|
+
export function nativeSet(serverNodeModules, projectRoot, target, named = []) {
|
|
179
|
+
const packages = new Map();
|
|
180
|
+
const missing = [];
|
|
181
|
+
const traced = packageDirs(serverNodeModules);
|
|
182
|
+
const optional = new Map();
|
|
183
|
+
for (const dir of traced) {
|
|
184
|
+
for (const [name, version] of Object.entries(readJson(join(dir, 'package.json'))?.optionalDependencies ?? {})) {
|
|
185
|
+
optional.set(name, version);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (const dir of traced) {
|
|
189
|
+
const rel = relative(serverNodeModules, dir).split(sep).join('/');
|
|
190
|
+
const pkg = readJson(join(dir, 'package.json'));
|
|
191
|
+
if (!pkg || runsOn(pkg, target)) {
|
|
192
|
+
packages.set(rel, installedCopy(pkg, projectRoot) ?? dir);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const name = pkg.name ?? rel;
|
|
196
|
+
const wanted = nameFor(name, target);
|
|
197
|
+
// A platform package with no counterpart for the target - fsevents, say,
|
|
198
|
+
// which only exists on macOS - is one the package itself does without.
|
|
199
|
+
if (!wanted || !optional.has(wanted))
|
|
200
|
+
continue;
|
|
201
|
+
const installed = [join(projectRoot, 'node_modules', wanted), join(dir, '..', '..', wanted)].find((candidate) => existsSync(join(candidate, 'package.json')));
|
|
202
|
+
const found = installed && readJson(join(installed, 'package.json'));
|
|
203
|
+
if (installed && found && runsOn(found, target)) {
|
|
204
|
+
packages.set(rel.slice(0, rel.length - name.length) + wanted, installed);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
missing.push({ name: wanted, version: optional.get(wanted) });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { packages: onDisk(packages, named), missing };
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The packages that have to be on disk, out of all Nitro traced: one with a
|
|
214
|
+
* `.node` or `.wasm`, or built for one platform; one that depends on such a
|
|
215
|
+
* package, since it finds it from its own location; what those depend on, so
|
|
216
|
+
* their imports resolve beside them; and any the app names in
|
|
217
|
+
* serverExternalPackages, whose author said it reads its own files. The rest
|
|
218
|
+
* - a polyfill, tslib - Bun bundles as it always did.
|
|
219
|
+
*/
|
|
220
|
+
function onDisk(packages, named) {
|
|
221
|
+
const info = new Map([...packages].map(([rel, dir]) => {
|
|
222
|
+
const pkg = readJson(join(dir, 'package.json')) ?? {};
|
|
223
|
+
const deps = Object.keys({ ...pkg.dependencies, ...pkg.optionalDependencies, ...pkg.peerDependencies });
|
|
224
|
+
const native = !!(pkg.os || pkg.cpu) || files(dir, true).some((file) => /\.(node|wasm)$/.test(file));
|
|
225
|
+
return [rel, { name: pkg.name ?? rel, deps, native }];
|
|
226
|
+
}));
|
|
227
|
+
const disk = new Set([...info].filter(([, p]) => p.native || named.includes(p.name)).map(([rel]) => rel));
|
|
228
|
+
const names = () => new Set([...disk].map((rel) => info.get(rel).name));
|
|
229
|
+
for (let grew = true; grew;) {
|
|
230
|
+
grew = false;
|
|
231
|
+
const onDiskNames = names();
|
|
232
|
+
for (const [rel, p] of info) {
|
|
233
|
+
if (!disk.has(rel) && p.deps.some((dep) => onDiskNames.has(dep))) {
|
|
234
|
+
disk.add(rel);
|
|
235
|
+
grew = true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (let grew = true; grew;) {
|
|
240
|
+
grew = false;
|
|
241
|
+
const wanted = new Set([...disk].flatMap((rel) => info.get(rel).deps));
|
|
242
|
+
for (const [rel, p] of info) {
|
|
243
|
+
if (!disk.has(rel) && wanted.has(p.name)) {
|
|
244
|
+
disk.add(rel);
|
|
245
|
+
grew = true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return new Map([...packages].filter(([rel]) => disk.has(rel)));
|
|
250
|
+
}
|
|
251
|
+
export function missingMessage(missing, target, host) {
|
|
252
|
+
return (`[rsc-kit] The binary is compiled for ${describePlatform(target)}, and these native packages for it are not installed:\n\n` +
|
|
253
|
+
missing.map((m) => ` ${m.name}${m.version ? `@${m.version}` : ''}`).join('\n') +
|
|
254
|
+
'\n\n' +
|
|
255
|
+
`Install them for that platform, then compile again:\n\n` +
|
|
256
|
+
` bun install ${[...new Set([host.os, target.os])].map((os) => `--os=${os}`).join(' ')} ${[...new Set([host.arch, target.arch])].map((cpu) => `--cpu=${cpu}`).join(' ')}\n\n` +
|
|
257
|
+
`This machine's own stay installed beside them, since the build loads them here.\n` +
|
|
258
|
+
`Compiled from this machine's packages instead, the binary would start and crash on the first import of one.`);
|
|
259
|
+
}
|
|
260
|
+
/** The entry a package exports for `sub` ('.' or './x'), under the conditions an import or a require uses. */
|
|
261
|
+
function entryOf(pkgDir, sub, conditions, exists) {
|
|
262
|
+
const pkg = readJson(join(pkgDir, 'package.json'));
|
|
263
|
+
if (!pkg)
|
|
264
|
+
return undefined;
|
|
265
|
+
const pick = (target) => {
|
|
266
|
+
if (typeof target === 'string')
|
|
267
|
+
return exists(join(pkgDir, target)) ? target : undefined;
|
|
268
|
+
if (Array.isArray(target)) {
|
|
269
|
+
for (const item of target) {
|
|
270
|
+
const found = pick(item);
|
|
271
|
+
if (found)
|
|
272
|
+
return found;
|
|
273
|
+
}
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
if (target && typeof target === 'object') {
|
|
277
|
+
for (const condition of conditions) {
|
|
278
|
+
if (condition in target) {
|
|
279
|
+
const found = pick(target[condition]);
|
|
280
|
+
if (found)
|
|
281
|
+
return found;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return undefined;
|
|
286
|
+
};
|
|
287
|
+
const exported = pkg.exports;
|
|
288
|
+
if (exported !== undefined && exported !== null) {
|
|
289
|
+
const keys = typeof exported === 'object' && !Array.isArray(exported) ? Object.keys(exported) : [];
|
|
290
|
+
const isMap = keys.length > 0 && keys.every((key) => key.startsWith('.'));
|
|
291
|
+
if (!isMap)
|
|
292
|
+
return sub === '.' ? pick(exported) : undefined;
|
|
293
|
+
const map = exported;
|
|
294
|
+
if (sub in map)
|
|
295
|
+
return pick(map[sub]);
|
|
296
|
+
// A pattern: "./*": "./dist/*.js".
|
|
297
|
+
for (const key of keys) {
|
|
298
|
+
const star = key.indexOf('*');
|
|
299
|
+
if (star === -1)
|
|
300
|
+
continue;
|
|
301
|
+
const before = key.slice(0, star);
|
|
302
|
+
const after = key.slice(star + 1);
|
|
303
|
+
if (sub.startsWith(before) && sub.endsWith(after)) {
|
|
304
|
+
const middle = sub.slice(before.length, sub.length - after.length);
|
|
305
|
+
const target = JSON.parse(JSON.stringify(map[key]).replaceAll('*', middle));
|
|
306
|
+
return pick(target);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
const candidates = sub === '.'
|
|
312
|
+
? [pkg.main, pkg.main && `${pkg.main}.js`, pkg.main && `${pkg.main}/index.js`, 'index.js']
|
|
313
|
+
: [sub, `${sub}.js`, `${sub}.json`, `${sub}/index.js`];
|
|
314
|
+
return candidates.find((candidate) => !!candidate && exists(join(pkgDir, candidate)));
|
|
315
|
+
}
|
|
316
|
+
const IMPORT_CONDITIONS = ['bun', 'node', 'import', 'module', 'default'];
|
|
317
|
+
const REQUIRE_CONDITIONS = ['bun', 'node', 'require', 'default'];
|
|
318
|
+
/** The file a package specifier names, looked up the way Node walks node_modules from `fromDir`. */
|
|
319
|
+
function resolveInTree(specifier, fromDir, treeRoot, kind, exists) {
|
|
320
|
+
const parts = specifier.split('/');
|
|
321
|
+
const name = specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
322
|
+
const sub = '.' + specifier.slice(name.length);
|
|
323
|
+
for (let dir = fromDir; dir.startsWith(treeRoot); dir = dirname(dir)) {
|
|
324
|
+
const pkgDir = join(dir, 'node_modules', name);
|
|
325
|
+
if (exists(join(pkgDir, 'package.json'))) {
|
|
326
|
+
const entry = entryOf(pkgDir, sub, kind === 'import' ? IMPORT_CONDITIONS : REQUIRE_CONDITIONS, exists);
|
|
327
|
+
return entry && join(pkgDir, entry);
|
|
328
|
+
}
|
|
329
|
+
if (dir === treeRoot)
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
function isBare(specifier) {
|
|
335
|
+
return !specifier.startsWith('.') && !specifier.startsWith('/') && !specifier.includes(':') && !isBuiltin(specifier);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Every package name in a module's imports and requires, rewritten to the
|
|
339
|
+
* relative path of the file it resolves to inside the tree. A name the tree
|
|
340
|
+
* does not have - a branch for another platform, an optional peer - is left
|
|
341
|
+
* as it is. A file the parser cannot read is left whole; its requires reach
|
|
342
|
+
* the hook at runtime.
|
|
343
|
+
*/
|
|
344
|
+
export function rewriteSpecifiers(code, file, treeRoot, parse, exists = existsSync) {
|
|
345
|
+
let ast;
|
|
346
|
+
try {
|
|
347
|
+
ast = parse(code);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return code;
|
|
351
|
+
}
|
|
352
|
+
const edits = [];
|
|
353
|
+
const rewrite = (literal, kind) => {
|
|
354
|
+
const node = literal;
|
|
355
|
+
if (!node || node.type !== 'Literal' || typeof node.value !== 'string' || !isBare(node.value))
|
|
356
|
+
return;
|
|
357
|
+
const resolved = resolveInTree(node.value, dirname(file), treeRoot, kind, exists);
|
|
358
|
+
if (!resolved)
|
|
359
|
+
return;
|
|
360
|
+
let path = relative(dirname(file), resolved).split(sep).join('/');
|
|
361
|
+
if (!path.startsWith('.'))
|
|
362
|
+
path = `./${path}`;
|
|
363
|
+
edits.push({ start: node.start, end: node.end, text: JSON.stringify(path) });
|
|
364
|
+
};
|
|
365
|
+
const visit = (value) => {
|
|
366
|
+
if (Array.isArray(value)) {
|
|
367
|
+
for (const item of value)
|
|
368
|
+
visit(item);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (!value || typeof value !== 'object')
|
|
372
|
+
return;
|
|
373
|
+
const node = value;
|
|
374
|
+
switch (node.type) {
|
|
375
|
+
case 'ImportDeclaration':
|
|
376
|
+
case 'ExportNamedDeclaration':
|
|
377
|
+
case 'ExportAllDeclaration':
|
|
378
|
+
case 'ImportExpression':
|
|
379
|
+
rewrite(node.source, 'import');
|
|
380
|
+
break;
|
|
381
|
+
case 'CallExpression': {
|
|
382
|
+
const callee = node.callee;
|
|
383
|
+
const args = node.arguments;
|
|
384
|
+
const isRequire = (callee.type === 'Identifier' && callee.name === 'require') ||
|
|
385
|
+
(callee.type === 'MemberExpression' && callee.object?.name === 'require' && callee.property?.name === 'resolve');
|
|
386
|
+
if (isRequire && args.length > 0)
|
|
387
|
+
rewrite(args[0], 'require');
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
for (const key in node) {
|
|
392
|
+
if (key !== 'type' && key !== 'start' && key !== 'end')
|
|
393
|
+
visit(node[key]);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
visit(ast);
|
|
397
|
+
if (edits.length === 0)
|
|
398
|
+
return code;
|
|
399
|
+
edits.sort((a, b) => b.start - a.start);
|
|
400
|
+
let out = code;
|
|
401
|
+
for (const edit of edits)
|
|
402
|
+
out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
|
|
403
|
+
return out;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* What the binary runs before the first import of a native package: write the
|
|
407
|
+
* tree if this version has not, then answer each shim's require from it.
|
|
408
|
+
*
|
|
409
|
+
* Extraction is into a temporary directory renamed into place, so a tree that
|
|
410
|
+
* exists is a whole one: two copies starting at once each write their own,
|
|
411
|
+
* one rename wins, and the other discards its copy. A running copy touches its
|
|
412
|
+
* tree hourly, and a start removes the trees of other versions that nothing
|
|
413
|
+
* has touched for a day - never its own, and never one a running copy of
|
|
414
|
+
* another version is still using.
|
|
415
|
+
*/
|
|
416
|
+
export const RUNTIME_SOURCE = `'use strict'
|
|
417
|
+
const { hash, files } = require('./natives.mjs')
|
|
418
|
+
const fs = require('node:fs')
|
|
419
|
+
const path = require('node:path')
|
|
420
|
+
const Module = require('node:module')
|
|
421
|
+
|
|
422
|
+
const HOUR = 60 * 60 * 1000
|
|
423
|
+
const STALE = 24 * HOUR
|
|
424
|
+
|
|
425
|
+
let base
|
|
426
|
+
|
|
427
|
+
function prepare() {
|
|
428
|
+
const root = process.env.RSC_KIT_RUNTIME_DIR ? path.resolve(process.env.RSC_KIT_RUNTIME_DIR) : path.dirname(process.execPath)
|
|
429
|
+
const dir = path.join(root, hash)
|
|
430
|
+
|
|
431
|
+
if (!fs.existsSync(dir)) {
|
|
432
|
+
try {
|
|
433
|
+
fs.mkdirSync(root, { recursive: true })
|
|
434
|
+
const tmp = fs.mkdtempSync(path.join(root, '.' + hash + '-'))
|
|
435
|
+
for (const [rel, src] of files) {
|
|
436
|
+
const to = path.join(tmp, 'node_modules', rel)
|
|
437
|
+
fs.mkdirSync(path.dirname(to), { recursive: true })
|
|
438
|
+
fs.writeFileSync(to, fs.readFileSync(src))
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
fs.renameSync(tmp, dir)
|
|
442
|
+
console.log('[rsc-kit] native packages: wrote ' + files.length + ' files to ' + dir)
|
|
443
|
+
} catch (error) {
|
|
444
|
+
fs.rmSync(tmp, { recursive: true, force: true })
|
|
445
|
+
if (!fs.existsSync(dir)) throw error
|
|
446
|
+
}
|
|
447
|
+
} catch (error) {
|
|
448
|
+
error.message =
|
|
449
|
+
'[rsc-kit] the binary could not write its native packages to ' + root + ': ' + error.message +
|
|
450
|
+
'. Set RSC_KIT_RUNTIME_DIR to a directory it can write to.'
|
|
451
|
+
throw error
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const touch = () => { try { const now = new Date(); fs.utimesSync(dir, now, now) } catch {} }
|
|
456
|
+
touch()
|
|
457
|
+
setInterval(touch, HOUR).unref()
|
|
458
|
+
sweep(root, path.basename(dir))
|
|
459
|
+
|
|
460
|
+
base = path.join(dir, 'node_modules')
|
|
461
|
+
|
|
462
|
+
// A require whose name is computed - require(name) - was not rewritten at
|
|
463
|
+
// build time, and the binary's resolver cannot find it on disk. Answered
|
|
464
|
+
// from the tree, for files inside it only.
|
|
465
|
+
const resolve = Module._resolveFilename
|
|
466
|
+
Module._resolveFilename = function (request, parent, ...rest) {
|
|
467
|
+
try {
|
|
468
|
+
return resolve.call(this, request, parent, ...rest)
|
|
469
|
+
} catch (error) {
|
|
470
|
+
const from = parent && parent.filename
|
|
471
|
+
if (!from || !from.startsWith(dir) || request.startsWith('.') || request.startsWith('/') || Module.isBuiltin(request)) throw error
|
|
472
|
+
const found = lookup(request, path.dirname(from))
|
|
473
|
+
if (!found) throw error
|
|
474
|
+
return found
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function sweep(root, current) {
|
|
480
|
+
let entries
|
|
481
|
+
try { entries = fs.readdirSync(root, { withFileTypes: true }) } catch { return }
|
|
482
|
+
const now = Date.now()
|
|
483
|
+
for (const entry of entries) {
|
|
484
|
+
if (!entry.isDirectory() || entry.name === current) continue
|
|
485
|
+
const isTree = /^[0-9a-f]{16}$/.test(entry.name)
|
|
486
|
+
const isPartial = /^\\.[0-9a-f]{16}-/.test(entry.name)
|
|
487
|
+
if (!isTree && !isPartial) continue
|
|
488
|
+
try {
|
|
489
|
+
const age = now - fs.statSync(path.join(root, entry.name)).mtimeMs
|
|
490
|
+
if (age > (isTree ? STALE : HOUR)) fs.rmSync(path.join(root, entry.name), { recursive: true, force: true })
|
|
491
|
+
} catch {}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function lookup(request, fromDir) {
|
|
496
|
+
const parts = request.split('/')
|
|
497
|
+
const name = request.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]
|
|
498
|
+
const sub = request.slice(name.length)
|
|
499
|
+
for (let dir = fromDir; dir.startsWith(base) || dir === path.dirname(base); dir = path.dirname(dir)) {
|
|
500
|
+
const pkgDir = path.join(dir, 'node_modules', name)
|
|
501
|
+
if (fs.existsSync(path.join(pkgDir, 'package.json'))) {
|
|
502
|
+
if (sub) return path.join(pkgDir, sub)
|
|
503
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8'))
|
|
504
|
+
return path.join(pkgDir, pkg.main || 'index.js')
|
|
505
|
+
}
|
|
506
|
+
if (dir === path.dirname(base)) break
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
module.exports = function load(entry) {
|
|
511
|
+
if (!base) prepare()
|
|
512
|
+
const loaded = require(path.join(base, entry))
|
|
513
|
+
// A package with only an ES module: required, it is a namespace, which an
|
|
514
|
+
// import of the shim would otherwise take as a CommonJS module's exports.
|
|
515
|
+
if (Object.prototype.toString.call(loaded) === '[object Module]') return { __esModule: true, ...loaded }
|
|
516
|
+
return loaded
|
|
517
|
+
}
|
|
518
|
+
`;
|
|
519
|
+
/**
|
|
520
|
+
* Write the compile tree for a server whose build traced external packages,
|
|
521
|
+
* and point compile.mjs at it. Returns undefined, and touches nothing, when
|
|
522
|
+
* there is nothing traced.
|
|
523
|
+
*
|
|
524
|
+
* The tree is a copy of the server with each traced package replaced by a
|
|
525
|
+
* shim, so `bun build --compile` bundles the shim and embeds the package's
|
|
526
|
+
* files: the server's own chunks import packages by name, and Bun resolves a
|
|
527
|
+
* name from the importing file's location, so the copy is what makes those
|
|
528
|
+
* names land on the shims rather than on the real packages beside the server.
|
|
529
|
+
*/
|
|
530
|
+
export function writeCompileTree(options) {
|
|
531
|
+
const { serverDir, projectRoot, target, parse, named = [] } = options;
|
|
532
|
+
const serverNodeModules = join(serverDir, 'node_modules');
|
|
533
|
+
const compileEntry = join(serverDir, 'compile.mjs');
|
|
534
|
+
if (!existsSync(compileEntry) || packageDirs(serverNodeModules).length === 0)
|
|
535
|
+
return undefined;
|
|
536
|
+
const { packages, missing } = nativeSet(serverNodeModules, projectRoot, target, named);
|
|
537
|
+
if (missing.length > 0)
|
|
538
|
+
return { missing };
|
|
539
|
+
if (packages.size === 0)
|
|
540
|
+
return undefined;
|
|
541
|
+
const tree = join(dirname(serverDir), COMPILE_TREE_DIR);
|
|
542
|
+
const embedded = join(tree, '.native', 'node_modules');
|
|
543
|
+
rmSync(tree, { recursive: true, force: true });
|
|
544
|
+
// The server, without the stored pages (the inline module carries them).
|
|
545
|
+
// Its node_modules too, links as they are, so a package that stays bundled
|
|
546
|
+
// is found exactly where it was; the ones going to disk are replaced by
|
|
547
|
+
// shims below.
|
|
548
|
+
cpSync(serverDir, tree, {
|
|
549
|
+
recursive: true,
|
|
550
|
+
verbatimSymlinks: true,
|
|
551
|
+
filter: (path) => {
|
|
552
|
+
const rel = relative(serverDir, path).split(sep).join('/');
|
|
553
|
+
return rel !== 'rsc-static' && !rel.startsWith('rsc-static/');
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
// Each package intact, so a file it reads beside its own code - a .wasm, a
|
|
557
|
+
// data file, its package.json - is where it looks.
|
|
558
|
+
for (const [rel, from] of packages) {
|
|
559
|
+
cpSync(from, join(embedded, rel), {
|
|
560
|
+
recursive: true,
|
|
561
|
+
dereference: true,
|
|
562
|
+
filter: (path) => !relative(from, path).split(sep).includes('node_modules'),
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
const list = files(embedded);
|
|
566
|
+
const hasher = createHash('sha256');
|
|
567
|
+
for (const rel of list) {
|
|
568
|
+
const path = join(embedded, rel);
|
|
569
|
+
if (/\.(c|m)?js$/.test(rel)) {
|
|
570
|
+
const code = readFileSync(path, 'utf-8');
|
|
571
|
+
const rewritten = rewriteSpecifiers(code, path, dirname(embedded), parse);
|
|
572
|
+
if (rewritten !== code)
|
|
573
|
+
writeFileSync(path, rewritten);
|
|
574
|
+
}
|
|
575
|
+
hasher.update(rel);
|
|
576
|
+
hasher.update('\0');
|
|
577
|
+
hasher.update(readFileSync(path));
|
|
578
|
+
}
|
|
579
|
+
const hash = hasher.digest('hex').slice(0, 16);
|
|
580
|
+
const tops = [...packages.keys()].filter((rel) => !rel.includes('/node_modules/'));
|
|
581
|
+
// What the app's chunks import by name. The entry each one loads is looked
|
|
582
|
+
// up now, so the runtime only joins a path - the require entry, since the
|
|
583
|
+
// shim is a require: an import of a CommonJS module's default is the whole
|
|
584
|
+
// module, and of an ES module's namespace is the namespace.
|
|
585
|
+
for (const name of tops) {
|
|
586
|
+
const entry = resolveInTree(name, dirname(embedded), dirname(embedded), 'require', existsSync) ??
|
|
587
|
+
resolveInTree(name, dirname(embedded), dirname(embedded), 'import', existsSync);
|
|
588
|
+
const shimDir = join(tree, 'node_modules', name);
|
|
589
|
+
const runtime = relative(shimDir, join(tree, 'rsc-kit-native.cjs')).split(sep).join('/');
|
|
590
|
+
rmSync(shimDir, { recursive: true, force: true });
|
|
591
|
+
mkdirSync(shimDir, { recursive: true });
|
|
592
|
+
writeFileSync(join(shimDir, 'package.json'), JSON.stringify({ name, main: 'index.cjs' }) + '\n');
|
|
593
|
+
writeFileSync(join(shimDir, 'index.cjs'), entry
|
|
594
|
+
? `module.exports = require(${JSON.stringify(runtime)})(${JSON.stringify(relative(embedded, entry).split(sep).join('/'))})\n`
|
|
595
|
+
: `throw new Error(${JSON.stringify(`[rsc-kit] ${name} has no entry the binary can load`)})\n`);
|
|
596
|
+
}
|
|
597
|
+
writeFileSync(join(tree, 'natives.mjs'), '// Every file of the packages that run from disk, embedded by importing it as a file.\n' +
|
|
598
|
+
list.map((rel, i) => `import f${i} from ${JSON.stringify(`./.native/node_modules/${rel}`)} with { type: "file" };`).join('\n') +
|
|
599
|
+
`\nexport const hash = ${JSON.stringify(hash)};\n` +
|
|
600
|
+
`export const files = [\n${list.map((rel, i) => ` [${JSON.stringify(rel)}, f${i}]`).join(',\n')}\n];\n`);
|
|
601
|
+
writeFileSync(join(tree, 'rsc-kit-native.cjs'), RUNTIME_SOURCE);
|
|
602
|
+
writeFileSync(compileEntry, '// The entry to compile into one binary. This app has native packages, so the\n' +
|
|
603
|
+
`// entry is a copy of the server whose imports of them load them from disk:\n` +
|
|
604
|
+
`// ${tops.join(', ')}.\n` +
|
|
605
|
+
`import "../${COMPILE_TREE_DIR}/compile.mjs";\n`);
|
|
606
|
+
return { hash, files: list.length, packages: tops };
|
|
607
|
+
}
|
|
608
|
+
/** The size of a directory, for the build's one line about it. */
|
|
609
|
+
export function sizeOf(dir) {
|
|
610
|
+
return files(dir).reduce((sum, rel) => sum + statSync(join(dir, rel)).size, 0);
|
|
611
|
+
}
|
|
612
|
+
//# sourceMappingURL=native.js.map
|