@astroscope/boot 0.1.3 → 0.2.1
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 +61 -3
- package/dist/index.cjs +89 -44
- package/dist/index.d.cts +2 -24
- package/dist/index.d.ts +2 -24
- package/dist/index.js +84 -45
- package/dist/types-CxpusND2.d.cts +18 -0
- package/dist/types-CxpusND2.d.ts +18 -0
- package/dist/warmup.cjs +79 -0
- package/dist/warmup.d.cts +22 -0
- package/dist/warmup.d.ts +22 -0
- package/dist/warmup.js +53 -0
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -20,15 +20,17 @@ npm install @astroscope/boot
|
|
|
20
20
|
|
|
21
21
|
```ts
|
|
22
22
|
// src/boot.ts
|
|
23
|
-
|
|
23
|
+
import type { BootContext } from "@astroscope/boot";
|
|
24
|
+
|
|
25
|
+
export async function onStartup({ dev, host, port }: BootContext) {
|
|
24
26
|
console.log("Starting up...");
|
|
25
27
|
|
|
26
28
|
await someAsyncInitialization();
|
|
27
29
|
|
|
28
|
-
console.log(
|
|
30
|
+
console.log(`Ready at ${host}:${port} (dev: ${dev})`);
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
export async function onShutdown() {
|
|
33
|
+
export async function onShutdown({ dev }: BootContext) {
|
|
32
34
|
console.log("Shutting down...");
|
|
33
35
|
|
|
34
36
|
await closeConnections();
|
|
@@ -50,6 +52,23 @@ export default defineConfig({
|
|
|
50
52
|
});
|
|
51
53
|
```
|
|
52
54
|
|
|
55
|
+
## Boot Context
|
|
56
|
+
|
|
57
|
+
Both `onStartup` and `onShutdown` receive a `BootContext` object:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
interface BootContext {
|
|
61
|
+
/** Whether running in development mode */
|
|
62
|
+
dev: boolean;
|
|
63
|
+
/** Server host (from Astro config or HOST env var) */
|
|
64
|
+
host: string;
|
|
65
|
+
/** Server port (from Astro config or PORT env var) */
|
|
66
|
+
port: number;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
In development, `host` and `port` are read from the actual server address. In production, they default to Astro config values but can be overridden via `HOST` and `PORT` environment variables at runtime.
|
|
71
|
+
|
|
53
72
|
## Lifecycle Hooks
|
|
54
73
|
|
|
55
74
|
### `onStartup`
|
|
@@ -70,6 +89,45 @@ Called when the server is shutting down (SIGTERM in production, server close in
|
|
|
70
89
|
- Cleaning up resources
|
|
71
90
|
- Graceful shutdown of external services
|
|
72
91
|
|
|
92
|
+
## V8 Warmup
|
|
93
|
+
|
|
94
|
+
The package includes a warmup utility that pre-imports all page modules and middleware to warm up the V8 JIT compiler, reducing cold start latency for the first requests.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// src/boot.ts
|
|
98
|
+
import type { BootContext } from "@astroscope/boot";
|
|
99
|
+
import { warmup } from "@astroscope/boot/warmup";
|
|
100
|
+
|
|
101
|
+
export async function onStartup({ host, port }: BootContext) {
|
|
102
|
+
const result = await warmup();
|
|
103
|
+
|
|
104
|
+
if (result.success.length > 0) {
|
|
105
|
+
console.log(`Warmed up ${result.success.length} modules in ${result.duration}ms`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (result.failed.length > 0) {
|
|
109
|
+
console.warn(`Failed to warm up: ${result.failed.join(", ")}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(`Server ready at ${host}:${port}`);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### `WarmupResult`
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
interface WarmupResult {
|
|
120
|
+
/** Modules that were successfully loaded */
|
|
121
|
+
success: string[];
|
|
122
|
+
/** Modules that failed to load */
|
|
123
|
+
failed: string[];
|
|
124
|
+
/** Time taken in milliseconds */
|
|
125
|
+
duration: number;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
In development mode, `warmup()` is a no-op that returns empty results. In production, it reads a manifest generated during the build and imports all discovered page modules and middleware in parallel.
|
|
130
|
+
|
|
73
131
|
## Options
|
|
74
132
|
|
|
75
133
|
### `entry`
|
package/dist/index.cjs
CHANGED
|
@@ -30,11 +30,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
boot: () => boot,
|
|
33
34
|
default: () => boot
|
|
34
35
|
});
|
|
35
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
|
|
38
|
+
// src/integration.ts
|
|
36
39
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
37
40
|
var import_node_path = __toESM(require("path"), 1);
|
|
41
|
+
var import_magic_string = __toESM(require("magic-string"), 1);
|
|
38
42
|
|
|
39
43
|
// src/ignored.ts
|
|
40
44
|
var ignoredSuffixes = [
|
|
@@ -79,47 +83,67 @@ var ignoredSuffixes = [
|
|
|
79
83
|
".less"
|
|
80
84
|
];
|
|
81
85
|
|
|
82
|
-
// src/
|
|
86
|
+
// src/integration.ts
|
|
83
87
|
function resolveEntry(entry) {
|
|
84
88
|
if (entry) return entry;
|
|
85
|
-
if (import_node_fs.default.existsSync("src/boot.ts")) return "src/boot.ts";
|
|
86
89
|
if (import_node_fs.default.existsSync("src/boot/index.ts")) return "src/boot/index.ts";
|
|
87
90
|
return "src/boot.ts";
|
|
88
91
|
}
|
|
92
|
+
var WARMUP_MANIFEST_FILE = "warmup-manifest.json";
|
|
93
|
+
function getServerDefaults(config) {
|
|
94
|
+
return {
|
|
95
|
+
host: typeof config?.server?.host === "string" ? config.server.host : config?.server?.host === true ? "0.0.0.0" : "localhost",
|
|
96
|
+
port: config?.server?.port ?? 4321
|
|
97
|
+
};
|
|
98
|
+
}
|
|
89
99
|
function boot(options = {}) {
|
|
90
100
|
const entry = resolveEntry(options.entry);
|
|
91
101
|
const hmr = options.hmr ?? false;
|
|
92
102
|
let isBuild = false;
|
|
93
103
|
let isSSR = false;
|
|
94
104
|
let bootChunkRef = null;
|
|
105
|
+
let astroConfig = null;
|
|
106
|
+
let pageModules = [];
|
|
107
|
+
let middlewarePath = null;
|
|
95
108
|
return {
|
|
96
109
|
name: "@astroscope/boot",
|
|
97
110
|
hooks: {
|
|
98
|
-
"astro:config:setup": ({ command, updateConfig, logger }) => {
|
|
111
|
+
"astro:config:setup": ({ command, updateConfig, logger, config }) => {
|
|
99
112
|
isBuild = command === "build";
|
|
113
|
+
astroConfig = config;
|
|
100
114
|
updateConfig({
|
|
101
115
|
vite: {
|
|
102
116
|
plugins: [
|
|
103
117
|
{
|
|
104
118
|
name: "@astroscope/boot",
|
|
119
|
+
enforce: "pre",
|
|
105
120
|
configureServer(server) {
|
|
106
121
|
if (isBuild) return;
|
|
122
|
+
const getBootContext = () => {
|
|
123
|
+
const addr = server.httpServer?.address();
|
|
124
|
+
if (addr && typeof addr === "object") {
|
|
125
|
+
const host2 = addr.address === "::" || addr.address === "0.0.0.0" ? "localhost" : addr.address;
|
|
126
|
+
return { dev: true, host: host2, port: addr.port };
|
|
127
|
+
}
|
|
128
|
+
const defaults = getServerDefaults(astroConfig);
|
|
129
|
+
const host = process.env["HOST"] ?? defaults.host;
|
|
130
|
+
const port = process.env["PORT"] ? Number(process.env["PORT"]) : defaults.port;
|
|
131
|
+
return { dev: true, host, port };
|
|
132
|
+
};
|
|
107
133
|
server.httpServer?.once("listening", async () => {
|
|
108
134
|
try {
|
|
135
|
+
const bootContext = getBootContext();
|
|
109
136
|
const module2 = await server.ssrLoadModule(`/${entry}`);
|
|
110
|
-
|
|
111
|
-
await module2.onStartup();
|
|
112
|
-
}
|
|
137
|
+
await module2.onStartup?.(bootContext);
|
|
113
138
|
} catch (error) {
|
|
114
139
|
logger.error(`Error running startup script: ${error}`);
|
|
115
140
|
}
|
|
116
141
|
});
|
|
117
142
|
server.httpServer?.once("close", async () => {
|
|
118
143
|
try {
|
|
144
|
+
const bootContext = getBootContext();
|
|
119
145
|
const module2 = await server.ssrLoadModule(`/${entry}`);
|
|
120
|
-
|
|
121
|
-
await module2.onShutdown();
|
|
122
|
-
}
|
|
146
|
+
await module2.onShutdown?.(bootContext);
|
|
123
147
|
} catch (error) {
|
|
124
148
|
logger.error(`Error running shutdown script: ${error}`);
|
|
125
149
|
}
|
|
@@ -146,22 +170,19 @@ function boot(options = {}) {
|
|
|
146
170
|
const rerunBoot = async (changedFile) => {
|
|
147
171
|
logger.info(`boot dependency changed: ${changedFile}, rerunning hooks...`);
|
|
148
172
|
try {
|
|
173
|
+
const bootContext = getBootContext();
|
|
149
174
|
const oldModule = await server.ssrLoadModule(bootModuleId);
|
|
150
|
-
|
|
151
|
-
await oldModule.onShutdown();
|
|
152
|
-
}
|
|
175
|
+
await oldModule.onShutdown?.(bootContext);
|
|
153
176
|
server.moduleGraph.invalidateAll();
|
|
154
177
|
const newModule = await server.ssrLoadModule(bootModuleId);
|
|
155
|
-
|
|
156
|
-
await newModule.onStartup();
|
|
157
|
-
}
|
|
178
|
+
await newModule.onStartup?.(bootContext);
|
|
158
179
|
} catch (error) {
|
|
159
180
|
logger.error(`Error during boot HMR: ${error}`);
|
|
160
181
|
}
|
|
161
182
|
};
|
|
162
183
|
const shouldIgnore = (filePath) => {
|
|
163
|
-
const
|
|
164
|
-
return ignoredSuffixes.some((suffix) =>
|
|
184
|
+
const p = filePath.toLowerCase();
|
|
185
|
+
return ignoredSuffixes.some((suffix) => p.endsWith(suffix));
|
|
165
186
|
};
|
|
166
187
|
server.watcher.on("change", async (changedPath) => {
|
|
167
188
|
if (shouldIgnore(changedPath)) return;
|
|
@@ -172,48 +193,68 @@ function boot(options = {}) {
|
|
|
172
193
|
});
|
|
173
194
|
}
|
|
174
195
|
},
|
|
175
|
-
configResolved(
|
|
176
|
-
isSSR = !!
|
|
196
|
+
configResolved(config2) {
|
|
197
|
+
isSSR = !!config2.build?.ssr;
|
|
177
198
|
},
|
|
178
199
|
buildStart() {
|
|
179
200
|
if (!isSSR) return;
|
|
180
201
|
try {
|
|
181
|
-
bootChunkRef = this.emitFile({
|
|
182
|
-
type: "chunk",
|
|
183
|
-
id: entry,
|
|
184
|
-
name: "boot"
|
|
185
|
-
});
|
|
202
|
+
bootChunkRef = this.emitFile({ type: "chunk", id: entry, name: "boot" });
|
|
186
203
|
} catch {
|
|
187
204
|
}
|
|
188
205
|
},
|
|
189
|
-
|
|
190
|
-
if (!isSSR) return;
|
|
191
|
-
const outDir = outputOptions.dir;
|
|
192
|
-
if (!outDir || !bootChunkRef) return;
|
|
193
|
-
const entryPath = import_node_path.default.join(outDir, "entry.mjs");
|
|
194
|
-
if (!import_node_fs.default.existsSync(entryPath)) {
|
|
195
|
-
logger.warn("entry.mjs not found - boot injection skipped");
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
206
|
+
generateBundle(_, bundle) {
|
|
207
|
+
if (!isSSR || !bootChunkRef) return;
|
|
198
208
|
const bootChunkName = this.getFileName(bootChunkRef);
|
|
199
209
|
if (!bootChunkName) {
|
|
200
210
|
logger.warn("boot chunk not found");
|
|
201
211
|
return;
|
|
202
212
|
}
|
|
203
|
-
const
|
|
204
|
-
if (
|
|
205
|
-
logger.warn(
|
|
206
|
-
|
|
207
|
-
|
|
213
|
+
const entryChunk = bundle["entry.mjs"];
|
|
214
|
+
if (!entryChunk || entryChunk.type !== "chunk") {
|
|
215
|
+
logger.warn("entry.mjs not found - boot injection skipped");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
pageModules = [];
|
|
219
|
+
middlewarePath = null;
|
|
220
|
+
for (const [fileName, chunk] of Object.entries(bundle)) {
|
|
221
|
+
if (chunk.type !== "chunk") continue;
|
|
222
|
+
if (fileName.startsWith("pages/") && fileName.endsWith(".mjs")) {
|
|
223
|
+
pageModules.push(fileName);
|
|
224
|
+
}
|
|
225
|
+
if (fileName.includes("_astro-internal_middleware") || fileName.includes("_noop-middleware")) {
|
|
226
|
+
middlewarePath = fileName;
|
|
227
|
+
}
|
|
208
228
|
}
|
|
209
|
-
|
|
210
|
-
const bootImport = `
|
|
211
|
-
|
|
212
|
-
|
|
229
|
+
const { host, port } = getServerDefaults(astroConfig);
|
|
230
|
+
const bootImport = `globalThis.__astroscope_server_url = import.meta.url;
|
|
231
|
+
import * as __boot from './${bootChunkName}';
|
|
232
|
+
const __bootContext = { dev: false, host: process.env.HOST ?? '${host}', port: process.env.PORT ? Number(process.env.PORT) : ${port} };
|
|
233
|
+
await __boot.onStartup?.(__bootContext);
|
|
234
|
+
if (__boot.onShutdown) process.on('SIGTERM', async () => { await __boot.onShutdown(__bootContext); process.exit(0); });
|
|
213
235
|
`;
|
|
214
|
-
|
|
215
|
-
|
|
236
|
+
const s = new import_magic_string.default(entryChunk.code);
|
|
237
|
+
s.prepend(bootImport);
|
|
238
|
+
entryChunk.code = s.toString();
|
|
239
|
+
if (entryChunk.map) {
|
|
240
|
+
entryChunk.map = s.generateMap({ hires: true });
|
|
241
|
+
}
|
|
216
242
|
logger.info(`injected ${bootChunkName} into entry.mjs`);
|
|
243
|
+
},
|
|
244
|
+
writeBundle(outputOptions) {
|
|
245
|
+
if (!isSSR) return;
|
|
246
|
+
const outDir = outputOptions.dir;
|
|
247
|
+
if (!outDir) return;
|
|
248
|
+
const modules = [];
|
|
249
|
+
if (middlewarePath) {
|
|
250
|
+
modules.push(`./${middlewarePath}`);
|
|
251
|
+
}
|
|
252
|
+
for (const page of pageModules) {
|
|
253
|
+
modules.push(`./${page}`);
|
|
254
|
+
}
|
|
255
|
+
const manifestPath = import_node_path.default.join(outDir, "chunks", WARMUP_MANIFEST_FILE);
|
|
256
|
+
import_node_fs.default.writeFileSync(manifestPath, JSON.stringify({ modules }));
|
|
257
|
+
logger.info(`generated warmup for ${pageModules.length} pages`);
|
|
217
258
|
}
|
|
218
259
|
}
|
|
219
260
|
]
|
|
@@ -223,3 +264,7 @@ if (__boot.onShutdown) process.on('SIGTERM', async () => { await __boot.onShutdo
|
|
|
223
264
|
}
|
|
224
265
|
};
|
|
225
266
|
}
|
|
267
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
268
|
+
0 && (module.exports = {
|
|
269
|
+
boot
|
|
270
|
+
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AstroIntegration } from 'astro';
|
|
2
|
+
export { B as BootContext, W as WarmupResult } from './types-CxpusND2.cjs';
|
|
2
3
|
|
|
3
4
|
interface BootOptions {
|
|
4
5
|
/**
|
|
@@ -17,30 +18,7 @@ interface BootOptions {
|
|
|
17
18
|
*
|
|
18
19
|
* Runs `onStartup` and `onShutdown` functions exported from your boot file
|
|
19
20
|
* during server startup and shutdown.
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* ```ts
|
|
23
|
-
* // astro.config.ts
|
|
24
|
-
* import { defineConfig } from "astro/config";
|
|
25
|
-
* import boot from "@astroscope/boot";
|
|
26
|
-
*
|
|
27
|
-
* export default defineConfig({
|
|
28
|
-
* integrations: [boot()],
|
|
29
|
-
* });
|
|
30
|
-
* ```
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* ```ts
|
|
34
|
-
* // src/boot.ts
|
|
35
|
-
* export async function onStartup() {
|
|
36
|
-
* console.log("Server starting...");
|
|
37
|
-
* }
|
|
38
|
-
*
|
|
39
|
-
* export async function onShutdown() {
|
|
40
|
-
* console.log("Server shutting down...");
|
|
41
|
-
* }
|
|
42
|
-
* ```
|
|
43
21
|
*/
|
|
44
22
|
declare function boot(options?: BootOptions): AstroIntegration;
|
|
45
23
|
|
|
46
|
-
export { type BootOptions, boot as default };
|
|
24
|
+
export { type BootOptions, boot, boot as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AstroIntegration } from 'astro';
|
|
2
|
+
export { B as BootContext, W as WarmupResult } from './types-CxpusND2.js';
|
|
2
3
|
|
|
3
4
|
interface BootOptions {
|
|
4
5
|
/**
|
|
@@ -17,30 +18,7 @@ interface BootOptions {
|
|
|
17
18
|
*
|
|
18
19
|
* Runs `onStartup` and `onShutdown` functions exported from your boot file
|
|
19
20
|
* during server startup and shutdown.
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* ```ts
|
|
23
|
-
* // astro.config.ts
|
|
24
|
-
* import { defineConfig } from "astro/config";
|
|
25
|
-
* import boot from "@astroscope/boot";
|
|
26
|
-
*
|
|
27
|
-
* export default defineConfig({
|
|
28
|
-
* integrations: [boot()],
|
|
29
|
-
* });
|
|
30
|
-
* ```
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* ```ts
|
|
34
|
-
* // src/boot.ts
|
|
35
|
-
* export async function onStartup() {
|
|
36
|
-
* console.log("Server starting...");
|
|
37
|
-
* }
|
|
38
|
-
*
|
|
39
|
-
* export async function onShutdown() {
|
|
40
|
-
* console.log("Server shutting down...");
|
|
41
|
-
* }
|
|
42
|
-
* ```
|
|
43
21
|
*/
|
|
44
22
|
declare function boot(options?: BootOptions): AstroIntegration;
|
|
45
23
|
|
|
46
|
-
export { type BootOptions, boot as default };
|
|
24
|
+
export { type BootOptions, boot, boot as default };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/integration.ts
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import MagicString from "magic-string";
|
|
4
5
|
|
|
5
6
|
// src/ignored.ts
|
|
6
7
|
var ignoredSuffixes = [
|
|
@@ -45,47 +46,67 @@ var ignoredSuffixes = [
|
|
|
45
46
|
".less"
|
|
46
47
|
];
|
|
47
48
|
|
|
48
|
-
// src/
|
|
49
|
+
// src/integration.ts
|
|
49
50
|
function resolveEntry(entry) {
|
|
50
51
|
if (entry) return entry;
|
|
51
|
-
if (fs.existsSync("src/boot.ts")) return "src/boot.ts";
|
|
52
52
|
if (fs.existsSync("src/boot/index.ts")) return "src/boot/index.ts";
|
|
53
53
|
return "src/boot.ts";
|
|
54
54
|
}
|
|
55
|
+
var WARMUP_MANIFEST_FILE = "warmup-manifest.json";
|
|
56
|
+
function getServerDefaults(config) {
|
|
57
|
+
return {
|
|
58
|
+
host: typeof config?.server?.host === "string" ? config.server.host : config?.server?.host === true ? "0.0.0.0" : "localhost",
|
|
59
|
+
port: config?.server?.port ?? 4321
|
|
60
|
+
};
|
|
61
|
+
}
|
|
55
62
|
function boot(options = {}) {
|
|
56
63
|
const entry = resolveEntry(options.entry);
|
|
57
64
|
const hmr = options.hmr ?? false;
|
|
58
65
|
let isBuild = false;
|
|
59
66
|
let isSSR = false;
|
|
60
67
|
let bootChunkRef = null;
|
|
68
|
+
let astroConfig = null;
|
|
69
|
+
let pageModules = [];
|
|
70
|
+
let middlewarePath = null;
|
|
61
71
|
return {
|
|
62
72
|
name: "@astroscope/boot",
|
|
63
73
|
hooks: {
|
|
64
|
-
"astro:config:setup": ({ command, updateConfig, logger }) => {
|
|
74
|
+
"astro:config:setup": ({ command, updateConfig, logger, config }) => {
|
|
65
75
|
isBuild = command === "build";
|
|
76
|
+
astroConfig = config;
|
|
66
77
|
updateConfig({
|
|
67
78
|
vite: {
|
|
68
79
|
plugins: [
|
|
69
80
|
{
|
|
70
81
|
name: "@astroscope/boot",
|
|
82
|
+
enforce: "pre",
|
|
71
83
|
configureServer(server) {
|
|
72
84
|
if (isBuild) return;
|
|
85
|
+
const getBootContext = () => {
|
|
86
|
+
const addr = server.httpServer?.address();
|
|
87
|
+
if (addr && typeof addr === "object") {
|
|
88
|
+
const host2 = addr.address === "::" || addr.address === "0.0.0.0" ? "localhost" : addr.address;
|
|
89
|
+
return { dev: true, host: host2, port: addr.port };
|
|
90
|
+
}
|
|
91
|
+
const defaults = getServerDefaults(astroConfig);
|
|
92
|
+
const host = process.env["HOST"] ?? defaults.host;
|
|
93
|
+
const port = process.env["PORT"] ? Number(process.env["PORT"]) : defaults.port;
|
|
94
|
+
return { dev: true, host, port };
|
|
95
|
+
};
|
|
73
96
|
server.httpServer?.once("listening", async () => {
|
|
74
97
|
try {
|
|
98
|
+
const bootContext = getBootContext();
|
|
75
99
|
const module = await server.ssrLoadModule(`/${entry}`);
|
|
76
|
-
|
|
77
|
-
await module.onStartup();
|
|
78
|
-
}
|
|
100
|
+
await module.onStartup?.(bootContext);
|
|
79
101
|
} catch (error) {
|
|
80
102
|
logger.error(`Error running startup script: ${error}`);
|
|
81
103
|
}
|
|
82
104
|
});
|
|
83
105
|
server.httpServer?.once("close", async () => {
|
|
84
106
|
try {
|
|
107
|
+
const bootContext = getBootContext();
|
|
85
108
|
const module = await server.ssrLoadModule(`/${entry}`);
|
|
86
|
-
|
|
87
|
-
await module.onShutdown();
|
|
88
|
-
}
|
|
109
|
+
await module.onShutdown?.(bootContext);
|
|
89
110
|
} catch (error) {
|
|
90
111
|
logger.error(`Error running shutdown script: ${error}`);
|
|
91
112
|
}
|
|
@@ -112,22 +133,19 @@ function boot(options = {}) {
|
|
|
112
133
|
const rerunBoot = async (changedFile) => {
|
|
113
134
|
logger.info(`boot dependency changed: ${changedFile}, rerunning hooks...`);
|
|
114
135
|
try {
|
|
136
|
+
const bootContext = getBootContext();
|
|
115
137
|
const oldModule = await server.ssrLoadModule(bootModuleId);
|
|
116
|
-
|
|
117
|
-
await oldModule.onShutdown();
|
|
118
|
-
}
|
|
138
|
+
await oldModule.onShutdown?.(bootContext);
|
|
119
139
|
server.moduleGraph.invalidateAll();
|
|
120
140
|
const newModule = await server.ssrLoadModule(bootModuleId);
|
|
121
|
-
|
|
122
|
-
await newModule.onStartup();
|
|
123
|
-
}
|
|
141
|
+
await newModule.onStartup?.(bootContext);
|
|
124
142
|
} catch (error) {
|
|
125
143
|
logger.error(`Error during boot HMR: ${error}`);
|
|
126
144
|
}
|
|
127
145
|
};
|
|
128
146
|
const shouldIgnore = (filePath) => {
|
|
129
|
-
const
|
|
130
|
-
return ignoredSuffixes.some((suffix) =>
|
|
147
|
+
const p = filePath.toLowerCase();
|
|
148
|
+
return ignoredSuffixes.some((suffix) => p.endsWith(suffix));
|
|
131
149
|
};
|
|
132
150
|
server.watcher.on("change", async (changedPath) => {
|
|
133
151
|
if (shouldIgnore(changedPath)) return;
|
|
@@ -138,48 +156,68 @@ function boot(options = {}) {
|
|
|
138
156
|
});
|
|
139
157
|
}
|
|
140
158
|
},
|
|
141
|
-
configResolved(
|
|
142
|
-
isSSR = !!
|
|
159
|
+
configResolved(config2) {
|
|
160
|
+
isSSR = !!config2.build?.ssr;
|
|
143
161
|
},
|
|
144
162
|
buildStart() {
|
|
145
163
|
if (!isSSR) return;
|
|
146
164
|
try {
|
|
147
|
-
bootChunkRef = this.emitFile({
|
|
148
|
-
type: "chunk",
|
|
149
|
-
id: entry,
|
|
150
|
-
name: "boot"
|
|
151
|
-
});
|
|
165
|
+
bootChunkRef = this.emitFile({ type: "chunk", id: entry, name: "boot" });
|
|
152
166
|
} catch {
|
|
153
167
|
}
|
|
154
168
|
},
|
|
155
|
-
|
|
156
|
-
if (!isSSR) return;
|
|
157
|
-
const outDir = outputOptions.dir;
|
|
158
|
-
if (!outDir || !bootChunkRef) return;
|
|
159
|
-
const entryPath = path.join(outDir, "entry.mjs");
|
|
160
|
-
if (!fs.existsSync(entryPath)) {
|
|
161
|
-
logger.warn("entry.mjs not found - boot injection skipped");
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
169
|
+
generateBundle(_, bundle) {
|
|
170
|
+
if (!isSSR || !bootChunkRef) return;
|
|
164
171
|
const bootChunkName = this.getFileName(bootChunkRef);
|
|
165
172
|
if (!bootChunkName) {
|
|
166
173
|
logger.warn("boot chunk not found");
|
|
167
174
|
return;
|
|
168
175
|
}
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
-
logger.warn(
|
|
172
|
-
|
|
173
|
-
);
|
|
176
|
+
const entryChunk = bundle["entry.mjs"];
|
|
177
|
+
if (!entryChunk || entryChunk.type !== "chunk") {
|
|
178
|
+
logger.warn("entry.mjs not found - boot injection skipped");
|
|
179
|
+
return;
|
|
174
180
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (
|
|
181
|
+
pageModules = [];
|
|
182
|
+
middlewarePath = null;
|
|
183
|
+
for (const [fileName, chunk] of Object.entries(bundle)) {
|
|
184
|
+
if (chunk.type !== "chunk") continue;
|
|
185
|
+
if (fileName.startsWith("pages/") && fileName.endsWith(".mjs")) {
|
|
186
|
+
pageModules.push(fileName);
|
|
187
|
+
}
|
|
188
|
+
if (fileName.includes("_astro-internal_middleware") || fileName.includes("_noop-middleware")) {
|
|
189
|
+
middlewarePath = fileName;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const { host, port } = getServerDefaults(astroConfig);
|
|
193
|
+
const bootImport = `globalThis.__astroscope_server_url = import.meta.url;
|
|
194
|
+
import * as __boot from './${bootChunkName}';
|
|
195
|
+
const __bootContext = { dev: false, host: process.env.HOST ?? '${host}', port: process.env.PORT ? Number(process.env.PORT) : ${port} };
|
|
196
|
+
await __boot.onStartup?.(__bootContext);
|
|
197
|
+
if (__boot.onShutdown) process.on('SIGTERM', async () => { await __boot.onShutdown(__bootContext); process.exit(0); });
|
|
179
198
|
`;
|
|
180
|
-
|
|
181
|
-
|
|
199
|
+
const s = new MagicString(entryChunk.code);
|
|
200
|
+
s.prepend(bootImport);
|
|
201
|
+
entryChunk.code = s.toString();
|
|
202
|
+
if (entryChunk.map) {
|
|
203
|
+
entryChunk.map = s.generateMap({ hires: true });
|
|
204
|
+
}
|
|
182
205
|
logger.info(`injected ${bootChunkName} into entry.mjs`);
|
|
206
|
+
},
|
|
207
|
+
writeBundle(outputOptions) {
|
|
208
|
+
if (!isSSR) return;
|
|
209
|
+
const outDir = outputOptions.dir;
|
|
210
|
+
if (!outDir) return;
|
|
211
|
+
const modules = [];
|
|
212
|
+
if (middlewarePath) {
|
|
213
|
+
modules.push(`./${middlewarePath}`);
|
|
214
|
+
}
|
|
215
|
+
for (const page of pageModules) {
|
|
216
|
+
modules.push(`./${page}`);
|
|
217
|
+
}
|
|
218
|
+
const manifestPath = path.join(outDir, "chunks", WARMUP_MANIFEST_FILE);
|
|
219
|
+
fs.writeFileSync(manifestPath, JSON.stringify({ modules }));
|
|
220
|
+
logger.info(`generated warmup for ${pageModules.length} pages`);
|
|
183
221
|
}
|
|
184
222
|
}
|
|
185
223
|
]
|
|
@@ -190,5 +228,6 @@ if (__boot.onShutdown) process.on('SIGTERM', async () => { await __boot.onShutdo
|
|
|
190
228
|
};
|
|
191
229
|
}
|
|
192
230
|
export {
|
|
231
|
+
boot,
|
|
193
232
|
boot as default
|
|
194
233
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface BootContext {
|
|
2
|
+
/** Whether running in development mode (vite dev server) */
|
|
3
|
+
dev: boolean;
|
|
4
|
+
/** Server host from Astro config */
|
|
5
|
+
host: string;
|
|
6
|
+
/** Server port from Astro config */
|
|
7
|
+
port: number;
|
|
8
|
+
}
|
|
9
|
+
interface WarmupResult {
|
|
10
|
+
/** Modules that were successfully loaded */
|
|
11
|
+
success: string[];
|
|
12
|
+
/** Modules that failed to load */
|
|
13
|
+
failed: string[];
|
|
14
|
+
/** Time taken in milliseconds */
|
|
15
|
+
duration: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type { BootContext as B, WarmupResult as W };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface BootContext {
|
|
2
|
+
/** Whether running in development mode (vite dev server) */
|
|
3
|
+
dev: boolean;
|
|
4
|
+
/** Server host from Astro config */
|
|
5
|
+
host: string;
|
|
6
|
+
/** Server port from Astro config */
|
|
7
|
+
port: number;
|
|
8
|
+
}
|
|
9
|
+
interface WarmupResult {
|
|
10
|
+
/** Modules that were successfully loaded */
|
|
11
|
+
success: string[];
|
|
12
|
+
/** Modules that failed to load */
|
|
13
|
+
failed: string[];
|
|
14
|
+
/** Time taken in milliseconds */
|
|
15
|
+
duration: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type { BootContext as B, WarmupResult as W };
|
package/dist/warmup.cjs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/warmup.ts
|
|
21
|
+
var warmup_exports = {};
|
|
22
|
+
__export(warmup_exports, {
|
|
23
|
+
warmup: () => warmup
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(warmup_exports);
|
|
26
|
+
var import_node_fs = require("fs");
|
|
27
|
+
var import_node_path = require("path");
|
|
28
|
+
var import_node_url = require("url");
|
|
29
|
+
var import_meta = {};
|
|
30
|
+
var WARMUP_MANIFEST_FILE = "warmup-manifest.json";
|
|
31
|
+
function isDevMode() {
|
|
32
|
+
return Boolean(import_meta.env?.["DEV"]);
|
|
33
|
+
}
|
|
34
|
+
function loadManifest() {
|
|
35
|
+
if (isDevMode()) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const serverUrl = globalThis.__astroscope_server_url;
|
|
39
|
+
if (!serverUrl) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const serverDir = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(serverUrl));
|
|
43
|
+
const manifestPath = (0, import_node_path.join)(serverDir, "chunks", WARMUP_MANIFEST_FILE);
|
|
44
|
+
if (!(0, import_node_fs.existsSync)(manifestPath)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const manifest = JSON.parse((0, import_node_fs.readFileSync)(manifestPath, "utf-8"));
|
|
48
|
+
return {
|
|
49
|
+
modules: manifest.modules ?? [],
|
|
50
|
+
serverDir
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async function warmup() {
|
|
54
|
+
const manifest = loadManifest();
|
|
55
|
+
if (!manifest || manifest.modules.length === 0) {
|
|
56
|
+
return { success: [], failed: [], duration: 0 };
|
|
57
|
+
}
|
|
58
|
+
const { modules, serverDir } = manifest;
|
|
59
|
+
const start = Date.now();
|
|
60
|
+
const resolvedModules = modules.map((mod) => {
|
|
61
|
+
const absolutePath = (0, import_node_path.resolve)(serverDir, mod);
|
|
62
|
+
return (0, import_node_url.pathToFileURL)(absolutePath).href;
|
|
63
|
+
});
|
|
64
|
+
const results = await Promise.allSettled(resolvedModules.map((mod) => import(mod)));
|
|
65
|
+
const success = [];
|
|
66
|
+
const failed = [];
|
|
67
|
+
for (let i = 0; i < results.length; i++) {
|
|
68
|
+
if (results[i].status === "fulfilled") {
|
|
69
|
+
success.push(modules[i]);
|
|
70
|
+
} else {
|
|
71
|
+
failed.push(modules[i]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { success, failed, duration: Date.now() - start };
|
|
75
|
+
}
|
|
76
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
77
|
+
0 && (module.exports = {
|
|
78
|
+
warmup
|
|
79
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { W as WarmupResult } from './types-CxpusND2.cjs';
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
var __astroscope_server_url: string | undefined;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Warms up V8 by importing all page modules and middleware.
|
|
8
|
+
*
|
|
9
|
+
* In development mode, this is a no-op that returns empty results.
|
|
10
|
+
* In production, reads the warmup manifest and imports all discovered modules.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { warmup } from '@astroscope/boot/warmup';
|
|
15
|
+
*
|
|
16
|
+
* const result = await warmup();
|
|
17
|
+
* console.log(`warmed up ${result.success.length} modules`);
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
declare function warmup(): Promise<WarmupResult>;
|
|
21
|
+
|
|
22
|
+
export { WarmupResult, warmup };
|
package/dist/warmup.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { W as WarmupResult } from './types-CxpusND2.js';
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
var __astroscope_server_url: string | undefined;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Warms up V8 by importing all page modules and middleware.
|
|
8
|
+
*
|
|
9
|
+
* In development mode, this is a no-op that returns empty results.
|
|
10
|
+
* In production, reads the warmup manifest and imports all discovered modules.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { warmup } from '@astroscope/boot/warmup';
|
|
15
|
+
*
|
|
16
|
+
* const result = await warmup();
|
|
17
|
+
* console.log(`warmed up ${result.success.length} modules`);
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
declare function warmup(): Promise<WarmupResult>;
|
|
21
|
+
|
|
22
|
+
export { WarmupResult, warmup };
|
package/dist/warmup.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/warmup.ts
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { dirname, join, resolve } from "path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
5
|
+
var WARMUP_MANIFEST_FILE = "warmup-manifest.json";
|
|
6
|
+
function isDevMode() {
|
|
7
|
+
return Boolean(import.meta.env?.["DEV"]);
|
|
8
|
+
}
|
|
9
|
+
function loadManifest() {
|
|
10
|
+
if (isDevMode()) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const serverUrl = globalThis.__astroscope_server_url;
|
|
14
|
+
if (!serverUrl) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const serverDir = dirname(fileURLToPath(serverUrl));
|
|
18
|
+
const manifestPath = join(serverDir, "chunks", WARMUP_MANIFEST_FILE);
|
|
19
|
+
if (!existsSync(manifestPath)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
23
|
+
return {
|
|
24
|
+
modules: manifest.modules ?? [],
|
|
25
|
+
serverDir
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function warmup() {
|
|
29
|
+
const manifest = loadManifest();
|
|
30
|
+
if (!manifest || manifest.modules.length === 0) {
|
|
31
|
+
return { success: [], failed: [], duration: 0 };
|
|
32
|
+
}
|
|
33
|
+
const { modules, serverDir } = manifest;
|
|
34
|
+
const start = Date.now();
|
|
35
|
+
const resolvedModules = modules.map((mod) => {
|
|
36
|
+
const absolutePath = resolve(serverDir, mod);
|
|
37
|
+
return pathToFileURL(absolutePath).href;
|
|
38
|
+
});
|
|
39
|
+
const results = await Promise.allSettled(resolvedModules.map((mod) => import(mod)));
|
|
40
|
+
const success = [];
|
|
41
|
+
const failed = [];
|
|
42
|
+
for (let i = 0; i < results.length; i++) {
|
|
43
|
+
if (results[i].status === "fulfilled") {
|
|
44
|
+
success.push(modules[i]);
|
|
45
|
+
} else {
|
|
46
|
+
failed.push(modules[i]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { success, failed, duration: Date.now() - start };
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
warmup
|
|
53
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astroscope/boot",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Startup and graceful shutdown hooks for Astro SSR",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"import": "./dist/index.js",
|
|
12
12
|
"require": "./dist/index.cjs"
|
|
13
|
+
},
|
|
14
|
+
"./warmup": {
|
|
15
|
+
"types": "./dist/warmup.d.ts",
|
|
16
|
+
"import": "./dist/warmup.js"
|
|
13
17
|
}
|
|
14
18
|
},
|
|
15
19
|
"files": [
|
|
@@ -37,17 +41,20 @@
|
|
|
37
41
|
},
|
|
38
42
|
"homepage": "https://github.com/smnbbrv/astroscope/tree/main/packages/boot#readme",
|
|
39
43
|
"scripts": {
|
|
40
|
-
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
44
|
+
"build": "tsup src/index.ts src/warmup.ts --format esm,cjs --dts",
|
|
41
45
|
"typecheck": "tsc --noEmit",
|
|
42
46
|
"lint": "eslint 'src/**/*.{ts,tsx}'",
|
|
43
47
|
"lint:fix": "eslint 'src/**/*.{ts,tsx}' --fix"
|
|
44
48
|
},
|
|
45
49
|
"devDependencies": {
|
|
46
|
-
"astro": "^5.
|
|
50
|
+
"astro": "^5.17.1",
|
|
47
51
|
"tsup": "^8.5.1",
|
|
48
52
|
"typescript": "^5.9.3"
|
|
49
53
|
},
|
|
50
54
|
"peerDependencies": {
|
|
51
55
|
"astro": "^5.0.0"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"magic-string": "^0.30.21"
|
|
52
59
|
}
|
|
53
60
|
}
|