@webstir-io/webstir 0.1.1 → 0.1.3
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 +13 -0
- package/assets/deployment/docker/.dockerignore +7 -0
- package/assets/deployment/docker/Dockerfile +17 -0
- package/assets/deployment/docker/README.md +44 -0
- package/assets/deployment/docker/example.env +3 -0
- package/assets/features/client_nav/client_nav.ts +369 -264
- package/assets/features/client_nav/document_navigation.ts +344 -0
- package/assets/features/client_nav/form_enhancement.ts +275 -0
- package/assets/templates/api/src/backend/index.ts +71 -10
- package/assets/templates/api/src/backend/tsconfig.json +6 -1
- package/assets/templates/full/src/backend/index.ts +71 -10
- package/assets/templates/full/src/backend/module.ts +515 -0
- package/assets/templates/full/src/backend/tests/progressive-enhancement.test.ts +180 -0
- package/assets/templates/full/src/backend/tsconfig.json +6 -1
- package/assets/templates/full/src/frontend/app/scripts/features/client-nav.ts +574 -0
- package/assets/templates/full/src/frontend/app/scripts/features/document-navigation.ts +344 -0
- package/assets/templates/full/src/frontend/app/scripts/features/form-enhancement.ts +275 -0
- package/assets/templates/full/src/frontend/pages/home/index.css +8 -0
- package/assets/templates/full/src/frontend/pages/home/index.html +6 -1
- package/assets/templates/full/src/frontend/pages/home/tests/home.test.ts +12 -2
- package/assets/templates/spa/src/frontend/pages/home/tests/home.test.ts +10 -2
- package/package.json +31 -13
- package/scripts/check-feature-projections.mjs +87 -0
- package/scripts/check-full-demo-sync.mjs +89 -0
- package/scripts/check-package-install.mjs +537 -0
- package/scripts/check-standalone-install.mjs +221 -0
- package/scripts/pack-standalone.mjs +52 -28
- package/scripts/publish.sh +9 -0
- package/scripts/run-tests.mjs +103 -0
- package/scripts/sync-assets.mjs +175 -17
- package/src/add-backend-compat.ts +628 -0
- package/src/add-backend.ts +155 -27
- package/src/add.ts +111 -4
- package/src/agent.ts +393 -0
- package/src/api-watch.ts +7 -4
- package/src/backend-inspect.ts +70 -2
- package/src/backend-runtime.ts +22 -14
- package/src/build.ts +1 -3
- package/src/bun-generated-frontend-watch.ts +209 -0
- package/src/bun-globals.d.ts +23 -0
- package/src/bun-spa-document.ts +310 -0
- package/src/bun-spa-routes.ts +159 -0
- package/src/bun-spa-watch.ts +29 -0
- package/src/bun-ssg-watch.ts +304 -0
- package/src/cli.ts +381 -50
- package/src/compile-tests.ts +37 -29
- package/src/dev-server.ts +215 -144
- package/src/doctor.ts +164 -0
- package/src/enable-assets.ts +18 -1
- package/src/enable.ts +133 -41
- package/src/execute.ts +30 -4
- package/src/external-workspace.ts +178 -0
- package/src/format.ts +296 -17
- package/src/frontend-inspect.ts +32 -0
- package/src/frontend-watch.ts +27 -102
- package/src/full-watch.ts +13 -18
- package/src/index.ts +7 -0
- package/src/init-assets.ts +41 -11
- package/src/init.ts +85 -71
- package/src/inspect.ts +112 -0
- package/src/mcp/run-cli-json.ts +46 -0
- package/src/mcp/server.ts +307 -0
- package/src/operations.ts +176 -0
- package/src/providers.ts +20 -18
- package/src/refresh.ts +29 -3
- package/src/repair.ts +110 -43
- package/src/runtime-filter.ts +41 -0
- package/src/runtime.ts +1 -1
- package/src/smoke.ts +48 -16
- package/src/test.ts +54 -16
- package/src/testing-runtime.ts +273 -0
- package/src/types.ts +1 -4
- package/src/watch-events.ts +46 -17
- package/src/watch.ts +25 -14
- package/src/workspace-lock.ts +207 -0
- package/src/workspace-watcher.ts +10 -6
- package/src/workspace.ts +4 -2
- package/src/watch-daemon-client.ts +0 -171
package/src/compile-tests.ts
CHANGED
|
@@ -6,41 +6,46 @@ import ts from 'typescript';
|
|
|
6
6
|
import type { TestModule } from '@webstir-io/webstir-testing';
|
|
7
7
|
|
|
8
8
|
const TESTING_PACKAGE_SPECIFIER = '@webstir-io/webstir-testing';
|
|
9
|
-
const TESTING_RUNTIME_SPECIFIER = import.meta.resolve(
|
|
9
|
+
const TESTING_RUNTIME_SPECIFIER = import.meta.resolve('./testing-runtime.ts');
|
|
10
10
|
|
|
11
|
-
export async function compileTestModules(
|
|
11
|
+
export async function compileTestModules(
|
|
12
|
+
workspaceRoot: string,
|
|
13
|
+
modules: readonly TestModule[],
|
|
14
|
+
): Promise<void> {
|
|
12
15
|
const shimPath = await ensureTestingRuntimeShim(workspaceRoot);
|
|
13
16
|
|
|
14
|
-
await Promise.all(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
await Promise.all(
|
|
18
|
+
modules.map(async (module) => {
|
|
19
|
+
if (!module.compiledPath) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
await mkdir(path.dirname(module.compiledPath), { recursive: true });
|
|
24
|
+
|
|
25
|
+
if (module.sourcePath.endsWith('.js')) {
|
|
26
|
+
const source = await readFile(module.sourcePath, 'utf8');
|
|
27
|
+
const rewritten = rewriteTestingImports(source, module.compiledPath, shimPath);
|
|
28
|
+
await writeFile(module.compiledPath, rewritten, 'utf8');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
20
31
|
|
|
21
|
-
if (module.sourcePath.endsWith('.js')) {
|
|
22
32
|
const source = await readFile(module.sourcePath, 'utf8');
|
|
23
|
-
const
|
|
33
|
+
const output = ts.transpileModule(source, {
|
|
34
|
+
fileName: module.sourcePath,
|
|
35
|
+
compilerOptions: {
|
|
36
|
+
target: ts.ScriptTarget.ES2022,
|
|
37
|
+
module: ts.ModuleKind.ESNext,
|
|
38
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
39
|
+
verbatimModuleSyntax: true,
|
|
40
|
+
rewriteRelativeImportExtensions: true,
|
|
41
|
+
},
|
|
42
|
+
reportDiagnostics: false,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const rewritten = rewriteTestingImports(output.outputText, module.compiledPath, shimPath);
|
|
24
46
|
await writeFile(module.compiledPath, rewritten, 'utf8');
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const source = await readFile(module.sourcePath, 'utf8');
|
|
29
|
-
const output = ts.transpileModule(source, {
|
|
30
|
-
fileName: module.sourcePath,
|
|
31
|
-
compilerOptions: {
|
|
32
|
-
target: ts.ScriptTarget.ES2022,
|
|
33
|
-
module: ts.ModuleKind.ESNext,
|
|
34
|
-
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
35
|
-
verbatimModuleSyntax: true,
|
|
36
|
-
rewriteRelativeImportExtensions: true,
|
|
37
|
-
},
|
|
38
|
-
reportDiagnostics: false,
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const rewritten = rewriteTestingImports(output.outputText, module.compiledPath, shimPath);
|
|
42
|
-
await writeFile(module.compiledPath, rewritten, 'utf8');
|
|
43
|
-
}));
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
async function ensureTestingRuntimeShim(workspaceRoot: string): Promise<string> {
|
|
@@ -52,7 +57,10 @@ async function ensureTestingRuntimeShim(workspaceRoot: string): Promise<string>
|
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
function rewriteTestingImports(source: string, compiledPath: string, shimPath: string): string {
|
|
55
|
-
const relativeShim = path
|
|
60
|
+
const relativeShim = path
|
|
61
|
+
.relative(path.dirname(compiledPath), shimPath)
|
|
62
|
+
.split(path.sep)
|
|
63
|
+
.join('/');
|
|
56
64
|
const specifier = relativeShim.startsWith('.') ? relativeShim : `./${relativeShim}`;
|
|
57
65
|
|
|
58
66
|
return source
|
package/src/dev-server.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import http, { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
3
|
-
import { createReadStream } from 'node:fs';
|
|
4
2
|
import { access } from 'node:fs/promises';
|
|
5
3
|
|
|
6
4
|
import type { HotUpdatePayload, WatchStatus } from './watch-events.ts';
|
|
@@ -41,14 +39,39 @@ const MIME_TYPES: Record<string, string> = {
|
|
|
41
39
|
|
|
42
40
|
const RESERVED_PREFIXES = ['__webstir', 'api', 'fonts', 'images', 'media', 'pages', 'sse'];
|
|
43
41
|
const STATIC_EXTENSIONS = new Set([
|
|
44
|
-
'.css',
|
|
45
|
-
'.
|
|
46
|
-
'.
|
|
42
|
+
'.css',
|
|
43
|
+
'.js',
|
|
44
|
+
'.mjs',
|
|
45
|
+
'.png',
|
|
46
|
+
'.jpg',
|
|
47
|
+
'.jpeg',
|
|
48
|
+
'.gif',
|
|
49
|
+
'.svg',
|
|
50
|
+
'.webp',
|
|
51
|
+
'.ico',
|
|
52
|
+
'.woff',
|
|
53
|
+
'.woff2',
|
|
54
|
+
'.ttf',
|
|
55
|
+
'.otf',
|
|
56
|
+
'.eot',
|
|
57
|
+
'.mp3',
|
|
58
|
+
'.m4a',
|
|
59
|
+
'.wav',
|
|
60
|
+
'.ogg',
|
|
61
|
+
'.mp4',
|
|
62
|
+
'.webm',
|
|
63
|
+
'.mov',
|
|
64
|
+
'.json',
|
|
65
|
+
'.txt',
|
|
66
|
+
'.xml',
|
|
67
|
+
'.map',
|
|
47
68
|
]);
|
|
48
|
-
const CONTENT_HASH_PATTERN =
|
|
69
|
+
const CONTENT_HASH_PATTERN =
|
|
70
|
+
/\.[a-f0-9]{8,64}\.(css|js|png|jpg|jpeg|gif|svg|webp|ico|woff2?|ttf|otf|eot|mp3|m4a|wav|ogg|mp4|webm|mov)$/i;
|
|
49
71
|
|
|
50
72
|
interface SseClient {
|
|
51
|
-
|
|
73
|
+
send(message: string): void;
|
|
74
|
+
close(): void;
|
|
52
75
|
}
|
|
53
76
|
|
|
54
77
|
export class DevServer {
|
|
@@ -57,7 +80,7 @@ export class DevServer {
|
|
|
57
80
|
private readonly port: number;
|
|
58
81
|
private readonly apiProxyOrigin?: string;
|
|
59
82
|
private readonly clients = new Set<SseClient>();
|
|
60
|
-
private server?:
|
|
83
|
+
private server?: ReturnType<typeof Bun.serve>;
|
|
61
84
|
|
|
62
85
|
public constructor(options: DevServerOptions) {
|
|
63
86
|
this.buildRoot = path.resolve(options.buildRoot);
|
|
@@ -71,16 +94,13 @@ export class DevServer {
|
|
|
71
94
|
return this.getAddress();
|
|
72
95
|
}
|
|
73
96
|
|
|
74
|
-
this.server =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
this.server!.off('error', reject);
|
|
82
|
-
resolve();
|
|
83
|
-
});
|
|
97
|
+
this.server = Bun.serve({
|
|
98
|
+
hostname: this.host,
|
|
99
|
+
idleTimeout: 0,
|
|
100
|
+
port: this.port,
|
|
101
|
+
fetch: (request) => {
|
|
102
|
+
return this.handleRequest(request);
|
|
103
|
+
},
|
|
84
104
|
});
|
|
85
105
|
|
|
86
106
|
return this.getAddress();
|
|
@@ -90,7 +110,7 @@ export class DevServer {
|
|
|
90
110
|
await this.broadcastRaw('data: shutdown\n\n');
|
|
91
111
|
|
|
92
112
|
for (const client of this.clients) {
|
|
93
|
-
client.
|
|
113
|
+
client.close();
|
|
94
114
|
}
|
|
95
115
|
this.clients.clear();
|
|
96
116
|
|
|
@@ -100,16 +120,7 @@ export class DevServer {
|
|
|
100
120
|
|
|
101
121
|
const server = this.server;
|
|
102
122
|
this.server = undefined;
|
|
103
|
-
|
|
104
|
-
server.close((error) => {
|
|
105
|
-
if (error) {
|
|
106
|
-
reject(error);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
resolve();
|
|
111
|
-
});
|
|
112
|
-
});
|
|
123
|
+
server.stop();
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
public async publishStatus(status: WatchStatus): Promise<void> {
|
|
@@ -129,139 +140,127 @@ export class DevServer {
|
|
|
129
140
|
throw new Error('Dev server has not started.');
|
|
130
141
|
}
|
|
131
142
|
|
|
132
|
-
const address = this.server.address();
|
|
133
|
-
if (!address || typeof address === 'string') {
|
|
134
|
-
throw new Error('Dev server did not expose a TCP address.');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
143
|
const originHost = this.host === '0.0.0.0' ? '127.0.0.1' : this.host;
|
|
138
144
|
return {
|
|
139
145
|
host: originHost,
|
|
140
|
-
port:
|
|
141
|
-
origin: `http://${originHost}:${
|
|
146
|
+
port: this.server.port,
|
|
147
|
+
origin: `http://${originHost}:${this.server.port}`,
|
|
142
148
|
};
|
|
143
149
|
}
|
|
144
150
|
|
|
145
|
-
private async handleRequest(
|
|
146
|
-
request
|
|
147
|
-
|
|
148
|
-
): Promise<void> {
|
|
149
|
-
if (!request.url) {
|
|
150
|
-
this.writeText(response, 400, 'Bad request.');
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const method = request.method ?? 'GET';
|
|
155
|
-
|
|
156
|
-
const requestUrl = new URL(request.url, 'http://localhost');
|
|
151
|
+
private async handleRequest(request: Request): Promise<Response> {
|
|
152
|
+
const method = request.method || 'GET';
|
|
153
|
+
const requestUrl = new URL(request.url);
|
|
157
154
|
const { pathname } = requestUrl;
|
|
155
|
+
|
|
158
156
|
if (pathname === '/sse') {
|
|
159
|
-
this.handleSse(
|
|
160
|
-
return;
|
|
157
|
+
return this.handleSse(request);
|
|
161
158
|
}
|
|
162
159
|
|
|
163
160
|
const apiProxyPath = getApiProxyPath(pathname);
|
|
164
161
|
if (apiProxyPath !== null && this.apiProxyOrigin) {
|
|
165
|
-
await this.handleApiProxy(request,
|
|
166
|
-
return;
|
|
162
|
+
return await this.handleApiProxy(request, requestUrl, apiProxyPath);
|
|
167
163
|
}
|
|
168
164
|
|
|
169
165
|
if (method !== 'GET' && method !== 'HEAD') {
|
|
170
|
-
|
|
171
|
-
return;
|
|
166
|
+
return textResponse(405, 'Method not allowed.');
|
|
172
167
|
}
|
|
173
168
|
|
|
174
169
|
const candidates = getStaticCandidatePaths(pathname);
|
|
175
170
|
const resolved = await resolveStaticFile(this.buildRoot, candidates);
|
|
176
171
|
if (!resolved) {
|
|
177
|
-
|
|
178
|
-
return;
|
|
172
|
+
return textResponse(404, 'Not found.');
|
|
179
173
|
}
|
|
180
174
|
|
|
181
175
|
const lowerRelativePath = resolved.relativePath.toLowerCase();
|
|
182
176
|
const extension = path.extname(lowerRelativePath).toLowerCase();
|
|
183
|
-
|
|
184
|
-
|
|
177
|
+
const headers = new Headers({
|
|
178
|
+
'Content-Type': MIME_TYPES[extension] ?? 'application/octet-stream',
|
|
179
|
+
});
|
|
180
|
+
setCacheHeaders(headers, lowerRelativePath);
|
|
185
181
|
|
|
186
182
|
if (method === 'HEAD') {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
183
|
+
return new Response(null, {
|
|
184
|
+
status: 200,
|
|
185
|
+
headers,
|
|
186
|
+
});
|
|
190
187
|
}
|
|
191
188
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
this.writeText(response, 500, 'Failed to read file.');
|
|
196
|
-
} else {
|
|
197
|
-
response.destroy();
|
|
198
|
-
}
|
|
189
|
+
return new Response(Bun.file(resolved.absolutePath), {
|
|
190
|
+
status: 200,
|
|
191
|
+
headers,
|
|
199
192
|
});
|
|
200
|
-
stream.pipe(response);
|
|
201
193
|
}
|
|
202
194
|
|
|
203
195
|
private async handleApiProxy(
|
|
204
|
-
request:
|
|
205
|
-
response: ServerResponse<IncomingMessage>,
|
|
196
|
+
request: Request,
|
|
206
197
|
requestUrl: URL,
|
|
207
|
-
apiProxyPath: string
|
|
208
|
-
): Promise<
|
|
198
|
+
apiProxyPath: string,
|
|
199
|
+
): Promise<Response> {
|
|
209
200
|
const targetUrl = new URL(apiProxyPath + requestUrl.search, this.apiProxyOrigin);
|
|
210
201
|
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
headers: {
|
|
216
|
-
...request.headers,
|
|
217
|
-
host: targetUrl.host,
|
|
218
|
-
connection: 'close',
|
|
219
|
-
},
|
|
220
|
-
}, (proxyResponse) => {
|
|
221
|
-
response.writeHead(proxyResponse.statusCode ?? 502, proxyResponse.headers);
|
|
222
|
-
proxyResponse.pipe(response);
|
|
223
|
-
proxyResponse.once('end', resolve);
|
|
224
|
-
proxyResponse.once('error', () => {
|
|
225
|
-
if (!response.headersSent) {
|
|
226
|
-
this.writeText(response, 502, 'Backend proxy read failed.');
|
|
227
|
-
} else {
|
|
228
|
-
response.destroy();
|
|
229
|
-
}
|
|
230
|
-
resolve();
|
|
231
|
-
});
|
|
232
|
-
});
|
|
202
|
+
try {
|
|
203
|
+
const requestInit = createProxyRequestInit(request, targetUrl);
|
|
204
|
+
const proxyResponse = await fetch(targetUrl, requestInit);
|
|
205
|
+
const headers = rewriteProxyResponseHeaders(proxyResponse.headers, targetUrl);
|
|
233
206
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
} else {
|
|
238
|
-
response.destroy();
|
|
239
|
-
}
|
|
240
|
-
resolve();
|
|
207
|
+
return new Response(request.method !== 'HEAD' ? proxyResponse.body : null, {
|
|
208
|
+
status: proxyResponse.status || 502,
|
|
209
|
+
headers,
|
|
241
210
|
});
|
|
211
|
+
} catch {
|
|
212
|
+
return textResponse(502, 'Backend proxy failed.');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
242
215
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
216
|
+
private handleSse(request: Request): Response {
|
|
217
|
+
const encoder = new TextEncoder();
|
|
218
|
+
let client: SseClient | undefined;
|
|
247
219
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
220
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
221
|
+
start: (controller) => {
|
|
222
|
+
const cleanup = () => {
|
|
223
|
+
if (!client) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
251
226
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
227
|
+
this.clients.delete(client);
|
|
228
|
+
try {
|
|
229
|
+
controller.close();
|
|
230
|
+
} catch {
|
|
231
|
+
// The stream is already closed.
|
|
232
|
+
}
|
|
233
|
+
request.signal.removeEventListener('abort', cleanup);
|
|
234
|
+
client = undefined;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
client = {
|
|
238
|
+
send: (message) => {
|
|
239
|
+
try {
|
|
240
|
+
controller.enqueue(encoder.encode(message));
|
|
241
|
+
} catch {
|
|
242
|
+
cleanup();
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
close: cleanup,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
this.clients.add(client);
|
|
249
|
+
controller.enqueue(encoder.encode('\n'));
|
|
250
|
+
request.signal.addEventListener('abort', cleanup, { once: true });
|
|
251
|
+
},
|
|
252
|
+
cancel: () => {
|
|
253
|
+
client?.close();
|
|
254
|
+
},
|
|
257
255
|
});
|
|
258
256
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
257
|
+
return new Response(stream, {
|
|
258
|
+
status: 200,
|
|
259
|
+
headers: {
|
|
260
|
+
'Content-Type': 'text/event-stream',
|
|
261
|
+
'Cache-Control': 'no-cache',
|
|
262
|
+
Connection: 'keep-alive',
|
|
263
|
+
},
|
|
265
264
|
});
|
|
266
265
|
}
|
|
267
266
|
|
|
@@ -272,22 +271,13 @@ export class DevServer {
|
|
|
272
271
|
private async broadcastRaw(message: string): Promise<void> {
|
|
273
272
|
for (const client of Array.from(this.clients)) {
|
|
274
273
|
try {
|
|
275
|
-
client.
|
|
274
|
+
client.send(message);
|
|
276
275
|
} catch {
|
|
276
|
+
client.close();
|
|
277
277
|
this.clients.delete(client);
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
280
|
}
|
|
281
|
-
|
|
282
|
-
private writeText(
|
|
283
|
-
response: ServerResponse<IncomingMessage>,
|
|
284
|
-
statusCode: number,
|
|
285
|
-
body: string
|
|
286
|
-
): void {
|
|
287
|
-
response.statusCode = statusCode;
|
|
288
|
-
response.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
289
|
-
response.end(body);
|
|
290
|
-
}
|
|
291
281
|
}
|
|
292
282
|
|
|
293
283
|
export function getStaticCandidatePaths(pathname: string): readonly string[] {
|
|
@@ -309,7 +299,7 @@ export function getStaticCandidatePaths(pathname: string): readonly string[] {
|
|
|
309
299
|
candidates.push(path.posix.join('pages', relativePath, 'index.html'));
|
|
310
300
|
}
|
|
311
301
|
|
|
312
|
-
return Array.from(new Set(candidates.map(candidate => candidate.replace(/^\/+/, ''))));
|
|
302
|
+
return Array.from(new Set(candidates.map((candidate) => candidate.replace(/^\/+/, ''))));
|
|
313
303
|
}
|
|
314
304
|
|
|
315
305
|
export function getApiProxyPath(pathname: string): string | null {
|
|
@@ -325,9 +315,73 @@ export function getApiProxyPath(pathname: string): string | null {
|
|
|
325
315
|
return null;
|
|
326
316
|
}
|
|
327
317
|
|
|
318
|
+
function rewriteProxyResponseHeaders(headers: Headers, targetUrl: URL): Headers {
|
|
319
|
+
const nextHeaders = new Headers(headers);
|
|
320
|
+
const location = headers.get('location');
|
|
321
|
+
if (location) {
|
|
322
|
+
nextHeaders.set('location', rewriteProxyLocation(location, targetUrl));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return nextHeaders;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function createProxyRequestInit(
|
|
329
|
+
request: Request,
|
|
330
|
+
targetUrl: URL,
|
|
331
|
+
): RequestInit & { duplex?: 'half' } {
|
|
332
|
+
const headers = new Headers(request.headers);
|
|
333
|
+
headers.set('host', targetUrl.host);
|
|
334
|
+
headers.set('connection', 'close');
|
|
335
|
+
|
|
336
|
+
const requestInit: RequestInit & { duplex?: 'half' } = {
|
|
337
|
+
method: request.method,
|
|
338
|
+
headers,
|
|
339
|
+
redirect: 'manual',
|
|
340
|
+
signal: request.signal,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
if (methodAllowsBody(request.method)) {
|
|
344
|
+
requestInit.body = request.body;
|
|
345
|
+
if (request.body) {
|
|
346
|
+
requestInit.duplex = 'half';
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return requestInit;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function rewriteProxyLocation(value: string, targetUrl: URL): string {
|
|
354
|
+
const trimmed = value.trim();
|
|
355
|
+
if (!trimmed) {
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (trimmed.startsWith('/')) {
|
|
360
|
+
return prefixApiMount(trimmed);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const resolved = new URL(trimmed, targetUrl.origin);
|
|
365
|
+
if (resolved.origin !== targetUrl.origin) {
|
|
366
|
+
return value;
|
|
367
|
+
}
|
|
368
|
+
return prefixApiMount(`${resolved.pathname}${resolved.search}${resolved.hash}`);
|
|
369
|
+
} catch {
|
|
370
|
+
return value;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function prefixApiMount(pathname: string): string {
|
|
375
|
+
if (pathname === '/api' || pathname.startsWith('/api/')) {
|
|
376
|
+
return pathname;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return pathname === '/' ? '/api' : `/api${pathname}`;
|
|
380
|
+
}
|
|
381
|
+
|
|
328
382
|
async function resolveStaticFile(
|
|
329
383
|
buildRoot: string,
|
|
330
|
-
relativePaths: readonly string[]
|
|
384
|
+
relativePaths: readonly string[],
|
|
331
385
|
): Promise<{ absolutePath: string; relativePath: string } | null> {
|
|
332
386
|
for (const relativePath of relativePaths) {
|
|
333
387
|
const absolutePath = path.resolve(buildRoot, relativePath);
|
|
@@ -363,31 +417,48 @@ function getGenericFileCandidates(relativePath: string): readonly string[] {
|
|
|
363
417
|
}
|
|
364
418
|
|
|
365
419
|
function hasReservedPrefix(relativePath: string): boolean {
|
|
366
|
-
return RESERVED_PREFIXES.some(
|
|
420
|
+
return RESERVED_PREFIXES.some(
|
|
421
|
+
(prefix) => relativePath === prefix || relativePath.startsWith(`${prefix}/`),
|
|
422
|
+
);
|
|
367
423
|
}
|
|
368
424
|
|
|
369
|
-
function setCacheHeaders(
|
|
425
|
+
function setCacheHeaders(headers: Headers, relativePath: string): void {
|
|
370
426
|
if (CONTENT_HASH_PATTERN.test(relativePath)) {
|
|
371
|
-
|
|
427
|
+
headers.set('Cache-Control', 'public, max-age=31536000, immutable');
|
|
372
428
|
return;
|
|
373
429
|
}
|
|
374
430
|
|
|
375
431
|
if (relativePath.endsWith('refresh.js') || relativePath.endsWith('hmr.js')) {
|
|
376
|
-
|
|
377
|
-
response.setHeader('Pragma', 'no-cache');
|
|
378
|
-
response.setHeader('Expires', '0');
|
|
432
|
+
setNoCacheHeaders(headers);
|
|
379
433
|
return;
|
|
380
434
|
}
|
|
381
435
|
|
|
382
436
|
const extension = path.extname(relativePath).toLowerCase();
|
|
383
|
-
if (extension === '.html' || extension === '') {
|
|
384
|
-
|
|
385
|
-
response.setHeader('Pragma', 'no-cache');
|
|
386
|
-
response.setHeader('Expires', '0');
|
|
437
|
+
if (extension === '.html' || extension === '' || extension === '.json') {
|
|
438
|
+
setNoCacheHeaders(headers);
|
|
387
439
|
return;
|
|
388
440
|
}
|
|
389
441
|
|
|
390
442
|
if (STATIC_EXTENSIONS.has(extension)) {
|
|
391
|
-
|
|
443
|
+
headers.set('Cache-Control', 'no-cache, must-revalidate');
|
|
392
444
|
}
|
|
393
445
|
}
|
|
446
|
+
|
|
447
|
+
function setNoCacheHeaders(headers: Headers): void {
|
|
448
|
+
headers.set('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
449
|
+
headers.set('Pragma', 'no-cache');
|
|
450
|
+
headers.set('Expires', '0');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function textResponse(statusCode: number, body: string): Response {
|
|
454
|
+
return new Response(body, {
|
|
455
|
+
status: statusCode,
|
|
456
|
+
headers: {
|
|
457
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function methodAllowsBody(method: string): boolean {
|
|
463
|
+
return method !== 'GET' && method !== 'HEAD';
|
|
464
|
+
}
|