@celsoft/platform 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/dist/adapters/cloudflare.d.ts +14 -0
  3. package/dist/adapters/cloudflare.js +78 -0
  4. package/dist/adapters/cloudflare.js.map +1 -0
  5. package/dist/adapters/github.d.ts +10 -0
  6. package/dist/adapters/github.js +47 -0
  7. package/dist/adapters/github.js.map +1 -0
  8. package/dist/adapters/neon.d.ts +11 -0
  9. package/dist/adapters/neon.js +49 -0
  10. package/dist/adapters/neon.js.map +1 -0
  11. package/dist/adapters/process.d.ts +14 -0
  12. package/dist/adapters/process.js +46 -0
  13. package/dist/adapters/process.js.map +1 -0
  14. package/dist/cli.d.ts +2 -0
  15. package/dist/cli.js +157 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/config.d.ts +25 -0
  18. package/dist/config.js +98 -0
  19. package/dist/config.js.map +1 -0
  20. package/dist/fakes.d.ts +53 -0
  21. package/dist/fakes.js +89 -0
  22. package/dist/fakes.js.map +1 -0
  23. package/dist/init.d.ts +19 -0
  24. package/dist/init.js +96 -0
  25. package/dist/init.js.map +1 -0
  26. package/dist/lifecycle.d.ts +19 -0
  27. package/dist/lifecycle.js +89 -0
  28. package/dist/lifecycle.js.map +1 -0
  29. package/dist/ports.d.ts +49 -0
  30. package/dist/ports.js +2 -0
  31. package/dist/ports.js.map +1 -0
  32. package/dist/run.d.ts +9 -0
  33. package/dist/run.js +29 -0
  34. package/dist/run.js.map +1 -0
  35. package/dist/runtime/index.d.ts +22 -0
  36. package/dist/runtime/index.js +29 -0
  37. package/dist/runtime/index.js.map +1 -0
  38. package/dist/secrets.d.ts +5 -0
  39. package/dist/secrets.js +42 -0
  40. package/dist/secrets.js.map +1 -0
  41. package/dist/target.d.ts +15 -0
  42. package/dist/target.js +15 -0
  43. package/dist/target.js.map +1 -0
  44. package/dist/workflow.d.ts +9 -0
  45. package/dist/workflow.js +48 -0
  46. package/dist/workflow.js.map +1 -0
  47. package/dist/wrangler-config.d.ts +9 -0
  48. package/dist/wrangler-config.js +17 -0
  49. package/dist/wrangler-config.js.map +1 -0
  50. package/dist/write-schema.d.ts +1 -0
  51. package/dist/write-schema.js +4 -0
  52. package/dist/write-schema.js.map +1 -0
  53. package/package.json +32 -4
  54. package/schema.json +105 -0
  55. package/README.md +0 -3
@@ -0,0 +1,53 @@
1
+ import type { Branch, Cloudflare, Hyperdrive, Migrator, Neon, Wrangler, Zone } from "./ports.js";
2
+ export declare class FakeNeon implements Neon {
3
+ branches: Branch[];
4
+ private next;
5
+ defaultBranch(): Promise<Branch>;
6
+ findBranch(_project: string, name: string): Promise<Branch | undefined>;
7
+ listBranches(_project: string, namePrefix: string): Promise<Branch[]>;
8
+ createBranch(_project: string, name: string): Promise<{
9
+ id: string;
10
+ name: string;
11
+ }>;
12
+ deleteBranch(_project: string, branchId: string): Promise<void>;
13
+ connectionUri(_project: string, branchId: string): Promise<string>;
14
+ }
15
+ export declare class FakeCloudflare implements Cloudflare {
16
+ zones: Zone[];
17
+ hyperdrives: Hyperdrive[];
18
+ workers: Set<string>;
19
+ private next;
20
+ accountId(): Promise<string>;
21
+ findZone(hostname: string): Promise<Zone | undefined>;
22
+ findHyperdrive(name: string): Promise<Hyperdrive | undefined>;
23
+ createHyperdrive(name: string, uri: string): Promise<{
24
+ id: string;
25
+ name: string;
26
+ origin: string;
27
+ cached: boolean;
28
+ }>;
29
+ updateHyperdrive(id: string, uri: string): Promise<Hyperdrive>;
30
+ deleteHyperdrive(id: string): Promise<void>;
31
+ findWorker(name: string): Promise<boolean>;
32
+ deleteWorker(name: string): Promise<void>;
33
+ }
34
+ export declare class FakeWrangler implements Wrangler {
35
+ private readonly cloudflare;
36
+ deploys: {
37
+ config: Record<string, unknown>;
38
+ secrets: Record<string, string>;
39
+ }[];
40
+ constructor(cloudflare: FakeCloudflare);
41
+ deploy(config: object, secrets: Record<string, string>): Promise<void>;
42
+ }
43
+ export declare class FakeMigrator implements Migrator {
44
+ runs: string[];
45
+ failWith: Error | undefined;
46
+ migrate(_command: string, databaseUrl: string): Promise<void>;
47
+ }
48
+ export declare function fakePorts(): {
49
+ neon: FakeNeon;
50
+ cloudflare: FakeCloudflare;
51
+ wrangler: FakeWrangler;
52
+ migrator: FakeMigrator;
53
+ };
package/dist/fakes.js ADDED
@@ -0,0 +1,89 @@
1
+ import { originOf } from "./target.js";
2
+ export class FakeNeon {
3
+ branches = [{ id: "br-prod", name: "production" }];
4
+ next = 1;
5
+ async defaultBranch() {
6
+ return this.branches[0];
7
+ }
8
+ async findBranch(_project, name) {
9
+ return this.branches.find((b) => b.name === name);
10
+ }
11
+ async listBranches(_project, namePrefix) {
12
+ return this.branches.filter((b) => b.name.startsWith(namePrefix));
13
+ }
14
+ async createBranch(_project, name) {
15
+ const branch = { id: `br-${this.next++}`, name };
16
+ this.branches.push(branch);
17
+ return branch;
18
+ }
19
+ async deleteBranch(_project, branchId) {
20
+ this.branches = this.branches.filter((b) => b.id !== branchId);
21
+ }
22
+ async connectionUri(_project, branchId) {
23
+ return `postgresql://owner:secret@${branchId}.neon.test/app`;
24
+ }
25
+ }
26
+ export class FakeCloudflare {
27
+ zones = [{ id: "zone-1", name: "example.com" }];
28
+ hyperdrives = [];
29
+ workers = new Set();
30
+ next = 1;
31
+ async accountId() {
32
+ return "acct-1";
33
+ }
34
+ async findZone(hostname) {
35
+ return this.zones.find((z) => hostname === z.name || hostname.endsWith(`.${z.name}`));
36
+ }
37
+ async findHyperdrive(name) {
38
+ return this.hyperdrives.find((h) => h.name === name);
39
+ }
40
+ async createHyperdrive(name, uri) {
41
+ const hyperdrive = { id: `hd-${this.next++}`, name, origin: originOf(uri), cached: false };
42
+ this.hyperdrives.push(hyperdrive);
43
+ return hyperdrive;
44
+ }
45
+ async updateHyperdrive(id, uri) {
46
+ const hyperdrive = this.hyperdrives.find((h) => h.id === id);
47
+ hyperdrive.origin = originOf(uri);
48
+ hyperdrive.cached = false;
49
+ return hyperdrive;
50
+ }
51
+ async deleteHyperdrive(id) {
52
+ this.hyperdrives = this.hyperdrives.filter((h) => h.id !== id);
53
+ }
54
+ async findWorker(name) {
55
+ return this.workers.has(name);
56
+ }
57
+ async deleteWorker(name) {
58
+ this.workers.delete(name);
59
+ }
60
+ }
61
+ export class FakeWrangler {
62
+ cloudflare;
63
+ deploys = [];
64
+ constructor(cloudflare) {
65
+ this.cloudflare = cloudflare;
66
+ }
67
+ async deploy(config, secrets) {
68
+ const c = config;
69
+ this.deploys.push({ config: c, secrets });
70
+ this.cloudflare.workers.add(c.name);
71
+ }
72
+ }
73
+ export class FakeMigrator {
74
+ runs = [];
75
+ failWith;
76
+ async migrate(_command, databaseUrl) {
77
+ if (this.failWith)
78
+ throw this.failWith;
79
+ this.runs.push(databaseUrl);
80
+ }
81
+ }
82
+ export function fakePorts() {
83
+ const neon = new FakeNeon();
84
+ const cloudflare = new FakeCloudflare();
85
+ const wrangler = new FakeWrangler(cloudflare);
86
+ const migrator = new FakeMigrator();
87
+ return { neon, cloudflare, wrangler, migrator };
88
+ }
89
+ //# sourceMappingURL=fakes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fakes.js","sourceRoot":"","sources":["../src/fakes.ts"],"names":[],"mappings":"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"}
package/dist/init.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { Config } from "./config.js";
2
+ import type { Cloudflare, GitHub, Neon } from "./ports.js";
3
+ import type { Run } from "./run.js";
4
+ export declare const TOKEN_HELP: Record<string, string>;
5
+ export interface InitInput {
6
+ repoRoot: string;
7
+ appDir: string;
8
+ config: Config;
9
+ variables: Record<string, string>;
10
+ run: Run;
11
+ connect: (tokens: Record<string, string>) => {
12
+ cloudflare: Cloudflare;
13
+ neon: Neon;
14
+ github: GitHub;
15
+ };
16
+ }
17
+ export declare function tokenNames(config: Config): string[];
18
+ export declare function init({ repoRoot, appDir, config, variables, run, connect }: InitInput): Promise<void>;
19
+ export declare function checkWorkflow(repoRoot: string, appDir: string, config: Config, variables: string[]): Promise<void>;
package/dist/init.js ADDED
@@ -0,0 +1,96 @@
1
+ import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, join, relative } from "node:path";
3
+ import { PreflightError } from "./lifecycle.js";
4
+ import { parseDotenv, SECRETS_FILE, VARS_FILE } from "./secrets.js";
5
+ import { renderWorkflow, WORKFLOW_PATH } from "./workflow.js";
6
+ export const TOKEN_HELP = {
7
+ CLOUDFLARE_API_TOKEN: "Cloudflare API token; see SETUP.md or the platform README for its permissions",
8
+ NEON_API_KEY: "Neon API key scoped to the app's Neon project",
9
+ };
10
+ export function tokenNames(config) {
11
+ return config.database ? ["CLOUDFLARE_API_TOKEN", "NEON_API_KEY"] : ["CLOUDFLARE_API_TOKEN"];
12
+ }
13
+ export async function init({ repoRoot, appDir, config, variables, run, connect }) {
14
+ const names = tokenNames(config);
15
+ const tokens = await prepareSecretsFile(repoRoot, names, run);
16
+ if (Object.keys(variables).length > 0)
17
+ await ensureIgnored(repoRoot, VARS_FILE, run);
18
+ const { cloudflare, neon, github } = connect(tokens);
19
+ await cloudflare.accountId();
20
+ const zone = await cloudflare.findZone(config.hostname);
21
+ if (!zone)
22
+ throw new PreflightError(`the Cloudflare token cannot see a zone for ${config.hostname}; check its Zone Resources`);
23
+ run.note(`✓ Cloudflare account and zone ${zone.name}`);
24
+ if (config.database) {
25
+ await neon.defaultBranch(config.database.neonProject);
26
+ run.note(`✓ Neon project ${config.database.neonProject}`);
27
+ }
28
+ for (const name of names) {
29
+ await run.mutate(`store ${name} as a GitHub secret`, () => github.setSecret(name, tokens[name]), undefined);
30
+ }
31
+ for (const [name, value] of Object.entries(variables)) {
32
+ await run.mutate(`store ${name} as a GitHub variable`, () => github.setVariable(name, value), undefined);
33
+ }
34
+ for (const environment of ["production", "preview"]) {
35
+ await run.mutate(`ensure GitHub Environment ${environment}`, () => github.ensureEnvironment(environment), undefined);
36
+ }
37
+ const workflow = await expectedWorkflow(repoRoot, appDir, config, Object.keys(variables));
38
+ const path = join(repoRoot, WORKFLOW_PATH);
39
+ if ((await readOptional(path)) !== workflow) {
40
+ await run.mutate(`write ${WORKFLOW_PATH}`, async () => {
41
+ await mkdir(dirname(path), { recursive: true });
42
+ await writeFile(path, workflow);
43
+ }, undefined);
44
+ }
45
+ else {
46
+ run.note(`✓ ${WORKFLOW_PATH}`);
47
+ }
48
+ }
49
+ export async function checkWorkflow(repoRoot, appDir, config, variables) {
50
+ const expected = await expectedWorkflow(repoRoot, appDir, config, variables);
51
+ if ((await readOptional(join(repoRoot, WORKFLOW_PATH))) !== expected) {
52
+ throw new PreflightError(`${WORKFLOW_PATH} differs from what this platform version writes; run \`platform init\` and commit the result`);
53
+ }
54
+ }
55
+ async function ensureIgnored(repoRoot, file, run) {
56
+ const gitignore = join(repoRoot, ".gitignore");
57
+ const ignored = (await readOptional(gitignore))?.split(/\r?\n/).some((line) => line.trim() === file || line.trim() === `/${file}`);
58
+ if (!ignored)
59
+ await run.mutate(`add ${file} to .gitignore`, () => appendFile(gitignore, `\n${file}\n`), undefined);
60
+ }
61
+ async function prepareSecretsFile(repoRoot, names, run) {
62
+ await ensureIgnored(repoRoot, SECRETS_FILE, run);
63
+ const path = join(repoRoot, SECRETS_FILE);
64
+ const text = (await readOptional(path)) ?? "";
65
+ const declared = new Set([...text.matchAll(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/gm)].map((m) => m[1]));
66
+ const additions = names.filter((name) => !declared.has(name)).map((name) => `\n# ${TOKEN_HELP[name]}\n${name}=\n`);
67
+ if (additions.length > 0) {
68
+ await run.mutate(`add blank entries to ${SECRETS_FILE}`, () => writeFile(path, text + additions.join(""), { mode: 0o600 }), undefined);
69
+ }
70
+ const values = parseDotenv(text);
71
+ const empty = names.filter((name) => !values[name]);
72
+ if (empty.length > 0) {
73
+ throw new PreflightError(`fill ${empty.join(" and ")} in ${path}, then rerun \`platform init\``);
74
+ }
75
+ return values;
76
+ }
77
+ async function expectedWorkflow(repoRoot, appDir, config, variables) {
78
+ return renderWorkflow({
79
+ appDir: relative(repoRoot, appDir) || ".",
80
+ packageManager: await packageManager(repoRoot),
81
+ secrets: [...tokenNames(config), ...config.secrets],
82
+ variables,
83
+ });
84
+ }
85
+ async function packageManager(repoRoot) {
86
+ return (await readOptional(join(repoRoot, "pnpm-lock.yaml"))) !== undefined ? "pnpm" : "npm";
87
+ }
88
+ async function readOptional(path) {
89
+ try {
90
+ return await readFile(path, "utf8");
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ }
96
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { Config } from "./config.js";
2
+ import type { Ports } from "./ports.js";
3
+ import type { Run } from "./run.js";
4
+ import { type Target } from "./target.js";
5
+ export interface Context {
6
+ config: Config;
7
+ ports: Ports;
8
+ run: Run;
9
+ secrets: Record<string, string>;
10
+ now?: () => Date;
11
+ }
12
+ export declare class PreflightError extends Error {
13
+ }
14
+ export declare function deploy(ctx: Context, target: Target): Promise<{
15
+ hostname: string;
16
+ }>;
17
+ export declare function cleanup(ctx: Context, pr: number): Promise<void>;
18
+ export declare const BACKUP_PREFIX = "backup-";
19
+ export declare const BACKUPS_KEPT = 3;
@@ -0,0 +1,89 @@
1
+ import { names, originOf, previewHostname } from "./target.js";
2
+ import { wranglerConfig } from "./wrangler-config.js";
3
+ export class PreflightError extends Error {
4
+ }
5
+ export async function deploy(ctx, target) {
6
+ const { config, ports, run } = ctx;
7
+ const secrets = requireSecrets(config, ctx.secrets);
8
+ const n = names(config, target);
9
+ const accountId = await ports.cloudflare.accountId();
10
+ const zone = await ports.cloudflare.findZone(config.hostname);
11
+ if (!zone)
12
+ throw new PreflightError(`no Cloudflare zone for ${config.hostname}; add the apex domain to this account, or check the token's zone access`);
13
+ const hostname = target.kind === "prod" ? config.hostname : previewHostname(n.worker, zone.name);
14
+ let hyperdriveId;
15
+ if (config.database) {
16
+ const project = config.database.neonProject;
17
+ const prod = await ports.neon.defaultBranch(project);
18
+ const branch = target.kind === "prod"
19
+ ? prod
20
+ : await run.ensure(`Neon branch ${n.neonBranch}`, () => ports.neon.findBranch(project, n.neonBranch), () => ports.neon.createBranch(project, n.neonBranch, prod.id, true), { id: "(new)", name: n.neonBranch });
21
+ if (target.kind === "prod")
22
+ await backup(ctx, project, prod);
23
+ const uri = run.dryRun && branch.id === "(new)" ? undefined : await ports.neon.connectionUri(project, branch.id);
24
+ if (uri) {
25
+ await run.mutate(`run migrations on ${branch.name}`, () => ports.migrator.migrate(config.database.migrate, uri), undefined);
26
+ }
27
+ else {
28
+ run.note(`would run migrations on ${branch.name}`);
29
+ }
30
+ hyperdriveId = await ensureHyperdrive(ctx, n.hyperdrive, uri);
31
+ }
32
+ const wrangler = wranglerConfig({ config, worker: n.worker, hostname, accountId, hyperdriveId });
33
+ await run.mutate(`deploy Worker ${n.worker} at https://${hostname}`, () => ports.wrangler.deploy(wrangler, secrets), undefined);
34
+ return { hostname };
35
+ }
36
+ export async function cleanup(ctx, pr) {
37
+ const { config, ports, run } = ctx;
38
+ const n = names(config, { kind: "preview", pr });
39
+ if (await ports.cloudflare.findWorker(n.worker)) {
40
+ await run.mutate(`delete Worker ${n.worker}`, () => ports.cloudflare.deleteWorker(n.worker), undefined);
41
+ }
42
+ const hyperdrive = await ports.cloudflare.findHyperdrive(n.hyperdrive);
43
+ if (hyperdrive) {
44
+ await run.mutate(`delete Hyperdrive config ${n.hyperdrive}`, () => ports.cloudflare.deleteHyperdrive(hyperdrive.id), undefined);
45
+ }
46
+ if (config.database) {
47
+ const project = config.database.neonProject;
48
+ const branch = await ports.neon.findBranch(project, n.neonBranch);
49
+ if (branch) {
50
+ await run.mutate(`delete Neon branch ${branch.name}`, () => ports.neon.deleteBranch(project, branch.id), undefined);
51
+ }
52
+ }
53
+ }
54
+ export const BACKUP_PREFIX = "backup-";
55
+ export const BACKUPS_KEPT = 3;
56
+ // Neon includes 10 branches per project, so only the newest few backups are kept.
57
+ async function backup({ ports, run, now }, project, prod) {
58
+ const name = `${BACKUP_PREFIX}${(now?.() ?? new Date()).toISOString().replace(/[-:]/g, "").slice(0, 15)}`;
59
+ await run.mutate(`create Neon backup branch ${name}`, () => ports.neon.createBranch(project, name, prod.id, false), undefined);
60
+ const backups = (await ports.neon.listBranches(project, BACKUP_PREFIX)).sort((a, b) => b.name.localeCompare(a.name));
61
+ for (const old of backups.slice(BACKUPS_KEPT)) {
62
+ await run.mutate(`delete old Neon backup branch ${old.name}`, () => ports.neon.deleteBranch(project, old.id), undefined);
63
+ }
64
+ }
65
+ async function ensureHyperdrive({ ports, run }, name, uri) {
66
+ const existing = await ports.cloudflare.findHyperdrive(name);
67
+ if (!uri) {
68
+ run.note(existing ? `would check Hyperdrive config ${name}` : `would create Hyperdrive config ${name}`);
69
+ return existing?.id ?? "(new)";
70
+ }
71
+ if (!existing) {
72
+ return (await run.mutate(`create Hyperdrive config ${name}`, () => ports.cloudflare.createHyperdrive(name, uri), { id: "(new)", name, origin: "", cached: false })).id;
73
+ }
74
+ if (existing.origin !== originOf(uri) || existing.cached) {
75
+ await run.mutate(`update Hyperdrive config ${name} (new origin or caching on)`, () => ports.cloudflare.updateHyperdrive(existing.id, uri), existing);
76
+ }
77
+ else {
78
+ run.note(`✓ Hyperdrive config ${name}`);
79
+ }
80
+ return existing.id;
81
+ }
82
+ function requireSecrets(config, available) {
83
+ const missing = config.secrets.filter((name) => !available[name]);
84
+ if (missing.length > 0) {
85
+ throw new PreflightError(`missing secrets: ${missing.join(", ")}; add them to the GitHub Environment, or to .env.secrets for local runs`);
86
+ }
87
+ return Object.fromEntries(config.secrets.map((name) => [name, available[name]]));
88
+ }
89
+ //# sourceMappingURL=lifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ export interface Branch {
2
+ id: string;
3
+ name: string;
4
+ }
5
+ export interface Neon {
6
+ defaultBranch(project: string): Promise<Branch>;
7
+ findBranch(project: string, name: string): Promise<Branch | undefined>;
8
+ listBranches(project: string, namePrefix: string): Promise<Branch[]>;
9
+ createBranch(project: string, name: string, parentId: string, compute: boolean): Promise<Branch>;
10
+ deleteBranch(project: string, branchId: string): Promise<void>;
11
+ connectionUri(project: string, branchId: string): Promise<string>;
12
+ }
13
+ export interface Zone {
14
+ id: string;
15
+ name: string;
16
+ }
17
+ export interface Hyperdrive {
18
+ id: string;
19
+ name: string;
20
+ origin: string;
21
+ cached: boolean;
22
+ }
23
+ export interface Cloudflare {
24
+ accountId(): Promise<string>;
25
+ findZone(hostname: string): Promise<Zone | undefined>;
26
+ findHyperdrive(name: string): Promise<Hyperdrive | undefined>;
27
+ createHyperdrive(name: string, connectionUri: string): Promise<Hyperdrive>;
28
+ updateHyperdrive(id: string, connectionUri: string): Promise<Hyperdrive>;
29
+ deleteHyperdrive(id: string): Promise<void>;
30
+ findWorker(name: string): Promise<boolean>;
31
+ deleteWorker(name: string): Promise<void>;
32
+ }
33
+ export interface Wrangler {
34
+ deploy(wranglerConfig: object, secrets: Record<string, string>): Promise<void>;
35
+ }
36
+ export interface Migrator {
37
+ migrate(command: string, databaseUrl: string): Promise<void>;
38
+ }
39
+ export interface GitHub {
40
+ setSecret(name: string, value: string): Promise<void>;
41
+ setVariable(name: string, value: string): Promise<void>;
42
+ ensureEnvironment(name: string): Promise<void>;
43
+ }
44
+ export interface Ports {
45
+ neon: Neon;
46
+ cloudflare: Cloudflare;
47
+ wrangler: Wrangler;
48
+ migrator: Migrator;
49
+ }
package/dist/ports.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":""}
package/dist/run.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export type Log = (line: string) => void;
2
+ export declare class Run {
3
+ readonly dryRun: boolean;
4
+ private readonly log;
5
+ constructor(dryRun: boolean, log: Log);
6
+ ensure<T>(label: string, find: () => Promise<T | undefined>, create: () => Promise<T>, placeholder: T): Promise<T>;
7
+ mutate<T>(action: string, apply: () => Promise<T>, placeholder: T): Promise<T>;
8
+ note(line: string): void;
9
+ }
package/dist/run.js ADDED
@@ -0,0 +1,29 @@
1
+ export class Run {
2
+ dryRun;
3
+ log;
4
+ constructor(dryRun, log) {
5
+ this.dryRun = dryRun;
6
+ this.log = log;
7
+ }
8
+ async ensure(label, find, create, placeholder) {
9
+ const found = await find();
10
+ if (found !== undefined) {
11
+ this.log(`✓ ${label}`);
12
+ return found;
13
+ }
14
+ return this.mutate(`create ${label}`, create, placeholder);
15
+ }
16
+ async mutate(action, apply, placeholder) {
17
+ if (this.dryRun) {
18
+ this.log(`~ would ${action}`);
19
+ return placeholder;
20
+ }
21
+ const result = await apply();
22
+ this.log(`+ ${action}`);
23
+ return result;
24
+ }
25
+ note(line) {
26
+ this.log(` ${line}`);
27
+ }
28
+ }
29
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,GAAG;IAEH,MAAM;IACE,GAAG;IAFtB,YACW,MAAe,EACP,GAAQ;sBADhB,MAAM;mBACE,GAAG;IACnB,CAAC;IAEJ,KAAK,CAAC,MAAM,CAAI,KAAa,EAAE,IAAkC,EAAE,MAAwB,EAAE,WAAc;QACzG,MAAM,KAAK,GAAG,MAAM,IAAI,EAAE,CAAC;QAC3B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC;YACvB,OAAO,KAAK,CAAC;QACf,CAAC;QACD,OAAO,IAAI,CAAC,MAAM,CAAC,UAAU,KAAK,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;IAC7D,CAAC;IAED,KAAK,CAAC,MAAM,CAAI,MAAc,EAAE,KAAuB,EAAE,WAAc;QACrE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,GAAG,CAAC,WAAW,MAAM,EAAE,CAAC,CAAC;YAC9B,OAAO,WAAW,CAAC;QACrB,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,KAAK,EAAE,CAAC;QAC7B,IAAI,CAAC,GAAG,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC;QACxB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IAAI,CAAC,IAAY;QACf,IAAI,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IACxB,CAAC;CACF"}
@@ -0,0 +1,22 @@
1
+ import postgres from "postgres";
2
+ export type Sql = postgres.Sql;
3
+ export interface PlatformBindings {
4
+ DB?: {
5
+ connectionString: string;
6
+ };
7
+ }
8
+ export interface ExecutionContext {
9
+ waitUntil(promise: Promise<unknown>): void;
10
+ }
11
+ export interface AppContext<Env> {
12
+ env: Env;
13
+ readonly sql: Sql;
14
+ waitUntil(promise: Promise<unknown>): void;
15
+ }
16
+ export type Handler<Env> = (request: Request, app: AppContext<Env>) => Response | Promise<Response>;
17
+ export interface AppOptions {
18
+ connect?: (connectionString: string) => Sql;
19
+ }
20
+ export declare function defineApp<Secrets extends object = object>(handler: Handler<Secrets & PlatformBindings>, options?: AppOptions): {
21
+ fetch(request: Request, env: Secrets & PlatformBindings, ctx: ExecutionContext): Promise<Response>;
22
+ };
@@ -0,0 +1,29 @@
1
+ import postgres from "postgres";
2
+ const defaultConnect = (connectionString) => postgres(connectionString, { max: 5, fetch_types: false });
3
+ // `Secrets` names the app's secrets, such as `defineApp<{ SESSION_SECRET: string }>`; the
4
+ // platform's own bindings are added to it.
5
+ export function defineApp(handler, options = {}) {
6
+ const connect = options.connect ?? defaultConnect;
7
+ return {
8
+ async fetch(request, env, ctx) {
9
+ let sql;
10
+ const app = {
11
+ env,
12
+ get sql() {
13
+ if (!env.DB)
14
+ throw new Error("this app has no database; add `database` to platform.jsonc");
15
+ return (sql ??= connect(env.DB.connectionString));
16
+ },
17
+ waitUntil: (promise) => ctx.waitUntil(promise),
18
+ };
19
+ try {
20
+ return await handler(request, app);
21
+ }
22
+ finally {
23
+ if (sql)
24
+ ctx.waitUntil(sql.end({ timeout: 5 }));
25
+ }
26
+ },
27
+ };
28
+ }
29
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,UAAU,CAAC;AAwBhC,MAAM,cAAc,GAAG,CAAC,gBAAwB,EAAE,EAAE,CAAC,QAAQ,CAAC,gBAAgB,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC;AAEhH,0FAA0F;AAC1F,2CAA2C;AAC3C,MAAM,UAAU,SAAS,CAAkC,OAA4C,EAAE,OAAO,GAAe,EAAE;IAC/H,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,cAAc,CAAC;IAElD,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,OAAgB,EAAE,GAA+B,EAAE,GAAqB;YAClF,IAAI,GAAoB,CAAC;YACzB,MAAM,GAAG,GAA2C;gBAClD,GAAG;gBACH,IAAI,GAAG;oBACL,IAAI,CAAC,GAAG,CAAC,EAAE;wBAAE,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;oBAC3F,OAAO,CAAC,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC,CAAC;gBACpD,CAAC;gBACD,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC;aAC/C,CAAC;YAEF,IAAI,CAAC;gBACH,OAAO,MAAM,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;YACrC,CAAC;oBAAS,CAAC;gBACT,IAAI,GAAG;oBAAE,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAClD,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,5 @@
1
+ export declare const SECRETS_FILE = ".env.secrets";
2
+ export declare const VARS_FILE = ".env";
3
+ export declare function parseDotenv(text: string): Record<string, string>;
4
+ export declare function loadSecrets(startDir: string, names: string[], env?: NodeJS.ProcessEnv): Promise<Record<string, string>>;
5
+ export declare function loadVars(startDir: string, names: string[], env?: NodeJS.ProcessEnv): Promise<Record<string, string>>;
@@ -0,0 +1,42 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ export const SECRETS_FILE = ".env.secrets";
4
+ export const VARS_FILE = ".env";
5
+ export function parseDotenv(text) {
6
+ const values = {};
7
+ for (const line of text.split(/\r?\n/)) {
8
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line);
9
+ if (!match)
10
+ continue;
11
+ const [, name, raw] = match;
12
+ const value = /^(['"])(.*)\1$/.exec(raw)?.[2] ?? raw;
13
+ if (value)
14
+ values[name] = value;
15
+ }
16
+ return values;
17
+ }
18
+ // Locally secrets come from the nearest .env.secrets; in CI the workflow passes each named
19
+ // secret as an environment variable. The environment wins.
20
+ export async function loadSecrets(startDir, names, env = process.env) {
21
+ return { ...(await findEnvFile(startDir, SECRETS_FILE)), ...pick(env, names) };
22
+ }
23
+ // Non-secret values platform.jsonc references as ${NAME}: the nearest .env locally,
24
+ // GitHub Actions variables passed by name in CI.
25
+ export async function loadVars(startDir, names, env = process.env) {
26
+ return { ...(await findEnvFile(startDir, VARS_FILE)), ...pick(env, names) };
27
+ }
28
+ function pick(env, names) {
29
+ return Object.fromEntries(names.filter((name) => env[name]).map((name) => [name, env[name]]));
30
+ }
31
+ async function findEnvFile(dir, name) {
32
+ for (let current = dir;; current = dirname(current)) {
33
+ try {
34
+ return parseDotenv(await readFile(join(current, name), "utf8"));
35
+ }
36
+ catch {
37
+ if (dirname(current) === current)
38
+ return {};
39
+ }
40
+ }
41
+ }
42
+ //# sourceMappingURL=secrets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secrets.js","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC;AAC3C,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC;AAEhC,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,MAAM,KAAK,GAAG,4DAA4D,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtF,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,MAAM,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC;QAC5B,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,GAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,GAAI,CAAC;QACvD,IAAI,KAAK;YAAE,MAAM,CAAC,IAAK,CAAC,GAAG,KAAK,CAAC;IACnC,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,2FAA2F;AAC3F,2DAA2D;AAC3D,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,QAAgB,EAAE,KAAe,EAAE,GAAG,GAAsB,OAAO,CAAC,GAAG;IACvG,OAAO,EAAE,GAAG,CAAC,MAAM,WAAW,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,CAAC;AACjF,CAAC;AAED,oFAAoF;AACpF,iDAAiD;AACjD,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,QAAgB,EAAE,KAAe,EAAE,GAAG,GAAsB,OAAO,CAAC,GAAG;IACpG,OAAO,EAAE,GAAG,CAAC,MAAM,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,CAAC;AAC9E,CAAC;AAED,SAAS,IAAI,CAAC,GAAsB,EAAE,KAAe;IACnD,OAAO,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAE,CAAC,CAAC,CAAC,CAAC;AACjG,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,GAAW,EAAE,IAAY;IAClD,KAAK,IAAI,OAAO,GAAG,GAAG,GAAI,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACrD,IAAI,CAAC;YACH,OAAO,WAAW,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QAClE,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO;gBAAE,OAAO,EAAE,CAAC;QAC9C,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { Config } from "./config.js";
2
+ export type Target = {
3
+ kind: "prod";
4
+ } | {
5
+ kind: "preview";
6
+ pr: number;
7
+ };
8
+ export interface Names {
9
+ worker: string;
10
+ hyperdrive: string;
11
+ neonBranch: string | undefined;
12
+ }
13
+ export declare function names(config: Config, target: Target): Names;
14
+ export declare function previewHostname(worker: string, zoneName: string): string;
15
+ export declare function originOf(connectionUri: string): string;