supabase-worker-previews 0.0.0 → 0.1.1

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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -1
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +152 -0
  5. package/dist/cloudflare.d.ts +37 -0
  6. package/dist/cloudflare.js +95 -0
  7. package/dist/commands/branches.d.ts +62 -0
  8. package/dist/commands/branches.js +273 -0
  9. package/dist/commands/doctor.d.ts +24 -0
  10. package/dist/commands/doctor.js +289 -0
  11. package/dist/commands/init.d.ts +11 -0
  12. package/dist/commands/init.js +132 -0
  13. package/dist/commands/pr.d.ts +37 -0
  14. package/dist/commands/pr.js +78 -0
  15. package/dist/commands/prune.d.ts +32 -0
  16. package/dist/commands/prune.js +110 -0
  17. package/dist/config.d.ts +50 -0
  18. package/dist/config.js +105 -0
  19. package/dist/feedback.d.ts +64 -0
  20. package/dist/feedback.js +238 -0
  21. package/dist/github.d.ts +33 -0
  22. package/dist/github.js +78 -0
  23. package/dist/http.d.ts +7 -0
  24. package/dist/http.js +44 -0
  25. package/dist/jsonc.d.ts +2 -0
  26. package/dist/jsonc.js +56 -0
  27. package/dist/preview-name.d.ts +3 -0
  28. package/dist/preview-name.js +15 -0
  29. package/dist/run.d.ts +21 -0
  30. package/dist/run.js +59 -0
  31. package/dist/runtime.d.ts +56 -0
  32. package/dist/runtime.js +121 -0
  33. package/dist/style.d.ts +8 -0
  34. package/dist/style.js +21 -0
  35. package/dist/suggest.d.ts +2 -0
  36. package/dist/suggest.js +22 -0
  37. package/dist/supabase.d.ts +78 -0
  38. package/dist/supabase.js +168 -0
  39. package/dist/usage.d.ts +46 -0
  40. package/dist/usage.js +297 -0
  41. package/package.json +84 -9
  42. package/templates/default-privileges.sql +9 -0
  43. package/templates/supabase-previews-action.yml +29 -0
  44. package/templates/supabase-previews.yml +38 -0
@@ -0,0 +1,238 @@
1
+ import { branchFor, previewKey } from "./commands/branches.js";
2
+ import { GitHubApi } from "./github.js";
3
+ export const COMMENT_MARKER = "<!-- supabase-worker-previews -->";
4
+ const PAYLOAD_KEY = "swpPullRequest";
5
+ const STATUS = {
6
+ checking: "Checking",
7
+ passed: "**Passed**",
8
+ failed: "**Failed**",
9
+ removed: "Removed",
10
+ "cleanup-failed": "**Cleanup failed**",
11
+ };
12
+ const cell = (text) => text.replace(/\|/g, "\\|").replace(/\n/g, " ");
13
+ const code = (text) => `\`${cell(text)}\``;
14
+ function databaseCell(db, dashboardUrl) {
15
+ if (!db)
16
+ return "Unknown";
17
+ const name = `[${code(db.name)}](${dashboardUrl})`;
18
+ const ref = db.ref ? ` (${code(db.ref)})` : "";
19
+ return db.kind === "shared" ? `Shared ${name}${ref}` : `Own branch ${name}${ref}`;
20
+ }
21
+ function reason(error) {
22
+ const text = error.length > 1500 ? `${error.slice(0, 1500)}...` : error;
23
+ return text.includes("\n") ? `\n\n\`\`\`\n${text}\n\`\`\`` : ` ${text}`;
24
+ }
25
+ export function renderComment(s) {
26
+ const commit = `[${code(s.sha.slice(0, 7))}](${s.commitUrl})`;
27
+ const updated = s.updated.toISOString().slice(0, 16).replace("T", " ");
28
+ const status = s.runUrl ? `${STATUS[s.phase]} ([logs](${s.runUrl}))` : STATUS[s.phase];
29
+ const lines = [COMMENT_MARKER, `**Supabase Worker Preview** for ${code(s.worker)}`, ""];
30
+ if (s.phase === "removed") {
31
+ lines.push(s.database?.kind === "own"
32
+ ? `The Preview and its own database ${code(s.database.name)} were removed when this PR closed.`
33
+ : `The Preview was removed when this PR closed. The shared database stays.`, "", `<sub>Last commit ${commit}, ${updated} UTC</sub>`);
34
+ return lines.join("\n");
35
+ }
36
+ const preview = s.previewUrl ? `[Visit Preview](${s.previewUrl})` : "Not deployed yet";
37
+ lines.push("| Status | Preview | Database | Commit | Updated (UTC) |", "| :-- | :-- | :-- | :-- | :-- |", `| ${status} | ${preview} | ${databaseCell(s.database, s.dashboardUrl)} | ${commit} | ${updated} |`);
38
+ if (s.error)
39
+ lines.push("", `**Reason:**${reason(s.error)}`);
40
+ return lines.join("\n");
41
+ }
42
+ /** `::warning::` lines end at a newline, so GitHub's escapes keep a multi-line message whole. */
43
+ export function warning(message) {
44
+ return `::warning::${message.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A")}`;
45
+ }
46
+ /**
47
+ * The PR comment and GitHub deployment for one `swp pr` run. Any GitHub API
48
+ * failure becomes a warning; only the wrapped database work decides the job.
49
+ */
50
+ export class Feedback {
51
+ event;
52
+ deps;
53
+ github;
54
+ commentOn;
55
+ deploymentsOn;
56
+ commentId;
57
+ branch;
58
+ sha;
59
+ environment;
60
+ server;
61
+ runUrl;
62
+ constructor(event, deps) {
63
+ this.event = event;
64
+ this.deps = deps;
65
+ const env = deps.env ?? process.env;
66
+ this.github = deps.github ?? new GitHubApi(deps.githubToken, event.repository.full_name, deps.fetchImpl);
67
+ this.commentOn = deps.config.prComment !== false && !deps.runner.dryRun;
68
+ this.deploymentsOn = deps.config.githubDeployments !== false && !deps.runner.dryRun;
69
+ this.branch = event.pull_request.head.ref;
70
+ this.sha = event.pull_request.head.sha;
71
+ this.environment = (deps.config.deploymentEnvironment ?? "Preview").replaceAll("{branch}", this.branch);
72
+ this.server = env.GITHUB_SERVER_URL ?? "https://github.com";
73
+ if (env.GITHUB_RUN_ID)
74
+ this.runUrl = `${this.server}/${event.repository.full_name}/actions/runs/${env.GITHUB_RUN_ID}`;
75
+ }
76
+ /** Reports on `work` (up and check) from start to finish, and rethrows its error. */
77
+ async run(isolated, work) {
78
+ if (!this.commentOn && !this.deploymentsOn)
79
+ return work();
80
+ await this.comment({
81
+ phase: "checking",
82
+ previewUrl: await this.previewUrl(),
83
+ database: await this.database(isolated),
84
+ });
85
+ const deployment = await this.startDeployment();
86
+ try {
87
+ await work();
88
+ }
89
+ catch (err) {
90
+ const message = err.message;
91
+ const previewUrl = await this.previewUrl();
92
+ await this.comment({
93
+ phase: "failed",
94
+ previewUrl,
95
+ database: await this.database(isolated),
96
+ error: message,
97
+ });
98
+ await this.deploymentStatus(deployment, {
99
+ state: "failure",
100
+ environmentUrl: previewUrl,
101
+ description: message,
102
+ });
103
+ throw err;
104
+ }
105
+ const previewUrl = await this.previewUrl();
106
+ await this.comment({ phase: "passed", previewUrl, database: await this.database(isolated) });
107
+ await this.deploymentStatus(deployment, {
108
+ state: "success",
109
+ environmentUrl: previewUrl,
110
+ description: "The Preview serves the expected database",
111
+ });
112
+ await this.deactivateDeployments(deployment);
113
+ }
114
+ /** Reports on `work` (down) when the PR closes. */
115
+ /** `isolated`: the PR had its own database, which Supabase's integration may already have deleted. */
116
+ async close(work, isolated = false) {
117
+ if (!this.commentOn && !this.deploymentsOn)
118
+ return work();
119
+ const own = await branchFor(this.branch, this.deps).catch(() => undefined);
120
+ const database = own && !own.persistent
121
+ ? { kind: "own", name: own.name, ref: own.project_ref }
122
+ : isolated
123
+ ? { kind: "own", name: this.branch }
124
+ : undefined;
125
+ try {
126
+ await work();
127
+ }
128
+ catch (err) {
129
+ await this.comment({ phase: "cleanup-failed", database, error: err.message });
130
+ throw err;
131
+ }
132
+ await this.comment({ phase: "removed", database });
133
+ await this.deactivateDeployments();
134
+ }
135
+ async database(isolated) {
136
+ const { config } = this.deps;
137
+ if (isolated) {
138
+ const own = await branchFor(this.branch, this.deps).catch(() => undefined);
139
+ return { kind: "own", name: own?.name ?? this.branch, ref: own?.project_ref };
140
+ }
141
+ const shared = await this.deps.supabase
142
+ .listBranches(config.supabaseProjectRef)
143
+ .then((all) => all.find((b) => b.name === config.sharedBranch))
144
+ .catch(() => undefined);
145
+ return { kind: "shared", name: config.sharedBranch, ref: shared?.project_ref };
146
+ }
147
+ async previewUrl() {
148
+ const preview = await this.deps.cloudflare
149
+ .findPreview(previewKey(this.branch, this.deps.config, this.event.number))
150
+ .catch(() => undefined);
151
+ return preview?.urls[0]?.replace(/\/+$/, "");
152
+ }
153
+ async comment(state) {
154
+ if (!this.commentOn)
155
+ return;
156
+ const body = renderComment({
157
+ ...state,
158
+ worker: this.deps.config.worker,
159
+ sha: this.sha,
160
+ commitUrl: `${this.server}/${this.event.repository.full_name}/pull/${this.event.number}/commits/${this.sha}`,
161
+ dashboardUrl: `https://supabase.com/dashboard/project/${this.deps.config.supabaseProjectRef}/branches`,
162
+ updated: (this.deps.clock ?? (() => new Date()))(),
163
+ runUrl: this.runUrl,
164
+ });
165
+ const ok = await this.safe("PR comment", async () => {
166
+ this.commentId ??= await this.github.findComment(this.event.number, COMMENT_MARKER);
167
+ if (this.commentId)
168
+ await this.github.updateComment(this.commentId, body);
169
+ else
170
+ this.commentId = await this.github.createComment(this.event.number, body);
171
+ });
172
+ if (!ok)
173
+ this.commentOn = false;
174
+ }
175
+ async startDeployment() {
176
+ if (!this.deploymentsOn)
177
+ return undefined;
178
+ let id;
179
+ const ok = await this.safe("GitHub deployment", async () => {
180
+ id = await this.github.createDeployment({
181
+ sha: this.sha,
182
+ environment: this.environment,
183
+ description: `Worker Preview of ${this.branch}`,
184
+ payload: { [PAYLOAD_KEY]: this.event.number },
185
+ });
186
+ await this.github.setDeploymentStatus(id, { state: "in_progress", logUrl: this.runUrl });
187
+ });
188
+ if (!ok)
189
+ this.deploymentsOn = false;
190
+ return id;
191
+ }
192
+ async deploymentStatus(id, args) {
193
+ if (id === undefined || !this.deploymentsOn)
194
+ return;
195
+ await this.safe("GitHub deployment status", () => this.github.setDeploymentStatus(id, { ...args, logUrl: this.runUrl }));
196
+ }
197
+ /**
198
+ * Transient deployments are never marked inactive automatically, so this PR's
199
+ * older ones are retired here. Other PRs share the environment.
200
+ */
201
+ async deactivateDeployments(keep) {
202
+ if (!this.deploymentsOn)
203
+ return;
204
+ await this.safe("GitHub deployment cleanup", async () => {
205
+ for (const { id, payload } of await this.github.listDeployments(this.environment)) {
206
+ if (id === keep || pullRequestOf(payload) !== this.event.number)
207
+ continue;
208
+ if (await this.github.isInactive(id))
209
+ continue;
210
+ await this.github.setDeploymentStatus(id, { state: "inactive" });
211
+ }
212
+ });
213
+ }
214
+ async safe(what, fn) {
215
+ try {
216
+ await fn();
217
+ return true;
218
+ }
219
+ catch (err) {
220
+ this.deps.runner.log(warning(`${what} skipped: ${err.message}`));
221
+ return false;
222
+ }
223
+ }
224
+ }
225
+ /** GitHub may hand back a deployment payload as a JSON string. */
226
+ function pullRequestOf(payload) {
227
+ let value = payload;
228
+ if (typeof value === "string") {
229
+ try {
230
+ value = JSON.parse(value);
231
+ }
232
+ catch {
233
+ return undefined;
234
+ }
235
+ }
236
+ const pr = value?.[PAYLOAD_KEY];
237
+ return typeof pr === "number" ? pr : undefined;
238
+ }
@@ -0,0 +1,33 @@
1
+ export type DeploymentState = "in_progress" | "success" | "failure" | "error" | "inactive";
2
+ export type Deployment = {
3
+ id: number;
4
+ payload?: unknown;
5
+ };
6
+ export declare class GitHubApi {
7
+ private token;
8
+ private repo;
9
+ private fetchImpl;
10
+ private base;
11
+ constructor(token: string, repo: string, fetchImpl?: typeof fetch, base?: string);
12
+ private call;
13
+ private all;
14
+ /** The id of the first comment on the issue or PR whose body starts with `marker`. */
15
+ findComment(issue: number, marker: string): Promise<number | undefined>;
16
+ createComment(issue: number, body: string): Promise<number>;
17
+ updateComment(id: number, body: string): Promise<void>;
18
+ /** Skips the default merge and commit status checks, which would refuse a deployment while CI runs. */
19
+ createDeployment(args: {
20
+ sha: string;
21
+ environment: string;
22
+ description: string;
23
+ payload: Record<string, unknown>;
24
+ }): Promise<number>;
25
+ setDeploymentStatus(id: number, args: {
26
+ state: DeploymentState;
27
+ environmentUrl?: string;
28
+ logUrl?: string;
29
+ description?: string;
30
+ }): Promise<void>;
31
+ isInactive(id: number): Promise<boolean>;
32
+ listDeployments(environment: string): Promise<Deployment[]>;
33
+ }
package/dist/github.js ADDED
@@ -0,0 +1,78 @@
1
+ import { apiErrorMessage } from "./http.js";
2
+ export class GitHubApi {
3
+ token;
4
+ repo;
5
+ fetchImpl;
6
+ base;
7
+ constructor(token, repo, fetchImpl = fetch, base = process.env.GITHUB_API_URL ?? "https://api.github.com") {
8
+ this.token = token;
9
+ this.repo = repo;
10
+ this.fetchImpl = fetchImpl;
11
+ this.base = base;
12
+ }
13
+ async call(method, path, body) {
14
+ const res = await this.fetchImpl(`${this.base}/repos/${this.repo}${path}`, {
15
+ method,
16
+ headers: {
17
+ authorization: `Bearer ${this.token}`,
18
+ accept: "application/vnd.github+json",
19
+ "x-github-api-version": "2022-11-28",
20
+ ...(body === undefined ? {} : { "content-type": "application/json" }),
21
+ },
22
+ body: body === undefined ? undefined : JSON.stringify(body),
23
+ });
24
+ if (!res.ok)
25
+ throw new Error(apiErrorMessage(`GitHub API ${method} ${path}`, res.status, await res.text(), "GITHUB_TOKEN"));
26
+ return (await res.json());
27
+ }
28
+ async all(path) {
29
+ const all = [];
30
+ const sep = path.includes("?") ? "&" : "?";
31
+ for (let page = 1;; page += 1) {
32
+ const batch = await this.call("GET", `${path}${sep}per_page=100&page=${page}`);
33
+ all.push(...batch);
34
+ if (batch.length < 100)
35
+ return all;
36
+ }
37
+ }
38
+ /** The id of the first comment on the issue or PR whose body starts with `marker`. */
39
+ async findComment(issue, marker) {
40
+ const comments = await this.all(`/issues/${issue}/comments`);
41
+ return comments.find((c) => c.body?.startsWith(marker))?.id;
42
+ }
43
+ async createComment(issue, body) {
44
+ return (await this.call("POST", `/issues/${issue}/comments`, { body })).id;
45
+ }
46
+ async updateComment(id, body) {
47
+ await this.call("PATCH", `/issues/comments/${id}`, { body });
48
+ }
49
+ /** Skips the default merge and commit status checks, which would refuse a deployment while CI runs. */
50
+ async createDeployment(args) {
51
+ const deployment = await this.call("POST", "/deployments", {
52
+ ref: args.sha,
53
+ environment: args.environment,
54
+ payload: args.payload,
55
+ description: args.description,
56
+ auto_merge: false,
57
+ required_contexts: [],
58
+ transient_environment: true,
59
+ production_environment: false,
60
+ });
61
+ return deployment.id;
62
+ }
63
+ async setDeploymentStatus(id, args) {
64
+ await this.call("POST", `/deployments/${id}/statuses`, {
65
+ state: args.state,
66
+ ...(args.environmentUrl ? { environment_url: args.environmentUrl } : {}),
67
+ ...(args.logUrl ? { log_url: args.logUrl } : {}),
68
+ ...(args.description ? { description: args.description.slice(0, 140) } : {}),
69
+ });
70
+ }
71
+ async isInactive(id) {
72
+ const statuses = await this.all(`/deployments/${id}/statuses`);
73
+ return statuses.some((s) => s.state === "inactive");
74
+ }
75
+ listDeployments(environment) {
76
+ return this.all(`/deployments?environment=${encodeURIComponent(environment)}`);
77
+ }
78
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export declare const DOCS_URL = "https://github.com/mattruby/supabase-worker-previews#readme";
2
+ export declare const QUICKSTART_URL = "https://github.com/mattruby/supabase-worker-previews/blob/main/docs/quickstart.md";
3
+ export declare const TOKENS_URL = "https://github.com/mattruby/supabase-worker-previews/blob/main/docs/tokens.md";
4
+ /** An API error body in one short line: the JSON message when there is one, else the text, cut. */
5
+ export declare function summarizeBody(body: string, max?: number): string;
6
+ /** `<prefix>: <status> <summary>`, plus what a 401 or 403 says about the token that made the call. */
7
+ export declare function apiErrorMessage(prefix: string, status: number, body: string, variable: string): string;
package/dist/http.js ADDED
@@ -0,0 +1,44 @@
1
+ const REPO = "https://github.com/mattruby/supabase-worker-previews";
2
+ export const DOCS_URL = `${REPO}#readme`;
3
+ export const QUICKSTART_URL = `${REPO}/blob/main/docs/quickstart.md`;
4
+ export const TOKENS_URL = `${REPO}/blob/main/docs/tokens.md`;
5
+ /** An API error body in one short line: the JSON message when there is one, else the text, cut. */
6
+ export function summarizeBody(body, max = 200) {
7
+ let text = body;
8
+ try {
9
+ text = messageOf(JSON.parse(body)) ?? body;
10
+ }
11
+ catch {
12
+ const title = /<title>([^<]*)<\/title>/i.exec(body)?.[1];
13
+ if (title)
14
+ text = title;
15
+ }
16
+ text = text.replace(/\s+/g, " ").trim();
17
+ return text.length > max ? `${text.slice(0, max - 3)}...` : text;
18
+ }
19
+ function messageOf(json) {
20
+ if (!json || typeof json !== "object")
21
+ return undefined;
22
+ const o = json;
23
+ for (const value of [o.message, o.msg, o.error])
24
+ if (typeof value === "string" && value)
25
+ return value;
26
+ if (o.error && typeof o.error === "object")
27
+ return messageOf(o.error);
28
+ if (Array.isArray(o.errors)) {
29
+ const messages = o.errors.map(messageOf).filter((m) => m !== undefined);
30
+ if (messages.length)
31
+ return messages.join("; ");
32
+ }
33
+ return undefined;
34
+ }
35
+ /** `<prefix>: <status> <summary>`, plus what a 401 or 403 says about the token that made the call. */
36
+ export function apiErrorMessage(prefix, status, body, variable) {
37
+ const summary = summarizeBody(body);
38
+ const line = `${prefix}: ${status}${summary ? ` ${summary}` : ""}`;
39
+ if (status === 401)
40
+ return `${line}. ${variable} is invalid or expired; see ${TOKENS_URL}`;
41
+ if (status === 403)
42
+ return `${line}. ${variable} lacks a permission this call needs; see ${TOKENS_URL}`;
43
+ return line;
44
+ }
@@ -0,0 +1,2 @@
1
+ /** Parses JSON with comments and trailing commas, as wrangler.jsonc allows. */
2
+ export declare function parseJsonc(text: string): unknown;
package/dist/jsonc.js ADDED
@@ -0,0 +1,56 @@
1
+ /** Parses JSON with comments and trailing commas, as wrangler.jsonc allows. */
2
+ export function parseJsonc(text) {
3
+ return JSON.parse(dropTrailingCommas(stripComments(text)));
4
+ }
5
+ function stripComments(text) {
6
+ let out = "";
7
+ for (let i = 0; i < text.length; i++) {
8
+ const c = text[i];
9
+ if (c === '"') {
10
+ const end = stringEnd(text, i);
11
+ out += text.slice(i, end);
12
+ i = end - 1;
13
+ }
14
+ else if (c === "/" && text[i + 1] === "/") {
15
+ while (i < text.length && text[i] !== "\n")
16
+ i++;
17
+ out += "\n";
18
+ }
19
+ else if (c === "/" && text[i + 1] === "*") {
20
+ const end = text.indexOf("*/", i + 2);
21
+ i = end === -1 ? text.length : end + 1;
22
+ }
23
+ else {
24
+ out += c;
25
+ }
26
+ }
27
+ return out;
28
+ }
29
+ function dropTrailingCommas(text) {
30
+ let out = "";
31
+ for (let i = 0; i < text.length; i++) {
32
+ const c = text[i];
33
+ if (c === '"') {
34
+ const end = stringEnd(text, i);
35
+ out += text.slice(i, end);
36
+ i = end - 1;
37
+ }
38
+ else if (c === "," && /^\s*[}\]]/.test(text.slice(i + 1))) {
39
+ continue;
40
+ }
41
+ else {
42
+ out += c;
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+ /** Index just past the string literal that opens at `start`. */
48
+ function stringEnd(text, start) {
49
+ for (let i = start + 1; i < text.length; i++) {
50
+ if (text[i] === "\\")
51
+ i++;
52
+ else if (text[i] === '"')
53
+ return i + 1;
54
+ }
55
+ return text.length;
56
+ }
@@ -0,0 +1,3 @@
1
+ /** A git branch as a DNS label, to match a Preview that was named by slug rather than by raw branch. */
2
+ export declare function previewName(branch: string): string;
3
+ export declare function previewWildcard(worker: string, subdomain: string): string;
@@ -0,0 +1,15 @@
1
+ /** A git branch as a DNS label, to match a Preview that was named by slug rather than by raw branch. */
2
+ export function previewName(branch) {
3
+ const name = branch
4
+ .toLowerCase()
5
+ .replace(/[^a-z0-9]+/g, "-")
6
+ .replace(/^-+|-+$/g, "")
7
+ .slice(0, 40)
8
+ .replace(/-+$/, "");
9
+ if (!name)
10
+ throw new Error(`Cannot make a Preview name from branch "${branch}"`);
11
+ return name;
12
+ }
13
+ export function previewWildcard(worker, subdomain) {
14
+ return `https://*-${worker}.${subdomain}.workers.dev/**`;
15
+ }
package/dist/run.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ export type ExecOptions = {
2
+ env?: Record<string, string>;
3
+ input?: string;
4
+ captureStderr?: boolean;
5
+ };
6
+ export type Runner = {
7
+ dryRun: boolean;
8
+ log: (line: string) => void;
9
+ /** Runs a command, or in dry-run mode only prints it. Throws on a non-zero exit. */
10
+ exec: (cmd: string, args: string[], opts?: ExecOptions) => string;
11
+ };
12
+ export declare function makeRunner(dryRun: boolean, log?: (line: string) => void): Runner;
13
+ /**
14
+ * `--flag value`, `--flag=value` and bare `--flag`. A flag in `booleans` never
15
+ * takes the next argument, so `swp --dry-run up` keeps `up` as the command.
16
+ */
17
+ export declare function parseArgs(argv: string[], booleans?: ReadonlySet<string>): {
18
+ positional: string[];
19
+ flags: Record<string, string | true>;
20
+ };
21
+ export declare const sleep: (ms: number) => Promise<void>;
package/dist/run.js ADDED
@@ -0,0 +1,59 @@
1
+ import { spawnSync } from "node:child_process";
2
+ export function makeRunner(dryRun, log = console.log) {
3
+ return {
4
+ dryRun,
5
+ log,
6
+ exec(cmd, args, opts) {
7
+ const shown = [cmd, ...args.map((a) => (/\s/.test(a) ? JSON.stringify(a) : a))].join(" ");
8
+ if (dryRun) {
9
+ log(` $ ${shown}${opts?.input ? ` (stdin: ${opts.input.length} bytes)` : ""}`);
10
+ return "";
11
+ }
12
+ log(` $ ${shown}`);
13
+ const res = spawnSync(cmd, args, {
14
+ encoding: "utf8",
15
+ env: { ...process.env, ...(opts?.env ?? {}) },
16
+ input: opts?.input,
17
+ stdio: [opts?.input ? "pipe" : "inherit", "pipe", opts?.captureStderr ? "pipe" : "inherit"],
18
+ maxBuffer: 64 * 1024 * 1024,
19
+ });
20
+ if (res.error)
21
+ throw res.error;
22
+ if (res.status !== 0) {
23
+ const tail = opts?.captureStderr ? (res.stderr ?? "").trim().split("\n").slice(-8).join("\n") : "";
24
+ throw new Error(`\`${shown}\` exited ${res.status}${tail ? `:\n${tail}` : ""}`);
25
+ }
26
+ return res.stdout ?? "";
27
+ },
28
+ };
29
+ }
30
+ /**
31
+ * `--flag value`, `--flag=value` and bare `--flag`. A flag in `booleans` never
32
+ * takes the next argument, so `swp --dry-run up` keeps `up` as the command.
33
+ */
34
+ export function parseArgs(argv, booleans = new Set()) {
35
+ const positional = [];
36
+ const flags = {};
37
+ for (let i = 0; i < argv.length; i++) {
38
+ const a = argv[i];
39
+ if (a === "-h")
40
+ flags.help = true;
41
+ else if (a === "-v")
42
+ flags.version = true;
43
+ else if (!a.startsWith("--"))
44
+ positional.push(a);
45
+ else {
46
+ const eq = a.indexOf("=");
47
+ const k = eq === -1 ? a.slice(2) : a.slice(2, eq);
48
+ const next = argv[i + 1];
49
+ if (eq !== -1)
50
+ flags[k] = a.slice(eq + 1);
51
+ else if (!booleans.has(k) && next !== undefined && !next.startsWith("-"))
52
+ flags[k] = argv[++i];
53
+ else
54
+ flags[k] = true;
55
+ }
56
+ }
57
+ return { positional, flags };
58
+ }
59
+ export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Worker-side half: one build serves any Supabase database. The Worker reads
3
+ * its Supabase values at request time (from wrangler vars, or a Preview's
4
+ * SUPABASE_OVERRIDE secret) and hands the public ones to the browser, so a
5
+ * Preview never needs its own build.
6
+ */
7
+ export declare const OVERRIDE_SECRET = "SUPABASE_OVERRIDE";
8
+ export declare const DEFAULT_GLOBAL = "__SUPABASE_PUBLIC__";
9
+ export declare const IDENTITY_PATH = "/.well-known/supabase-preview";
10
+ export declare const SUPABASE_VARS: readonly ["SUPABASE_URL", "SUPABASE_PUBLISHABLE_KEY", "SUPABASE_SERVICE_ROLE_KEY", "SUPABASE_PROJECT_REF"];
11
+ export type SupabaseValues = Record<(typeof SUPABASE_VARS)[number], string>;
12
+ export type PublicConfig = {
13
+ supabaseUrl: string;
14
+ supabaseKey: string;
15
+ };
16
+ /**
17
+ * The override replaces all four values. It is one secret, not four, because a
18
+ * Worker var and secret must never share a name.
19
+ */
20
+ export declare function parseOverride(raw: unknown): SupabaseValues | null;
21
+ /**
22
+ * `env` with any SUPABASE_OVERRIDE applied. Also patches `process.env` when
23
+ * nodejs_compat provides it, for code that reads Supabase settings from there.
24
+ */
25
+ export declare function resolveEnv<E extends object>(env: E): E;
26
+ export declare function publicConfigFromEnv(env: object): PublicConfig | null;
27
+ export declare function publicConfigScript(config: PublicConfig, globalName?: string): string;
28
+ /** Browser side: the values the Worker injected, or null under a dev server that did not. */
29
+ export declare function readPublicConfig(globalName?: string, scope?: object): PublicConfig | null;
30
+ /** `https://<ref>.supabase.co` gives `<ref>`; a custom API domain gives null. */
31
+ export declare function projectRefOf(url: string | undefined): string | null;
32
+ export declare function injectIntoHtml(response: Response, html: string): Promise<Response>;
33
+ export declare function identityResponse(env: object): Response;
34
+ export type PreviewOptions = {
35
+ /** Window property the browser reads the public config from. */
36
+ globalName?: string;
37
+ /** Inject the public config into HTML responses. Default true. */
38
+ inject?: boolean;
39
+ /** Serve which database this deployment uses at /.well-known/supabase-preview. Default true. */
40
+ identity?: boolean;
41
+ };
42
+ /** Any Worker handler object, including `ExportedHandler<Env>`, whose `fetch` takes the workers `Request`. */
43
+ type Handler = {
44
+ fetch?: (request: never, env: never, ctx: never) => unknown;
45
+ };
46
+ /** The env type a handler's `fetch` declares, so callers never pass it as a generic. */
47
+ type EnvOf<H> = H extends {
48
+ fetch?: (request: never, env: infer E, ...rest: never[]) => unknown;
49
+ } ? E & object : object;
50
+ /**
51
+ * Wraps a Worker's default export. Every handler sees the override applied;
52
+ * `fetch` also injects the public config and answers the identity route that
53
+ * `swp check` reads.
54
+ */
55
+ export declare function withSupabasePreviews<H extends object, E extends object = EnvOf<H>>(handler: H & Handler, options?: PreviewOptions): H;
56
+ export {};