@sigil-dev/grimoire 0.7.4 → 0.7.6
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/.grimoire/_routes.dom.js +8 -0
- package/.grimoire/_routes.hydrate.js +8 -0
- package/.grimoire/tsconfig.generated.json +11 -0
- package/.grimoire/types/ambient.d.ts +59 -0
- package/.grimoire/types/api/hello/$types.d.ts +50 -0
- package/.grimoire/types/api/items/$types.d.ts +50 -0
- package/.grimoire/types/echo/$types.d.ts +50 -0
- package/.grimoire/types/env-private.d.ts +5 -0
- package/.grimoire/types/env-public.d.ts +5 -0
- package/.grimoire/types/mixed/$types.d.ts +50 -0
- package/.grimoire/types/params/[docId]/$types.d.ts +52 -0
- package/.grimoire/types/reject/$types.d.ts +50 -0
- package/index.ts +34 -34
- package/package.json +8 -4
- package/preload.js +2 -0
- package/public/__grimoire__/hydrate.js +585 -0
- package/public/__grimoire__/index.js +490 -0
- package/src/client/head.ts +29 -0
- package/src/client/router.ts +224 -76
- package/src/env/index.ts +25 -0
- package/src/env/plugin.ts +13 -0
- package/src/env/private.ts +5 -0
- package/src/env/public.ts +7 -0
- package/src/env/typegen.ts +51 -0
- package/src/integrations/vite.ts +72 -72
- package/src/rendering/head.ts +22 -2
- package/src/rendering/hydrate.ts +81 -26
- package/src/rendering/index.ts +199 -186
- package/src/rendering/ssrPlugin.ts +53 -42
- package/src/routing/manifest-gen.ts +39 -26
- package/src/routing/router.ts +106 -98
- package/src/routing/scanner.ts +135 -129
- package/src/routing/transform-routes.ts +101 -96
- package/src/server/build.ts +147 -90
- package/src/server/coordinator.ts +306 -297
- package/src/server/hooks.ts +24 -3
- package/src/server/index.ts +148 -71
- package/src/server/worker.ts +59 -59
- package/src/typegen/index.ts +353 -340
- package/src/types.ts +269 -260
- package/test/context.test.ts +52 -52
- package/test/hydration.test.ts +119 -119
- package/test/middleware.test.ts +223 -221
- package/test/rendering.test.ts +425 -425
- package/test/routing.test.ts +83 -45
- package/test/scanning.test.ts +181 -169
- package/test/server.test.ts +229 -229
- package/test/streaming.test.ts +106 -106
- package/test/transform-routes.test.ts +84 -84
- package/test/typegen.test.ts +19 -1
package/src/server/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { isAbsolute, join } from "node:path";
|
|
2
|
+
import { plugin as bunPlugin, type ServerWebSocket } from "bun";
|
|
2
3
|
import { renderRoute } from "../rendering";
|
|
3
4
|
import { registerSSRPlugin } from "../rendering/ssrPlugin";
|
|
4
5
|
import { findClosestError, matchRoute } from "../routing/router";
|
|
@@ -10,6 +11,8 @@ import { buildProject } from "./build";
|
|
|
10
11
|
import { createCookies } from "./cookie-utils";
|
|
11
12
|
import type {
|
|
12
13
|
Handle,
|
|
14
|
+
HandleError,
|
|
15
|
+
HandleFetch,
|
|
13
16
|
InitFunction,
|
|
14
17
|
RequestEvent,
|
|
15
18
|
ResolveFunction,
|
|
@@ -17,15 +20,23 @@ import type {
|
|
|
17
20
|
import { runDeserializeLocals, runHook, runRequestHooks } from "./plugins";
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
|
-
* Try to load hooks.
|
|
23
|
+
* Try to load hooks.server.ts from the project root.
|
|
21
24
|
*/
|
|
22
|
-
async function loadHooks(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
async function loadHooks(projectRoot: string): Promise<{
|
|
26
|
+
handle?: Handle;
|
|
27
|
+
init?: InitFunction;
|
|
28
|
+
handleError?: HandleError;
|
|
29
|
+
handleFetch?: HandleFetch;
|
|
30
|
+
}> {
|
|
25
31
|
const hooksPath = `${projectRoot}/hooks.server.ts`;
|
|
26
32
|
try {
|
|
27
33
|
const mod = await import(hooksPath);
|
|
28
|
-
return {
|
|
34
|
+
return {
|
|
35
|
+
handle: mod.handle,
|
|
36
|
+
init: mod.init,
|
|
37
|
+
handleError: mod.handleError,
|
|
38
|
+
handleFetch: mod.handleFetch,
|
|
39
|
+
};
|
|
29
40
|
} catch {
|
|
30
41
|
// no hooks file — that's fine
|
|
31
42
|
return {};
|
|
@@ -48,16 +59,15 @@ export async function createServer(config: GrimoireConfig = {}) {
|
|
|
48
59
|
finalConfig = plugin.config?.(finalConfig) ?? finalConfig;
|
|
49
60
|
}
|
|
50
61
|
|
|
51
|
-
//w timings????
|
|
52
62
|
const {
|
|
53
63
|
port = 3000,
|
|
54
64
|
host = "localhost",
|
|
55
65
|
plugins = [],
|
|
56
66
|
routes = "src/routes",
|
|
67
|
+
cspNonce,
|
|
57
68
|
_skipBuild = false,
|
|
58
69
|
} = finalConfig;
|
|
59
70
|
|
|
60
|
-
registerSSRPlugin(plugins);
|
|
61
71
|
let tree: any;
|
|
62
72
|
if (!config._skipBuild) {
|
|
63
73
|
const { result, tree: _tree } = await buildProject(finalConfig, plugins);
|
|
@@ -65,27 +75,51 @@ export async function createServer(config: GrimoireConfig = {}) {
|
|
|
65
75
|
tree = _tree;
|
|
66
76
|
} else {
|
|
67
77
|
// worker — build already done by loom, just scan routes
|
|
68
|
-
const { isAbsolute, join } = await import("node:path");
|
|
69
|
-
const { scanRoutes } = await import("../routing/scanner");
|
|
70
78
|
const routesDir = isAbsolute(routes) ? routes : join(process.cwd(), routes);
|
|
79
|
+
const { scanRoutes } = await import("../routing/scanner");
|
|
71
80
|
tree = await scanRoutes(routesDir, process.cwd());
|
|
72
81
|
}
|
|
73
82
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
83
|
+
// SSR plugin should NOT INTNERCEPT FILES BUNDLEDFOR CLIENT
|
|
84
|
+
// WHAT ARE WE DOINGGGGGGGGG
|
|
85
|
+
registerSSRPlugin(plugins);
|
|
86
|
+
|
|
87
|
+
// Load hooks.server.ts
|
|
88
|
+
const {
|
|
89
|
+
handle: hooksHandle,
|
|
90
|
+
init: hooksInit,
|
|
91
|
+
handleError: hooksHandleError,
|
|
92
|
+
handleFetch: hooksHandleFetch,
|
|
93
|
+
} = await loadHooks(process.cwd());
|
|
78
94
|
|
|
79
95
|
// Run init hook if present
|
|
80
96
|
await hooksInit?.();
|
|
81
97
|
|
|
98
|
+
// Register alias plugin so dynamically-imported +server.ts routes can resolve custom aliases
|
|
99
|
+
const aliases = finalConfig.alias ?? {};
|
|
100
|
+
if (Object.keys(aliases).length > 0) {
|
|
101
|
+
bunPlugin({
|
|
102
|
+
name: "grimoire-alias",
|
|
103
|
+
setup(build) {
|
|
104
|
+
for (const [prefix, target] of Object.entries(aliases)) {
|
|
105
|
+
const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
106
|
+
build.onResolve(
|
|
107
|
+
{ filter: new RegExp(`^${escaped}(/|$)`) },
|
|
108
|
+
(args) => ({
|
|
109
|
+
path: args.path.replace(prefix, join(process.cwd(), target)),
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
82
117
|
const server = Bun.serve({
|
|
83
118
|
port,
|
|
84
119
|
hostname: host,
|
|
85
120
|
fetch: async (req) => {
|
|
86
|
-
// Shared locals object for this request
|
|
87
|
-
// hooks.
|
|
88
|
-
// For server/WebSocket routes, auth must use cookies/headers in upgrade().
|
|
121
|
+
// Shared locals object for this request.
|
|
122
|
+
// hooks.server.ts handle() runs for ALL routes (page, server, WebSocket).
|
|
89
123
|
// If this request came from a coordinator, deserialize its locals
|
|
90
124
|
const rawLocals = req.headers.get("X-Grimoire-Locals");
|
|
91
125
|
const locals: App.Locals = rawLocals
|
|
@@ -115,49 +149,6 @@ export async function createServer(config: GrimoireConfig = {}) {
|
|
|
115
149
|
return new Response("Not Found", { status: 404 });
|
|
116
150
|
}
|
|
117
151
|
|
|
118
|
-
// API routes (+server.ts)
|
|
119
|
-
if (matched.route.type === "server") {
|
|
120
|
-
const mod = await import(matched.route.filePath);
|
|
121
|
-
|
|
122
|
-
// WebSocket upgrade path
|
|
123
|
-
const isWsUpgrade =
|
|
124
|
-
req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
125
|
-
if (isWsUpgrade && mod.websocket) {
|
|
126
|
-
let extraData: Record<string, unknown> = {};
|
|
127
|
-
if (mod.upgrade) {
|
|
128
|
-
try {
|
|
129
|
-
const result = await mod.upgrade({
|
|
130
|
-
request: req,
|
|
131
|
-
params: matched.params,
|
|
132
|
-
url,
|
|
133
|
-
locals,
|
|
134
|
-
});
|
|
135
|
-
if (result && typeof result === "object") extraData = result;
|
|
136
|
-
} catch {
|
|
137
|
-
return new Response("Upgrade Required", { status: 426 });
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
const wsData: _WsInternalData = {
|
|
141
|
-
params: matched.params,
|
|
142
|
-
__handler: mod.websocket,
|
|
143
|
-
...extraData,
|
|
144
|
-
};
|
|
145
|
-
//@ts-expect-error i dont know what you are talking about please
|
|
146
|
-
if (server.upgrade(req, { data: wsData })) {
|
|
147
|
-
// Bun sends the 101 response — return undefined to signal no HTTP response
|
|
148
|
-
return undefined as unknown as Response;
|
|
149
|
-
}
|
|
150
|
-
return new Response("Upgrade Required", { status: 426 });
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// HTTP method dispatch
|
|
154
|
-
const handler = mod[req.method];
|
|
155
|
-
if (!handler) {
|
|
156
|
-
return new Response("Method Not Allowed", { status: 405 });
|
|
157
|
-
}
|
|
158
|
-
return handler({ request: req, params: matched.params, url, locals });
|
|
159
|
-
}
|
|
160
|
-
|
|
161
152
|
const HTTP_METHODS = [
|
|
162
153
|
"GET",
|
|
163
154
|
"POST",
|
|
@@ -179,12 +170,72 @@ export async function createServer(config: GrimoireConfig = {}) {
|
|
|
179
170
|
params: matched.params,
|
|
180
171
|
locals,
|
|
181
172
|
cookies,
|
|
173
|
+
route: { id: matched.route.path },
|
|
174
|
+
fetch: globalThis.fetch,
|
|
182
175
|
setHeaders: (headers) => Object.assign(setHeadersMap, headers),
|
|
183
176
|
};
|
|
184
177
|
|
|
185
|
-
//
|
|
178
|
+
// Patch event.fetch if handleFetch hook is defined
|
|
179
|
+
if (hooksHandleFetch) {
|
|
180
|
+
event.fetch = (reqInfo: RequestInfo | URL, init?: RequestInit) => {
|
|
181
|
+
const r = new Request(reqInfo, init);
|
|
182
|
+
return Promise.resolve(
|
|
183
|
+
hooksHandleFetch({ request: r, fetch: globalThis.fetch, event }),
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Resolve function: runs the actual route logic (server, form action, or page)
|
|
186
189
|
const resolve: ResolveFunction = async (evt) => {
|
|
187
|
-
//
|
|
190
|
+
// API routes (+server.ts) — pass through hooks chain like all other routes
|
|
191
|
+
if (matched.route.type === "server") {
|
|
192
|
+
const mod = await import(matched.route.filePath);
|
|
193
|
+
|
|
194
|
+
// WebSocket upgrade path
|
|
195
|
+
const isWsUpgrade =
|
|
196
|
+
evt.request.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
197
|
+
if (isWsUpgrade && mod.websocket) {
|
|
198
|
+
let extraData: Record<string, unknown> = {};
|
|
199
|
+
if (mod.upgrade) {
|
|
200
|
+
try {
|
|
201
|
+
const result = await mod.upgrade({
|
|
202
|
+
request: evt.request,
|
|
203
|
+
params: matched.params,
|
|
204
|
+
url: evt.url,
|
|
205
|
+
locals: evt.locals,
|
|
206
|
+
});
|
|
207
|
+
if (result && typeof result === "object") extraData = result;
|
|
208
|
+
} catch {
|
|
209
|
+
return new Response("Upgrade Required", { status: 426 });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const wsData: _WsInternalData = {
|
|
213
|
+
params: matched.params,
|
|
214
|
+
__handler: mod.websocket,
|
|
215
|
+
...extraData,
|
|
216
|
+
};
|
|
217
|
+
//@ts-expect-error i dont know what you are talking about please
|
|
218
|
+
if (server.upgrade(req, { data: wsData })) {
|
|
219
|
+
// Bun sends the 101 response — return undefined to signal no HTTP response
|
|
220
|
+
return undefined as unknown as Response;
|
|
221
|
+
}
|
|
222
|
+
return new Response("Upgrade Required", { status: 426 });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// HTTP method dispatch
|
|
226
|
+
const handler = mod[evt.request.method];
|
|
227
|
+
if (!handler) {
|
|
228
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
229
|
+
}
|
|
230
|
+
return handler({
|
|
231
|
+
request: evt.request,
|
|
232
|
+
params: matched.params,
|
|
233
|
+
url: evt.url,
|
|
234
|
+
locals: evt.locals,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// form actions (+page.server.ts)
|
|
188
239
|
if (
|
|
189
240
|
matched.pageServer &&
|
|
190
241
|
HTTP_METHODS.includes(evt.request.method as any)
|
|
@@ -264,6 +315,7 @@ export async function createServer(config: GrimoireConfig = {}) {
|
|
|
264
315
|
undefined,
|
|
265
316
|
evt.locals,
|
|
266
317
|
plugins,
|
|
318
|
+
cspNonce,
|
|
267
319
|
);
|
|
268
320
|
|
|
269
321
|
if (evt.request.headers.get("x-grimoire-navigate") === "1") {
|
|
@@ -291,17 +343,42 @@ export async function createServer(config: GrimoireConfig = {}) {
|
|
|
291
343
|
return response;
|
|
292
344
|
};
|
|
293
345
|
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
346
|
+
// Run through hooks chain, or resolve directly if no hooks defined
|
|
347
|
+
let response: Response;
|
|
348
|
+
try {
|
|
349
|
+
if (!hooksHandle) {
|
|
350
|
+
response = await resolve(event);
|
|
351
|
+
} else {
|
|
352
|
+
response = await hooksHandle({ event, resolve });
|
|
353
|
+
}
|
|
354
|
+
} catch (err) {
|
|
355
|
+
console.error(
|
|
356
|
+
`[grimoire] ${event.request.method} ${event.url.pathname} failed:`,
|
|
357
|
+
err,
|
|
358
|
+
);
|
|
359
|
+
await hooksHandleError?.({
|
|
360
|
+
error: err,
|
|
361
|
+
event,
|
|
362
|
+
status: 500,
|
|
363
|
+
message: "Internal Server Error",
|
|
364
|
+
});
|
|
365
|
+
const errorHtmlPath = `${process.cwd()}/error.html`;
|
|
366
|
+
try {
|
|
367
|
+
const errorFile = Bun.file(errorHtmlPath);
|
|
368
|
+
if (await errorFile.exists()) {
|
|
369
|
+
return new Response(errorFile, {
|
|
370
|
+
status: 500,
|
|
371
|
+
headers: { "Content-Type": "text/html" },
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
} catch {}
|
|
375
|
+
const message =
|
|
376
|
+
process.env.NODE_ENV === "production"
|
|
377
|
+
? "Internal Server Error"
|
|
378
|
+
: `${err instanceof Error ? `${err.message}\n${err.stack}` : String(err)}`;
|
|
379
|
+
return new Response(message, { status: 500 });
|
|
297
380
|
}
|
|
298
381
|
|
|
299
|
-
// Run through hooks chain
|
|
300
|
-
let response = await hooksHandle({
|
|
301
|
-
event,
|
|
302
|
-
resolve,
|
|
303
|
-
});
|
|
304
|
-
|
|
305
382
|
// Apply setHeaders
|
|
306
383
|
const setCookieHeaders = cookies.toHeaders();
|
|
307
384
|
if (
|
package/src/server/worker.ts
CHANGED
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import type { GrimoireConfig, GrimoirePlugin, WorkerMode } from "../types";
|
|
3
|
-
import { createServer } from "./index";
|
|
4
|
-
import { runDeserializeLocals } from "./plugins";
|
|
5
|
-
|
|
6
|
-
export interface WorkerOptions {
|
|
7
|
-
config: GrimoireConfig;
|
|
8
|
-
plugins: GrimoirePlugin[];
|
|
9
|
-
secret: string;
|
|
10
|
-
mode: WorkerMode;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export async function startWorker(options: WorkerOptions) {
|
|
14
|
-
const { secret, plugins } = options;
|
|
15
|
-
|
|
16
|
-
const workerAuthPlugin: GrimoirePlugin = {
|
|
17
|
-
name: "__grimoire-worker-auth",
|
|
18
|
-
async onRequest(req, next) {
|
|
19
|
-
if (req.headers.get("X-Grimoire-Internal") !== secret) {
|
|
20
|
-
return new Response("Forbidden", { status: 403 });
|
|
21
|
-
}
|
|
22
|
-
return next();
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
return createServer({
|
|
27
|
-
...options.config,
|
|
28
|
-
plugins: [workerAuthPlugin, ...plugins],
|
|
29
|
-
_skipBuild: true,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// bootstrap when spawned by coordinator
|
|
34
|
-
if (process.env.GRIMOIRE_INTERNAL_SECRET) {
|
|
35
|
-
const secret = process.env.GRIMOIRE_INTERNAL_SECRET;
|
|
36
|
-
const port = Number(process.env.GRIMOIRE_WORKER_PORT ?? 3001);
|
|
37
|
-
const cwd = process.cwd();
|
|
38
|
-
|
|
39
|
-
let config: GrimoireConfig = {};
|
|
40
|
-
try {
|
|
41
|
-
const mod = await import(join(cwd, "sigil.config.ts"));
|
|
42
|
-
config = mod.default ?? {};
|
|
43
|
-
} catch {}
|
|
44
|
-
|
|
45
|
-
let finalConfig: GrimoireConfig = { ...config, port, host: "127.0.0.1" };
|
|
46
|
-
const plugins = finalConfig.plugins ?? [];
|
|
47
|
-
for (const plugin of plugins) {
|
|
48
|
-
finalConfig = plugin.config?.(finalConfig) ?? finalConfig;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
await startWorker({
|
|
52
|
-
config: finalConfig,
|
|
53
|
-
plugins,
|
|
54
|
-
secret,
|
|
55
|
-
mode: (process.env.GRIMOIRE_MODE ?? "full") as WorkerMode,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
process.send?.({ ready: true });
|
|
59
|
-
}
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { GrimoireConfig, GrimoirePlugin, WorkerMode } from "../types";
|
|
3
|
+
import { createServer } from "./index";
|
|
4
|
+
import { runDeserializeLocals } from "./plugins";
|
|
5
|
+
|
|
6
|
+
export interface WorkerOptions {
|
|
7
|
+
config: GrimoireConfig;
|
|
8
|
+
plugins: GrimoirePlugin[];
|
|
9
|
+
secret: string;
|
|
10
|
+
mode: WorkerMode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function startWorker(options: WorkerOptions) {
|
|
14
|
+
const { secret, plugins } = options;
|
|
15
|
+
|
|
16
|
+
const workerAuthPlugin: GrimoirePlugin = {
|
|
17
|
+
name: "__grimoire-worker-auth",
|
|
18
|
+
async onRequest(req, next) {
|
|
19
|
+
if (req.headers.get("X-Grimoire-Internal") !== secret) {
|
|
20
|
+
return new Response("Forbidden", { status: 403 });
|
|
21
|
+
}
|
|
22
|
+
return next();
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return createServer({
|
|
27
|
+
...options.config,
|
|
28
|
+
plugins: [workerAuthPlugin, ...plugins],
|
|
29
|
+
_skipBuild: true,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// bootstrap when spawned by coordinator
|
|
34
|
+
if (process.env.GRIMOIRE_INTERNAL_SECRET) {
|
|
35
|
+
const secret = process.env.GRIMOIRE_INTERNAL_SECRET;
|
|
36
|
+
const port = Number(process.env.GRIMOIRE_WORKER_PORT ?? 3001);
|
|
37
|
+
const cwd = process.cwd();
|
|
38
|
+
|
|
39
|
+
let config: GrimoireConfig = {};
|
|
40
|
+
try {
|
|
41
|
+
const mod = await import(join(cwd, "sigil.config.ts"));
|
|
42
|
+
config = mod.default ?? {};
|
|
43
|
+
} catch {}
|
|
44
|
+
|
|
45
|
+
let finalConfig: GrimoireConfig = { ...config, port, host: "127.0.0.1" };
|
|
46
|
+
const plugins = finalConfig.plugins ?? [];
|
|
47
|
+
for (const plugin of plugins) {
|
|
48
|
+
finalConfig = plugin.config?.(finalConfig) ?? finalConfig;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await startWorker({
|
|
52
|
+
config: finalConfig,
|
|
53
|
+
plugins,
|
|
54
|
+
secret,
|
|
55
|
+
mode: (process.env.GRIMOIRE_MODE ?? "full") as WorkerMode,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
process.send?.({ ready: true });
|
|
59
|
+
}
|