ras-stack 0.39.4 → 0.40.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 (75) hide show
  1. package/README.md +17 -8
  2. package/dist/auth/settings.d.ts +1 -1
  3. package/dist/auth/settings.js +1 -1
  4. package/dist/auth/settings.js.map +1 -1
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +3 -1
  7. package/dist/cli.js.map +1 -1
  8. package/dist/create/index.d.ts +1 -0
  9. package/dist/create/index.js +69 -0
  10. package/dist/create/index.js.map +1 -0
  11. package/dist/runtime/dev.js +1 -1
  12. package/dist/runtime/dev.js.map +1 -1
  13. package/dist/runtime/index.js +77 -13
  14. package/dist/runtime/index.js.map +1 -1
  15. package/examples/full-stack/.env.example +16 -0
  16. package/examples/full-stack/.oxfmtrc.json +7 -0
  17. package/examples/full-stack/Dockerfile +37 -0
  18. package/examples/full-stack/Dockerfile.standalone +28 -0
  19. package/examples/full-stack/centrifugo.json +14 -0
  20. package/examples/full-stack/dockerignore.template +8 -0
  21. package/examples/full-stack/drizzle/0000_production_reference.sql +114 -0
  22. package/examples/full-stack/drizzle/meta/_journal.json +13 -0
  23. package/examples/full-stack/e2e/full-stack.spec.ts +45 -0
  24. package/examples/full-stack/gitignore.template +9 -0
  25. package/examples/full-stack/oxlint.json +4 -0
  26. package/examples/full-stack/package.json +58 -0
  27. package/examples/full-stack/playwright.config.ts +7 -0
  28. package/examples/full-stack/pnpm-workspace.template.yaml +12 -0
  29. package/examples/full-stack/ras-stack.assets.json +4 -0
  30. package/examples/full-stack/scripts/containerRuntime.ts +29 -0
  31. package/examples/full-stack/scripts/database.test.ts +121 -0
  32. package/examples/full-stack/scripts/database.ts +105 -0
  33. package/examples/full-stack/src/client/auth.ts +3 -0
  34. package/examples/full-stack/src/client/queries.ts +4 -0
  35. package/examples/full-stack/src/client/queryClient.ts +1 -0
  36. package/examples/full-stack/src/client/useRealtime.ts +20 -0
  37. package/examples/full-stack/src/posthog.ts +17 -0
  38. package/examples/full-stack/src/routeTree.gen.ts +230 -0
  39. package/examples/full-stack/src/router.tsx +17 -0
  40. package/examples/full-stack/src/routes/__root.tsx +38 -0
  41. package/examples/full-stack/src/routes/api/auth.$.ts +8 -0
  42. package/examples/full-stack/src/routes/api/centrifugo.connect.ts +29 -0
  43. package/examples/full-stack/src/routes/api/health.ts +6 -0
  44. package/examples/full-stack/src/routes/api/live.ts +5 -0
  45. package/examples/full-stack/src/routes/api/ready.ts +30 -0
  46. package/examples/full-stack/src/routes/api/uploads.$id.ts +40 -0
  47. package/examples/full-stack/src/routes/api/uploads.ts +25 -0
  48. package/examples/full-stack/src/routes/index.tsx +187 -0
  49. package/examples/full-stack/src/server/app.test.ts +24 -0
  50. package/examples/full-stack/src/server/app.ts +172 -0
  51. package/examples/full-stack/src/server/auth-flow.test.ts +148 -0
  52. package/examples/full-stack/src/server/auth.ts +60 -0
  53. package/examples/full-stack/src/server/environment.test.ts +43 -0
  54. package/examples/full-stack/src/server/environment.ts +67 -0
  55. package/examples/full-stack/src/server/fns.ts +38 -0
  56. package/examples/full-stack/src/server/messages.test.ts +38 -0
  57. package/examples/full-stack/src/server/messages.ts +28 -0
  58. package/examples/full-stack/src/server/migration.test.ts +34 -0
  59. package/examples/full-stack/src/server/outbox.test.ts +130 -0
  60. package/examples/full-stack/src/server/outbox.ts +110 -0
  61. package/examples/full-stack/src/server/posthog.test.ts +30 -0
  62. package/examples/full-stack/src/server/rate-limit.test.ts +32 -0
  63. package/examples/full-stack/src/server/rate-limit.ts +29 -0
  64. package/examples/full-stack/src/server/rpc.ts +16 -0
  65. package/examples/full-stack/src/server/schema.ts +134 -0
  66. package/examples/full-stack/src/server/session.test.ts +56 -0
  67. package/examples/full-stack/src/server/session.ts +13 -0
  68. package/examples/full-stack/src/server/uploads.test.ts +142 -0
  69. package/examples/full-stack/src/server/uploads.ts +199 -0
  70. package/examples/full-stack/src/start.ts +12 -0
  71. package/examples/full-stack/src/styles.css +42 -0
  72. package/examples/full-stack/tsconfig.json +8 -0
  73. package/examples/full-stack/vite.config.ts +31 -0
  74. package/examples/full-stack/vitest.config.ts +3 -0
  75. package/package.json +10 -8
package/README.md CHANGED
@@ -54,7 +54,7 @@ Applications still configure every upstream library directly. This table describ
54
54
 
55
55
  The application keeps schemas, migrations, repositories, routes, authorization, templates, upload rules, realtime payloads, storage, deployment topology, and UI. There is no shared application factory or giant configuration object.
56
56
 
57
- The [`examples/full-stack`](examples/full-stack) workspace shows the boundaries together and tests them through `workspace:*`. It is an integration contract and reference, not a starter to copy.
57
+ The [`examples/full-stack`](examples/full-stack) workspace shows the boundaries together and tests them through `workspace:*`. It is the repository's integration contract and the canonical source for the generated starter; once generated, the application owns its copied code and normal semver dependency.
58
58
 
59
59
  ## Pick what you need 🧩
60
60
 
@@ -70,7 +70,7 @@ Nodemailer, Centrifuge, `better-sqlite3`, Drizzle, Postgres.js, and `tus-js-clie
70
70
  pnpm add nodemailer
71
71
  pnpm add centrifuge
72
72
  pnpm add tus-js-client
73
- pnpm add better-sqlite3 drizzle-orm
73
+ pnpm add better-auth @better-auth/drizzle-adapter better-sqlite3 drizzle-orm
74
74
  pnpm add postgres drizzle-orm
75
75
  ```
76
76
 
@@ -87,6 +87,14 @@ Start with the narrowest public entrypoint that owns the repeated mechanic:
87
87
  | Compiler, lint, CI, release, and preview mechanics | `ras-stack/config/*`, `actions/*`, `.github/workflows/*`, `ras-stack/preview/*` | Triggers, permissions, services, deployment, and verification |
88
88
  | Generated repository policy files | `ras policy` | Which policies apply and every declared override |
89
89
 
90
+ To start from the production reference instead of assembling entrypoints individually:
91
+
92
+ ```sh
93
+ pnpm dlx ras-stack create my-app
94
+ ```
95
+
96
+ The scaffold includes Better Auth, checked-in migrations, durable uploads, SMTP flows, a transactional realtime outbox, production health/lifecycle behavior, and its tests. It remains ordinary application code rather than a second framework API.
97
+
90
98
  ## Dokploy previews 🚀
91
99
 
92
100
  Three reusable workflows provide the standard pull-request preview lifecycle:
@@ -99,12 +107,13 @@ Applications supply only their package, application prefix, domain, port, enviro
99
107
 
100
108
  ## Guides 📚
101
109
 
102
- | Guide | What it covers |
103
- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
104
- | [Application primitives](docs/application-primitives.md) | Authentication, request security, databases, realtime clients, email, uploads, and stateful development resources |
105
- | [Repository tooling](docs/repository-tooling.md) | TypeScript and Oxlint configuration, generated policy, GitHub Actions, previews, releases, and production runtime composition |
106
- | [PostHog integration](docs/posthog.md) | Browser/server setup, identity and session correlation, ingest proxying, shutdown, coverage declarations, and source-map responsibility |
107
- | [Full-stack example](docs/full-stack-example.md) | The `workspace:*` integration contract, local development, production container, and two-browser journey |
110
+ | Guide | What it covers |
111
+ | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
112
+ | [Application primitives](docs/application-primitives.md) | Authentication, request security, databases, realtime clients, email, uploads, and stateful development resources |
113
+ | [Repository tooling](docs/repository-tooling.md) | TypeScript and Oxlint configuration, generated policy, GitHub Actions, previews, releases, and production runtime composition |
114
+ | [PostHog integration](docs/posthog.md) | Browser/server setup, identity and session correlation, ingest proxying, shutdown, coverage declarations, and source-map responsibility |
115
+ | [Full-stack example](docs/full-stack-example.md) | The `workspace:*` integration contract, local development, production container, and two-browser journey |
116
+ | [Production operations](docs/production-reference-operations.md) | Migration, backup/restore, rollback, configuration, proxy, shutdown, and supply-chain boundaries |
108
117
 
109
118
  ## Development 🛠️
110
119
 
@@ -20,7 +20,7 @@ export declare function standardRateLimitOptions(customRules?: Record<string, Ra
20
20
  window: number;
21
21
  max: number;
22
22
  };
23
- '/forget-password': {
23
+ '/request-password-reset': {
24
24
  window: number;
25
25
  max: number;
26
26
  };
@@ -10,7 +10,7 @@ export function standardRateLimitOptions(customRules = {}) {
10
10
  customRules: {
11
11
  '/sign-in/email': { window: 60, max: 20 },
12
12
  '/sign-up/email': { window: 60, max: 15 },
13
- '/forget-password': { window: 60, max: 5 },
13
+ '/request-password-reset': { window: 60, max: 5 },
14
14
  ...customRules,
15
15
  },
16
16
  };
@@ -1 +1 @@
1
- {"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/auth/settings.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,sBAAsB;IACpC,OAAO,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,CAAA;AAClE,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,WAAW,GAAkC,EAAE;IACtF,OAAO;QACL,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,UAAmB;QAC5B,MAAM,EAAE,EAAE;QACV,GAAG,EAAE,GAAG;QACR,WAAW,EAAE;YACX,gBAAgB,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;YACzC,gBAAgB,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;YACzC,kBAAkB,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE;YAC1C,GAAG,WAAW;SACf;KACF,CAAA;AACH,CAAC","sourcesContent":["export type RateLimitRule = { window: number; max: number }\n\nexport function standardSessionOptions() {\n return { expiresIn: 60 * 60 * 24 * 90, updateAge: 60 * 60 * 24 }\n}\n\nexport function standardRateLimitOptions(customRules: Record<string, RateLimitRule> = {}) {\n return {\n enabled: true,\n storage: 'database' as const,\n window: 60,\n max: 120,\n customRules: {\n '/sign-in/email': { window: 60, max: 20 },\n '/sign-up/email': { window: 60, max: 15 },\n '/forget-password': { window: 60, max: 5 },\n ...customRules,\n },\n }\n}\n"]}
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/auth/settings.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,sBAAsB;IACpC,OAAO,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,CAAA;AAClE,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,WAAW,GAAkC,EAAE;IACtF,OAAO;QACL,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,UAAmB;QAC5B,MAAM,EAAE,EAAE;QACV,GAAG,EAAE,GAAG;QACR,WAAW,EAAE;YACX,gBAAgB,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;YACzC,gBAAgB,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;YACzC,yBAAyB,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE;YACjD,GAAG,WAAW;SACf;KACF,CAAA;AACH,CAAC","sourcesContent":["export type RateLimitRule = { window: number; max: number }\n\nexport function standardSessionOptions() {\n return { expiresIn: 60 * 60 * 24 * 90, updateAge: 60 * 60 * 24 }\n}\n\nexport function standardRateLimitOptions(customRules: Record<string, RateLimitRule> = {}) {\n return {\n enabled: true,\n storage: 'database' as const,\n window: 60,\n max: 120,\n customRules: {\n '/sign-in/email': { window: 60, max: 20 },\n '/sign-up/email': { window: 60, max: 15 },\n '/request-password-reset': { window: 60, max: 5 },\n ...customRules,\n },\n }\n}\n"]}
package/dist/cli.d.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { runAssetsCli } from './build/cli.js';
3
+ import { runCreateCli } from './create/index.js';
3
4
  import { runInitCli } from './policy/init-cli.js';
4
5
  import { runPolicyCli } from './policy/cli.js';
5
6
  import { runPreviewCli } from './preview/cli.js';
6
7
  import { runRealtimeCli } from './runtime/dev-cli.js';
7
8
  declare const commands: {
8
9
  readonly assets: typeof runAssetsCli;
10
+ readonly create: typeof runCreateCli;
9
11
  readonly init: typeof runInitCli;
10
12
  readonly policy: typeof runPolicyCli;
11
13
  readonly preview: typeof runPreviewCli;
package/dist/cli.js CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { runAssetsCli } from './build/cli.js';
3
+ import { runCreateCli } from './create/index.js';
3
4
  import { runInitCli } from './policy/init-cli.js';
4
5
  import { runPolicyCli } from './policy/cli.js';
5
6
  import { runPreviewCli } from './preview/cli.js';
6
7
  import { runRealtimeCli } from './runtime/dev-cli.js';
7
8
  const commands = {
8
9
  assets: runAssetsCli,
10
+ create: runCreateCli,
9
11
  init: runInitCli,
10
12
  policy: runPolicyCli,
11
13
  preview: runPreviewCli,
@@ -20,7 +22,7 @@ export function parseRasCommand(arguments_) {
20
22
  export async function runRasCli(arguments_) {
21
23
  const parsed = parseRasCommand(arguments_);
22
24
  if (!parsed) {
23
- console.error('usage: ras <assets|init|policy|preview|realtime> [arguments]');
25
+ console.error('usage: ras <assets|create|init|policy|preview|realtime> [arguments]');
24
26
  process.exitCode = 2;
25
27
  return;
26
28
  }
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAErD,MAAM,QAAQ,GAAG;IACf,MAAM,EAAE,YAAY;IACpB,IAAI,EAAE,UAAU;IAChB,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,aAAa;IACtB,QAAQ,EAAE,cAAc;CAChB,CAAA;AAIV,MAAM,UAAU,eAAe,CAAC,UAAoB;IAClD,MAAM,CAAC,OAAO,EAAE,GAAG,aAAa,CAAC,GAAG,UAAU,CAAA;IAC9C,IAAI,CAAC,CAAC,OAAO,IAAI,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAAE,OAAO,SAAS,CAAA;IACpE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,OAAO,EAAE,OAAqB,EAAE,CAAA;AACrE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,UAAoB;IAClD,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAA;IAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,KAAK,CAAC,8DAA8D,CAAC,CAAA;QAC7E,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;QACpB,OAAM;IACR,CAAC;IACD,MAAM,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;AAClD,CAAC;AAED,IAAI,OAAO,IAAI,CAAC,IAAI;IAAE,MAAM,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA","sourcesContent":["#!/usr/bin/env node\nimport { runAssetsCli } from './build/cli.js'\nimport { runInitCli } from './policy/init-cli.js'\nimport { runPolicyCli } from './policy/cli.js'\nimport { runPreviewCli } from './preview/cli.js'\nimport { runRealtimeCli } from './runtime/dev-cli.js'\n\nconst commands = {\n assets: runAssetsCli,\n init: runInitCli,\n policy: runPolicyCli,\n preview: runPreviewCli,\n realtime: runRealtimeCli,\n} as const\n\nexport type RasCommand = keyof typeof commands\n\nexport function parseRasCommand(arguments_: string[]): { arguments: string[]; command: RasCommand } | undefined {\n const [command, ...argumentsRest] = arguments_\n if (!(command && Object.hasOwn(commands, command))) return undefined\n return { arguments: argumentsRest, command: command as RasCommand }\n}\n\nexport async function runRasCli(arguments_: string[]): Promise<void> {\n const parsed = parseRasCommand(arguments_)\n if (!parsed) {\n console.error('usage: ras <assets|init|policy|preview|realtime> [arguments]')\n process.exitCode = 2\n return\n }\n await commands[parsed.command](parsed.arguments)\n}\n\nif (import.meta.main) await runRasCli(process.argv.slice(2))\n"]}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAErD,MAAM,QAAQ,GAAG;IACf,MAAM,EAAE,YAAY;IACpB,MAAM,EAAE,YAAY;IACpB,IAAI,EAAE,UAAU;IAChB,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,aAAa;IACtB,QAAQ,EAAE,cAAc;CAChB,CAAA;AAIV,MAAM,UAAU,eAAe,CAAC,UAAoB;IAClD,MAAM,CAAC,OAAO,EAAE,GAAG,aAAa,CAAC,GAAG,UAAU,CAAA;IAC9C,IAAI,CAAC,CAAC,OAAO,IAAI,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAAE,OAAO,SAAS,CAAA;IACpE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,OAAO,EAAE,OAAqB,EAAE,CAAA;AACrE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,UAAoB;IAClD,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAA;IAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,KAAK,CAAC,qEAAqE,CAAC,CAAA;QACpF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;QACpB,OAAM;IACR,CAAC;IACD,MAAM,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;AAClD,CAAC;AAED,IAAI,OAAO,IAAI,CAAC,IAAI;IAAE,MAAM,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA","sourcesContent":["#!/usr/bin/env node\nimport { runAssetsCli } from './build/cli.js'\nimport { runCreateCli } from './create/index.js'\nimport { runInitCli } from './policy/init-cli.js'\nimport { runPolicyCli } from './policy/cli.js'\nimport { runPreviewCli } from './preview/cli.js'\nimport { runRealtimeCli } from './runtime/dev-cli.js'\n\nconst commands = {\n assets: runAssetsCli,\n create: runCreateCli,\n init: runInitCli,\n policy: runPolicyCli,\n preview: runPreviewCli,\n realtime: runRealtimeCli,\n} as const\n\nexport type RasCommand = keyof typeof commands\n\nexport function parseRasCommand(arguments_: string[]): { arguments: string[]; command: RasCommand } | undefined {\n const [command, ...argumentsRest] = arguments_\n if (!(command && Object.hasOwn(commands, command))) return undefined\n return { arguments: argumentsRest, command: command as RasCommand }\n}\n\nexport async function runRasCli(arguments_: string[]): Promise<void> {\n const parsed = parseRasCommand(arguments_)\n if (!parsed) {\n console.error('usage: ras <assets|create|init|policy|preview|realtime> [arguments]')\n process.exitCode = 2\n return\n }\n await commands[parsed.command](parsed.arguments)\n}\n\nif (import.meta.main) await runRasCli(process.argv.slice(2))\n"]}
@@ -0,0 +1 @@
1
+ export declare function runCreateCli(arguments_: string[]): Promise<void>;
@@ -0,0 +1,69 @@
1
+ import { copyFile, cp, mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ export async function runCreateCli(arguments_) {
4
+ const dryRun = arguments_.includes('--dry-run');
5
+ const positional = arguments_.filter((argument) => argument !== '--dry-run');
6
+ if (positional.length !== 1) {
7
+ console.error('usage: ras create <directory> [--dry-run]');
8
+ process.exitCode = 2;
9
+ return;
10
+ }
11
+ const destination = path.resolve(positional[0]);
12
+ const source = path.resolve(import.meta.dirname, '../../examples/full-stack');
13
+ await assertSource(source);
14
+ await assertEmptyDestination(destination);
15
+ if (dryRun) {
16
+ console.log(destination);
17
+ return;
18
+ }
19
+ await mkdir(destination, { recursive: true });
20
+ await cp(source, destination, {
21
+ recursive: true,
22
+ filter: (entry) => !excluded(path.relative(source, entry)),
23
+ });
24
+ await materializeTemplate(destination, 'gitignore.template', '.gitignore');
25
+ await materializeTemplate(destination, 'dockerignore.template', '.dockerignore');
26
+ await materializeTemplate(destination, 'pnpm-workspace.template.yaml', 'pnpm-workspace.yaml');
27
+ await copyFile(path.join(destination, 'Dockerfile.standalone'), path.join(destination, 'Dockerfile'));
28
+ await rm(path.join(destination, 'Dockerfile.standalone'));
29
+ const packageFile = path.join(destination, 'package.json');
30
+ const manifest = JSON.parse(await readFile(packageFile, 'utf8'));
31
+ const packageManifest = JSON.parse(await readFile(path.resolve(import.meta.dirname, '../../package.json'), 'utf8'));
32
+ manifest.name = packageName(path.basename(destination));
33
+ manifest.dependencies['ras-stack'] = `^${packageManifest.version}`;
34
+ manifest.scripts.build = manifest.scripts.build.replaceAll('node ../../dist/cli.js', 'ras');
35
+ await writeFile(packageFile, `${JSON.stringify(manifest, null, 2)}\n`);
36
+ console.log(destination);
37
+ }
38
+ async function materializeTemplate(directory, template, output) {
39
+ const source = path.join(directory, template);
40
+ await copyFile(source, path.join(directory, output));
41
+ await rm(source);
42
+ }
43
+ function excluded(relative) {
44
+ const [root] = relative.split(path.sep);
45
+ return ['.output', '.data', 'node_modules', 'test-results'].includes(root ?? '');
46
+ }
47
+ async function assertSource(source) {
48
+ if (!(await stat(path.join(source, 'package.json'))).isFile())
49
+ throw new Error('The packaged full-stack starter is missing');
50
+ }
51
+ async function assertEmptyDestination(destination) {
52
+ try {
53
+ const info = await stat(destination);
54
+ if (!info.isDirectory() || (await readdir(destination)).length > 0)
55
+ throw new Error(`Destination must be an empty directory: ${destination}`);
56
+ }
57
+ catch (error) {
58
+ if (error.code !== 'ENOENT')
59
+ throw error;
60
+ }
61
+ }
62
+ function packageName(name) {
63
+ const normalized = name
64
+ .toLowerCase()
65
+ .replaceAll(/[^a-z\d._-]/g, '-')
66
+ .replaceAll(/^-+|-+$/g, '');
67
+ return normalized || 'ras-stack-app';
68
+ }
69
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/create/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { copyFile, cp, mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\n\nexport async function runCreateCli(arguments_: string[]) {\n const dryRun = arguments_.includes('--dry-run')\n const positional = arguments_.filter((argument) => argument !== '--dry-run')\n if (positional.length !== 1) {\n console.error('usage: ras create <directory> [--dry-run]')\n process.exitCode = 2\n return\n }\n const destination = path.resolve(positional[0]!)\n const source = path.resolve(import.meta.dirname, '../../examples/full-stack')\n await assertSource(source)\n await assertEmptyDestination(destination)\n if (dryRun) {\n console.log(destination)\n return\n }\n await mkdir(destination, { recursive: true })\n await cp(source, destination, {\n recursive: true,\n filter: (entry) => !excluded(path.relative(source, entry)),\n })\n await materializeTemplate(destination, 'gitignore.template', '.gitignore')\n await materializeTemplate(destination, 'dockerignore.template', '.dockerignore')\n await materializeTemplate(destination, 'pnpm-workspace.template.yaml', 'pnpm-workspace.yaml')\n await copyFile(path.join(destination, 'Dockerfile.standalone'), path.join(destination, 'Dockerfile'))\n await rm(path.join(destination, 'Dockerfile.standalone'))\n const packageFile = path.join(destination, 'package.json')\n const manifest = JSON.parse(await readFile(packageFile, 'utf8')) as {\n name: string\n dependencies: Record<string, string>\n scripts: Record<string, string>\n }\n const packageManifest = JSON.parse(await readFile(path.resolve(import.meta.dirname, '../../package.json'), 'utf8')) as { version: string }\n manifest.name = packageName(path.basename(destination))\n manifest.dependencies['ras-stack'] = `^${packageManifest.version}`\n manifest.scripts.build = manifest.scripts.build!.replaceAll('node ../../dist/cli.js', 'ras')\n await writeFile(packageFile, `${JSON.stringify(manifest, null, 2)}\\n`)\n console.log(destination)\n}\n\nasync function materializeTemplate(directory: string, template: string, output: string) {\n const source = path.join(directory, template)\n await copyFile(source, path.join(directory, output))\n await rm(source)\n}\n\nfunction excluded(relative: string) {\n const [root] = relative.split(path.sep)\n return ['.output', '.data', 'node_modules', 'test-results'].includes(root ?? '')\n}\n\nasync function assertSource(source: string) {\n if (!(await stat(path.join(source, 'package.json'))).isFile()) throw new Error('The packaged full-stack starter is missing')\n}\n\nasync function assertEmptyDestination(destination: string) {\n try {\n const info = await stat(destination)\n if (!info.isDirectory() || (await readdir(destination)).length > 0)\n throw new Error(`Destination must be an empty directory: ${destination}`)\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error\n }\n}\n\nfunction packageName(name: string) {\n const normalized = name\n .toLowerCase()\n .replaceAll(/[^a-z\\d._-]/g, '-')\n .replaceAll(/^-+|-+$/g, '')\n return normalized || 'ras-stack-app'\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { access } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
- const runtimeImage = 'ghcr.io/richardsolomou/ras-stack-runtime-binaries:runtime-v1.0.0@sha256:5f82b2d53b93465bf91cc1bc90b292e94cbdd823cedd3f432dca94097e59163d';
4
+ const runtimeImage = 'ghcr.io/richardsolomou/ras-stack-runtime-binaries:runtime-v1.0.1@sha256:581a691b59a603685bee8ba9576e80d419b967b63017f82d778fce39ca6f0c0b';
5
5
  export async function runRealtimeDev(options, dependencies = {}) {
6
6
  const configPath = path.resolve(options.configPath);
7
7
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/runtime/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process'\nimport { access } from 'node:fs/promises'\nimport path from 'node:path'\n\nconst runtimeImage =\n 'ghcr.io/richardsolomou/ras-stack-runtime-binaries:runtime-v1.0.0@sha256:5f82b2d53b93465bf91cc1bc90b292e94cbdd823cedd3f432dca94097e59163d'\n\nexport type RealtimeDevOptions = {\n configPath: string\n containerName: string\n port: number\n bindAddress?: string\n origin: string\n secret: string\n detached?: boolean\n connectProxyEndpoint?: string\n}\n\ntype CommandResult = { code: number }\ntype RunCommand = (command: string, args: readonly string[]) => Promise<CommandResult>\ntype RealtimeDevDependencies = { remove?: (containerName: string) => Promise<void>; run?: RunCommand }\n\nexport async function runRealtimeDev(options: RealtimeDevOptions, dependencies: RealtimeDevDependencies = {}) {\n const configPath = path.resolve(options.configPath)\n try {\n await access(configPath)\n } catch (error) {\n throw new Error(`realtime config is unavailable: ${configPath}`, { cause: error })\n }\n const containerName = dockerName(options.containerName)\n const publicPort = tcpPort(options.port)\n const bindAddress = dockerBindAddress(options.bindAddress ?? '127.0.0.1')\n const origin = httpUrl(options.origin, 'origin')\n const secret = required(options.secret, 'secret')\n const connectProxyEndpoint = options.connectProxyEndpoint ? httpEndpoint(options.connectProxyEndpoint) : undefined\n const run = dependencies.run ?? runCommand\n const remove = dependencies.remove ?? removeContainer\n\n try {\n if (options.detached) await remove(containerName)\n const result = await run('docker', [\n 'run',\n '--rm',\n ...(options.detached ? ['-d'] : []),\n '--name',\n containerName,\n '--add-host',\n 'host.docker.internal:host-gateway',\n '-p',\n `${bindAddress}:${publicPort}:8000`,\n '-e',\n `CENTRIFUGO_CLIENT_TOKEN_HMAC_SECRET_KEY=${secret}`,\n '-e',\n 'CENTRIFUGO_CLIENT_SUBSCRIPTION_TOKEN_ENABLED=true',\n '-e',\n `CENTRIFUGO_CLIENT_SUBSCRIPTION_TOKEN_HMAC_SECRET_KEY=${secret}`,\n '-e',\n `CENTRIFUGO_HTTP_API_KEY=${secret}`,\n '-e',\n `CENTRIFUGO_CLIENT_ALLOWED_ORIGINS=${origin}`,\n '-e',\n `CENTRIFUGO_VAR_PROXY_SECRET=${secret}`,\n ...(connectProxyEndpoint ? ['-e', `CENTRIFUGO_CLIENT_PROXY_CONNECT_ENDPOINT=${connectProxyEndpoint}`] : []),\n '-v',\n `${configPath}:/centrifugo/config.json:ro`,\n runtimeImage,\n '/usr/local/bin/centrifugo',\n '--config=/centrifugo/config.json',\n '--health.enabled',\n ])\n if (result.code !== 0) throw new Error(`Docker exited with status ${result.code}`)\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n throw new Error('Docker is required but was not found', { cause: error })\n }\n throw error\n }\n}\n\nexport function parseRealtimeDevArguments(arguments_: readonly string[]): RealtimeDevOptions {\n const values = new Map<string, string>()\n let detached = false\n for (let index = 0; index < arguments_.length; index += 1) {\n const argument = arguments_[index]!\n if (argument === '--detach') {\n detached = true\n continue\n }\n if (!['--config', '--name', '--port', '--bind-address', '--origin', '--secret', '--connect-proxy-endpoint'].includes(argument)) {\n throw new Error(`unknown argument: ${argument}`)\n }\n const value = arguments_[index + 1]\n if (!value || value.startsWith('--')) throw new Error(`${argument} requires a value`)\n values.set(argument, value)\n index += 1\n }\n const connectProxyEndpoint = values.get('--connect-proxy-endpoint')\n const bindAddress = values.get('--bind-address')\n return {\n configPath: required(values.get('--config'), '--config'),\n containerName: required(values.get('--name'), '--name'),\n port: Number(required(values.get('--port'), '--port')),\n ...(bindAddress ? { bindAddress } : {}),\n origin: required(values.get('--origin'), '--origin'),\n secret: required(values.get('--secret'), '--secret'),\n ...(detached ? { detached: true } : {}),\n ...(connectProxyEndpoint ? { connectProxyEndpoint } : {}),\n }\n}\n\nasync function runCommand(command: string, args: readonly string[]) {\n return new Promise<CommandResult>((resolve, reject) => {\n const child = spawn(command, [...args], { stdio: 'inherit' })\n child.once('error', reject)\n child.once('exit', (code) => resolve({ code: code ?? 1 }))\n })\n}\n\nasync function removeContainer(containerName: string) {\n await new Promise<void>((resolve, reject) => {\n const child = spawn('docker', ['rm', '--force', containerName], { stdio: 'ignore' })\n child.once('error', reject)\n child.once('exit', () => resolve())\n })\n}\n\nfunction required(value: string | undefined, name: string) {\n const normalized = value?.trim()\n if (!normalized) throw new Error(`${name} is required`)\n return normalized\n}\n\nfunction dockerName(value: string) {\n const normalized = required(value, 'containerName')\n if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(normalized)) throw new Error('containerName is invalid')\n return normalized\n}\n\nfunction tcpPort(value: number) {\n if (!Number.isInteger(value) || value < 1 || value > 65_535) throw new Error('port must be a valid TCP port')\n return value\n}\n\nfunction dockerBindAddress(value: string) {\n if (value !== '127.0.0.1' && value !== '0.0.0.0') throw new Error('bindAddress must be 127.0.0.1 or 0.0.0.0')\n return value\n}\n\nfunction httpUrl(value: string, name: string) {\n let parsed: URL\n try {\n parsed = new URL(value)\n } catch {\n throw new Error(`${name} must be an HTTP URL`)\n }\n if (\n !['http:', 'https:'].includes(parsed.protocol) ||\n parsed.username ||\n parsed.password ||\n parsed.pathname !== '/' ||\n parsed.search ||\n parsed.hash\n ) {\n throw new Error(`${name} must be an HTTP origin`)\n }\n return parsed.origin\n}\n\nfunction httpEndpoint(value: string) {\n let parsed: URL\n try {\n parsed = new URL(value)\n } catch {\n throw new Error('connectProxyEndpoint must be an HTTP URL')\n }\n if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash) {\n throw new Error('connectProxyEndpoint must be an HTTP URL without credentials, query, or fragment')\n }\n return parsed.toString()\n}\n"]}
1
+ {"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/runtime/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process'\nimport { access } from 'node:fs/promises'\nimport path from 'node:path'\n\nconst runtimeImage =\n 'ghcr.io/richardsolomou/ras-stack-runtime-binaries:runtime-v1.0.1@sha256:581a691b59a603685bee8ba9576e80d419b967b63017f82d778fce39ca6f0c0b'\n\nexport type RealtimeDevOptions = {\n configPath: string\n containerName: string\n port: number\n bindAddress?: string\n origin: string\n secret: string\n detached?: boolean\n connectProxyEndpoint?: string\n}\n\ntype CommandResult = { code: number }\ntype RunCommand = (command: string, args: readonly string[]) => Promise<CommandResult>\ntype RealtimeDevDependencies = { remove?: (containerName: string) => Promise<void>; run?: RunCommand }\n\nexport async function runRealtimeDev(options: RealtimeDevOptions, dependencies: RealtimeDevDependencies = {}) {\n const configPath = path.resolve(options.configPath)\n try {\n await access(configPath)\n } catch (error) {\n throw new Error(`realtime config is unavailable: ${configPath}`, { cause: error })\n }\n const containerName = dockerName(options.containerName)\n const publicPort = tcpPort(options.port)\n const bindAddress = dockerBindAddress(options.bindAddress ?? '127.0.0.1')\n const origin = httpUrl(options.origin, 'origin')\n const secret = required(options.secret, 'secret')\n const connectProxyEndpoint = options.connectProxyEndpoint ? httpEndpoint(options.connectProxyEndpoint) : undefined\n const run = dependencies.run ?? runCommand\n const remove = dependencies.remove ?? removeContainer\n\n try {\n if (options.detached) await remove(containerName)\n const result = await run('docker', [\n 'run',\n '--rm',\n ...(options.detached ? ['-d'] : []),\n '--name',\n containerName,\n '--add-host',\n 'host.docker.internal:host-gateway',\n '-p',\n `${bindAddress}:${publicPort}:8000`,\n '-e',\n `CENTRIFUGO_CLIENT_TOKEN_HMAC_SECRET_KEY=${secret}`,\n '-e',\n 'CENTRIFUGO_CLIENT_SUBSCRIPTION_TOKEN_ENABLED=true',\n '-e',\n `CENTRIFUGO_CLIENT_SUBSCRIPTION_TOKEN_HMAC_SECRET_KEY=${secret}`,\n '-e',\n `CENTRIFUGO_HTTP_API_KEY=${secret}`,\n '-e',\n `CENTRIFUGO_CLIENT_ALLOWED_ORIGINS=${origin}`,\n '-e',\n `CENTRIFUGO_VAR_PROXY_SECRET=${secret}`,\n ...(connectProxyEndpoint ? ['-e', `CENTRIFUGO_CLIENT_PROXY_CONNECT_ENDPOINT=${connectProxyEndpoint}`] : []),\n '-v',\n `${configPath}:/centrifugo/config.json:ro`,\n runtimeImage,\n '/usr/local/bin/centrifugo',\n '--config=/centrifugo/config.json',\n '--health.enabled',\n ])\n if (result.code !== 0) throw new Error(`Docker exited with status ${result.code}`)\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n throw new Error('Docker is required but was not found', { cause: error })\n }\n throw error\n }\n}\n\nexport function parseRealtimeDevArguments(arguments_: readonly string[]): RealtimeDevOptions {\n const values = new Map<string, string>()\n let detached = false\n for (let index = 0; index < arguments_.length; index += 1) {\n const argument = arguments_[index]!\n if (argument === '--detach') {\n detached = true\n continue\n }\n if (!['--config', '--name', '--port', '--bind-address', '--origin', '--secret', '--connect-proxy-endpoint'].includes(argument)) {\n throw new Error(`unknown argument: ${argument}`)\n }\n const value = arguments_[index + 1]\n if (!value || value.startsWith('--')) throw new Error(`${argument} requires a value`)\n values.set(argument, value)\n index += 1\n }\n const connectProxyEndpoint = values.get('--connect-proxy-endpoint')\n const bindAddress = values.get('--bind-address')\n return {\n configPath: required(values.get('--config'), '--config'),\n containerName: required(values.get('--name'), '--name'),\n port: Number(required(values.get('--port'), '--port')),\n ...(bindAddress ? { bindAddress } : {}),\n origin: required(values.get('--origin'), '--origin'),\n secret: required(values.get('--secret'), '--secret'),\n ...(detached ? { detached: true } : {}),\n ...(connectProxyEndpoint ? { connectProxyEndpoint } : {}),\n }\n}\n\nasync function runCommand(command: string, args: readonly string[]) {\n return new Promise<CommandResult>((resolve, reject) => {\n const child = spawn(command, [...args], { stdio: 'inherit' })\n child.once('error', reject)\n child.once('exit', (code) => resolve({ code: code ?? 1 }))\n })\n}\n\nasync function removeContainer(containerName: string) {\n await new Promise<void>((resolve, reject) => {\n const child = spawn('docker', ['rm', '--force', containerName], { stdio: 'ignore' })\n child.once('error', reject)\n child.once('exit', () => resolve())\n })\n}\n\nfunction required(value: string | undefined, name: string) {\n const normalized = value?.trim()\n if (!normalized) throw new Error(`${name} is required`)\n return normalized\n}\n\nfunction dockerName(value: string) {\n const normalized = required(value, 'containerName')\n if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(normalized)) throw new Error('containerName is invalid')\n return normalized\n}\n\nfunction tcpPort(value: number) {\n if (!Number.isInteger(value) || value < 1 || value > 65_535) throw new Error('port must be a valid TCP port')\n return value\n}\n\nfunction dockerBindAddress(value: string) {\n if (value !== '127.0.0.1' && value !== '0.0.0.0') throw new Error('bindAddress must be 127.0.0.1 or 0.0.0.0')\n return value\n}\n\nfunction httpUrl(value: string, name: string) {\n let parsed: URL\n try {\n parsed = new URL(value)\n } catch {\n throw new Error(`${name} must be an HTTP URL`)\n }\n if (\n !['http:', 'https:'].includes(parsed.protocol) ||\n parsed.username ||\n parsed.password ||\n parsed.pathname !== '/' ||\n parsed.search ||\n parsed.hash\n ) {\n throw new Error(`${name} must be an HTTP origin`)\n }\n return parsed.origin\n}\n\nfunction httpEndpoint(value: string) {\n let parsed: URL\n try {\n parsed = new URL(value)\n } catch {\n throw new Error('connectProxyEndpoint must be an HTTP URL')\n }\n if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash) {\n throw new Error('connectProxyEndpoint must be an HTTP URL without credentials, query, or fragment')\n }\n return parsed.toString()\n}\n"]}
@@ -3,6 +3,9 @@ import { mkdir, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  export { runRealtimeDev } from './dev.js';
5
5
  export async function superviseProcesses(processes, options = {}) {
6
+ return runSupervisor(processes, options);
7
+ }
8
+ async function runSupervisor(processes, options, shutdownOrder) {
6
9
  if (processes.length === 0)
7
10
  throw new Error('at least one runtime process is required');
8
11
  const names = new Set();
@@ -27,12 +30,15 @@ export async function superviseProcesses(processes, options = {}) {
27
30
  stdio: 'inherit',
28
31
  }));
29
32
  const children = new Map();
33
+ const exits = new Map();
30
34
  let settled = false;
35
+ let phasedShutdown = false;
36
+ let expectedExit;
31
37
  let resolveResult;
32
38
  const result = new Promise((resolve) => {
33
39
  resolveResult = resolve;
34
40
  });
35
- const finish = async (status) => {
41
+ const finishImmediately = async (status) => {
36
42
  if (settled)
37
43
  return;
38
44
  settled = true;
@@ -41,29 +47,87 @@ export async function superviseProcesses(processes, options = {}) {
41
47
  await stopChildren([...children.keys()], shutdownTimeoutMs);
42
48
  resolveResult(status);
43
49
  };
44
- const onSignal = () => void finish(0);
50
+ const finishInPhases = async () => {
51
+ if (settled || phasedShutdown)
52
+ return;
53
+ phasedShutdown = true;
54
+ signalSource.off('SIGINT', onSignal);
55
+ signalSource.off('SIGTERM', onSignal);
56
+ const deadline = Date.now() + shutdownTimeoutMs;
57
+ for (const name of shutdownOrder ?? []) {
58
+ if (settled)
59
+ return;
60
+ const child = [...children].find(([, childName]) => childName === name)?.[0];
61
+ if (!child || !running(child))
62
+ continue;
63
+ expectedExit = child;
64
+ child.kill('SIGTERM');
65
+ // oxlint-disable-next-line no-await-in-loop -- Shutdown phases must remain dependency-ordered.
66
+ const exited = await beforeDeadline(exits.get(child), Math.max(0, deadline - Date.now()));
67
+ expectedExit = undefined;
68
+ if (settled)
69
+ return;
70
+ if (!exited) {
71
+ settled = true;
72
+ for (const runningChild of children.keys())
73
+ if (running(runningChild))
74
+ runningChild.kill('SIGKILL');
75
+ resolveResult(0);
76
+ return;
77
+ }
78
+ }
79
+ settled = true;
80
+ resolveResult(0);
81
+ };
82
+ const onSignal = () => {
83
+ if (shutdownOrder)
84
+ void finishInPhases();
85
+ else
86
+ void finishImmediately(0);
87
+ };
45
88
  signalSource.once('SIGINT', onSignal);
46
89
  signalSource.once('SIGTERM', onSignal);
47
90
  try {
48
91
  for (const specification of processes) {
49
92
  const child = spawnProcess(specification);
50
93
  children.set(child, specification.name);
51
- child.once('error', () => void finish(1));
52
- child.once('exit', (code) => void finish(code && code > 0 ? code : 1));
94
+ let resolveExit;
95
+ exits.set(child, new Promise((resolve) => {
96
+ resolveExit = resolve;
97
+ }));
98
+ child.once('error', () => void finishImmediately(1));
99
+ child.once('exit', (code) => {
100
+ resolveExit();
101
+ if (!phasedShutdown || child !== expectedExit)
102
+ void finishImmediately(code && code > 0 ? code : 1);
103
+ });
53
104
  }
54
105
  }
55
106
  catch (error) {
56
- await finish(1);
107
+ await finishImmediately(1);
57
108
  throw error;
58
109
  }
59
110
  return result;
60
111
  }
112
+ async function beforeDeadline(exited, timeoutMs) {
113
+ let timer;
114
+ const timeout = new Promise((resolve) => {
115
+ timer = setTimeout(() => resolve(false), timeoutMs);
116
+ });
117
+ const result = await Promise.race([exited.then(() => true), timeout]);
118
+ if (timer)
119
+ clearTimeout(timer);
120
+ return result;
121
+ }
122
+ function running(child) {
123
+ return child.exitCode === null && child.signalCode === null;
124
+ }
61
125
  async function stopChildren(children, timeoutMs) {
62
- const running = children.filter((child) => child.exitCode === null && child.signalCode === null);
63
- if (running.length === 0)
126
+ const active = children.filter(running);
127
+ if (active.length === 0)
64
128
  return;
65
- const exited = Promise.all(running.map((child) => new Promise((resolve) => child.once('exit', () => resolve())))).then(() => 'exited');
66
- for (const child of running)
129
+ const exited = Promise.all(active.map((child) => new Promise((resolve) => child.once('exit', () => resolve())))).then(() => 'exited');
130
+ for (const child of active)
67
131
  child.kill('SIGTERM');
68
132
  let timer;
69
133
  const timeout = new Promise((resolve) => {
@@ -73,8 +137,8 @@ async function stopChildren(children, timeoutMs) {
73
137
  if (timer)
74
138
  clearTimeout(timer);
75
139
  if (outcome === 'timeout') {
76
- for (const child of running)
77
- if (child.exitCode === null && child.signalCode === null)
140
+ for (const child of active)
141
+ if (running(child))
78
142
  child.kill('SIGKILL');
79
143
  }
80
144
  }
@@ -83,7 +147,7 @@ export async function runRealtimeStack(options) {
83
147
  const centrifugoConfigPath = absolutePath(options.centrifugo.configPath, 'centrifugo.configPath');
84
148
  await mkdir(path.dirname(configPath), { recursive: true });
85
149
  await writeFile(configPath, caddyRealtimeProxy(options.caddy.proxy), 'utf8');
86
- return superviseProcesses([
150
+ return runSupervisor([
87
151
  { name: 'app', ...options.app },
88
152
  {
89
153
  name: 'realtime',
@@ -99,7 +163,7 @@ export async function runRealtimeStack(options) {
99
163
  ...(options.caddy.cwd ? { cwd: options.caddy.cwd } : {}),
100
164
  env: { ...options.caddy.env, ...caddyRuntimeEnvironment(options.caddy.runtime) },
101
165
  },
102
- ], options.supervisor);
166
+ ], options.supervisor ?? {}, ['proxy', 'app', 'realtime']);
103
167
  }
104
168
  export function centrifugoEnvironment(options) {
105
169
  const apiKey = requiredValue(options.apiKey, 'apiKey');
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, type ChildProcess } from 'node:child_process'\nimport { mkdir, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\n\nexport { runRealtimeDev, type RealtimeDevOptions } from './dev.js'\n\nexport type RuntimeProcess = {\n name: string\n command: string\n args?: readonly string[]\n cwd?: string\n env?: NodeJS.ProcessEnv\n}\n\ntype SignalSource = Pick<NodeJS.Process, 'off' | 'once'>\ntype SpawnProcess = (process: RuntimeProcess) => ChildProcess\n\nexport type SupervisorOptions = {\n shutdownTimeoutMs?: number\n signalSource?: SignalSource\n spawn?: SpawnProcess\n}\n\nexport async function superviseProcesses(processes: readonly RuntimeProcess[], options: SupervisorOptions = {}) {\n if (processes.length === 0) throw new Error('at least one runtime process is required')\n const names = new Set<string>()\n for (const process of processes) {\n if (!process.name.trim()) throw new Error('runtime process names must not be empty')\n if (!process.command.trim()) throw new Error(`runtime process ${process.name} must have a command`)\n if (names.has(process.name)) throw new Error(`duplicate runtime process name: ${process.name}`)\n names.add(process.name)\n }\n\n const signalSource = options.signalSource ?? process\n const shutdownTimeoutMs = options.shutdownTimeoutMs ?? 10_000\n if (!Number.isSafeInteger(shutdownTimeoutMs) || shutdownTimeoutMs < 0) {\n throw new Error('shutdownTimeoutMs must be a non-negative integer')\n }\n const spawnProcess =\n options.spawn ??\n ((specification: RuntimeProcess) =>\n spawn(specification.command, [...(specification.args ?? [])], {\n cwd: specification.cwd,\n env: specification.env ?? process.env,\n stdio: 'inherit',\n }))\n const children = new Map<ChildProcess, string>()\n let settled = false\n let resolveResult!: (value: number) => void\n const result = new Promise<number>((resolve) => {\n resolveResult = resolve\n })\n\n const finish = async (status: number) => {\n if (settled) return\n settled = true\n signalSource.off('SIGINT', onSignal)\n signalSource.off('SIGTERM', onSignal)\n await stopChildren([...children.keys()], shutdownTimeoutMs)\n resolveResult(status)\n }\n const onSignal = () => void finish(0)\n signalSource.once('SIGINT', onSignal)\n signalSource.once('SIGTERM', onSignal)\n\n try {\n for (const specification of processes) {\n const child = spawnProcess(specification)\n children.set(child, specification.name)\n child.once('error', () => void finish(1))\n child.once('exit', (code) => void finish(code && code > 0 ? code : 1))\n }\n } catch (error) {\n await finish(1)\n throw error\n }\n return result\n}\n\nasync function stopChildren(children: ChildProcess[], timeoutMs: number) {\n const running = children.filter((child) => child.exitCode === null && child.signalCode === null)\n if (running.length === 0) return\n const exited = Promise.all(running.map((child) => new Promise<void>((resolve) => child.once('exit', () => resolve())))).then(\n () => 'exited' as const,\n )\n for (const child of running) child.kill('SIGTERM')\n let timer: ReturnType<typeof setTimeout> | undefined\n const timeout = new Promise<'timeout'>((resolve) => {\n timer = setTimeout(() => resolve('timeout'), timeoutMs)\n })\n const outcome = await Promise.race([exited, timeout])\n if (timer) clearTimeout(timer)\n if (outcome === 'timeout') {\n for (const child of running) if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL')\n }\n}\n\nexport type CentrifugoEnvironmentOptions = {\n apiKey: string\n clientTokenSecret?: string\n subscriptionTokenSecret?: string\n allowedOrigins?: string\n redisUrl?: string\n}\n\nexport type RealtimeStackOptions = {\n app: Omit<RuntimeProcess, 'name'>\n centrifugo: {\n command?: string\n configPath: string\n cwd?: string\n env?: NodeJS.ProcessEnv\n environment: CentrifugoEnvironmentOptions\n }\n caddy: {\n command?: string\n configPath: string\n cwd?: string\n env?: NodeJS.ProcessEnv\n proxy?: CaddyRealtimeProxyOptions\n runtime?: CaddyRuntimeEnvironmentOptions\n }\n supervisor?: SupervisorOptions\n}\n\nexport async function runRealtimeStack(options: RealtimeStackOptions) {\n const configPath = absolutePath(options.caddy.configPath, 'caddy.configPath')\n const centrifugoConfigPath = absolutePath(options.centrifugo.configPath, 'centrifugo.configPath')\n await mkdir(path.dirname(configPath), { recursive: true })\n await writeFile(configPath, caddyRealtimeProxy(options.caddy.proxy), 'utf8')\n return superviseProcesses(\n [\n { name: 'app', ...options.app },\n {\n name: 'realtime',\n command: options.centrifugo.command ?? 'centrifugo',\n args: [`--config=${centrifugoConfigPath}`],\n ...(options.centrifugo.cwd ? { cwd: options.centrifugo.cwd } : {}),\n env: { ...options.centrifugo.env, ...centrifugoEnvironment(options.centrifugo.environment) },\n },\n {\n name: 'proxy',\n command: options.caddy.command ?? 'caddy',\n args: ['run', '--config', configPath, '--adapter', 'caddyfile'],\n ...(options.caddy.cwd ? { cwd: options.caddy.cwd } : {}),\n env: { ...options.caddy.env, ...caddyRuntimeEnvironment(options.caddy.runtime) },\n },\n ],\n options.supervisor,\n )\n}\n\nexport function centrifugoEnvironment(options: CentrifugoEnvironmentOptions): NodeJS.ProcessEnv {\n const apiKey = requiredValue(options.apiKey, 'apiKey')\n const clientTokenSecret = options.clientTokenSecret?.trim()\n const subscriptionTokenSecret = options.subscriptionTokenSecret?.trim()\n return {\n CENTRIFUGO_HTTP_API_KEY: apiKey,\n CENTRIFUGO_CLIENT_ALLOWED_ORIGINS: options.allowedOrigins?.trim() || '*',\n CENTRIFUGO_HTTP_SERVER_ADDRESS: '127.0.0.1',\n CENTRIFUGO_HEALTH_ENABLED: 'true',\n ...(clientTokenSecret ? { CENTRIFUGO_CLIENT_TOKEN_HMAC_SECRET_KEY: clientTokenSecret } : {}),\n ...(subscriptionTokenSecret\n ? {\n CENTRIFUGO_CLIENT_SUBSCRIPTION_TOKEN_ENABLED: 'true',\n CENTRIFUGO_CLIENT_SUBSCRIPTION_TOKEN_HMAC_SECRET_KEY: subscriptionTokenSecret,\n }\n : {}),\n ...(options.redisUrl\n ? { CENTRIFUGO_ENGINE_TYPE: 'redis', CENTRIFUGO_ENGINE_REDIS_ADDRESS: requiredValue(options.redisUrl, 'redisUrl') }\n : {}),\n }\n}\n\nexport type CaddyRuntimeEnvironmentOptions = { configHome?: string; dataHome?: string }\n\nexport function caddyRuntimeEnvironment(options: CaddyRuntimeEnvironmentOptions = {}): NodeJS.ProcessEnv {\n const configHome = absolutePath(options.configHome ?? '/tmp/caddy-config', 'configHome')\n const dataHome = absolutePath(options.dataHome ?? '/tmp/caddy-data', 'dataHome')\n return { XDG_CONFIG_HOME: configHome, XDG_DATA_HOME: dataHome }\n}\n\nexport type CaddyRealtimeProxyOptions = { publicPort?: number; appPort?: number; realtimePort?: number; websocketPath?: string }\n\nexport function caddyRealtimeProxy(options: CaddyRealtimeProxyOptions = {}) {\n const publicPort = port(options.publicPort ?? 3000, 'publicPort')\n const appPort = port(options.appPort ?? 3001, 'appPort')\n const realtimePort = port(options.realtimePort ?? 8000, 'realtimePort')\n const websocketPath = options.websocketPath ?? '/connection/'\n if (!/^\\/[A-Za-z0-9._~/-]+\\/$/.test(websocketPath) || websocketPath.includes('//')) {\n throw new Error('websocketPath must be a normalized absolute directory path')\n }\n return `{\n\\tservers {\n\\t\\ttrusted_proxies static private_ranges\n\\t\\ttrusted_proxies_strict\n\\t}\n}\n\n:${publicPort} {\n\\troute {\n\\t\\t@foreignWebSocketOrigin \\`{path}.startsWith('${websocketPath}') && {http.request.header.Origin} != '' && {http.request.header.Origin} != 'http://' + {http.request.hostport} && {http.request.header.Origin} != 'https://' + {http.request.hostport}\\`\n\\t\\trespond @foreignWebSocketOrigin 403\n\n\\t\\thandle ${websocketPath}* {\n\\t\\t\\treverse_proxy 127.0.0.1:${realtimePort}\n\\t\\t}\n\n\\t\\thandle {\n\\t\\t\\treverse_proxy 127.0.0.1:${appPort}\n\\t\\t}\n\\t}\n}\n`\n}\n\nfunction requiredValue(value: string, name: string) {\n const normalized = value.trim()\n if (!normalized) throw new Error(`${name} is required`)\n return normalized\n}\n\nfunction port(value: number, name: string) {\n if (!Number.isInteger(value) || value < 1 || value > 65_535) throw new Error(`${name} must be a valid TCP port`)\n return value\n}\n\nfunction absolutePath(value: string, name: string) {\n if (!path.isAbsolute(value)) throw new Error(`${name} must be an absolute path`)\n return value\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, type ChildProcess } from 'node:child_process'\nimport { mkdir, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\n\nexport { runRealtimeDev, type RealtimeDevOptions } from './dev.js'\n\nexport type RuntimeProcess = {\n name: string\n command: string\n args?: readonly string[]\n cwd?: string\n env?: NodeJS.ProcessEnv\n}\n\ntype SignalSource = Pick<NodeJS.Process, 'off' | 'once'>\ntype SpawnProcess = (process: RuntimeProcess) => ChildProcess\n\nexport type SupervisorOptions = {\n shutdownTimeoutMs?: number\n signalSource?: SignalSource\n spawn?: SpawnProcess\n}\n\nexport async function superviseProcesses(processes: readonly RuntimeProcess[], options: SupervisorOptions = {}) {\n return runSupervisor(processes, options)\n}\n\nasync function runSupervisor(processes: readonly RuntimeProcess[], options: SupervisorOptions, shutdownOrder?: readonly string[]) {\n if (processes.length === 0) throw new Error('at least one runtime process is required')\n const names = new Set<string>()\n for (const process of processes) {\n if (!process.name.trim()) throw new Error('runtime process names must not be empty')\n if (!process.command.trim()) throw new Error(`runtime process ${process.name} must have a command`)\n if (names.has(process.name)) throw new Error(`duplicate runtime process name: ${process.name}`)\n names.add(process.name)\n }\n\n const signalSource = options.signalSource ?? process\n const shutdownTimeoutMs = options.shutdownTimeoutMs ?? 10_000\n if (!Number.isSafeInteger(shutdownTimeoutMs) || shutdownTimeoutMs < 0) {\n throw new Error('shutdownTimeoutMs must be a non-negative integer')\n }\n const spawnProcess =\n options.spawn ??\n ((specification: RuntimeProcess) =>\n spawn(specification.command, [...(specification.args ?? [])], {\n cwd: specification.cwd,\n env: specification.env ?? process.env,\n stdio: 'inherit',\n }))\n const children = new Map<ChildProcess, string>()\n const exits = new Map<ChildProcess, Promise<void>>()\n let settled = false\n let phasedShutdown = false\n let expectedExit: ChildProcess | undefined\n let resolveResult!: (value: number) => void\n const result = new Promise<number>((resolve) => {\n resolveResult = resolve\n })\n\n const finishImmediately = async (status: number) => {\n if (settled) return\n settled = true\n signalSource.off('SIGINT', onSignal)\n signalSource.off('SIGTERM', onSignal)\n await stopChildren([...children.keys()], shutdownTimeoutMs)\n resolveResult(status)\n }\n const finishInPhases = async () => {\n if (settled || phasedShutdown) return\n phasedShutdown = true\n signalSource.off('SIGINT', onSignal)\n signalSource.off('SIGTERM', onSignal)\n const deadline = Date.now() + shutdownTimeoutMs\n for (const name of shutdownOrder ?? []) {\n if (settled) return\n const child = [...children].find(([, childName]) => childName === name)?.[0]\n if (!child || !running(child)) continue\n expectedExit = child\n child.kill('SIGTERM')\n // oxlint-disable-next-line no-await-in-loop -- Shutdown phases must remain dependency-ordered.\n const exited = await beforeDeadline(exits.get(child)!, Math.max(0, deadline - Date.now()))\n expectedExit = undefined\n if (settled) return\n if (!exited) {\n settled = true\n for (const runningChild of children.keys()) if (running(runningChild)) runningChild.kill('SIGKILL')\n resolveResult(0)\n return\n }\n }\n settled = true\n resolveResult(0)\n }\n const onSignal = () => {\n if (shutdownOrder) void finishInPhases()\n else void finishImmediately(0)\n }\n signalSource.once('SIGINT', onSignal)\n signalSource.once('SIGTERM', onSignal)\n\n try {\n for (const specification of processes) {\n const child = spawnProcess(specification)\n children.set(child, specification.name)\n let resolveExit!: () => void\n exits.set(\n child,\n new Promise<void>((resolve) => {\n resolveExit = resolve\n }),\n )\n child.once('error', () => void finishImmediately(1))\n child.once('exit', (code) => {\n resolveExit()\n if (!phasedShutdown || child !== expectedExit) void finishImmediately(code && code > 0 ? code : 1)\n })\n }\n } catch (error) {\n await finishImmediately(1)\n throw error\n }\n return result\n}\n\nasync function beforeDeadline(exited: Promise<void>, timeoutMs: number) {\n let timer: ReturnType<typeof setTimeout> | undefined\n const timeout = new Promise<false>((resolve) => {\n timer = setTimeout(() => resolve(false), timeoutMs)\n })\n const result = await Promise.race([exited.then(() => true as const), timeout])\n if (timer) clearTimeout(timer)\n return result\n}\n\nfunction running(child: ChildProcess) {\n return child.exitCode === null && child.signalCode === null\n}\n\nasync function stopChildren(children: ChildProcess[], timeoutMs: number) {\n const active = children.filter(running)\n if (active.length === 0) return\n const exited = Promise.all(active.map((child) => new Promise<void>((resolve) => child.once('exit', () => resolve())))).then(\n () => 'exited' as const,\n )\n for (const child of active) child.kill('SIGTERM')\n let timer: ReturnType<typeof setTimeout> | undefined\n const timeout = new Promise<'timeout'>((resolve) => {\n timer = setTimeout(() => resolve('timeout'), timeoutMs)\n })\n const outcome = await Promise.race([exited, timeout])\n if (timer) clearTimeout(timer)\n if (outcome === 'timeout') {\n for (const child of active) if (running(child)) child.kill('SIGKILL')\n }\n}\n\nexport type CentrifugoEnvironmentOptions = {\n apiKey: string\n clientTokenSecret?: string\n subscriptionTokenSecret?: string\n allowedOrigins?: string\n redisUrl?: string\n}\n\nexport type RealtimeStackOptions = {\n app: Omit<RuntimeProcess, 'name'>\n centrifugo: {\n command?: string\n configPath: string\n cwd?: string\n env?: NodeJS.ProcessEnv\n environment: CentrifugoEnvironmentOptions\n }\n caddy: {\n command?: string\n configPath: string\n cwd?: string\n env?: NodeJS.ProcessEnv\n proxy?: CaddyRealtimeProxyOptions\n runtime?: CaddyRuntimeEnvironmentOptions\n }\n supervisor?: SupervisorOptions\n}\n\nexport async function runRealtimeStack(options: RealtimeStackOptions) {\n const configPath = absolutePath(options.caddy.configPath, 'caddy.configPath')\n const centrifugoConfigPath = absolutePath(options.centrifugo.configPath, 'centrifugo.configPath')\n await mkdir(path.dirname(configPath), { recursive: true })\n await writeFile(configPath, caddyRealtimeProxy(options.caddy.proxy), 'utf8')\n return runSupervisor(\n [\n { name: 'app', ...options.app },\n {\n name: 'realtime',\n command: options.centrifugo.command ?? 'centrifugo',\n args: [`--config=${centrifugoConfigPath}`],\n ...(options.centrifugo.cwd ? { cwd: options.centrifugo.cwd } : {}),\n env: { ...options.centrifugo.env, ...centrifugoEnvironment(options.centrifugo.environment) },\n },\n {\n name: 'proxy',\n command: options.caddy.command ?? 'caddy',\n args: ['run', '--config', configPath, '--adapter', 'caddyfile'],\n ...(options.caddy.cwd ? { cwd: options.caddy.cwd } : {}),\n env: { ...options.caddy.env, ...caddyRuntimeEnvironment(options.caddy.runtime) },\n },\n ],\n options.supervisor ?? {},\n ['proxy', 'app', 'realtime'],\n )\n}\n\nexport function centrifugoEnvironment(options: CentrifugoEnvironmentOptions): NodeJS.ProcessEnv {\n const apiKey = requiredValue(options.apiKey, 'apiKey')\n const clientTokenSecret = options.clientTokenSecret?.trim()\n const subscriptionTokenSecret = options.subscriptionTokenSecret?.trim()\n return {\n CENTRIFUGO_HTTP_API_KEY: apiKey,\n CENTRIFUGO_CLIENT_ALLOWED_ORIGINS: options.allowedOrigins?.trim() || '*',\n CENTRIFUGO_HTTP_SERVER_ADDRESS: '127.0.0.1',\n CENTRIFUGO_HEALTH_ENABLED: 'true',\n ...(clientTokenSecret ? { CENTRIFUGO_CLIENT_TOKEN_HMAC_SECRET_KEY: clientTokenSecret } : {}),\n ...(subscriptionTokenSecret\n ? {\n CENTRIFUGO_CLIENT_SUBSCRIPTION_TOKEN_ENABLED: 'true',\n CENTRIFUGO_CLIENT_SUBSCRIPTION_TOKEN_HMAC_SECRET_KEY: subscriptionTokenSecret,\n }\n : {}),\n ...(options.redisUrl\n ? { CENTRIFUGO_ENGINE_TYPE: 'redis', CENTRIFUGO_ENGINE_REDIS_ADDRESS: requiredValue(options.redisUrl, 'redisUrl') }\n : {}),\n }\n}\n\nexport type CaddyRuntimeEnvironmentOptions = { configHome?: string; dataHome?: string }\n\nexport function caddyRuntimeEnvironment(options: CaddyRuntimeEnvironmentOptions = {}): NodeJS.ProcessEnv {\n const configHome = absolutePath(options.configHome ?? '/tmp/caddy-config', 'configHome')\n const dataHome = absolutePath(options.dataHome ?? '/tmp/caddy-data', 'dataHome')\n return { XDG_CONFIG_HOME: configHome, XDG_DATA_HOME: dataHome }\n}\n\nexport type CaddyRealtimeProxyOptions = { publicPort?: number; appPort?: number; realtimePort?: number; websocketPath?: string }\n\nexport function caddyRealtimeProxy(options: CaddyRealtimeProxyOptions = {}) {\n const publicPort = port(options.publicPort ?? 3000, 'publicPort')\n const appPort = port(options.appPort ?? 3001, 'appPort')\n const realtimePort = port(options.realtimePort ?? 8000, 'realtimePort')\n const websocketPath = options.websocketPath ?? '/connection/'\n if (!/^\\/[A-Za-z0-9._~/-]+\\/$/.test(websocketPath) || websocketPath.includes('//')) {\n throw new Error('websocketPath must be a normalized absolute directory path')\n }\n return `{\n\\tservers {\n\\t\\ttrusted_proxies static private_ranges\n\\t\\ttrusted_proxies_strict\n\\t}\n}\n\n:${publicPort} {\n\\troute {\n\\t\\t@foreignWebSocketOrigin \\`{path}.startsWith('${websocketPath}') && {http.request.header.Origin} != '' && {http.request.header.Origin} != 'http://' + {http.request.hostport} && {http.request.header.Origin} != 'https://' + {http.request.hostport}\\`\n\\t\\trespond @foreignWebSocketOrigin 403\n\n\\t\\thandle ${websocketPath}* {\n\\t\\t\\treverse_proxy 127.0.0.1:${realtimePort}\n\\t\\t}\n\n\\t\\thandle {\n\\t\\t\\treverse_proxy 127.0.0.1:${appPort}\n\\t\\t}\n\\t}\n}\n`\n}\n\nfunction requiredValue(value: string, name: string) {\n const normalized = value.trim()\n if (!normalized) throw new Error(`${name} is required`)\n return normalized\n}\n\nfunction port(value: number, name: string) {\n if (!Number.isInteger(value) || value < 1 || value > 65_535) throw new Error(`${name} must be a valid TCP port`)\n return value\n}\n\nfunction absolutePath(value: string, name: string) {\n if (!path.isAbsolute(value)) throw new Error(`${name} must be an absolute path`)\n return value\n}\n"]}
@@ -0,0 +1,16 @@
1
+ APP_URL=http://localhost:3100
2
+ DATA_DIR=.data/example-full-stack
3
+ TRUST_PROXY=false
4
+ CENTRIFUGO_API_URL=http://127.0.0.1:8100/api
5
+ CENTRIFUGO_API_KEY=replace-for-local-development
6
+ CENTRIFUGO_PROXY_SECRET=replace-for-local-development
7
+ BETTER_AUTH_SECRET=replace-with-at-least-32-random-characters
8
+ SMTP_HOST=127.0.0.1
9
+ SMTP_PORT=1025
10
+ SMTP_SECURE=false
11
+ EMAIL_FROM=ras-stack@example.test
12
+ EMAIL_REQUIRE_VERIFICATION=true
13
+ UPLOAD_MAX_BYTES=1000000
14
+ UPLOAD_QUOTA_BYTES=5000000
15
+ UPLOAD_GLOBAL_QUOTA_BYTES=100000000
16
+ UPLOAD_GLOBAL_MAX_FILES=1024
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
3
+ "singleQuote": true,
4
+ "semi": false,
5
+ "printWidth": 140,
6
+ "ignorePatterns": ["src/routeTree.gen.ts", ".output/**", "node_modules/**"]
7
+ }
@@ -0,0 +1,37 @@
1
+ # syntax=docker/dockerfile:1
2
+ ARG RUNTIME_BINARIES_IMAGE=ghcr.io/richardsolomou/ras-stack-runtime-binaries:runtime-v1.0.1@sha256:581a691b59a603685bee8ba9576e80d419b967b63017f82d778fce39ca6f0c0b
3
+ FROM node:24-alpine AS build
4
+ WORKDIR /app
5
+ RUN apk add --no-cache python3 make g++
6
+ RUN corepack enable && corepack install --global pnpm@11.15.0
7
+ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
8
+ COPY examples/full-stack/package.json ./examples/full-stack/package.json
9
+ RUN pnpm fetch --frozen-lockfile
10
+ COPY config ./config
11
+ COPY dist ./dist
12
+ COPY src ./src
13
+ COPY scripts ./scripts
14
+ COPY tsconfig.json tsconfig.build.json LICENSE README.md ./
15
+ COPY examples/full-stack ./examples/full-stack
16
+ RUN pnpm install --offline --frozen-lockfile
17
+ RUN pnpm build && pnpm --filter @ras-stack/example-full-stack build
18
+
19
+ FROM ${RUNTIME_BINARIES_IMAGE} AS runtime-binaries
20
+
21
+ FROM node:24-alpine
22
+ WORKDIR /app
23
+ RUN apk add --no-cache tini \
24
+ && rm -rf /usr/local/lib/node_modules/corepack /usr/local/lib/node_modules/npm /opt/yarn-v1.22.22 \
25
+ && rm -f /usr/local/bin/corepack /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/yarn /usr/local/bin/yarnpkg \
26
+ && mkdir -p /data \
27
+ && chown -R node:node /app /data
28
+ COPY --from=build --chown=node:node /app/examples/full-stack/.output ./.output
29
+ COPY --from=runtime-binaries /usr/local/bin/centrifugo /usr/local/bin/centrifugo
30
+ COPY --from=runtime-binaries /usr/local/bin/caddy /usr/local/bin/caddy
31
+ COPY --chown=node:node examples/full-stack/centrifugo.json ./centrifugo.json
32
+ COPY --chown=node:node LICENSE ./
33
+ ENV NODE_ENV=production DATA_DIR=/data TRUST_PROXY=false CENTRIFUGO_CONFIG=/app/centrifugo.json CENTRIFUGO_API_URL=http://127.0.0.1:8100/api CENTRIFUGO_HTTP_SERVER_PORT=8100
34
+ EXPOSE 3100
35
+ HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=3 CMD wget -q --spider http://127.0.0.1:3100/api/ready || exit 1
36
+ USER node
37
+ ENTRYPOINT ["/sbin/tini", "-g", "--", "node", ".output/server/containerRuntime.mjs"]