arcway 0.1.14 → 0.1.16
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/client/page-loader.js +5 -48
- package/client/route-pattern.js +48 -0
- package/package.json +1 -4
- package/server/boot/index.js +5 -4
- package/server/docs/openapi.js +85 -84
- package/server/logger/index.js +23 -15
- package/server/pages/build-cache.js +118 -0
- package/server/pages/build-client.js +303 -83
- package/server/pages/build-context.js +172 -0
- package/server/pages/build-css.js +85 -9
- package/server/pages/build.js +15 -4
- package/server/pages/handler.js +7 -7
- package/server/pages/pages-router.js +20 -7
- package/server/pages/watcher.js +7 -2
- package/server/router/api-router.js +7 -54
- package/server/router/http-helpers.js +107 -0
- package/server/router/routes.js +4 -42
- package/server/session/index.js +2 -1
- package/server/web-server.js +4 -59
package/client/page-loader.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { compileRoutePattern, extractRouteParams } from './route-pattern.js';
|
|
2
|
+
|
|
1
3
|
const moduleCache = new Map();
|
|
2
4
|
const patternCache = new Map();
|
|
3
5
|
|
|
@@ -34,43 +36,11 @@ function readClientManifest() {
|
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
function compilePattern(pattern) {
|
|
38
|
-
const paramNames = [];
|
|
39
|
-
let catchAllParam;
|
|
40
|
-
|
|
41
|
-
const regexStr = pattern.replace(
|
|
42
|
-
/\/:([^/]+)|\/\*(\w+)\?|\/\*(\w+)/g,
|
|
43
|
-
(match, singleParam, optCatchAll, reqCatchAll) => {
|
|
44
|
-
if (singleParam) {
|
|
45
|
-
paramNames.push(singleParam);
|
|
46
|
-
return '/([^/]+)';
|
|
47
|
-
}
|
|
48
|
-
if (optCatchAll) {
|
|
49
|
-
paramNames.push(optCatchAll);
|
|
50
|
-
catchAllParam = optCatchAll;
|
|
51
|
-
return '(?:/(.+))?';
|
|
52
|
-
}
|
|
53
|
-
if (reqCatchAll) {
|
|
54
|
-
paramNames.push(reqCatchAll);
|
|
55
|
-
catchAllParam = reqCatchAll;
|
|
56
|
-
return '/(.+)';
|
|
57
|
-
}
|
|
58
|
-
return match;
|
|
59
|
-
},
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
regex: new RegExp(`^${regexStr}\\/?$`),
|
|
64
|
-
paramNames,
|
|
65
|
-
catchAllParam,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
39
|
function matchClientRoute(manifest, pathname) {
|
|
70
40
|
for (const route of manifest.routes) {
|
|
71
41
|
let compiled = patternCache.get(route.pattern);
|
|
72
42
|
if (!compiled) {
|
|
73
|
-
compiled =
|
|
43
|
+
compiled = compileRoutePattern(route.pattern);
|
|
74
44
|
patternCache.set(route.pattern, compiled);
|
|
75
45
|
}
|
|
76
46
|
const { regex, paramNames, catchAllParam } = compiled;
|
|
@@ -78,20 +48,8 @@ function matchClientRoute(manifest, pathname) {
|
|
|
78
48
|
const match = regex.exec(pathname);
|
|
79
49
|
if (!match) continue;
|
|
80
50
|
|
|
81
|
-
const params =
|
|
82
|
-
|
|
83
|
-
for (let i = 0; i < paramNames.length; i++) {
|
|
84
|
-
const name = paramNames[i];
|
|
85
|
-
const raw = match[i + 1];
|
|
86
|
-
if (name === catchAll) {
|
|
87
|
-
params[name] = raw ? raw.split('/').map(decodeURIComponent) : [];
|
|
88
|
-
} else {
|
|
89
|
-
params[name] = decodeURIComponent(raw);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
} catch {
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
51
|
+
const params = extractRouteParams(match, paramNames, catchAll);
|
|
52
|
+
if (!params) continue;
|
|
95
53
|
|
|
96
54
|
return { route, params };
|
|
97
55
|
}
|
|
@@ -146,7 +104,6 @@ async function loadPage(manifest, pathname) {
|
|
|
146
104
|
}
|
|
147
105
|
|
|
148
106
|
export {
|
|
149
|
-
compilePattern,
|
|
150
107
|
loadLoadingComponents,
|
|
151
108
|
loadPage,
|
|
152
109
|
matchClientRoute,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function compileRoutePattern(pattern) {
|
|
2
|
+
const paramNames = [];
|
|
3
|
+
let catchAllParam;
|
|
4
|
+
let isOptionalCatchAll = false;
|
|
5
|
+
const regexStr = pattern.replace(
|
|
6
|
+
/\/:([^/]+)|\/\*(\w+)\?|\/\*(\w+)/g,
|
|
7
|
+
(match, singleParam, optCatchAll, reqCatchAll) => {
|
|
8
|
+
if (singleParam) {
|
|
9
|
+
paramNames.push(singleParam);
|
|
10
|
+
return '/([^/]+)';
|
|
11
|
+
}
|
|
12
|
+
if (optCatchAll) {
|
|
13
|
+
paramNames.push(optCatchAll);
|
|
14
|
+
catchAllParam = optCatchAll;
|
|
15
|
+
isOptionalCatchAll = true;
|
|
16
|
+
return '(?:/(.+))?';
|
|
17
|
+
}
|
|
18
|
+
if (reqCatchAll) {
|
|
19
|
+
paramNames.push(reqCatchAll);
|
|
20
|
+
catchAllParam = reqCatchAll;
|
|
21
|
+
return '/(.+)';
|
|
22
|
+
}
|
|
23
|
+
return match;
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
const regex = new RegExp(`^${regexStr}/?$`);
|
|
27
|
+
return { regex, paramNames, catchAllParam, isOptionalCatchAll };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function extractRouteParams(match, paramNames, catchAllParam) {
|
|
31
|
+
const params = {};
|
|
32
|
+
try {
|
|
33
|
+
for (let i = 0; i < paramNames.length; i++) {
|
|
34
|
+
const name = paramNames[i];
|
|
35
|
+
const raw = match[i + 1];
|
|
36
|
+
if (name === catchAllParam) {
|
|
37
|
+
params[name] = raw ? raw.split('/').map(decodeURIComponent) : [];
|
|
38
|
+
} else {
|
|
39
|
+
params[name] = decodeURIComponent(raw);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return params;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { compileRoutePattern, extractRouteParams };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcway",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "A convention-based framework for building modular monoliths with strict domain boundaries.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -96,7 +96,6 @@
|
|
|
96
96
|
"swr": "^2.3.3",
|
|
97
97
|
"tailwind-merge": "^3.4.1",
|
|
98
98
|
"tailwindcss": "^4.1.18",
|
|
99
|
-
"tailwindcss-animate": "^1.0.7",
|
|
100
99
|
"vaul": "^1.1.2",
|
|
101
100
|
"ws": "^8.19.0",
|
|
102
101
|
"zod": "^3.24.0"
|
|
@@ -114,10 +113,8 @@
|
|
|
114
113
|
}
|
|
115
114
|
},
|
|
116
115
|
"devDependencies": {
|
|
117
|
-
"@chromatic-com/storybook": "^5.0.1",
|
|
118
116
|
"@storybook/addon-a11y": "^10.2.8",
|
|
119
117
|
"@storybook/addon-docs": "^10.2.8",
|
|
120
|
-
"@storybook/addon-onboarding": "^10.2.8",
|
|
121
118
|
"@storybook/addon-vitest": "^10.2.8",
|
|
122
119
|
"@storybook/react-vite": "^10.2.8",
|
|
123
120
|
"@tailwindcss/vite": "^4.1.18",
|
package/server/boot/index.js
CHANGED
|
@@ -59,8 +59,8 @@ async function boot(options) {
|
|
|
59
59
|
});
|
|
60
60
|
await jobRunner.init();
|
|
61
61
|
|
|
62
|
-
const fileWatcher = new FileWatcher(rootDir, { log });
|
|
63
|
-
await fileWatcher.start();
|
|
62
|
+
const fileWatcher = mode === 'development' ? new FileWatcher(rootDir, { log }) : null;
|
|
63
|
+
if (fileWatcher) await fileWatcher.start();
|
|
64
64
|
|
|
65
65
|
const appContext = { db, redis, events, queue, cache, files, mail, log, fileWatcher };
|
|
66
66
|
|
|
@@ -68,13 +68,14 @@ async function boot(options) {
|
|
|
68
68
|
|
|
69
69
|
const apiRouter = new ApiRouter(config.api, {
|
|
70
70
|
log,
|
|
71
|
+
mode,
|
|
71
72
|
fileWatcher,
|
|
72
73
|
appContext,
|
|
73
74
|
sessionConfig: config.session,
|
|
74
75
|
});
|
|
75
76
|
await apiRouter.init();
|
|
76
77
|
|
|
77
|
-
const pagesRouter = new PagesRouter(config, { rootDir, log, fileWatcher, appContext });
|
|
78
|
+
const pagesRouter = new PagesRouter(config, { rootDir, log, mode, fileWatcher, appContext });
|
|
78
79
|
await pagesRouter.init();
|
|
79
80
|
|
|
80
81
|
const healthDeps = {
|
|
@@ -117,7 +118,7 @@ async function boot(options) {
|
|
|
117
118
|
await pagesRouter.close();
|
|
118
119
|
await apiRouter.close();
|
|
119
120
|
await webServer.close();
|
|
120
|
-
await fileWatcher.close();
|
|
121
|
+
if (fileWatcher) await fileWatcher.close();
|
|
121
122
|
await destroyInfrastructure(infrastructure);
|
|
122
123
|
await mcpRouter.cleanup(rootDir);
|
|
123
124
|
};
|
package/server/docs/openapi.js
CHANGED
|
@@ -50,6 +50,89 @@ function buildQueryParameters(route) {
|
|
|
50
50
|
return [];
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
function buildOperationId(method, openApiPath) {
|
|
54
|
+
const path = openApiPath.replace(/[{}\/]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
|
|
55
|
+
return `${method}_${path}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function tryConvertSchema(schema) {
|
|
59
|
+
if (!schema) return undefined;
|
|
60
|
+
try {
|
|
61
|
+
return typeToOpenAPISchema(schema);
|
|
62
|
+
} catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveTags(route) {
|
|
68
|
+
const tags = route.config.meta?.tags;
|
|
69
|
+
if (tags && tags.length > 0) return tags;
|
|
70
|
+
return [route.pattern.split('/')[1] || 'default'];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildRequestBody(bodySchema) {
|
|
74
|
+
const converted = tryConvertSchema(bodySchema);
|
|
75
|
+
if (!converted) return undefined;
|
|
76
|
+
return {
|
|
77
|
+
required: true,
|
|
78
|
+
content: { 'application/json': { schema: converted } },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const VALIDATION_ERROR_RESPONSE = {
|
|
83
|
+
description: 'Validation error',
|
|
84
|
+
content: {
|
|
85
|
+
'application/json': {
|
|
86
|
+
schema: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: {
|
|
89
|
+
error: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
code: { type: 'string' },
|
|
93
|
+
message: { type: 'string' },
|
|
94
|
+
details: {},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const DEFAULT_SUCCESS_SCHEMA = { type: 'object', properties: { data: {} } };
|
|
104
|
+
|
|
105
|
+
function buildResponses(routeSchema) {
|
|
106
|
+
const successSchema = tryConvertSchema(routeSchema?.response) ?? DEFAULT_SUCCESS_SCHEMA;
|
|
107
|
+
const responses = {
|
|
108
|
+
200: {
|
|
109
|
+
description: 'Successful response',
|
|
110
|
+
content: { 'application/json': { schema: successSchema } },
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
if (routeSchema?.body || routeSchema?.query) {
|
|
114
|
+
responses[400] = VALIDATION_ERROR_RESPONSE;
|
|
115
|
+
}
|
|
116
|
+
return responses;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildOperation(route, method, openApiPath) {
|
|
120
|
+
const operation = { operationId: buildOperationId(method, openApiPath) };
|
|
121
|
+
const meta = route.config.meta;
|
|
122
|
+
if (meta?.summary) operation.summary = meta.summary;
|
|
123
|
+
if (meta?.description) operation.description = meta.description;
|
|
124
|
+
operation.tags = resolveTags(route);
|
|
125
|
+
|
|
126
|
+
const parameters = [...buildPathParameters(route), ...buildQueryParameters(route)];
|
|
127
|
+
if (parameters.length > 0) operation.parameters = parameters;
|
|
128
|
+
|
|
129
|
+
const requestBody = buildRequestBody(route.config.schema?.body);
|
|
130
|
+
if (requestBody) operation.requestBody = requestBody;
|
|
131
|
+
|
|
132
|
+
operation.responses = buildResponses(route.config.schema);
|
|
133
|
+
return operation;
|
|
134
|
+
}
|
|
135
|
+
|
|
53
136
|
function generateOpenAPISpec(routes, info) {
|
|
54
137
|
const spec = {
|
|
55
138
|
openapi: '3.0.3',
|
|
@@ -63,90 +146,8 @@ function generateOpenAPISpec(routes, info) {
|
|
|
63
146
|
for (const route of routes) {
|
|
64
147
|
const openApiPath = toOpenAPIPath(route.pattern);
|
|
65
148
|
const method = route.method.toLowerCase();
|
|
66
|
-
if (!spec.paths[openApiPath]) {
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
const operation = {
|
|
70
|
-
operationId: `${method}_${openApiPath
|
|
71
|
-
.replace(/[{}\/]/g, '_')
|
|
72
|
-
.replace(/_+/g, '_')
|
|
73
|
-
.replace(/^_|_$/g, '')}`,
|
|
74
|
-
};
|
|
75
|
-
const meta = route.config.meta;
|
|
76
|
-
if (meta?.summary) operation.summary = meta.summary;
|
|
77
|
-
if (meta?.description) operation.description = meta.description;
|
|
78
|
-
if (meta?.tags && meta.tags.length > 0) {
|
|
79
|
-
operation.tags = meta.tags;
|
|
80
|
-
} else {
|
|
81
|
-
const firstSegment = route.pattern.split('/')[1] || 'default';
|
|
82
|
-
operation.tags = [firstSegment];
|
|
83
|
-
}
|
|
84
|
-
const parameters = [...buildPathParameters(route), ...buildQueryParameters(route)];
|
|
85
|
-
if (parameters.length > 0) {
|
|
86
|
-
operation.parameters = parameters;
|
|
87
|
-
}
|
|
88
|
-
if (route.config.schema?.body) {
|
|
89
|
-
try {
|
|
90
|
-
const bodySchema = typeToOpenAPISchema(route.config.schema.body);
|
|
91
|
-
operation.requestBody = {
|
|
92
|
-
required: true,
|
|
93
|
-
content: {
|
|
94
|
-
'application/json': {
|
|
95
|
-
schema: bodySchema,
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
} catch {}
|
|
100
|
-
}
|
|
101
|
-
const responseSchema = route.config.schema?.response
|
|
102
|
-
? (() => {
|
|
103
|
-
try {
|
|
104
|
-
return typeToOpenAPISchema(route.config.schema.response);
|
|
105
|
-
} catch {
|
|
106
|
-
return void 0;
|
|
107
|
-
}
|
|
108
|
-
})()
|
|
109
|
-
: void 0;
|
|
110
|
-
operation.responses = {
|
|
111
|
-
200: {
|
|
112
|
-
description: 'Successful response',
|
|
113
|
-
content: {
|
|
114
|
-
'application/json': {
|
|
115
|
-
schema: responseSchema ?? {
|
|
116
|
-
type: 'object',
|
|
117
|
-
properties: {
|
|
118
|
-
data: {},
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
...(route.config.schema?.body || route.config.schema?.query
|
|
125
|
-
? {
|
|
126
|
-
400: {
|
|
127
|
-
description: 'Validation error',
|
|
128
|
-
content: {
|
|
129
|
-
'application/json': {
|
|
130
|
-
schema: {
|
|
131
|
-
type: 'object',
|
|
132
|
-
properties: {
|
|
133
|
-
error: {
|
|
134
|
-
type: 'object',
|
|
135
|
-
properties: {
|
|
136
|
-
code: { type: 'string' },
|
|
137
|
-
message: { type: 'string' },
|
|
138
|
-
details: {},
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
}
|
|
147
|
-
: {}),
|
|
148
|
-
};
|
|
149
|
-
spec.paths[openApiPath][method] = operation;
|
|
149
|
+
if (!spec.paths[openApiPath]) spec.paths[openApiPath] = {};
|
|
150
|
+
spec.paths[openApiPath][method] = buildOperation(route, method, openApiPath);
|
|
150
151
|
}
|
|
151
152
|
return spec;
|
|
152
153
|
}
|
package/server/logger/index.js
CHANGED
|
@@ -22,6 +22,23 @@ function wrapPino(p) {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
const QUERY_FILTERS = {
|
|
26
|
+
level: (v) => (e) => e.level === v,
|
|
27
|
+
logger: (v) => (e) => e.logger === v,
|
|
28
|
+
requestId: (v) => (e) => e.data?.requestId === v,
|
|
29
|
+
message: (v) => (e) => e.message === v,
|
|
30
|
+
method: (v) => (e) => e.data?.method === v.toUpperCase(),
|
|
31
|
+
path: (v) => (e) => e.data?.path?.includes(v),
|
|
32
|
+
status: (v) => (e) => e.data?.status === v,
|
|
33
|
+
minDurationMs: (v) => (e) => e.data?.durationMs >= v,
|
|
34
|
+
eventName: (v) => (e) =>
|
|
35
|
+
e.data?.eventName === v || e.data?.eventName?.includes(v),
|
|
36
|
+
since: (v) => {
|
|
37
|
+
const sinceTs = new Date(v).getTime();
|
|
38
|
+
return (e) => new Date(e.timestamp).getTime() >= sinceTs;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
25
42
|
class Logger {
|
|
26
43
|
_base;
|
|
27
44
|
_ring;
|
|
@@ -89,22 +106,13 @@ class Logger {
|
|
|
89
106
|
query(filters) {
|
|
90
107
|
if (!this._ring) return [];
|
|
91
108
|
let entries = this._ring.toArray();
|
|
92
|
-
if (filters
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (filters?.path) entries = entries.filter((e) => e.data?.path?.includes(filters.path));
|
|
98
|
-
if (filters?.status) entries = entries.filter((e) => e.data?.status === filters.status);
|
|
99
|
-
if (filters?.minDurationMs) entries = entries.filter((e) => e.data?.durationMs >= filters.minDurationMs);
|
|
100
|
-
if (filters?.eventName) {
|
|
101
|
-
entries = entries.filter((e) => e.data?.eventName === filters.eventName || e.data?.eventName?.includes(filters.eventName));
|
|
102
|
-
}
|
|
103
|
-
if (filters?.since) {
|
|
104
|
-
const sinceTs = new Date(filters.since).getTime();
|
|
105
|
-
entries = entries.filter((e) => new Date(e.timestamp).getTime() >= sinceTs);
|
|
109
|
+
if (!filters) return entries;
|
|
110
|
+
for (const [key, makePredicate] of Object.entries(QUERY_FILTERS)) {
|
|
111
|
+
const value = filters[key];
|
|
112
|
+
if (value === undefined || value === null || value === '' || value === false) continue;
|
|
113
|
+
entries = entries.filter(makePredicate(value));
|
|
106
114
|
}
|
|
107
|
-
if (filters
|
|
115
|
+
if (filters.limit && filters.limit > 0) entries = entries.slice(-filters.limit);
|
|
108
116
|
return entries;
|
|
109
117
|
}
|
|
110
118
|
|
|
@@ -194,10 +194,128 @@ async function buildWithCache({
|
|
|
194
194
|
return { cacheHit: false, metafile: result.metafile };
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
// Multi-file cache for bundles with several output files (client build with
|
|
198
|
+
// code-splitting). Each cache entry lives under <bucket>/<coarseKey>/ and the
|
|
199
|
+
// index is a sidecar <bucket>/<coarseKey>.meta.json.
|
|
200
|
+
async function lookupMultiFileCache({ rootDir, kind, coarseKey }) {
|
|
201
|
+
const bucket = bucketDir(rootDir, kind);
|
|
202
|
+
const metaPath = path.join(bucket, `${coarseKey}.meta.json`);
|
|
203
|
+
const cacheDir = path.join(bucket, coarseKey);
|
|
204
|
+
const meta = await readJson(metaPath);
|
|
205
|
+
if (!meta) return { hit: false, bucket, cacheDir, metaPath };
|
|
206
|
+
const currentHash = await hashInputs(meta.inputs);
|
|
207
|
+
if (currentHash === null || currentHash !== meta.inputsHash) {
|
|
208
|
+
return { hit: false, bucket, cacheDir, metaPath };
|
|
209
|
+
}
|
|
210
|
+
// Sanity-check every recorded output file still exists in the cache dir.
|
|
211
|
+
for (const rel of meta.outputs) {
|
|
212
|
+
try {
|
|
213
|
+
await fs.access(path.join(cacheDir, rel));
|
|
214
|
+
} catch {
|
|
215
|
+
return { hit: false, bucket, cacheDir, metaPath };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { hit: true, bucket, cacheDir, metaPath, meta };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function restoreMultiFileCache({ cacheDir, outputs, destDir }) {
|
|
222
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
223
|
+
await Promise.all(
|
|
224
|
+
outputs.map(async (rel) => {
|
|
225
|
+
const src = path.join(cacheDir, rel);
|
|
226
|
+
const dst = path.join(destDir, rel);
|
|
227
|
+
await fs.mkdir(path.dirname(dst), { recursive: true });
|
|
228
|
+
await fs.cp(src, dst);
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function storeMultiFileCache({
|
|
234
|
+
bucket,
|
|
235
|
+
coarseKey,
|
|
236
|
+
destDir,
|
|
237
|
+
outputs,
|
|
238
|
+
inputs,
|
|
239
|
+
metadata,
|
|
240
|
+
}) {
|
|
241
|
+
const cacheDir = path.join(bucket, coarseKey);
|
|
242
|
+
await fs.rm(cacheDir, { recursive: true, force: true });
|
|
243
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
244
|
+
await Promise.all(
|
|
245
|
+
outputs.map(async (rel) => {
|
|
246
|
+
const src = path.join(destDir, rel);
|
|
247
|
+
const dst = path.join(cacheDir, rel);
|
|
248
|
+
await fs.mkdir(path.dirname(dst), { recursive: true });
|
|
249
|
+
await cpAtomic(src, dst);
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
const inputsHash = await hashInputs(inputs);
|
|
253
|
+
const metaPath = path.join(bucket, `${coarseKey}.meta.json`);
|
|
254
|
+
await writeJsonAtomic(metaPath, {
|
|
255
|
+
inputs,
|
|
256
|
+
inputsHash,
|
|
257
|
+
outputs,
|
|
258
|
+
metadata,
|
|
259
|
+
mtime: Date.now(),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Walk every cache entry in <rootDir>/node_modules/.cache/arcway-pages/<kind>
|
|
264
|
+
// and drop any whose recorded inputs no longer hash to the stored value (or
|
|
265
|
+
// whose inputs are gone). Used by tests that intentionally mutate fixture
|
|
266
|
+
// sources to keep cross-fork cache state consistent. Best-effort: missing
|
|
267
|
+
// bucket → returns {removed: 0, kept: 0}; individual unlink failures are
|
|
268
|
+
// swallowed since a concurrent worker may already be removing the same entry.
|
|
269
|
+
async function pruneStaleCache({ rootDir, kind }) {
|
|
270
|
+
const bucket = bucketDir(rootDir, kind);
|
|
271
|
+
let entries;
|
|
272
|
+
try {
|
|
273
|
+
entries = await fs.readdir(bucket);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
if (err.code === 'ENOENT') return { removed: 0, kept: 0 };
|
|
276
|
+
throw err;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const metaFiles = entries.filter((f) => f.endsWith('.meta.json'));
|
|
280
|
+
let removed = 0;
|
|
281
|
+
let kept = 0;
|
|
282
|
+
|
|
283
|
+
for (const metaFile of metaFiles) {
|
|
284
|
+
const metaPath = path.join(bucket, metaFile);
|
|
285
|
+
const meta = await readJson(metaPath);
|
|
286
|
+
const stale =
|
|
287
|
+
!meta ||
|
|
288
|
+
!Array.isArray(meta.inputs) ||
|
|
289
|
+
(await hashInputs(meta.inputs)) !== meta.inputsHash;
|
|
290
|
+
|
|
291
|
+
if (!stale) {
|
|
292
|
+
kept += 1;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const baseKey = metaFile.slice(0, -'.meta.json'.length);
|
|
297
|
+
await Promise.allSettled([
|
|
298
|
+
fs.rm(metaPath, { force: true }),
|
|
299
|
+
fs.rm(path.join(bucket, `${baseKey}.js`), { force: true }),
|
|
300
|
+
fs.rm(path.join(bucket, `${baseKey}.js.map`), { force: true }),
|
|
301
|
+
fs.rm(path.join(bucket, baseKey), { recursive: true, force: true }),
|
|
302
|
+
]);
|
|
303
|
+
removed += 1;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return { removed, kept };
|
|
307
|
+
}
|
|
308
|
+
|
|
197
309
|
export {
|
|
198
310
|
buildWithCache,
|
|
199
311
|
computeConfigHash,
|
|
200
312
|
cacheRoot,
|
|
201
313
|
entryKey,
|
|
202
314
|
ESBUILD_VERSION,
|
|
315
|
+
sha256,
|
|
316
|
+
hashInputs,
|
|
317
|
+
lookupMultiFileCache,
|
|
318
|
+
pruneStaleCache,
|
|
319
|
+
restoreMultiFileCache,
|
|
320
|
+
storeMultiFileCache,
|
|
203
321
|
};
|