@thomas.f/prnext 0.1.12-alpha → 0.1.13-alpha
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 +262 -33
- package/build/actions.mjs +4 -0
- package/build/app-config.mjs +2 -0
- package/build/app.mjs +20 -5
- package/build/bundle-runtime.mjs +82 -0
- package/build/compiler.mjs +3 -1
- package/build/config.mjs +9 -2
- package/build/font-cache.mjs +50 -0
- package/build/font-files.mjs +5 -3
- package/build/fonts.mjs +1 -1
- package/build/index.mjs +5 -0
- package/build/metadata-conflicts.mjs +13 -0
- package/build/metadata-images.mjs +3 -3
- package/build/metadata-routes.mjs +3 -3
- package/build/native-packages.mjs +5 -1
- package/build/npm-packages.mjs +2 -1
- package/build/optimize-imports.mjs +112 -0
- package/build/proxy-body-config.mjs +10 -0
- package/build/scan.mjs +9 -7
- package/cli.mjs +16 -0
- package/compat/after.cjs +51 -0
- package/compat/index.d.ts +3 -1
- package/compat/server.cjs +2 -1
- package/compat/server.d.cts +2 -0
- package/native/launch.mjs +19 -0
- package/package.json +7 -7
- package/runtime/api.mjs +15 -5
- package/runtime/app-render.mjs +78 -33
- package/runtime/app-route-plan.mjs +45 -0
- package/runtime/app-routing-state.mjs +101 -21
- package/runtime/app-routing.mjs +37 -18
- package/runtime/app-segments.mjs +25 -11
- package/runtime/app-static.mjs +1 -1
- package/runtime/app-stream-html.mjs +2 -1
- package/runtime/cache-request.mjs +10 -2
- package/runtime/cloud/archive.mjs +316 -0
- package/runtime/cloud/client.mjs +658 -0
- package/runtime/cloud/environment.mjs +60 -0
- package/runtime/cloud/state.mjs +141 -0
- package/runtime/cloud/upload.mjs +48 -0
- package/runtime/html-envelope.mjs +70 -7
- package/runtime/http.mjs +7 -4
- package/runtime/middleware.mjs +3 -3
- package/runtime/request-transport.mjs +1 -1
- package/runtime/route-static.mjs +1 -1
- package/runtime/rsc-worker.mjs +27 -4
- package/runtime/stream-utils.mjs +22 -3
- package/runtime/transport.mjs +16 -0
- package/runtime/worker-metadata.mjs +23 -0
- package/runtime/worker.mjs +36 -9
package/runtime/app-segments.mjs
CHANGED
|
@@ -1,22 +1,36 @@
|
|
|
1
|
+
// Weak ownership follows the manifest. Mutable development segment names are
|
|
2
|
+
// re-read, and no request parameter or selected segment value is retained.
|
|
3
|
+
const syntax = new WeakMap();
|
|
4
|
+
function segmentSyntax(segment) {
|
|
5
|
+
const value = segment.segment || '';
|
|
6
|
+
const existing = syntax.get(segment);
|
|
7
|
+
if (existing?.value === value) return existing;
|
|
8
|
+
const parsed = { value, name: /^\[\[?(?:\.\.\.)?([^\]]+)\]\]?$/.exec(value)?.[1] };
|
|
9
|
+
if ((typeof segment === 'object' && segment !== null) || typeof segment === 'function') syntax.set(segment, parsed);
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
|
|
1
13
|
/** Each layout sees only dynamic parameters declared at or above its segment. */
|
|
2
14
|
export function paramsBySegment(segments, params = {}) {
|
|
3
15
|
const result = new Map();
|
|
4
16
|
const ancestors = Object.create(null);
|
|
5
17
|
for (const segment of segments) {
|
|
6
|
-
const
|
|
7
|
-
if (
|
|
18
|
+
const { name } = segmentSyntax(segment);
|
|
19
|
+
if (name && Object.hasOwn(params, name)) ancestors[name] = params[name];
|
|
8
20
|
result.set(segment, { ...ancestors });
|
|
9
21
|
}
|
|
10
22
|
return result;
|
|
11
23
|
}
|
|
12
24
|
|
|
13
|
-
export function selectedLayoutSegments(segments, params = {}) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (!
|
|
17
|
-
const
|
|
18
|
-
if (!
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
export function selectedLayoutSegments(segments, params = {}, start = 0) {
|
|
26
|
+
const selected = [];
|
|
27
|
+
for (let index = start, length = segments.length; index < length; index++) {
|
|
28
|
+
if (!(index in segments)) continue;
|
|
29
|
+
const { value, name } = segmentSyntax(segments[index]);
|
|
30
|
+
if (!value || value.startsWith('@')) continue;
|
|
31
|
+
if (!name) { selected.push(value); continue; }
|
|
32
|
+
const parameter = params[name];
|
|
33
|
+
if (parameter !== undefined) selected.push(Array.isArray(parameter) ? parameter.join('/') : parameter);
|
|
34
|
+
}
|
|
35
|
+
return selected;
|
|
22
36
|
}
|
package/runtime/app-static.mjs
CHANGED
|
@@ -61,7 +61,7 @@ export async function prerenderAppRoute(options) {
|
|
|
61
61
|
}
|
|
62
62
|
html = completeAppHtml(shell.shell, result.body, { ...options.route, css: [], fonts: [] });
|
|
63
63
|
} else {
|
|
64
|
-
html = completeAppHtml(await renderHtml(model.tree, model.router, { staticGeneration: true, strictMode: options.manifest?.config?.reactStrictMode !== false }), result.body, options.route);
|
|
64
|
+
html = completeAppHtml(await renderHtml(model.tree, model.router, { timeoutMs: options.route?.cacheConfig?.maxDuration ? options.route.cacheConfig.maxDuration * 1000 : undefined, staticGeneration: true, strictMode: options.manifest?.config?.reactStrictMode !== false }), result.body, options.route);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
if (staticMetadata(context)?.externalCache) return { dynamic: true, reason: 'External cache handlers require validation on every request' };
|
|
@@ -62,7 +62,8 @@ function flightTap(source) {
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
export async function renderProgressiveAppHtml({ result, request, route, responseHeaders, decodeFlight, signal, timeoutMs
|
|
65
|
+
export async function renderProgressiveAppHtml({ result, request, route, responseHeaders, decodeFlight, signal, timeoutMs, partial }) {
|
|
66
|
+
timeoutMs ??= route?.cacheConfig?.maxDuration ? route.cacheConfig.maxDuration * 1000 : 25_000;
|
|
66
67
|
const nonceAttribute = request.nonce ? ` nonce="${escapeHtml(request.nonce)}"` : '';
|
|
67
68
|
const tap = flightTap(result.body);
|
|
68
69
|
const abort = new AbortController();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runRequestContext, currentRequest } from '../compat/headers.cjs';
|
|
2
2
|
import { flushCacheInvalidations, flushCacheWork } from '../compat/data-cache.cjs';
|
|
3
|
+
import { createAfterScope } from '../compat/after.cjs';
|
|
3
4
|
|
|
4
5
|
// Keep background fills alive until the transport has sent the response. In
|
|
5
6
|
// particular, serving stale data must not wait for its replacement to finish.
|
|
@@ -8,15 +9,22 @@ export function withCacheRequest(options, phase, callback) {
|
|
|
8
9
|
// only needs metadata, and may survive the response for stale revalidation.
|
|
9
10
|
return runRequestContext({ ...options, body: undefined, routePattern: options.route?.pattern, cacheConfig: options.route?.cacheConfig, phase }, async () => {
|
|
10
11
|
const context = currentRequest();
|
|
12
|
+
if (phase === 'route' && options.route?.cacheConfig?.runtime !== 'edge') context.afterScope = createAfterScope({ signal: options.signal, timeoutMs: options.timeoutMs ?? (options.route?.cacheConfig?.maxDuration ? options.route.cacheConfig.maxDuration * 1000 : 25000) });
|
|
13
|
+
let finalized;
|
|
14
|
+
const finalizeCache = () => finalized ||= (async () => { await context.afterScope?.finish(); await flushCacheWork(context); })();
|
|
15
|
+
// The worker owns response completion even when application code throws.
|
|
16
|
+
// Register before invoking it so error responses do not await after().
|
|
17
|
+
options.deferFinalizer?.(finalizeCache);
|
|
11
18
|
try {
|
|
12
19
|
const response = await callback(context);
|
|
13
20
|
await flushCacheInvalidations(context);
|
|
14
21
|
context.cacheState.closed = true;
|
|
15
22
|
return { ...response,
|
|
16
23
|
...(context.draftMode || context.draftChanged ? { headers: { ...response.headers, 'cache-control': 'private, no-cache, no-store, max-age=0' } } : {}),
|
|
17
|
-
finalizeCache
|
|
24
|
+
finalizeCache };
|
|
18
25
|
} catch (error) {
|
|
19
|
-
|
|
26
|
+
context.cacheState.closed = true;
|
|
27
|
+
if (!options.deferFinalizer) await finalizeCache();
|
|
20
28
|
throw error;
|
|
21
29
|
}
|
|
22
30
|
});
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { lstat, readdir, readFile, realpath, open } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { deflateRawSync } from 'node:zlib';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import picomatch from 'picomatch';
|
|
6
|
+
|
|
7
|
+
const MiB = 1048576;
|
|
8
|
+
const alwaysExcluded = (name) =>
|
|
9
|
+
/^(?:node_modules|\.git|\.hg|\.svn|\.next|\.local|\.npm|\.ssh|\.aws|\.azure|\.vercel|coverage|\.DS_Store|__MACOSX)$/i.test(
|
|
10
|
+
name
|
|
11
|
+
) ||
|
|
12
|
+
/^\.prnext(?:[.-]|$)/i.test(name) ||
|
|
13
|
+
/^\.env(?:\.|$)/i.test(name) ||
|
|
14
|
+
/^(?:\.npmrc|\.yarnrc\.yml|\.netrc|id_rsa|id_ed25519)$/i.test(name) ||
|
|
15
|
+
/\.(?:pem|key|p12|pfx|log|tsbuildinfo)$/i.test(name);
|
|
16
|
+
const crcTable = Array.from({ length: 256 }, (_, n) => {
|
|
17
|
+
for (let i = 0; i < 8; i++) n = n & 1 ? 0xedb88320 ^ (n >>> 1) : n >>> 1;
|
|
18
|
+
return n >>> 0;
|
|
19
|
+
});
|
|
20
|
+
const crc32 = (bytes) => {
|
|
21
|
+
let crc = 0xffffffff;
|
|
22
|
+
for (const byte of bytes) crc = crcTable[(crc ^ byte) & 255] ^ (crc >>> 8);
|
|
23
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
24
|
+
};
|
|
25
|
+
async function ignoreRules(root) {
|
|
26
|
+
const rules = [];
|
|
27
|
+
for (const name of ['.gitignore', '.prnextignore']) {
|
|
28
|
+
const filename = path.join(root, name);
|
|
29
|
+
let source;
|
|
30
|
+
try {
|
|
31
|
+
const stat = await lstat(filename);
|
|
32
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 65536)
|
|
33
|
+
throw new Error(`${name} must be a regular file of at most 64 KiB.`);
|
|
34
|
+
source = await readFile(filename, 'utf8');
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error.code === 'ENOENT') continue;
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
for (let pattern of source.split(/\r?\n/)) {
|
|
40
|
+
pattern = pattern.trim();
|
|
41
|
+
if (!pattern || pattern.startsWith('#')) continue;
|
|
42
|
+
const include = pattern.startsWith('!');
|
|
43
|
+
if (include) pattern = pattern.slice(1);
|
|
44
|
+
const anchored = pattern.startsWith('/');
|
|
45
|
+
pattern = pattern.replace(/^\//, '').replace(/\/$/, '');
|
|
46
|
+
if (!pattern || pattern.length > 512)
|
|
47
|
+
throw new Error(`Invalid pattern in ${name}.`);
|
|
48
|
+
const match = picomatch(
|
|
49
|
+
!anchored && !pattern.includes('/') ? `**/${pattern}` : pattern,
|
|
50
|
+
{ dot: true }
|
|
51
|
+
);
|
|
52
|
+
rules.push({
|
|
53
|
+
include,
|
|
54
|
+
match,
|
|
55
|
+
prefix:
|
|
56
|
+
!anchored && !pattern.includes('/')
|
|
57
|
+
? ''
|
|
58
|
+
: picomatch.scan(pattern).base
|
|
59
|
+
});
|
|
60
|
+
if (rules.length > 2000)
|
|
61
|
+
throw new Error('Too many deployment ignore patterns.');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const ignored = (name) => {
|
|
65
|
+
let ignored = false;
|
|
66
|
+
const parts = name.split('/');
|
|
67
|
+
const prefixes = parts.map((_, i) => parts.slice(0, i + 1).join('/'));
|
|
68
|
+
for (const rule of rules)
|
|
69
|
+
if (prefixes.some((prefix) => rule.match(prefix)))
|
|
70
|
+
ignored = !rule.include;
|
|
71
|
+
return ignored;
|
|
72
|
+
};
|
|
73
|
+
ignored.canReinclude = (directory) =>
|
|
74
|
+
rules.some(
|
|
75
|
+
(rule) =>
|
|
76
|
+
rule.include &&
|
|
77
|
+
(!rule.prefix ||
|
|
78
|
+
rule.prefix === directory ||
|
|
79
|
+
rule.prefix.startsWith(directory + '/') ||
|
|
80
|
+
directory.startsWith(rule.prefix + '/'))
|
|
81
|
+
);
|
|
82
|
+
return ignored;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Build a deterministic, bounded ZIP without following links or running code. */
|
|
86
|
+
export async function archiveProject(
|
|
87
|
+
directory,
|
|
88
|
+
{
|
|
89
|
+
maxArchiveBytes = 7 * MiB,
|
|
90
|
+
maxSourceBytes = 100 * MiB,
|
|
91
|
+
maxFileBytes = 15 * MiB,
|
|
92
|
+
maxFiles = 3000,
|
|
93
|
+
signal,
|
|
94
|
+
outputFile
|
|
95
|
+
} = {}
|
|
96
|
+
) {
|
|
97
|
+
const root = await realpath(directory);
|
|
98
|
+
if (outputFile) {
|
|
99
|
+
const relative = path.relative(root,path.resolve(outputFile));
|
|
100
|
+
if (!relative || relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)) throw new Error('Temporary archive must be outside the project.');
|
|
101
|
+
}
|
|
102
|
+
const output = outputFile ? await open(outputFile, 'wx', 0o600) : null;
|
|
103
|
+
const hash = createHash('sha256');
|
|
104
|
+
const chunks = [];
|
|
105
|
+
const append = async (...parts) => {
|
|
106
|
+
for (const part of parts) { hash.update(part); if (output) await output.writeFile(part); else chunks.push(part); }
|
|
107
|
+
};
|
|
108
|
+
try {
|
|
109
|
+
const ignored = await ignoreRules(root);
|
|
110
|
+
const files = [],
|
|
111
|
+
central = [],
|
|
112
|
+
names = new Set();
|
|
113
|
+
let expanded = 0,
|
|
114
|
+
offset = 0,
|
|
115
|
+
centralSize = 0,
|
|
116
|
+
visited = 0,
|
|
117
|
+
excluded = 0,
|
|
118
|
+
packageJson;
|
|
119
|
+
async function walk(relative = '') {
|
|
120
|
+
for (const entry of (
|
|
121
|
+
await readdir(path.join(root, relative), { withFileTypes: true })
|
|
122
|
+
).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
|
|
123
|
+
signal?.throwIfAborted();
|
|
124
|
+
if (++visited > 20000)
|
|
125
|
+
throw new Error(
|
|
126
|
+
'Too many source entries. Deploy a smaller project folder.'
|
|
127
|
+
);
|
|
128
|
+
const name = relative ? `${relative}/${entry.name}` : entry.name;
|
|
129
|
+
if (alwaysExcluded(entry.name)) {
|
|
130
|
+
excluded++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (
|
|
134
|
+
/[\x00-\x1f\x7f\\:]/.test(name) ||
|
|
135
|
+
name
|
|
136
|
+
.split('/')
|
|
137
|
+
.some(
|
|
138
|
+
(p) =>
|
|
139
|
+
p === '..' ||
|
|
140
|
+
/[. ]$/.test(p) ||
|
|
141
|
+
/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(p)
|
|
142
|
+
) ||
|
|
143
|
+
Buffer.byteLength(name) > 1024
|
|
144
|
+
)
|
|
145
|
+
throw new Error(
|
|
146
|
+
'A source filename is not portable. Rename files with control characters, backslashes or trailing dots/spaces.'
|
|
147
|
+
);
|
|
148
|
+
if (entry.isSymbolicLink()) {
|
|
149
|
+
if (ignored(name)) {
|
|
150
|
+
excluded++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Symbolic links are not uploaded: ${name}. Exclude it in .prnextignore.`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (entry.isDirectory()) {
|
|
158
|
+
if (ignored(name) && !ignored.canReinclude(name)) {
|
|
159
|
+
excluded++;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
await walk(name);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (ignored(name)) {
|
|
166
|
+
excluded++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!entry.isFile())
|
|
170
|
+
throw new Error(`Only regular source files can be uploaded: ${name}`);
|
|
171
|
+
if (files.length >= maxFiles)
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Source exceeds ${maxFiles} files. Add exclusions to .prnextignore.`
|
|
174
|
+
);
|
|
175
|
+
const folded = name.toLowerCase();
|
|
176
|
+
if (names.has(folded))
|
|
177
|
+
throw new Error(`Case-conflicting source filename: ${name}`);
|
|
178
|
+
names.add(folded);
|
|
179
|
+
const filename = path.join(root, name);
|
|
180
|
+
const before = await lstat(filename);
|
|
181
|
+
if (
|
|
182
|
+
!before.isFile() ||
|
|
183
|
+
before.isSymbolicLink() ||
|
|
184
|
+
before.size > maxFileBytes
|
|
185
|
+
)
|
|
186
|
+
throw new Error(
|
|
187
|
+
`Source file is not regular or exceeds ${maxFileBytes / MiB} MiB: ${name}`
|
|
188
|
+
);
|
|
189
|
+
if (expanded + before.size > maxSourceBytes)
|
|
190
|
+
throw new Error(
|
|
191
|
+
`Expanded sources exceed ${maxSourceBytes / MiB} MiB. Add exclusions to .prnextignore.`
|
|
192
|
+
);
|
|
193
|
+
// Recheck identity after opening and after reading to catch concurrent edits.
|
|
194
|
+
const handle = await open(filename, 'r');
|
|
195
|
+
let contents;
|
|
196
|
+
try {
|
|
197
|
+
const opened = await handle.stat();
|
|
198
|
+
const current = await lstat(filename);
|
|
199
|
+
if (
|
|
200
|
+
current.isSymbolicLink() ||
|
|
201
|
+
!opened.isFile() ||
|
|
202
|
+
opened.dev !== before.dev ||
|
|
203
|
+
opened.ino !== before.ino ||
|
|
204
|
+
opened.size !== before.size
|
|
205
|
+
)
|
|
206
|
+
throw new Error(
|
|
207
|
+
`Source changed while packaging: ${name}. Try again.`
|
|
208
|
+
);
|
|
209
|
+
contents = Buffer.alloc(opened.size);
|
|
210
|
+
let read = 0;
|
|
211
|
+
while (read < contents.length) {
|
|
212
|
+
const next = await handle.read(
|
|
213
|
+
contents,
|
|
214
|
+
read,
|
|
215
|
+
contents.length - read,
|
|
216
|
+
read
|
|
217
|
+
);
|
|
218
|
+
if (!next.bytesRead)
|
|
219
|
+
throw new Error(
|
|
220
|
+
`Source changed while packaging: ${name}. Try again.`
|
|
221
|
+
);
|
|
222
|
+
read += next.bytesRead;
|
|
223
|
+
}
|
|
224
|
+
const after = await handle.stat();
|
|
225
|
+
if (after.size !== opened.size || after.mtimeMs !== opened.mtimeMs)
|
|
226
|
+
throw new Error(
|
|
227
|
+
`Source changed while packaging: ${name}. Try again.`
|
|
228
|
+
);
|
|
229
|
+
} finally {
|
|
230
|
+
await handle.close();
|
|
231
|
+
}
|
|
232
|
+
if (name === 'package.json') {
|
|
233
|
+
try {
|
|
234
|
+
packageJson = JSON.parse(contents.toString('utf8'));
|
|
235
|
+
} catch {
|
|
236
|
+
throw new Error('package.json is not valid JSON.');
|
|
237
|
+
}
|
|
238
|
+
if (
|
|
239
|
+
!packageJson ||
|
|
240
|
+
typeof packageJson.scripts?.build !== 'string' ||
|
|
241
|
+
!packageJson.scripts.build.trim()
|
|
242
|
+
)
|
|
243
|
+
throw new Error(
|
|
244
|
+
'Add a production build script to package.json before deploying.'
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
let compressed = deflateRawSync(contents, { level: 6 });
|
|
248
|
+
// The server rejects expansion ratios over 150. Store very repetitive files.
|
|
249
|
+
const method =
|
|
250
|
+
compressed.length < contents.length &&
|
|
251
|
+
contents.length / Math.max(1, compressed.length) <= 140
|
|
252
|
+
? 8
|
|
253
|
+
: 0;
|
|
254
|
+
if (!method) compressed = contents;
|
|
255
|
+
const encoded = Buffer.from(name),
|
|
256
|
+
crc = crc32(contents);
|
|
257
|
+
const header = Buffer.alloc(30),
|
|
258
|
+
record = Buffer.alloc(46);
|
|
259
|
+
header.writeUInt32LE(0x04034b50);
|
|
260
|
+
header.writeUInt16LE(20, 4);
|
|
261
|
+
header.writeUInt16LE(0x800, 6);
|
|
262
|
+
header.writeUInt16LE(method, 8);
|
|
263
|
+
header.writeUInt16LE(33, 12);
|
|
264
|
+
header.writeUInt32LE(crc, 14);
|
|
265
|
+
header.writeUInt32LE(compressed.length, 18);
|
|
266
|
+
header.writeUInt32LE(contents.length, 22);
|
|
267
|
+
header.writeUInt16LE(encoded.length, 26);
|
|
268
|
+
record.writeUInt32LE(0x02014b50);
|
|
269
|
+
record.writeUInt16LE(0x0314, 4);
|
|
270
|
+
record.writeUInt16LE(20, 6);
|
|
271
|
+
record.writeUInt16LE(0x800, 8);
|
|
272
|
+
record.writeUInt16LE(method, 10);
|
|
273
|
+
record.writeUInt16LE(33, 14);
|
|
274
|
+
record.writeUInt32LE(crc, 16);
|
|
275
|
+
record.writeUInt32LE(compressed.length, 20);
|
|
276
|
+
record.writeUInt32LE(contents.length, 24);
|
|
277
|
+
record.writeUInt16LE(encoded.length, 28);
|
|
278
|
+
record.writeUInt32LE(0x81a40000, 38);
|
|
279
|
+
record.writeUInt32LE(offset, 42);
|
|
280
|
+
offset += header.length + encoded.length + compressed.length;
|
|
281
|
+
centralSize += record.length + encoded.length;
|
|
282
|
+
if (offset + centralSize + 22 > maxArchiveBytes)
|
|
283
|
+
throw new Error(
|
|
284
|
+
`Source archive exceeds ${(maxArchiveBytes / MiB).toFixed(0)} MiB. Exclude unnecessary files in .prnextignore.`
|
|
285
|
+
);
|
|
286
|
+
await append(header, encoded, compressed);
|
|
287
|
+
central.push(record, encoded);
|
|
288
|
+
files.push(name);
|
|
289
|
+
expanded += contents.length;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
await walk();
|
|
293
|
+
if (!packageJson)
|
|
294
|
+
throw new Error(
|
|
295
|
+
'The selected directory needs a package.json with a build script; do not exclude it.'
|
|
296
|
+
);
|
|
297
|
+
const end = Buffer.alloc(22);
|
|
298
|
+
end.writeUInt32LE(0x06054b50);
|
|
299
|
+
end.writeUInt16LE(files.length, 8);
|
|
300
|
+
end.writeUInt16LE(files.length, 10);
|
|
301
|
+
end.writeUInt32LE(centralSize, 12);
|
|
302
|
+
end.writeUInt32LE(offset, 16);
|
|
303
|
+
await append(...central, end);
|
|
304
|
+
const buffer = output ? undefined : Buffer.concat(chunks);
|
|
305
|
+
return {
|
|
306
|
+
buffer,
|
|
307
|
+
path: outputFile,
|
|
308
|
+
size: offset + centralSize + 22,
|
|
309
|
+
files,
|
|
310
|
+
expandedBytes: expanded,
|
|
311
|
+
excluded,
|
|
312
|
+
digest: hash.digest('hex'),
|
|
313
|
+
packageJson
|
|
314
|
+
};
|
|
315
|
+
} finally { await output?.close(); }
|
|
316
|
+
}
|