@ericminassian/plat 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-types.d.ts +7 -0
- package/dist/api-types.js +3 -0
- package/dist/api-types.js.map +1 -0
- package/dist/api.d.ts +123 -0
- package/dist/api.js +271 -0
- package/dist/api.js.map +1 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +4 -0
- package/dist/bin.js.map +1 -0
- package/dist/build.d.ts +48 -0
- package/dist/build.js +185 -0
- package/dist/build.js.map +1 -0
- package/dist/commands/account.d.ts +2 -0
- package/dist/commands/account.js +45 -0
- package/dist/commands/account.js.map +1 -0
- package/dist/commands/apps.d.ts +2 -0
- package/dist/commands/apps.js +75 -0
- package/dist/commands/apps.js.map +1 -0
- package/dist/commands/common.d.ts +19 -0
- package/dist/commands/common.js +43 -0
- package/dist/commands/common.js.map +1 -0
- package/dist/commands/lifecycle.d.ts +3 -0
- package/dist/commands/lifecycle.js +106 -0
- package/dist/commands/lifecycle.js.map +1 -0
- package/dist/commands/manage.d.ts +2 -0
- package/dist/commands/manage.js +103 -0
- package/dist/commands/manage.js.map +1 -0
- package/dist/commands/observe.d.ts +8 -0
- package/dist/commands/observe.js +195 -0
- package/dist/commands/observe.js.map +1 -0
- package/dist/credentials.d.ts +13 -0
- package/dist/credentials.js +42 -0
- package/dist/credentials.js.map +1 -0
- package/dist/deploy.d.ts +23 -0
- package/dist/deploy.js +100 -0
- package/dist/deploy.js.map +1 -0
- package/dist/dev/crons.d.ts +16 -0
- package/dist/dev/crons.js +47 -0
- package/dist/dev/crons.js.map +1 -0
- package/dist/dev/entries.d.ts +6 -0
- package/dist/dev/entries.js +21 -0
- package/dist/dev/entries.js.map +1 -0
- package/dist/dev/index.d.ts +33 -0
- package/dist/dev/index.js +262 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/dev/local-stage.d.ts +46 -0
- package/dist/dev/local-stage.js +116 -0
- package/dist/dev/local-stage.js.map +1 -0
- package/dist/dev/pretty.d.ts +16 -0
- package/dist/dev/pretty.js +91 -0
- package/dist/dev/pretty.js.map +1 -0
- package/dist/dev/proc.d.ts +7 -0
- package/dist/dev/proc.js +16 -0
- package/dist/dev/proc.js.map +1 -0
- package/dist/dev/router.d.ts +67 -0
- package/dist/dev/router.js +184 -0
- package/dist/dev/router.js.map +1 -0
- package/dist/dev/server-entry.d.ts +1 -0
- package/dist/dev/server-entry.js +28 -0
- package/dist/dev/server-entry.js.map +1 -0
- package/dist/dev/test.d.ts +10 -0
- package/dist/dev/test.js +62 -0
- package/dist/dev/test.js.map +1 -0
- package/dist/dev/worker-host.d.ts +1 -0
- package/dist/dev/worker-host.js +67 -0
- package/dist/dev/worker-host.js.map +1 -0
- package/dist/dev/worker.d.ts +57 -0
- package/dist/dev/worker.js +216 -0
- package/dist/dev/worker.js.map +1 -0
- package/dist/github.d.ts +39 -0
- package/dist/github.js +108 -0
- package/dist/github.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +30 -0
- package/dist/init.js +114 -0
- package/dist/init.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.js +24 -0
- package/dist/main.js.map +1 -0
- package/dist/project.d.ts +21 -0
- package/dist/project.js +138 -0
- package/dist/project.js.map +1 -0
- package/dist/util/esbuild.d.ts +2 -0
- package/dist/util/esbuild.js +9 -0
- package/dist/util/esbuild.js.map +1 -0
- package/dist/util/net.d.ts +4 -0
- package/dist/util/net.js +33 -0
- package/dist/util/net.js.map +1 -0
- package/dist/util/output.d.ts +42 -0
- package/dist/util/output.js +105 -0
- package/dist/util/output.js.map +1 -0
- package/dist/util/prompt.d.ts +3 -0
- package/dist/util/prompt.js +22 -0
- package/dist/util/prompt.js.map +1 -0
- package/dist/util/time.d.ts +11 -0
- package/dist/util/time.js +29 -0
- package/dist/util/time.js.map +1 -0
- package/dist/util/zip.d.ts +10 -0
- package/dist/util/zip.js +45 -0
- package/dist/util/zip.js.map +1 -0
- package/package.json +50 -3
- package/src/api-types.ts +8 -0
- package/src/api.ts +341 -0
- package/src/bin.ts +4 -0
- package/src/build.ts +252 -0
- package/src/commands/account.ts +62 -0
- package/src/commands/apps.ts +102 -0
- package/src/commands/common.ts +51 -0
- package/src/commands/lifecycle.ts +140 -0
- package/src/commands/manage.ts +169 -0
- package/src/commands/observe.ts +276 -0
- package/src/credentials.ts +50 -0
- package/src/deploy.ts +138 -0
- package/src/dev/crons.ts +70 -0
- package/src/dev/entries.ts +23 -0
- package/src/dev/index.ts +281 -0
- package/src/dev/local-stage.ts +176 -0
- package/src/dev/pretty.ts +90 -0
- package/src/dev/proc.ts +15 -0
- package/src/dev/router.ts +246 -0
- package/src/dev/server-entry.ts +21 -0
- package/src/dev/test.ts +68 -0
- package/src/dev/worker-host.ts +62 -0
- package/src/dev/worker.ts +332 -0
- package/src/github.ts +121 -0
- package/src/index.ts +20 -0
- package/src/init.ts +136 -0
- package/src/main.ts +28 -0
- package/src/project.ts +143 -0
- package/src/util/esbuild.ts +10 -0
- package/src/util/net.ts +33 -0
- package/src/util/output.ts +117 -0
- package/src/util/prompt.ts +20 -0
- package/src/util/time.ts +32 -0
- package/src/util/zip.ts +46 -0
- package/template/.github/workflows/platform.yml +24 -0
- package/template/AGENTS.md +139 -0
- package/template/CLAUDE.md +1 -0
- package/template/README.md +13 -0
- package/template/_gitignore +8 -0
- package/template/package.json +31 -0
- package/template/plat.config.ts +31 -0
- package/template/src/server.ts +100 -0
- package/template/src/types.ts +21 -0
- package/template/src/worker.ts +32 -0
- package/template/test/server.test.ts +120 -0
- package/template/test/todos.integration.test.ts +30 -0
- package/template/tsconfig.json +18 -0
- package/template/vitest.config.ts +9 -0
- package/template/vitest.integration.config.ts +10 -0
- package/template/web/index.html +12 -0
- package/template/web/src/App.tsx +102 -0
- package/template/web/src/api.ts +11 -0
- package/template/web/src/main.tsx +10 -0
- package/template/web/src/styles.css +102 -0
- package/template/web/tsconfig.json +18 -0
- package/template/web/vite.config.ts +19 -0
- package/README.md +0 -3
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { createReadStream, existsSync, statSync } from 'node:fs';
|
|
2
|
+
import {
|
|
3
|
+
createServer,
|
|
4
|
+
request as httpRequest,
|
|
5
|
+
type IncomingMessage,
|
|
6
|
+
type Server,
|
|
7
|
+
type ServerResponse,
|
|
8
|
+
} from 'node:http';
|
|
9
|
+
import { connect, type Socket } from 'node:net';
|
|
10
|
+
import { extname, join, normalize, sep } from 'node:path';
|
|
11
|
+
import { decide, HOST_HEADER, type RouteRecord } from '@ericminassian/platform-core';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `plat dev`'s stand-in for the CloudFront router: the same `decide()` the
|
|
15
|
+
* edge function runs, against a local route record. Server routes go to the
|
|
16
|
+
* local web server; static routes go to the frontend dev server (original URL,
|
|
17
|
+
* so it can do HMR and its own SPA fallback) or to files in `web.static`.
|
|
18
|
+
*/
|
|
19
|
+
export interface Target {
|
|
20
|
+
host: string;
|
|
21
|
+
port: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface LocalRouterState {
|
|
25
|
+
record: RouteRecord;
|
|
26
|
+
server?: Target;
|
|
27
|
+
webDev?: Target;
|
|
28
|
+
/** A frontend dev server is configured but not accepting connections yet. */
|
|
29
|
+
webDevStarting?: boolean;
|
|
30
|
+
staticDir?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type LocalDecision =
|
|
34
|
+
| { kind: 'server'; target: Target }
|
|
35
|
+
| { kind: 'webdev'; target: Target }
|
|
36
|
+
| { kind: 'file'; path: string }
|
|
37
|
+
| { kind: 'unavailable'; reason: string }
|
|
38
|
+
| { kind: 'notfound' };
|
|
39
|
+
|
|
40
|
+
/** Pure routing decision, shared by the router and tests. */
|
|
41
|
+
export function localDecision(state: LocalRouterState, uri: string): LocalDecision {
|
|
42
|
+
const pathname = uri.split('?')[0] ?? '/';
|
|
43
|
+
const d = decide(state.record, pathname);
|
|
44
|
+
if (d.t === 'notfound') return { kind: 'notfound' };
|
|
45
|
+
if (d.t === 'server') {
|
|
46
|
+
return state.server
|
|
47
|
+
? { kind: 'server', target: state.server }
|
|
48
|
+
: { kind: 'unavailable', reason: 'the web server is starting' };
|
|
49
|
+
}
|
|
50
|
+
if (state.webDev) return { kind: 'webdev', target: state.webDev };
|
|
51
|
+
if (state.webDevStarting) return { kind: 'unavailable', reason: 'the frontend dev server is starting' };
|
|
52
|
+
if (!state.staticDir)
|
|
53
|
+
return { kind: 'unavailable', reason: 'no frontend: set web.static or web.dev in plat.config.ts' };
|
|
54
|
+
const prefix = `/${state.record.s ?? ''}`;
|
|
55
|
+
const rel = d.key.startsWith(prefix) ? d.key.slice(prefix.length) : d.key;
|
|
56
|
+
const file = safeJoin(state.staticDir, decodeURIComponent(rel));
|
|
57
|
+
return file ? { kind: 'file', path: file } : { kind: 'notfound' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Local route record for an app (mirrors what the engine writes to the KVS). */
|
|
61
|
+
export function localRouteRecord(opts: {
|
|
62
|
+
app: string;
|
|
63
|
+
stage: string;
|
|
64
|
+
deploymentId: string;
|
|
65
|
+
hasServer: boolean;
|
|
66
|
+
hasStatic: boolean;
|
|
67
|
+
serverRoutes: string[];
|
|
68
|
+
spa: boolean;
|
|
69
|
+
}): RouteRecord {
|
|
70
|
+
return {
|
|
71
|
+
v: 1,
|
|
72
|
+
app: opts.app,
|
|
73
|
+
stage: opts.stage,
|
|
74
|
+
d: opts.deploymentId,
|
|
75
|
+
...(opts.hasServer ? { o: 'local-server' } : {}),
|
|
76
|
+
...(opts.hasStatic ? { s: 'local' } : {}),
|
|
77
|
+
...(opts.hasServer && opts.hasStatic ? { r: opts.serverRoutes } : {}),
|
|
78
|
+
spa: opts.spa ? 1 : 0,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type Interceptor = (req: IncomingMessage, res: ServerResponse, url: URL) => Promise<boolean> | boolean;
|
|
83
|
+
|
|
84
|
+
export interface LocalRouterOptions {
|
|
85
|
+
port: number;
|
|
86
|
+
host?: string;
|
|
87
|
+
state: () => LocalRouterState;
|
|
88
|
+
/** Handled before routing (local sign-in, dev endpoints). Return true when handled. */
|
|
89
|
+
intercept?: Interceptor[];
|
|
90
|
+
onRequest?: (line: { method: string; url: string; status: number; ms: number; kind: string }) => void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function startLocalRouter(opts: LocalRouterOptions): Promise<Server> {
|
|
94
|
+
const server = createServer(async (req, res) => {
|
|
95
|
+
const started = Date.now();
|
|
96
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
97
|
+
let kind = 'intercept';
|
|
98
|
+
res.once('finish', () =>
|
|
99
|
+
opts.onRequest?.({
|
|
100
|
+
method: req.method ?? 'GET',
|
|
101
|
+
url: req.url ?? '/',
|
|
102
|
+
status: res.statusCode,
|
|
103
|
+
ms: Date.now() - started,
|
|
104
|
+
kind,
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
try {
|
|
108
|
+
for (const fn of opts.intercept ?? []) if (await fn(req, res, url)) return;
|
|
109
|
+
const d = localDecision(opts.state(), req.url ?? '/');
|
|
110
|
+
kind = d.kind;
|
|
111
|
+
switch (d.kind) {
|
|
112
|
+
case 'server':
|
|
113
|
+
return proxy(req, res, d.target, true);
|
|
114
|
+
case 'webdev':
|
|
115
|
+
return proxy(req, res, d.target, false);
|
|
116
|
+
case 'file':
|
|
117
|
+
return serveFile(res, d.path);
|
|
118
|
+
case 'unavailable':
|
|
119
|
+
return text(res, 503, `${d.reason}\n`);
|
|
120
|
+
case 'notfound':
|
|
121
|
+
return text(res, 404, 'Not found\n');
|
|
122
|
+
}
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (!res.headersSent) text(res, 502, `plat dev router error: ${(err as Error).message}\n`);
|
|
125
|
+
else res.destroy();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
server.on('upgrade', (req, socket, head) => {
|
|
130
|
+
const d = localDecision(opts.state(), req.url ?? '/');
|
|
131
|
+
// Web Functions don't do WebSockets; only the frontend dev server (HMR) gets upgrades.
|
|
132
|
+
if (d.kind !== 'webdev') {
|
|
133
|
+
socket.end('HTTP/1.1 501 Not Implemented\r\n\r\n');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
tunnel(req, socket as Socket, head, d.target);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
server.once('error', reject);
|
|
141
|
+
server.listen(opts.port, opts.host ?? '127.0.0.1', () => resolve(server));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Headers the edge would add, so apps see their public host. */
|
|
146
|
+
export function forwardedHeaders(
|
|
147
|
+
req: IncomingMessage,
|
|
148
|
+
toServer: boolean,
|
|
149
|
+
): Record<string, string | string[] | undefined> {
|
|
150
|
+
const headers: Record<string, string | string[] | undefined> = { ...req.headers };
|
|
151
|
+
const host = req.headers.host ?? 'localhost';
|
|
152
|
+
if (toServer) {
|
|
153
|
+
headers[HOST_HEADER] = host;
|
|
154
|
+
headers['x-forwarded-host'] = host;
|
|
155
|
+
headers['x-forwarded-proto'] = 'http';
|
|
156
|
+
}
|
|
157
|
+
return headers;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function proxy(req: IncomingMessage, res: ServerResponse, target: Target, toServer: boolean): Promise<void> {
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
const upstream = httpRequest(
|
|
163
|
+
{
|
|
164
|
+
host: target.host,
|
|
165
|
+
port: target.port,
|
|
166
|
+
method: req.method,
|
|
167
|
+
path: req.url,
|
|
168
|
+
headers: forwardedHeaders(req, toServer),
|
|
169
|
+
},
|
|
170
|
+
(up) => {
|
|
171
|
+
res.writeHead(up.statusCode ?? 502, up.statusMessage, up.headers);
|
|
172
|
+
up.pipe(res);
|
|
173
|
+
up.once('end', resolve);
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
upstream.once('error', (err) => {
|
|
177
|
+
if (!res.headersSent)
|
|
178
|
+
text(res, 502, `upstream ${target.host}:${target.port} unavailable: ${err.message}\n`);
|
|
179
|
+
resolve();
|
|
180
|
+
});
|
|
181
|
+
req.pipe(upstream);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function tunnel(req: IncomingMessage, socket: Socket, head: Buffer, target: Target): void {
|
|
186
|
+
const up = connect(target.port, target.host, () => {
|
|
187
|
+
const lines = [`${req.method} ${req.url} HTTP/${req.httpVersion}`];
|
|
188
|
+
for (let i = 0; i < req.rawHeaders.length; i += 2)
|
|
189
|
+
lines.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}`);
|
|
190
|
+
up.write(`${lines.join('\r\n')}\r\n\r\n`);
|
|
191
|
+
if (head.length) up.write(head);
|
|
192
|
+
up.pipe(socket);
|
|
193
|
+
socket.pipe(up);
|
|
194
|
+
});
|
|
195
|
+
const close = () => {
|
|
196
|
+
up.destroy();
|
|
197
|
+
socket.destroy();
|
|
198
|
+
};
|
|
199
|
+
up.once('error', close);
|
|
200
|
+
socket.once('error', close);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const MIME: Record<string, string> = {
|
|
204
|
+
'.html': 'text/html; charset=utf-8',
|
|
205
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
206
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
207
|
+
'.css': 'text/css; charset=utf-8',
|
|
208
|
+
'.json': 'application/json',
|
|
209
|
+
'.map': 'application/json',
|
|
210
|
+
'.svg': 'image/svg+xml',
|
|
211
|
+
'.png': 'image/png',
|
|
212
|
+
'.jpg': 'image/jpeg',
|
|
213
|
+
'.jpeg': 'image/jpeg',
|
|
214
|
+
'.gif': 'image/gif',
|
|
215
|
+
'.webp': 'image/webp',
|
|
216
|
+
'.ico': 'image/x-icon',
|
|
217
|
+
'.woff': 'font/woff',
|
|
218
|
+
'.woff2': 'font/woff2',
|
|
219
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
220
|
+
'.wasm': 'application/wasm',
|
|
221
|
+
'.webmanifest': 'application/manifest+json',
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
function serveFile(res: ServerResponse, path: string): void {
|
|
225
|
+
if (!existsSync(path) || !statSync(path).isFile()) {
|
|
226
|
+
text(res, 404, 'Not found\n');
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
res.writeHead(200, {
|
|
230
|
+
'content-type': MIME[extname(path)] ?? 'application/octet-stream',
|
|
231
|
+
'cache-control': 'no-cache',
|
|
232
|
+
});
|
|
233
|
+
createReadStream(path).pipe(res);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function text(res: ServerResponse, status: number, body: string): void {
|
|
237
|
+
res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' });
|
|
238
|
+
res.end(body);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Join without escaping `root` (path traversal). */
|
|
242
|
+
export function safeJoin(root: string, rel: string): string | null {
|
|
243
|
+
const full = normalize(join(root, rel));
|
|
244
|
+
const base = normalize(root.endsWith(sep) ? root : root + sep);
|
|
245
|
+
return full === normalize(root) || full.startsWith(base) ? full : null;
|
|
246
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Entry for the local web server under `plat dev` (run with `tsx watch`, so
|
|
2
|
+
// edits to the app restart it). Loads the app's fetch handler and starts it
|
|
3
|
+
// with the SDK server wrapper — the same wrapper the Web Function runs — using
|
|
4
|
+
// the app's own copy of the SDK so app code and wrapper share state.
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const entry = process.env.PLATFORM_ENTRY;
|
|
8
|
+
const sdkServer = process.env.PLATFORM_SDK_SERVER ?? '@ericminassian/platform/server';
|
|
9
|
+
if (!entry) {
|
|
10
|
+
console.error('PLATFORM_ENTRY is not set (this file is started by `plat dev`)');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const mod = (await import(pathToFileURL(entry).href)) as { default?: unknown };
|
|
15
|
+
if (!mod.default) {
|
|
16
|
+
console.error(`${entry} has no default export; export your app (e.g. \`export default app\`)`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const sdkUrl = sdkServer.startsWith('/') ? pathToFileURL(sdkServer).href : sdkServer;
|
|
20
|
+
const { startServer } = (await import(sdkUrl)) as typeof import('@ericminassian/platform/server');
|
|
21
|
+
await startServer(mod.default as Parameters<typeof startServer>[0]);
|
package/src/dev/test.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { EMULATOR_INSTALL_HINT } from '@ericminassian/platform-engine';
|
|
5
|
+
import { withLocalBin } from '../build.ts';
|
|
6
|
+
import { loadProject } from '../project.ts';
|
|
7
|
+
import { CliError, step, success } from '../util/output.ts';
|
|
8
|
+
import { localEnv, startLocalStage } from './local-stage.ts';
|
|
9
|
+
import { type Dispatcher, type LocalWorker, startLocalWorker, startWorkerDispatch } from './worker.ts';
|
|
10
|
+
|
|
11
|
+
export const INTEGRATION_CONFIG = 'vitest.integration.config.ts';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `plat test`: a throwaway emulator with this app's resources (stage `test`),
|
|
15
|
+
* then vitest with the same environment the deployed code gets. Uses
|
|
16
|
+
* `vitest.integration.config.ts` when present.
|
|
17
|
+
*/
|
|
18
|
+
export async function runTests(opts: { cwd: string; args: string[] }): Promise<number> {
|
|
19
|
+
const { root, manifest } = await loadProject(opts.cwd);
|
|
20
|
+
step('starting the AWS Emulator');
|
|
21
|
+
let stage: Awaited<ReturnType<typeof startLocalStage>>;
|
|
22
|
+
try {
|
|
23
|
+
stage = await startLocalStage({ root, manifest, stage: 'test', port: 3999 });
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if ((err as Error).message === EMULATOR_INSTALL_HINT)
|
|
26
|
+
throw new CliError('the AWS Emulator is not installed', { hint: EMULATOR_INSTALL_HINT });
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
success(`provisioned ${manifest.name} (stage test)`);
|
|
30
|
+
let worker: LocalWorker | undefined;
|
|
31
|
+
let dispatcher: Dispatcher | undefined;
|
|
32
|
+
try {
|
|
33
|
+
if (manifest.worker) {
|
|
34
|
+
worker = await startLocalWorker(stage, manifest, root, { watch: false });
|
|
35
|
+
worker.child.stdout?.pipe(process.stdout);
|
|
36
|
+
worker.child.stderr?.pipe(process.stderr);
|
|
37
|
+
dispatcher = await startWorkerDispatch(stage, manifest, worker);
|
|
38
|
+
}
|
|
39
|
+
const args = ['run'];
|
|
40
|
+
if (existsSync(join(root, INTEGRATION_CONFIG)) && !opts.args.includes('--config'))
|
|
41
|
+
args.push('--config', INTEGRATION_CONFIG);
|
|
42
|
+
args.push(...opts.args);
|
|
43
|
+
return await new Promise<number>((resolve, reject) => {
|
|
44
|
+
const child = spawn('vitest', args, {
|
|
45
|
+
cwd: root,
|
|
46
|
+
stdio: 'inherit',
|
|
47
|
+
shell: process.platform === 'win32',
|
|
48
|
+
env: withLocalBin(
|
|
49
|
+
root,
|
|
50
|
+
localEnv(stage, manifest, {
|
|
51
|
+
PLATFORM_TEST: '1',
|
|
52
|
+
...(worker ? { PLATFORM_LOCAL_WORKER_URL: `http://127.0.0.1:${worker.port}/` } : {}),
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
});
|
|
56
|
+
child.once('exit', (code) => resolve(code ?? 1));
|
|
57
|
+
child.once('error', (err) =>
|
|
58
|
+
reject(
|
|
59
|
+
new CliError(`could not run vitest: ${err.message}`, { hint: 'add vitest to devDependencies' }),
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
} finally {
|
|
64
|
+
dispatcher?.stop();
|
|
65
|
+
worker?.stop();
|
|
66
|
+
await stage.stop();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Local worker host for `plat dev` / `plat test`. The AWS Emulator's Lambda
|
|
2
|
+
// runtime is an embedded JS sandbox without node:http/https/net/async_hooks,
|
|
3
|
+
// so a worker that talks to AWS (through the SDK) can't run inside it. This
|
|
4
|
+
// process runs the worker in real Node instead: it builds the same handler the
|
|
5
|
+
// Lambda would (`createWorkerHandler(def)` from the app's SDK) and accepts
|
|
6
|
+
// Lambda-shaped events over a loopback HTTP endpoint. The CLI feeds it SQS
|
|
7
|
+
// messages, EventBridge events and crons from the emulator.
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { createServer } from 'node:http';
|
|
10
|
+
import { pathToFileURL } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const entry = process.env.PLATFORM_WORKER_ENTRY;
|
|
13
|
+
const sdkWorker = process.env.PLATFORM_SDK_WORKER ?? '@ericminassian/platform/worker';
|
|
14
|
+
const port = Number(process.env.PLATFORM_WORKER_PORT);
|
|
15
|
+
const timeoutMs = Number(process.env.PLATFORM_WORKER_TIMEOUT_MS ?? 60_000);
|
|
16
|
+
const functionName = process.env.PLATFORM_WORKER_FUNCTION ?? 'local-worker';
|
|
17
|
+
if (!entry || !port) {
|
|
18
|
+
console.error('PLATFORM_WORKER_ENTRY / PLATFORM_WORKER_PORT not set (this file is started by `plat dev`)');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const mod = (await import(pathToFileURL(entry).href)) as { default?: unknown };
|
|
23
|
+
if (!mod.default) {
|
|
24
|
+
console.error(
|
|
25
|
+
`${entry} has no default export; export your worker (\`export default defineWorker({...})\`)`,
|
|
26
|
+
);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const sdkUrl = sdkWorker.startsWith('/') ? pathToFileURL(sdkWorker).href : sdkWorker;
|
|
30
|
+
const { createWorkerHandler } = (await import(sdkUrl)) as typeof import('@ericminassian/platform/worker');
|
|
31
|
+
const handler = createWorkerHandler(mod.default as Parameters<typeof createWorkerHandler>[0]);
|
|
32
|
+
|
|
33
|
+
createServer((req, res) => {
|
|
34
|
+
let body = '';
|
|
35
|
+
req.setEncoding('utf8');
|
|
36
|
+
req.on('data', (c) => (body += c));
|
|
37
|
+
req.on('end', async () => {
|
|
38
|
+
const started = Date.now();
|
|
39
|
+
const context = {
|
|
40
|
+
awsRequestId: randomUUID(),
|
|
41
|
+
functionName,
|
|
42
|
+
functionVersion: '$LATEST',
|
|
43
|
+
invokedFunctionArn: `arn:aws:lambda:${process.env.AWS_REGION ?? 'us-east-1'}:000000000000:function:${functionName}`,
|
|
44
|
+
memoryLimitInMB: '512',
|
|
45
|
+
logGroupName: `/aws/lambda/${functionName}`,
|
|
46
|
+
logStreamName: 'local',
|
|
47
|
+
callbackWaitsForEmptyEventLoop: false,
|
|
48
|
+
getRemainingTimeInMillis: () => Math.max(0, started + timeoutMs - Date.now()),
|
|
49
|
+
};
|
|
50
|
+
let reply: unknown;
|
|
51
|
+
try {
|
|
52
|
+
reply = { ok: true, result: (await handler(JSON.parse(body || '{}'), context)) ?? null };
|
|
53
|
+
} catch (err) {
|
|
54
|
+
const e = err as Error;
|
|
55
|
+
reply = {
|
|
56
|
+
ok: false,
|
|
57
|
+
error: { name: e?.name ?? 'Error', message: e?.message ?? String(err), stack: e?.stack },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(reply));
|
|
61
|
+
});
|
|
62
|
+
}).listen(port, '127.0.0.1');
|