@astralarium/repo-dev 0.0.0-stage → 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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -2
  3. package/dist/allocation.d.ts +15 -0
  4. package/dist/allocation.js +28 -0
  5. package/dist/bin/db-image.d.ts +2 -0
  6. package/dist/bin/db-image.js +29 -0
  7. package/dist/bin/run-dev.d.ts +2 -0
  8. package/dist/bin/run-dev.js +15 -0
  9. package/dist/bin/siblings-inputs.d.ts +2 -0
  10. package/dist/bin/siblings-inputs.js +20 -0
  11. package/dist/config.d.ts +240 -0
  12. package/dist/config.js +90 -0
  13. package/dist/environment.d.ts +19 -0
  14. package/dist/environment.js +45 -0
  15. package/dist/exec.d.ts +4 -0
  16. package/dist/exec.js +14 -0
  17. package/dist/index.d.ts +12 -0
  18. package/dist/index.js +12 -0
  19. package/dist/launcher/hosts.d.ts +26 -0
  20. package/dist/launcher/hosts.js +152 -0
  21. package/dist/launcher/launcher.d.ts +23 -0
  22. package/dist/launcher/launcher.js +452 -0
  23. package/dist/launcher/servers.d.ts +39 -0
  24. package/dist/launcher/servers.js +154 -0
  25. package/dist/load-config.d.ts +3 -0
  26. package/dist/load-config.js +6 -0
  27. package/dist/ports.d.ts +3 -0
  28. package/dist/ports.js +32 -0
  29. package/dist/process.d.ts +43 -0
  30. package/dist/process.js +172 -0
  31. package/dist/resources.d.ts +23 -0
  32. package/dist/resources.js +74 -0
  33. package/dist/sibling-inputs.d.ts +5 -0
  34. package/dist/sibling-inputs.js +32 -0
  35. package/dist/source-hash.d.ts +2 -0
  36. package/dist/source-hash.js +29 -0
  37. package/dist/testkit/built-worker.d.ts +29 -0
  38. package/dist/testkit/built-worker.js +48 -0
  39. package/dist/testkit/env-template.d.ts +8 -0
  40. package/dist/testkit/env-template.js +14 -0
  41. package/dist/testkit/index.d.ts +8 -0
  42. package/dist/testkit/index.js +8 -0
  43. package/dist/testkit/ports.d.ts +8 -0
  44. package/dist/testkit/ports.js +20 -0
  45. package/dist/testkit/temp.d.ts +2 -0
  46. package/dist/testkit/temp.js +10 -0
  47. package/dist/testkit/test-cache.d.ts +14 -0
  48. package/dist/testkit/test-cache.js +174 -0
  49. package/dist/testkit/worker-build.d.ts +26 -0
  50. package/dist/testkit/worker-build.js +46 -0
  51. package/dist/vite.d.ts +96 -0
  52. package/dist/vite.js +254 -0
  53. package/dist/wait.d.ts +9 -0
  54. package/dist/wait.js +38 -0
  55. package/package.json +71 -3
package/dist/vite.js ADDED
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Vite plugins and config: the launcher's listener, Cloudflare plugin
3
+ * options, the build guard, the Tailwind dev-source exclusion, the
4
+ * prerender runtime and worker config, and rename recovery.
5
+ */
6
+ import { existsSync, globSync, readFileSync, realpathSync } from "node:fs";
7
+ import { dirname, join, relative, resolve } from "node:path";
8
+ import { parseLocalOrigins, parseOrigin } from "@astralarium/repo-env";
9
+ import { isDevFile, isServerFile, isTestFile, workspacePackageDirs, workspaceRoot, } from "@astralarium/repo-package-files";
10
+ import { ExpectedError } from "@astralarium/repo-telemetry";
11
+ import { normalizePath, } from "vite";
12
+ /**
13
+ * Listener of a launched app (`listenerEnv`): dev servers serve on
14
+ * `SITE_ORIGIN`'s port, over HTTPS with `SSL_KEY` / `SSL_CERT`; every
15
+ * server binds `DEV_SERVER_HOST` (default `localhost`) on a strict port
16
+ * and allows the `LOCAL_ORIGINS` hostnames. Config comes from process env:
17
+ * Vite reads no dot-env file.
18
+ */
19
+ export function devListener({ command, isPreview, mode }, env = process.env) {
20
+ // Vitest runs in mode `test`.
21
+ const development = command === "serve" && !isPreview && mode !== "test";
22
+ const origin = development
23
+ ? new URL(parseOrigin(env.SITE_ORIGIN))
24
+ : undefined;
25
+ const key = development && env.SSL_KEY;
26
+ const cert = development && env.SSL_CERT;
27
+ return {
28
+ envDir: false,
29
+ server: {
30
+ port: origin
31
+ ? Number(origin.port || (origin.protocol === "https:" ? 443 : 80))
32
+ : undefined,
33
+ // Blank from `appProcessEnv` outside a `host` server.
34
+ host: env.DEV_SERVER_HOST === undefined || env.DEV_SERVER_HOST === ""
35
+ ? "localhost"
36
+ : env.DEV_SERVER_HOST,
37
+ strictPort: true,
38
+ https: key && cert
39
+ ? { key: readFileSync(key, "utf8"), cert: readFileSync(cert, "utf8") }
40
+ : undefined,
41
+ allowedHosts: parseLocalOrigins(env.LOCAL_ORIGINS).hostnames,
42
+ },
43
+ };
44
+ }
45
+ /**
46
+ * Cloudflare Vite plugin options of a launched app: a `testEnv` build
47
+ * (`CLOUDFLARE_ENV`, the Vitest fixtures') keeps no state and opens no
48
+ * inspector; other servers open `DEV_INSPECTOR_PORT` (`listenerEnv`), or
49
+ * none when it is blank or unset.
50
+ */
51
+ export function cloudflareDevOptions({ testEnv = "test", env = process.env, } = {}) {
52
+ if (env.CLOUDFLARE_ENV === testEnv)
53
+ return { persistState: false, inspectorPort: false };
54
+ return {
55
+ inspectorPort: env.DEV_INSPECTOR_PORT
56
+ ? Number(env.DEV_INSPECTOR_PORT)
57
+ : false,
58
+ };
59
+ }
60
+ /** Inline limits the check set, across instances: a config may evaluate
61
+ * and resolve more than once. */
62
+ const inlineLimits = new WeakSet();
63
+ /**
64
+ * Fails a build shipping dev files or test support, or server-only
65
+ * modules in a client environment (`isDevFile`, `isTestFile`,
66
+ * `isServerFile` from `@astralarium/repo-package-files`). Dev assets never inline,
67
+ * so they emit and fail; library builds inline every asset, skipping
68
+ * the check.
69
+ */
70
+ export function buildGuard() {
71
+ let config;
72
+ let configured = false;
73
+ return {
74
+ name: "repo:build-guard",
75
+ apply: "build",
76
+ config(user) {
77
+ const limit = user.build?.assetsInlineLimit;
78
+ const inlineLimit = (file, content) => {
79
+ if (isDevFile(file))
80
+ return false;
81
+ if (typeof limit === "function")
82
+ return limit(file, content);
83
+ return limit === undefined ? undefined : content.length < limit;
84
+ };
85
+ inlineLimits.add(inlineLimit);
86
+ configured = true;
87
+ return {
88
+ build: { assetsInlineLimit: inlineLimit },
89
+ // Vite appends these to the user's worker plugins.
90
+ worker: { plugins: () => [buildGuard()] },
91
+ };
92
+ },
93
+ configResolved(resolved) {
94
+ config = resolved;
95
+ // Worker builds inherit the main build's limit, checked there.
96
+ const limit = resolved.build.assetsInlineLimit;
97
+ if (configured &&
98
+ !resolved.isWorker &&
99
+ !(typeof limit === "function" && inlineLimits.has(limit)))
100
+ throw new ExpectedError("repo:build-guard: a later plugin replaced build.assetsInlineLimit; dev assets could inline unchecked");
101
+ if (resolved.publicDir && isDevFile(join(resolved.publicDir, "_")))
102
+ throw new ExpectedError(`repo:build-guard: publicDir ${resolved.publicDir} is a dev folder`);
103
+ },
104
+ generateBundle(_, bundle) {
105
+ const assets = Object.values(bundle).flatMap((output) => output.type === "asset"
106
+ ? output.originalFileNames.map((name) => resolve(config.root, name))
107
+ : []);
108
+ const modules = [...this.getModuleIds()];
109
+ const found = (files, test) => [
110
+ ...new Set(files
111
+ .filter(test)
112
+ .map((id) => relative(config.root, id.replace(/^\0/, "").split("?")[0] ?? id))),
113
+ ].sort();
114
+ const failures = [];
115
+ const dev = found([...modules, ...assets], isDevFile);
116
+ if (dev.length > 0)
117
+ failures.push(`dev-only files in the build${config.isProduction ? "" : " (build with NODE_ENV=production)"}: ${dev.join(", ")}`);
118
+ const test = found(modules, isTestFile);
119
+ if (test.length > 0)
120
+ failures.push(`test support in the build: ${test.join(", ")}`);
121
+ // Web workers build apart from the environment that starts them.
122
+ const client = config.isWorker || this.environment.config.consumer === "client";
123
+ const server = client ? found(modules, isServerFile) : [];
124
+ if (server.length > 0)
125
+ failures.push(`server-only modules in the client build: ${server.join(", ")}`);
126
+ if (failures.length > 0)
127
+ this.error(failures.join("\n"));
128
+ },
129
+ };
130
+ }
131
+ /**
132
+ * Keeps dev classes out of built CSS: appends `@source not` for each
133
+ * workspace package's dev folders (`isDevFile`) to every Tailwind entry
134
+ * (CSS importing `tailwindcss`). Precedes `tailwindcss()`. Skips
135
+ * `?url`/`?raw`, which wrap the CSS as JS.
136
+ */
137
+ export function tailwindDevSources() {
138
+ let devDirs = [];
139
+ return {
140
+ name: "repo:tailwind-dev-sources",
141
+ apply: "build",
142
+ enforce: "pre",
143
+ configResolved(resolved) {
144
+ devDirs = workspaceDevDirs(resolved.root);
145
+ },
146
+ transform: {
147
+ filter: {
148
+ id: { include: /\.css(?:\?|$)/, exclude: /[?&](?:url|raw)\b/ },
149
+ code: /@import\s+["']tailwindcss["']/,
150
+ },
151
+ handler(code, id) {
152
+ const dir = dirname(id.split("?")[0] ?? id);
153
+ const sources = devDirs.map((devDir) => `@source not "${normalizePath(relative(dir, devDir))}";`);
154
+ return `${code}\n${sources.join("\n")}\n`;
155
+ },
156
+ },
157
+ };
158
+ }
159
+ /** Dev folders of the packages in `root`'s pnpm workspace, at any depth;
160
+ * `root` alone outside one. */
161
+ function workspaceDevDirs(root) {
162
+ const workspace = workspaceRoot(realpathSync(root));
163
+ const packages = workspace ? workspacePackageDirs(workspace) : [root];
164
+ return packages.flatMap((dir) => globSync("**/dev", { cwd: dir, exclude: ["**/node_modules/**"] })
165
+ .sort()
166
+ .map((path) => join(dir, path))
167
+ .filter((path) => isDevFile(join(path, "_"))));
168
+ }
169
+ /**
170
+ * Reuses the compiled SSR bundle inside an isolated prerender worker: the
171
+ * prerender environment's `entry` re-exports the SSR build's
172
+ * `wrangler.json` `main`, emitted under `runtime/`. Requires the Cloudflare
173
+ * Vite plugin: its entry worker builds as the `ssr` environment (TanStack
174
+ * Start's server) and writes `wrangler.json`; its
175
+ * `experimental.prerenderWorker` builds as `environment`.
176
+ */
177
+ export function prerenderRuntime({ entry, environment = "prerender", }) {
178
+ let entryPath;
179
+ let runtime;
180
+ return {
181
+ name: "repo:prerender-runtime",
182
+ apply: "build",
183
+ enforce: "pre",
184
+ applyToEnvironment: ({ name }) => name === environment,
185
+ configEnvironment(name) {
186
+ // Server environments drop emitted assets by default.
187
+ if (name === environment)
188
+ return { build: { emitAssets: true } };
189
+ },
190
+ configResolved(config) {
191
+ const server = config.environments.ssr;
192
+ if (!server)
193
+ throw new ExpectedError("repo:prerender-runtime: prerender requires the ssr environment");
194
+ // Module ids are normalized paths, `/`-separated on every platform.
195
+ entryPath = normalizePath(resolve(config.root, entry));
196
+ runtime = resolve(config.root, server.build.outDir);
197
+ },
198
+ resolveId(source) {
199
+ if (source !== "virtual:prerender-runtime")
200
+ return;
201
+ const config = join(runtime, "wrangler.json");
202
+ if (!existsSync(config))
203
+ this.error(`${config} is missing: prerender needs the Cloudflare Vite plugin's entry worker as the ssr environment`);
204
+ const { main } = JSON.parse(readFileSync(config, "utf8"));
205
+ return { id: `./runtime/${main}`, external: true };
206
+ },
207
+ load(id) {
208
+ if (id === entryPath)
209
+ return 'export { default } from "virtual:prerender-runtime";';
210
+ },
211
+ generateBundle() {
212
+ for (const file of globSync("**/*.{js,mjs,wasm}", { cwd: runtime }))
213
+ this.emitFile({
214
+ type: "asset",
215
+ fileName: `runtime/${file}`,
216
+ source: readFileSync(join(runtime, file)),
217
+ });
218
+ },
219
+ };
220
+ }
221
+ /**
222
+ * Isolates the prerender worker from runtime bindings and secrets: the
223
+ * Cloudflare Vite plugin's `experimental.prerenderWorker.config`. The
224
+ * worker runs the entry worker's bundle (`main`, compatibility date and
225
+ * flags) with `vars` as its whole environment; the entry worker's
226
+ * bindings, other vars, and required secrets stay behind.
227
+ */
228
+ export function prerenderWorkerConfig({ name, vars, }) {
229
+ return (_, { entryWorkerConfig }) => ({
230
+ name,
231
+ main: entryWorkerConfig.main,
232
+ compatibility_date: entryWorkerConfig.compatibility_date,
233
+ compatibility_flags: entryWorkerConfig.compatibility_flags,
234
+ secrets: { required: [] },
235
+ vars: vars(entryWorkerConfig.vars),
236
+ });
237
+ }
238
+ /**
239
+ * Re-transforms a deleted file's importers, so a rename reaches them.
240
+ * Vite soft-invalidates them: their code keeps the deleted import path.
241
+ */
242
+ export function renameRecovery() {
243
+ return {
244
+ name: "repo:rename-recovery",
245
+ apply: "serve",
246
+ hotUpdate({ type, modules }) {
247
+ if (type !== "delete")
248
+ return;
249
+ const importers = modules.flatMap((mod) => [...mod.importers]);
250
+ if (importers.length > 0)
251
+ return [...modules, ...importers];
252
+ },
253
+ };
254
+ }
package/dist/wait.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /** Default `waitFor` deadline. */
2
+ export declare const READY_TIMEOUT_MS = 120000;
3
+ /** TCP connect succeeds within 1s. */
4
+ export declare function isPortOpen(port: number, host?: string): Promise<boolean>;
5
+ /** Polls `probe` each second until true; rejects with `message` at the deadline. */
6
+ export declare function waitFor(probe: () => boolean | Promise<boolean>, message: string, timeoutMs?: number, signal?: AbortSignal): Promise<void>;
7
+ /** Polls until localhost `port` refuses connections; rejects with
8
+ * `message` after 15s. */
9
+ export declare function waitForPortClose(port: number, message: string, signal?: AbortSignal): Promise<void>;
package/dist/wait.js ADDED
@@ -0,0 +1,38 @@
1
+ /** Readiness polling: TCP probes and a deadline loop. */
2
+ import { connect } from "node:net";
3
+ import { setTimeout as sleep } from "node:timers/promises";
4
+ import { ExpectedError } from "@astralarium/repo-telemetry";
5
+ /** Default `waitFor` deadline. */
6
+ export const READY_TIMEOUT_MS = 120_000;
7
+ /** TCP connect succeeds within 1s. */
8
+ export function isPortOpen(port, host = "127.0.0.1") {
9
+ return new Promise((done) => {
10
+ const sock = connect({ host, port, timeout: 1_000 });
11
+ sock.once("connect", () => {
12
+ sock.destroy();
13
+ done(true);
14
+ });
15
+ sock.once("error", () => done(false));
16
+ sock.once("timeout", () => {
17
+ sock.destroy();
18
+ done(false);
19
+ });
20
+ });
21
+ }
22
+ /** Polls `probe` each second until true; rejects with `message` at the deadline. */
23
+ export async function waitFor(probe, message, timeoutMs = READY_TIMEOUT_MS, signal) {
24
+ const deadline = Date.now() + timeoutMs;
25
+ for (;;) {
26
+ signal?.throwIfAborted();
27
+ if (await probe())
28
+ return;
29
+ if (Date.now() > deadline)
30
+ throw new ExpectedError(message);
31
+ await sleep(1_000, undefined, { signal });
32
+ }
33
+ }
34
+ /** Polls until localhost `port` refuses connections; rejects with
35
+ * `message` after 15s. */
36
+ export function waitForPortClose(port, message, signal) {
37
+ return waitFor(async () => !(await isPortOpen(port, "localhost")), message, 15_000, signal);
38
+ }
package/package.json CHANGED
@@ -1,6 +1,74 @@
1
1
  {
2
2
  "name": "@astralarium/repo-dev",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "homepage": "https://github.com/astralarium/repo/tree/main/packages/repo-dev#readme",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/astralarium/repo.git",
9
+ "directory": "packages/repo-dev"
10
+ },
11
+ "type": "module",
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "exports": {
16
+ ".": "./dist/index.js",
17
+ "./launcher": "./dist/launcher/launcher.js",
18
+ "./testkit": "./dist/testkit/index.js",
19
+ "./vite": "./dist/vite.js"
20
+ },
21
+ "bin": {
22
+ "db-image": "./dist/bin/db-image.js",
23
+ "run-dev": "./dist/bin/run-dev.js",
24
+ "siblings-inputs": "./dist/bin/siblings-inputs.js"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "dependencies": {
30
+ "json5": "^2.2.3",
31
+ "undici": "^7.29.0",
32
+ "zod": "^4.6.7",
33
+ "@astralarium/repo-telemetry": "^0.1.0",
34
+ "@astralarium/repo-env": "^0.1.0",
35
+ "@astralarium/repo-package-files": "^0.1.0",
36
+ "@astralarium/repo-deploy-info": "^0.1.0"
37
+ },
38
+ "devDependencies": {
39
+ "@cloudflare/vite-plugin": "^1.54.10",
40
+ "@types/node": "^24.2.0",
41
+ "eslint": "^10.2.1",
42
+ "turbo": "^2.10.13",
43
+ "typescript": "^6.0.3",
44
+ "vite": "^8.3.4",
45
+ "vitest": "^5.0.3",
46
+ "wrangler": "^4.132.0",
47
+ "@astralarium/repo-config-typescript": "^0.1.0",
48
+ "@astralarium/repo-config-eslint": "^0.1.0"
49
+ },
50
+ "peerDependencies": {
51
+ "turbo": "^2.10.13",
52
+ "vite": "^8.3.4",
53
+ "vitest": "^5.0.3"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "turbo": {
57
+ "optional": true
58
+ },
59
+ "vite": {
60
+ "optional": true
61
+ },
62
+ "vitest": {
63
+ "optional": true
64
+ }
65
+ },
66
+ "engines": {
67
+ "node": ">=24.2"
68
+ },
69
+ "scripts": {
70
+ "check-types": "tsc --noEmit && tsc -p tsconfig.build.json --noEmit",
71
+ "lint": "eslint",
72
+ "test": "vitest"
73
+ }
6
74
  }