@astroscope/boot 0.1.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 ADDED
@@ -0,0 +1,106 @@
1
+ # @astroscope/boot
2
+
3
+ Run initialization and cleanup code for your Astro server.
4
+
5
+ ## Examples
6
+
7
+ See the [demo/boot](../../demo/boot) directory for a working example.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ bun add @astroscope/boot
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ 1. Create a boot file at `src/boot.ts` (or `src/boot/index.ts`):
18
+
19
+ ```ts
20
+ // src/boot.ts
21
+ export async function onStartup() {
22
+ console.log("Starting up...");
23
+
24
+ await someAsyncInitialization();
25
+
26
+ console.log("Ready!");
27
+ }
28
+
29
+ export async function onShutdown() {
30
+ console.log("Shutting down...");
31
+
32
+ await closeConnections();
33
+
34
+ console.log("Goodbye!");
35
+ }
36
+ ```
37
+
38
+ 2. Add the integration to your Astro config:
39
+
40
+ ```ts
41
+ // astro.config.ts
42
+ import { defineConfig } from "astro/config";
43
+ import boot from "@astroscope/boot";
44
+
45
+ export default defineConfig({
46
+ output: "server",
47
+ integrations: [boot()],
48
+ });
49
+ ```
50
+
51
+ ## Lifecycle Hooks
52
+
53
+ ### `onStartup`
54
+
55
+ Called before the server starts handling requests. Use this for:
56
+
57
+ - Database connection initialization
58
+ - Loading configuration
59
+ - Warming caches
60
+ - Setting up external service clients
61
+
62
+ ### `onShutdown`
63
+
64
+ Called when the server is shutting down (SIGTERM in production, server close in development). Use this for:
65
+
66
+ - Closing database connections
67
+ - Flushing buffers
68
+ - Cleaning up resources
69
+ - Graceful shutdown of external services
70
+
71
+ ## Options
72
+
73
+ ### `entry`
74
+
75
+ Path to the boot file relative to the project root.
76
+
77
+ - **Type**: `string`
78
+ - **Default**: `"src/boot.ts"`
79
+
80
+ ```ts
81
+ boot({ entry: "src/startup.ts" });
82
+ ```
83
+
84
+ ### `hmr`
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).
87
+
88
+ - **Type**: `boolean`
89
+ - **Default**: `false`
90
+
91
+ ```ts
92
+ boot({ hmr: true });
93
+ ```
94
+
95
+ ## How it works
96
+
97
+ - **Development**: The boot file runs _after_ the dev server starts listening (Vite limitation). `onShutdown` is called when the dev server closes.
98
+ - **Production**: `onStartup` runs _before_ the server starts handling requests. `onShutdown` is called on SIGTERM.
99
+
100
+ ## Requirements
101
+
102
+ - Only works with SSR output mode (`output: "server"`)
103
+
104
+ ## License
105
+
106
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ default: () => boot
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+ var import_node_fs = __toESM(require("fs"), 1);
37
+ var import_node_path = __toESM(require("path"), 1);
38
+ function resolveEntry(entry) {
39
+ if (entry) return entry;
40
+ if (import_node_fs.default.existsSync("src/boot.ts")) return "src/boot.ts";
41
+ if (import_node_fs.default.existsSync("src/boot/index.ts")) return "src/boot/index.ts";
42
+ return "src/boot.ts";
43
+ }
44
+ function boot(options = {}) {
45
+ const entry = resolveEntry(options.entry);
46
+ const hmr = options.hmr ?? false;
47
+ let isBuild = false;
48
+ let isSSR = false;
49
+ let bootChunkRef = null;
50
+ return {
51
+ name: "@astroscope/boot",
52
+ hooks: {
53
+ "astro:config:setup": ({ command, updateConfig, logger }) => {
54
+ isBuild = command === "build";
55
+ updateConfig({
56
+ vite: {
57
+ plugins: [
58
+ {
59
+ name: "@astroscope/boot",
60
+ configureServer(server) {
61
+ if (isBuild) return;
62
+ server.httpServer?.once("listening", async () => {
63
+ try {
64
+ const module2 = await server.ssrLoadModule(
65
+ `/${entry}`
66
+ );
67
+ if (module2.onStartup) {
68
+ await module2.onStartup();
69
+ }
70
+ } catch (error) {
71
+ logger.error(`Error running startup script: ${error}`);
72
+ }
73
+ });
74
+ server.httpServer?.once("close", async () => {
75
+ try {
76
+ const module2 = await server.ssrLoadModule(
77
+ `/${entry}`
78
+ );
79
+ if (module2.onShutdown) {
80
+ await module2.onShutdown();
81
+ }
82
+ } catch (error) {
83
+ logger.error(`Error running shutdown script: ${error}`);
84
+ }
85
+ });
86
+ if (hmr) {
87
+ server.watcher.on("change", async (changedPath) => {
88
+ if (!changedPath.endsWith(entry)) return;
89
+ logger.info("boot file changed, re-running onStartup...");
90
+ try {
91
+ server.moduleGraph.invalidateAll();
92
+ const module2 = await server.ssrLoadModule(
93
+ `/${entry}`
94
+ );
95
+ if (module2.onStartup) {
96
+ await module2.onStartup();
97
+ }
98
+ } catch (error) {
99
+ logger.error(`Error running startup script: ${error}`);
100
+ }
101
+ });
102
+ }
103
+ },
104
+ configResolved(config) {
105
+ isSSR = !!config.build?.ssr;
106
+ },
107
+ buildStart() {
108
+ if (!isSSR) return;
109
+ try {
110
+ bootChunkRef = this.emitFile({
111
+ type: "chunk",
112
+ id: entry,
113
+ name: "boot"
114
+ });
115
+ } catch {
116
+ }
117
+ },
118
+ writeBundle(outputOptions) {
119
+ const outDir = outputOptions.dir;
120
+ if (!outDir || !bootChunkRef) return;
121
+ const entryPath = import_node_path.default.join(outDir, "entry.mjs");
122
+ if (!import_node_fs.default.existsSync(entryPath)) return;
123
+ const bootChunkName = this.getFileName(bootChunkRef);
124
+ if (!bootChunkName) {
125
+ logger.warn("boot chunk not found");
126
+ return;
127
+ }
128
+ const sourcemapPath = `${entryPath}.map`;
129
+ if (import_node_fs.default.existsSync(sourcemapPath)) {
130
+ logger.warn(
131
+ "sourcemap detected for entry.mjs - line numbers may be off by 2 lines due to boot injection"
132
+ );
133
+ }
134
+ 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); });
138
+ `;
139
+ content = bootImport + content;
140
+ import_node_fs.default.writeFileSync(entryPath, content);
141
+ logger.info(`injected ${bootChunkName} into entry.mjs`);
142
+ }
143
+ }
144
+ ]
145
+ }
146
+ });
147
+ }
148
+ }
149
+ };
150
+ }
@@ -0,0 +1,17 @@
1
+ import { AstroIntegration } from 'astro';
2
+
3
+ interface BootOptions {
4
+ /**
5
+ * Path to the boot file relative to the project root.
6
+ * @default "src/boot.ts"
7
+ */
8
+ entry?: string;
9
+ /**
10
+ * Enable HMR for the boot file. When true, `onStartup` will re-run when the boot file changes.
11
+ * @default false
12
+ */
13
+ hmr?: boolean;
14
+ }
15
+ declare function boot(options?: BootOptions): AstroIntegration;
16
+
17
+ export { type BootOptions, boot as default };
@@ -0,0 +1,17 @@
1
+ import { AstroIntegration } from 'astro';
2
+
3
+ interface BootOptions {
4
+ /**
5
+ * Path to the boot file relative to the project root.
6
+ * @default "src/boot.ts"
7
+ */
8
+ entry?: string;
9
+ /**
10
+ * Enable HMR for the boot file. When true, `onStartup` will re-run when the boot file changes.
11
+ * @default false
12
+ */
13
+ hmr?: boolean;
14
+ }
15
+ declare function boot(options?: BootOptions): AstroIntegration;
16
+
17
+ export { type BootOptions, boot as default };
package/dist/index.js ADDED
@@ -0,0 +1,119 @@
1
+ // src/index.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ function resolveEntry(entry) {
5
+ if (entry) return entry;
6
+ if (fs.existsSync("src/boot.ts")) return "src/boot.ts";
7
+ if (fs.existsSync("src/boot/index.ts")) return "src/boot/index.ts";
8
+ return "src/boot.ts";
9
+ }
10
+ function boot(options = {}) {
11
+ const entry = resolveEntry(options.entry);
12
+ const hmr = options.hmr ?? false;
13
+ let isBuild = false;
14
+ let isSSR = false;
15
+ let bootChunkRef = null;
16
+ return {
17
+ name: "@astroscope/boot",
18
+ hooks: {
19
+ "astro:config:setup": ({ command, updateConfig, logger }) => {
20
+ isBuild = command === "build";
21
+ updateConfig({
22
+ vite: {
23
+ plugins: [
24
+ {
25
+ name: "@astroscope/boot",
26
+ configureServer(server) {
27
+ if (isBuild) return;
28
+ server.httpServer?.once("listening", async () => {
29
+ try {
30
+ const module = await server.ssrLoadModule(
31
+ `/${entry}`
32
+ );
33
+ if (module.onStartup) {
34
+ await module.onStartup();
35
+ }
36
+ } catch (error) {
37
+ logger.error(`Error running startup script: ${error}`);
38
+ }
39
+ });
40
+ server.httpServer?.once("close", async () => {
41
+ try {
42
+ const module = await server.ssrLoadModule(
43
+ `/${entry}`
44
+ );
45
+ if (module.onShutdown) {
46
+ await module.onShutdown();
47
+ }
48
+ } catch (error) {
49
+ logger.error(`Error running shutdown script: ${error}`);
50
+ }
51
+ });
52
+ if (hmr) {
53
+ server.watcher.on("change", async (changedPath) => {
54
+ if (!changedPath.endsWith(entry)) return;
55
+ logger.info("boot file changed, re-running onStartup...");
56
+ try {
57
+ server.moduleGraph.invalidateAll();
58
+ const module = await server.ssrLoadModule(
59
+ `/${entry}`
60
+ );
61
+ if (module.onStartup) {
62
+ await module.onStartup();
63
+ }
64
+ } catch (error) {
65
+ logger.error(`Error running startup script: ${error}`);
66
+ }
67
+ });
68
+ }
69
+ },
70
+ configResolved(config) {
71
+ isSSR = !!config.build?.ssr;
72
+ },
73
+ buildStart() {
74
+ if (!isSSR) return;
75
+ try {
76
+ bootChunkRef = this.emitFile({
77
+ type: "chunk",
78
+ id: entry,
79
+ name: "boot"
80
+ });
81
+ } catch {
82
+ }
83
+ },
84
+ writeBundle(outputOptions) {
85
+ const outDir = outputOptions.dir;
86
+ if (!outDir || !bootChunkRef) return;
87
+ const entryPath = path.join(outDir, "entry.mjs");
88
+ if (!fs.existsSync(entryPath)) return;
89
+ const bootChunkName = this.getFileName(bootChunkRef);
90
+ if (!bootChunkName) {
91
+ logger.warn("boot chunk not found");
92
+ return;
93
+ }
94
+ const sourcemapPath = `${entryPath}.map`;
95
+ if (fs.existsSync(sourcemapPath)) {
96
+ logger.warn(
97
+ "sourcemap detected for entry.mjs - line numbers may be off by 2 lines due to boot injection"
98
+ );
99
+ }
100
+ 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); });
104
+ `;
105
+ content = bootImport + content;
106
+ fs.writeFileSync(entryPath, content);
107
+ logger.info(`injected ${bootChunkName} into entry.mjs`);
108
+ }
109
+ }
110
+ ]
111
+ }
112
+ });
113
+ }
114
+ }
115
+ };
116
+ }
117
+ export {
118
+ boot as default
119
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@astroscope/boot",
3
+ "version": "0.1.0",
4
+ "description": "Boot integration for Astro",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "sideEffects": false,
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/smnbbrv/astroscope.git",
25
+ "directory": "packages/boot"
26
+ },
27
+ "keywords": [
28
+ "astro",
29
+ "astro-integration",
30
+ "boot",
31
+ "frameworks"
32
+ ],
33
+ "author": "smnbbrv",
34
+ "license": "MIT",
35
+ "bugs": {
36
+ "url": "https://github.com/smnbbrv/astroscope/issues"
37
+ },
38
+ "homepage": "https://github.com/smnbbrv/astroscope/tree/main/packages/boot#readme",
39
+ "scripts": {
40
+ "build": "tsup src/index.ts --format esm,cjs --dts",
41
+ "typecheck": "tsc --noEmit",
42
+ "lint": "eslint 'src/**/*.{ts,tsx}'",
43
+ "lint:fix": "eslint 'src/**/*.{ts,tsx}' --fix"
44
+ },
45
+ "devDependencies": {
46
+ "astro": "^5.1.0",
47
+ "tsup": "^8.5.1",
48
+ "typescript": "^5.9.3"
49
+ },
50
+ "peerDependencies": {
51
+ "astro": "^5.0.0"
52
+ }
53
+ }