@astroscope/boot 0.1.3 → 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/README.md CHANGED
@@ -20,15 +20,17 @@ npm install @astroscope/boot
20
20
 
21
21
  ```ts
22
22
  // src/boot.ts
23
- export async function onStartup() {
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("Ready!");
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/index.ts
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
- if (module2.onStartup) {
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
- if (module2.onShutdown) {
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
- if (oldModule.onShutdown) {
151
- await oldModule.onShutdown();
152
- }
175
+ await oldModule.onShutdown?.(bootContext);
153
176
  server.moduleGraph.invalidateAll();
154
177
  const newModule = await server.ssrLoadModule(bootModuleId);
155
- if (newModule.onStartup) {
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 path2 = filePath.toLowerCase();
164
- return ignoredSuffixes.some((suffix) => path2.endsWith(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,67 @@ function boot(options = {}) {
172
193
  });
173
194
  }
174
195
  },
175
- configResolved(config) {
176
- isSSR = !!config.build?.ssr;
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
- writeBundle(outputOptions) {
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 sourcemapPath = `${entryPath}.map`;
204
- if (import_node_fs.default.existsSync(sourcemapPath)) {
205
- logger.warn(
206
- "sourcemap detected for entry.mjs - line numbers may be off by 2 lines due to boot injection"
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
- let content = import_node_fs.default.readFileSync(entryPath, "utf-8");
229
+ const { host, port } = getServerDefaults(astroConfig);
210
230
  const bootImport = `import * as __boot from './${bootChunkName}';
211
- await __boot.onStartup?.();
212
- if (__boot.onShutdown) process.on('SIGTERM', async () => { await __boot.onShutdown(); process.exit(0); });
231
+ const __bootContext = { dev: false, host: process.env.HOST ?? '${host}', port: process.env.PORT ? Number(process.env.PORT) : ${port} };
232
+ await __boot.onStartup?.(__bootContext);
233
+ if (__boot.onShutdown) process.on('SIGTERM', async () => { await __boot.onShutdown(__bootContext); process.exit(0); });
213
234
  `;
214
- content = bootImport + content;
215
- import_node_fs.default.writeFileSync(entryPath, content);
235
+ const s = new import_magic_string.default(entryChunk.code);
236
+ s.prepend(bootImport);
237
+ entryChunk.code = s.toString();
238
+ if (entryChunk.map) {
239
+ entryChunk.map = s.generateMap({ hires: true });
240
+ }
216
241
  logger.info(`injected ${bootChunkName} into entry.mjs`);
242
+ },
243
+ writeBundle(outputOptions) {
244
+ if (!isSSR) return;
245
+ const outDir = outputOptions.dir;
246
+ if (!outDir) return;
247
+ const modules = [];
248
+ if (middlewarePath) {
249
+ modules.push(`./${middlewarePath}`);
250
+ }
251
+ for (const page of pageModules) {
252
+ modules.push(`./${page}`);
253
+ }
254
+ const manifestPath = import_node_path.default.join(outDir, "chunks", WARMUP_MANIFEST_FILE);
255
+ import_node_fs.default.writeFileSync(manifestPath, JSON.stringify({ modules }));
256
+ logger.info(`generated warmup for ${pageModules.length} pages`);
217
257
  }
218
258
  }
219
259
  ]
@@ -223,3 +263,7 @@ if (__boot.onShutdown) process.on('SIGTERM', async () => { await __boot.onShutdo
223
263
  }
224
264
  };
225
265
  }
266
+ // Annotate the CommonJS export names for ESM import in node:
267
+ 0 && (module.exports = {
268
+ boot
269
+ });
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/index.ts
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/index.ts
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
- if (module.onStartup) {
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
- if (module.onShutdown) {
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
- if (oldModule.onShutdown) {
117
- await oldModule.onShutdown();
118
- }
138
+ await oldModule.onShutdown?.(bootContext);
119
139
  server.moduleGraph.invalidateAll();
120
140
  const newModule = await server.ssrLoadModule(bootModuleId);
121
- if (newModule.onStartup) {
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 path2 = filePath.toLowerCase();
130
- return ignoredSuffixes.some((suffix) => path2.endsWith(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,67 @@ function boot(options = {}) {
138
156
  });
139
157
  }
140
158
  },
141
- configResolved(config) {
142
- isSSR = !!config.build?.ssr;
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
- writeBundle(outputOptions) {
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 sourcemapPath = `${entryPath}.map`;
170
- if (fs.existsSync(sourcemapPath)) {
171
- logger.warn(
172
- "sourcemap detected for entry.mjs - line numbers may be off by 2 lines due to boot injection"
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
- let content = fs.readFileSync(entryPath, "utf-8");
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);
176
193
  const bootImport = `import * as __boot from './${bootChunkName}';
177
- await __boot.onStartup?.();
178
- if (__boot.onShutdown) process.on('SIGTERM', async () => { await __boot.onShutdown(); process.exit(0); });
194
+ const __bootContext = { dev: false, host: process.env.HOST ?? '${host}', port: process.env.PORT ? Number(process.env.PORT) : ${port} };
195
+ await __boot.onStartup?.(__bootContext);
196
+ if (__boot.onShutdown) process.on('SIGTERM', async () => { await __boot.onShutdown(__bootContext); process.exit(0); });
179
197
  `;
180
- content = bootImport + content;
181
- fs.writeFileSync(entryPath, content);
198
+ const s = new MagicString(entryChunk.code);
199
+ s.prepend(bootImport);
200
+ entryChunk.code = s.toString();
201
+ if (entryChunk.map) {
202
+ entryChunk.map = s.generateMap({ hires: true });
203
+ }
182
204
  logger.info(`injected ${bootChunkName} into entry.mjs`);
205
+ },
206
+ writeBundle(outputOptions) {
207
+ if (!isSSR) return;
208
+ const outDir = outputOptions.dir;
209
+ if (!outDir) return;
210
+ const modules = [];
211
+ if (middlewarePath) {
212
+ modules.push(`./${middlewarePath}`);
213
+ }
214
+ for (const page of pageModules) {
215
+ modules.push(`./${page}`);
216
+ }
217
+ const manifestPath = path.join(outDir, "chunks", WARMUP_MANIFEST_FILE);
218
+ fs.writeFileSync(manifestPath, JSON.stringify({ modules }));
219
+ logger.info(`generated warmup for ${pageModules.length} pages`);
183
220
  }
184
221
  }
185
222
  ]
@@ -190,5 +227,6 @@ if (__boot.onShutdown) process.on('SIGTERM', async () => { await __boot.onShutdo
190
227
  };
191
228
  }
192
229
  export {
230
+ boot,
193
231
  boot as default
194
232
  };
@@ -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 };
@@ -0,0 +1,63 @@
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 loadModules() {
35
+ if (isDevMode()) {
36
+ return [];
37
+ }
38
+ const __dirname = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
39
+ const manifest = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path.join)(__dirname, WARMUP_MANIFEST_FILE), "utf-8"));
40
+ return manifest.modules ?? [];
41
+ }
42
+ async function warmup() {
43
+ const modules = loadModules();
44
+ if (isDevMode() || modules.length === 0) {
45
+ return { success: [], failed: [], duration: 0 };
46
+ }
47
+ const start = Date.now();
48
+ const results = await Promise.allSettled(modules.map((mod) => import(mod)));
49
+ const success = [];
50
+ const failed = [];
51
+ for (let i = 0; i < results.length; i++) {
52
+ if (results[i].status === "fulfilled") {
53
+ success.push(modules[i]);
54
+ } else {
55
+ failed.push(modules[i]);
56
+ }
57
+ }
58
+ return { success, failed, duration: Date.now() - start };
59
+ }
60
+ // Annotate the CommonJS export names for ESM import in node:
61
+ 0 && (module.exports = {
62
+ warmup
63
+ });
@@ -0,0 +1,11 @@
1
+ import { W as WarmupResult } from './types-CxpusND2.cjs';
2
+
3
+ /**
4
+ * Warms up V8 by importing all page modules and middleware.
5
+ *
6
+ * In development mode, this is a no-op that returns empty results.
7
+ * In production, reads the warmup manifest and imports all discovered modules.
8
+ */
9
+ declare function warmup(): Promise<WarmupResult>;
10
+
11
+ export { WarmupResult, warmup };
@@ -0,0 +1,11 @@
1
+ import { W as WarmupResult } from './types-CxpusND2.js';
2
+
3
+ /**
4
+ * Warms up V8 by importing all page modules and middleware.
5
+ *
6
+ * In development mode, this is a no-op that returns empty results.
7
+ * In production, reads the warmup manifest and imports all discovered modules.
8
+ */
9
+ declare function warmup(): Promise<WarmupResult>;
10
+
11
+ export { WarmupResult, warmup };
package/dist/warmup.js ADDED
@@ -0,0 +1,37 @@
1
+ // src/warmup.ts
2
+ import { readFileSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { fileURLToPath } from "url";
5
+ var WARMUP_MANIFEST_FILE = "warmup-manifest.json";
6
+ function isDevMode() {
7
+ return Boolean(import.meta.env?.["DEV"]);
8
+ }
9
+ function loadModules() {
10
+ if (isDevMode()) {
11
+ return [];
12
+ }
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const manifest = JSON.parse(readFileSync(join(__dirname, WARMUP_MANIFEST_FILE), "utf-8"));
15
+ return manifest.modules ?? [];
16
+ }
17
+ async function warmup() {
18
+ const modules = loadModules();
19
+ if (isDevMode() || modules.length === 0) {
20
+ return { success: [], failed: [], duration: 0 };
21
+ }
22
+ const start = Date.now();
23
+ const results = await Promise.allSettled(modules.map((mod) => import(mod)));
24
+ const success = [];
25
+ const failed = [];
26
+ for (let i = 0; i < results.length; i++) {
27
+ if (results[i].status === "fulfilled") {
28
+ success.push(modules[i]);
29
+ } else {
30
+ failed.push(modules[i]);
31
+ }
32
+ }
33
+ return { success, failed, duration: Date.now() - start };
34
+ }
35
+ export {
36
+ warmup
37
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astroscope/boot",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
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,7 +41,7 @@
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"
@@ -49,5 +53,8 @@
49
53
  },
50
54
  "peerDependencies": {
51
55
  "astro": "^5.0.0"
56
+ },
57
+ "dependencies": {
58
+ "magic-string": "^0.30.21"
52
59
  }
53
60
  }