@astroscope/boot 0.1.0 → 0.1.2

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
@@ -1,6 +1,6 @@
1
1
  # @astroscope/boot
2
2
 
3
- Run initialization and cleanup code for your Astro server.
3
+ Startup and graceful shutdown hooks for Astro SSR. Run initialization code before the server starts and cleanup code when it shuts down.
4
4
 
5
5
  ## Examples
6
6
 
@@ -9,7 +9,7 @@ See the [demo/boot](../../demo/boot) directory for a working example.
9
9
  ## Installation
10
10
 
11
11
  ```bash
12
- bun add @astroscope/boot
12
+ npm install @astroscope/boot
13
13
  ```
14
14
 
15
15
  ## Usage
@@ -83,7 +83,7 @@ boot({ entry: "src/startup.ts" });
83
83
 
84
84
  ### `hmr`
85
85
 
86
- Re-run `onStartup` when the boot file changes during development. This is disabled by default to avoid side effects, because `onStartup` may perform operations that should only run once (e.g., database connections).
86
+ Re-run `onStartup` when the boot file changes during development. This is disabled by default to avoid side effects, because `onStartup` may perform operations that should only run once (e.g., database connections). Please ensure your `onShutdown` function destroys any resources created by `onStartup` to prevent leaks / unexpected behavior.
87
87
 
88
88
  - **Type**: `boolean`
89
89
  - **Default**: `false`
package/dist/index.cjs CHANGED
@@ -35,6 +35,51 @@ __export(index_exports, {
35
35
  module.exports = __toCommonJS(index_exports);
36
36
  var import_node_fs = __toESM(require("fs"), 1);
37
37
  var import_node_path = __toESM(require("path"), 1);
38
+
39
+ // src/ignored.ts
40
+ var ignoredSuffixes = [
41
+ // type definitions
42
+ ".d.ts",
43
+ ".d.mts",
44
+ ".d.cts",
45
+ // images
46
+ ".png",
47
+ ".jpg",
48
+ ".jpeg",
49
+ ".gif",
50
+ ".svg",
51
+ ".webp",
52
+ ".avif",
53
+ ".ico",
54
+ // fonts
55
+ ".woff",
56
+ ".woff2",
57
+ ".ttf",
58
+ ".otf",
59
+ ".eot",
60
+ // other static assets
61
+ ".pdf",
62
+ ".mp3",
63
+ ".mp4",
64
+ ".webm",
65
+ ".ogg",
66
+ ".wav",
67
+ // data/config that boot typically doesn't import
68
+ ".json",
69
+ ".yaml",
70
+ ".yml",
71
+ ".toml",
72
+ ".md",
73
+ ".mdx",
74
+ ".txt",
75
+ // styles (handled by Vite's CSS HMR)
76
+ ".css",
77
+ ".scss",
78
+ ".sass",
79
+ ".less"
80
+ ];
81
+
82
+ // src/index.ts
38
83
  function resolveEntry(entry) {
39
84
  if (entry) return entry;
40
85
  if (import_node_fs.default.existsSync("src/boot.ts")) return "src/boot.ts";
@@ -61,9 +106,7 @@ function boot(options = {}) {
61
106
  if (isBuild) return;
62
107
  server.httpServer?.once("listening", async () => {
63
108
  try {
64
- const module2 = await server.ssrLoadModule(
65
- `/${entry}`
66
- );
109
+ const module2 = await server.ssrLoadModule(`/${entry}`);
67
110
  if (module2.onStartup) {
68
111
  await module2.onStartup();
69
112
  }
@@ -73,9 +116,7 @@ function boot(options = {}) {
73
116
  });
74
117
  server.httpServer?.once("close", async () => {
75
118
  try {
76
- const module2 = await server.ssrLoadModule(
77
- `/${entry}`
78
- );
119
+ const module2 = await server.ssrLoadModule(`/${entry}`);
79
120
  if (module2.onShutdown) {
80
121
  await module2.onShutdown();
81
122
  }
@@ -84,19 +125,49 @@ function boot(options = {}) {
84
125
  }
85
126
  });
86
127
  if (hmr) {
87
- server.watcher.on("change", async (changedPath) => {
88
- if (!changedPath.endsWith(entry)) return;
89
- logger.info("boot file changed, re-running onStartup...");
128
+ const bootModuleId = `/${entry}`;
129
+ const bootFilePath = import_node_path.default.resolve(server.config.root, entry);
130
+ const getBootDependencies = () => {
131
+ const deps = /* @__PURE__ */ new Set();
132
+ const bootModules = server.moduleGraph.getModulesByFile(bootFilePath);
133
+ const bootModule = bootModules ? [...bootModules][0] : void 0;
134
+ if (!bootModule) return deps;
135
+ const collectDeps = (mod, visited = /* @__PURE__ */ new Set()) => {
136
+ if (!mod?.file || visited.has(mod.file)) return;
137
+ visited.add(mod.file);
138
+ deps.add(mod.file);
139
+ for (const imported of mod.importedModules) {
140
+ collectDeps(imported, visited);
141
+ }
142
+ };
143
+ collectDeps(bootModule);
144
+ return deps;
145
+ };
146
+ const rerunBoot = async (changedFile) => {
147
+ logger.info(`boot dependency changed: ${changedFile}, rerunning hooks...`);
90
148
  try {
149
+ const oldModule = await server.ssrLoadModule(bootModuleId);
150
+ if (oldModule.onShutdown) {
151
+ await oldModule.onShutdown();
152
+ }
91
153
  server.moduleGraph.invalidateAll();
92
- const module2 = await server.ssrLoadModule(
93
- `/${entry}`
94
- );
95
- if (module2.onStartup) {
96
- await module2.onStartup();
154
+ const newModule = await server.ssrLoadModule(bootModuleId);
155
+ if (newModule.onStartup) {
156
+ await newModule.onStartup();
97
157
  }
98
158
  } catch (error) {
99
- logger.error(`Error running startup script: ${error}`);
159
+ logger.error(`Error during boot HMR: ${error}`);
160
+ }
161
+ };
162
+ const shouldIgnore = (filePath) => {
163
+ const path2 = filePath.toLowerCase();
164
+ return ignoredSuffixes.some((suffix) => path2.endsWith(suffix));
165
+ };
166
+ server.watcher.on("change", async (changedPath) => {
167
+ if (shouldIgnore(changedPath)) return;
168
+ const bootDeps = getBootDependencies();
169
+ if (bootDeps.has(changedPath)) {
170
+ await rerunBoot(changedPath);
100
171
  }
101
172
  });
102
173
  }
@@ -116,10 +187,14 @@ function boot(options = {}) {
116
187
  }
117
188
  },
118
189
  writeBundle(outputOptions) {
190
+ if (!isSSR) return;
119
191
  const outDir = outputOptions.dir;
120
192
  if (!outDir || !bootChunkRef) return;
121
193
  const entryPath = import_node_path.default.join(outDir, "entry.mjs");
122
- if (!import_node_fs.default.existsSync(entryPath)) return;
194
+ if (!import_node_fs.default.existsSync(entryPath)) {
195
+ logger.warn("entry.mjs not found - boot injection skipped");
196
+ return;
197
+ }
123
198
  const bootChunkName = this.getFileName(bootChunkRef);
124
199
  if (!bootChunkName) {
125
200
  logger.warn("boot chunk not found");
@@ -132,9 +207,9 @@ function boot(options = {}) {
132
207
  );
133
208
  }
134
209
  let content = import_node_fs.default.readFileSync(entryPath, "utf-8");
135
- const bootImport = `import { onStartup, onShutdown } from './${bootChunkName}';
136
- await onStartup?.();
137
- if (onShutdown) process.on('SIGTERM', async () => { await onShutdown(); process.exit(0); });
210
+ 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); });
138
213
  `;
139
214
  content = bootImport + content;
140
215
  import_node_fs.default.writeFileSync(entryPath, content);
package/dist/index.d.cts CHANGED
@@ -5,13 +5,42 @@ interface BootOptions {
5
5
  * Path to the boot file relative to the project root.
6
6
  * @default "src/boot.ts"
7
7
  */
8
- entry?: string;
8
+ entry?: string | undefined;
9
9
  /**
10
10
  * Enable HMR for the boot file. When true, `onStartup` will re-run when the boot file changes.
11
11
  * @default false
12
12
  */
13
- hmr?: boolean;
13
+ hmr?: boolean | undefined;
14
14
  }
15
+ /**
16
+ * Astro integration for application lifecycle hooks.
17
+ *
18
+ * Runs `onStartup` and `onShutdown` functions exported from your boot file
19
+ * 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
+ */
15
44
  declare function boot(options?: BootOptions): AstroIntegration;
16
45
 
17
46
  export { type BootOptions, boot as default };
package/dist/index.d.ts CHANGED
@@ -5,13 +5,42 @@ interface BootOptions {
5
5
  * Path to the boot file relative to the project root.
6
6
  * @default "src/boot.ts"
7
7
  */
8
- entry?: string;
8
+ entry?: string | undefined;
9
9
  /**
10
10
  * Enable HMR for the boot file. When true, `onStartup` will re-run when the boot file changes.
11
11
  * @default false
12
12
  */
13
- hmr?: boolean;
13
+ hmr?: boolean | undefined;
14
14
  }
15
+ /**
16
+ * Astro integration for application lifecycle hooks.
17
+ *
18
+ * Runs `onStartup` and `onShutdown` functions exported from your boot file
19
+ * 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
+ */
15
44
  declare function boot(options?: BootOptions): AstroIntegration;
16
45
 
17
46
  export { type BootOptions, boot as default };
package/dist/index.js CHANGED
@@ -1,6 +1,51 @@
1
1
  // src/index.ts
2
2
  import fs from "fs";
3
3
  import path from "path";
4
+
5
+ // src/ignored.ts
6
+ var ignoredSuffixes = [
7
+ // type definitions
8
+ ".d.ts",
9
+ ".d.mts",
10
+ ".d.cts",
11
+ // images
12
+ ".png",
13
+ ".jpg",
14
+ ".jpeg",
15
+ ".gif",
16
+ ".svg",
17
+ ".webp",
18
+ ".avif",
19
+ ".ico",
20
+ // fonts
21
+ ".woff",
22
+ ".woff2",
23
+ ".ttf",
24
+ ".otf",
25
+ ".eot",
26
+ // other static assets
27
+ ".pdf",
28
+ ".mp3",
29
+ ".mp4",
30
+ ".webm",
31
+ ".ogg",
32
+ ".wav",
33
+ // data/config that boot typically doesn't import
34
+ ".json",
35
+ ".yaml",
36
+ ".yml",
37
+ ".toml",
38
+ ".md",
39
+ ".mdx",
40
+ ".txt",
41
+ // styles (handled by Vite's CSS HMR)
42
+ ".css",
43
+ ".scss",
44
+ ".sass",
45
+ ".less"
46
+ ];
47
+
48
+ // src/index.ts
4
49
  function resolveEntry(entry) {
5
50
  if (entry) return entry;
6
51
  if (fs.existsSync("src/boot.ts")) return "src/boot.ts";
@@ -27,9 +72,7 @@ function boot(options = {}) {
27
72
  if (isBuild) return;
28
73
  server.httpServer?.once("listening", async () => {
29
74
  try {
30
- const module = await server.ssrLoadModule(
31
- `/${entry}`
32
- );
75
+ const module = await server.ssrLoadModule(`/${entry}`);
33
76
  if (module.onStartup) {
34
77
  await module.onStartup();
35
78
  }
@@ -39,9 +82,7 @@ function boot(options = {}) {
39
82
  });
40
83
  server.httpServer?.once("close", async () => {
41
84
  try {
42
- const module = await server.ssrLoadModule(
43
- `/${entry}`
44
- );
85
+ const module = await server.ssrLoadModule(`/${entry}`);
45
86
  if (module.onShutdown) {
46
87
  await module.onShutdown();
47
88
  }
@@ -50,19 +91,49 @@ function boot(options = {}) {
50
91
  }
51
92
  });
52
93
  if (hmr) {
53
- server.watcher.on("change", async (changedPath) => {
54
- if (!changedPath.endsWith(entry)) return;
55
- logger.info("boot file changed, re-running onStartup...");
94
+ const bootModuleId = `/${entry}`;
95
+ const bootFilePath = path.resolve(server.config.root, entry);
96
+ const getBootDependencies = () => {
97
+ const deps = /* @__PURE__ */ new Set();
98
+ const bootModules = server.moduleGraph.getModulesByFile(bootFilePath);
99
+ const bootModule = bootModules ? [...bootModules][0] : void 0;
100
+ if (!bootModule) return deps;
101
+ const collectDeps = (mod, visited = /* @__PURE__ */ new Set()) => {
102
+ if (!mod?.file || visited.has(mod.file)) return;
103
+ visited.add(mod.file);
104
+ deps.add(mod.file);
105
+ for (const imported of mod.importedModules) {
106
+ collectDeps(imported, visited);
107
+ }
108
+ };
109
+ collectDeps(bootModule);
110
+ return deps;
111
+ };
112
+ const rerunBoot = async (changedFile) => {
113
+ logger.info(`boot dependency changed: ${changedFile}, rerunning hooks...`);
56
114
  try {
115
+ const oldModule = await server.ssrLoadModule(bootModuleId);
116
+ if (oldModule.onShutdown) {
117
+ await oldModule.onShutdown();
118
+ }
57
119
  server.moduleGraph.invalidateAll();
58
- const module = await server.ssrLoadModule(
59
- `/${entry}`
60
- );
61
- if (module.onStartup) {
62
- await module.onStartup();
120
+ const newModule = await server.ssrLoadModule(bootModuleId);
121
+ if (newModule.onStartup) {
122
+ await newModule.onStartup();
63
123
  }
64
124
  } catch (error) {
65
- logger.error(`Error running startup script: ${error}`);
125
+ logger.error(`Error during boot HMR: ${error}`);
126
+ }
127
+ };
128
+ const shouldIgnore = (filePath) => {
129
+ const path2 = filePath.toLowerCase();
130
+ return ignoredSuffixes.some((suffix) => path2.endsWith(suffix));
131
+ };
132
+ server.watcher.on("change", async (changedPath) => {
133
+ if (shouldIgnore(changedPath)) return;
134
+ const bootDeps = getBootDependencies();
135
+ if (bootDeps.has(changedPath)) {
136
+ await rerunBoot(changedPath);
66
137
  }
67
138
  });
68
139
  }
@@ -82,10 +153,14 @@ function boot(options = {}) {
82
153
  }
83
154
  },
84
155
  writeBundle(outputOptions) {
156
+ if (!isSSR) return;
85
157
  const outDir = outputOptions.dir;
86
158
  if (!outDir || !bootChunkRef) return;
87
159
  const entryPath = path.join(outDir, "entry.mjs");
88
- if (!fs.existsSync(entryPath)) return;
160
+ if (!fs.existsSync(entryPath)) {
161
+ logger.warn("entry.mjs not found - boot injection skipped");
162
+ return;
163
+ }
89
164
  const bootChunkName = this.getFileName(bootChunkRef);
90
165
  if (!bootChunkName) {
91
166
  logger.warn("boot chunk not found");
@@ -98,9 +173,9 @@ function boot(options = {}) {
98
173
  );
99
174
  }
100
175
  let content = fs.readFileSync(entryPath, "utf-8");
101
- const bootImport = `import { onStartup, onShutdown } from './${bootChunkName}';
102
- await onStartup?.();
103
- if (onShutdown) process.on('SIGTERM', async () => { await onShutdown(); process.exit(0); });
176
+ 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); });
104
179
  `;
105
180
  content = bootImport + content;
106
181
  fs.writeFileSync(entryPath, content);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@astroscope/boot",
3
- "version": "0.1.0",
4
- "description": "Boot integration for Astro",
3
+ "version": "0.1.2",
4
+ "description": "Startup and graceful shutdown hooks for Astro SSR",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -43,7 +43,7 @@
43
43
  "lint:fix": "eslint 'src/**/*.{ts,tsx}' --fix"
44
44
  },
45
45
  "devDependencies": {
46
- "astro": "^5.1.0",
46
+ "astro": "^5.16.9",
47
47
  "tsup": "^8.5.1",
48
48
  "typescript": "^5.9.3"
49
49
  },