@auersperg/flare 0.0.0-stage → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +123 -2
- package/dist/cli/chunk-5cq8wgte.js +327 -0
- package/dist/cli/chunk-786w6vrj.js +169 -0
- package/dist/cli/chunk-vx7kmczm.js +379 -0
- package/dist/cli/chunk-x14smx1y.js +113 -0
- package/dist/cli/chunk-xff4b01y.js +18 -0
- package/dist/cli/flare.js +78 -0
- package/dist/sdk/backup.d.ts +22 -0
- package/dist/sdk/backup.js +83 -0
- package/dist/sdk/client.d.ts +80 -0
- package/dist/sdk/client.js +565 -0
- package/dist/sdk/cloudflare.d.ts +112 -0
- package/dist/sdk/cloudflare.js +391 -0
- package/dist/sdk/database.d.ts +39 -0
- package/dist/sdk/database.js +265 -0
- package/dist/sdk/protocol.d.ts +47 -0
- package/dist/sdk/protocol.js +76 -0
- package/dist/sdk/react.d.ts +18 -0
- package/dist/sdk/react.js +46 -0
- package/dist/sdk/schema.d.ts +43 -0
- package/dist/sdk/schema.js +143 -0
- package/dist/sdk/server.d.ts +146 -0
- package/dist/sdk/server.js +78 -0
- package/package.json +65 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adam Auer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,124 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Flare 0.3 — SDK and CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Applications install one `@auersperg/flare` package, including the portable SDK and lazy Bun CLI. No internal package archives, ORM, Agents, Wrangler or workerd are installed into an application.
|
|
4
|
+
|
|
5
|
+
- `@auersperg/flare`: `createClient({url,getToken})`, references, errors, file upload helpers.
|
|
6
|
+
- `@auersperg/flare/react`: `FlareProvider`, `useFlare`, `useQuery(api.tasks.list,args)` → data or loading `undefined`.
|
|
7
|
+
- `@auersperg/flare/server`: `defineSchema`, `defineTable`, `z`, function declarations and contracts.
|
|
8
|
+
- `@auersperg/flare/cloudflare`: native facet/action factories, backend only.
|
|
9
|
+
- `@auersperg/flare/protocol`: transport types and limits.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// flare/schema.ts
|
|
13
|
+
import { defineSchema, defineTable, z } from "@auersperg/flare/server";
|
|
14
|
+
export default defineSchema({
|
|
15
|
+
tasks: defineTable(
|
|
16
|
+
z.object({ title: z.string(), done: z.boolean().default(false) }),
|
|
17
|
+
).index("by_done", ["done"]),
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Functions import typed declarations from `./_generated/server`; `bunx --bun --package @auersperg/flare@next flare dev` generates this module before compilation. Read: `db.get(table,id)`, `db.query(table).withIndex(name,q=>q.eq(field,value)).order("desc").paginate({limit,cursor})`, `take(100)` and `first()`. Write: `insert`, `patch`, `replace`, `delete`; IDs and creation timestamps are automatic. Insert/replace accept validator input types, including omitted defaults. Queries expose no SQL, writes, session or storage capability.
|
|
22
|
+
|
|
23
|
+
Standard Schema / Standard JSON Schema validators define one contract for CLI, documents, function arguments/results and dashboard forms. All writes and schema activation validate data; reads decode already validated records, without running the same table validator again on each row. Output validation still applies to every function call. Mutations are transactional and capabilities expire when the invocation finishes. Unawaited database work and scheduled jobs must settle before commit.
|
|
24
|
+
|
|
25
|
+
0.3 is intentionally incompatible with the SQL/Drizzle API and release ABI 1–3. No compatibility adapters or historical app SQL journals remain. Use a fresh environment; local builds do not erase existing cloud data. Browser/server-JS clients remain portable; only CLI/build tooling requires Bun. No automatic retry of ambiguous mutations.
|
|
26
|
+
|
|
27
|
+
MIT-licensed; npm preview package `@auersperg/flare`. See the root architecture and maintenance documents for cloud acceptance and remaining product gates.
|
|
28
|
+
|
|
29
|
+
Scheduling: mutation `jobs.schedule(path,args,{delaySeconds})` and `jobs.cancel(id)` share the transaction. Cancel only removes a not-yet-enqueued outbox entry; it never aborts a running action. `paginate` adapts record count to its byte budget; `take` fails instead of silently truncating bulky results.
|
|
30
|
+
|
|
31
|
+
## Function model
|
|
32
|
+
|
|
33
|
+
Exactly three kinds: `query`, `mutation`, `action`. Declare each query once. JS: `client.onUpdate(ref,args,next,onError?)` delivers the first snapshot and later updates; `client.query(ref,args)` is the one-off alternative. Writes use `client.mutation`, external work uses `client.action`. The old `live`, `useLive`, `call` and `subscribe` APIs have been removed, not aliased. Regenerate references and redeploy applications with the updated CLI.
|
|
34
|
+
|
|
35
|
+
React uses one `useQuery(ref,args)` inside `FlareProvider`; `"skip"` makes no request. It returns values directly (not a custom result wrapper), handles cleanup and reconnect, and clears values for identity/auth changes. Function/auth errors reach the React error boundary. HTTP one-off reads do not require a persistent socket. Transport names such as `/v1/live` describe WebSockets, not another function type.
|
|
36
|
+
|
|
37
|
+
## Scoped files and background work
|
|
38
|
+
|
|
39
|
+
Actions use native scoped capabilities, not account-wide R2/Queue bindings. `ctx.files.list({limit:50,cursor})` returns `{page,isDone,continueCursor}`; pass the returned cursor to read the next page. No array alias or silent last-100 truncation remains. Upload/download grants expire, uploads stream to R2, and deletion invalidates outstanding download links.
|
|
40
|
+
|
|
41
|
+
Mutation outbox entries and data commit together. Native Queues batches confirm enqueue before removing outbox rows; retries remain per-message and at-least-once. Application external effects must use idempotency keys. Receipts have a bounded 30-day window, not perpetual exactly-once delivery.
|
|
42
|
+
|
|
43
|
+
## CLI
|
|
44
|
+
|
|
45
|
+
The CLI is compiled separately from the portable SDK in this package. Bun 1.4.3+; no local dashboard or emulator.
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
bun add @auersperg/flare@next # npm preview, next tag
|
|
49
|
+
bunx --bun --package @auersperg/flare@next flare dev # browser approval → project → native schema/types → cloud-dev watch
|
|
50
|
+
bunx --bun --package @auersperg/flare@next flare deploy # build and confirm production deployment
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Schema and functions live in `flare/`. `_generated/server.ts` supplies schema-typed query/mutation declarations; `_generated/api.ts` supplies browser-safe references. SQL journals and db-generation commands are not needed. Compilation/checksums happen locally, not on an application request. Frontend changes do not trigger backend deployment; framework-prefixed public URL env entries preserve other variables.
|
|
54
|
+
|
|
55
|
+
Advanced explicit commands:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
flare build --entry app.ts --outdir release
|
|
59
|
+
flare link --platform https://api.example.com/control --project PROJECT_UUID
|
|
60
|
+
flare dev --entry app.ts
|
|
61
|
+
flare deploy --env dev --release release
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Immutable output contains facet/action modules, `schema.json`, manifest, checksums, generated TypeScript API and OpenAPI 3.1. Only ABI 4 is accepted. No old `tables` declarations, `--migrations` flag or Drizzle pipeline.
|
|
65
|
+
|
|
66
|
+
Browser approval is protected by online Access/owner authorization and same-origin CSRF. Project-scoped credentials stay outside source/frontend env. Dev publishes to an already provisioned platform; general `platform init` is not implemented. No npm publication or cloud provisioning occurs during local verification.
|
|
67
|
+
|
|
68
|
+
## Immutable releases
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
bunx --bun --package @auersperg/flare@next flare releases --env dev
|
|
72
|
+
bunx --bun --package @auersperg/flare@next flare deploy --env prod --cmd "bun run build"
|
|
73
|
+
bunx --bun --package @auersperg/flare@next flare promote --env prod --hash DEV_BUILD_HASH --cmd "bun run build"
|
|
74
|
+
bunx --bun --package @auersperg/flare@next flare rollback --env prod --hash PREVIOUS_BUILD_HASH
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
History keeps the last five **distinct successful backend builds**, with their first publication date. Failed candidates are not promotable. Active builds and job/outbox references are protected from garbage collection. Repeating an unchanged build is not a new version.
|
|
78
|
+
|
|
79
|
+
Promote copies the selected immutable dev bundle to prod without recompiling or copying development data. Rollback activates compatible historical code against current data; it does not rewind the database. Both require explicit confirmation (`--confirm PROJECT_UUID` in CI), validate schema/job compatibility and compare the target's expected active hash. A changed target returns a conflict, never an automatic write retry.
|
|
80
|
+
|
|
81
|
+
`--cmd` generates client references for the exact selected bundle and builds the frontend **before** activating the backend. The child receives the selected environment URL via `FLARE_URL`, `PUBLIC_FLARE_URL`, `VITE_FLARE_URL`, `NEXT_PUBLIC_FLARE_URL` and `EXPO_PUBLIC_FLARE_URL`; local dev env files remain unchanged. A failed build does not publish backend code. Framework build variables must be public URLs, not credentials.
|
|
82
|
+
|
|
83
|
+
Frontend hosting stays with native Wrangler or another hosting tool. An application release script can sequence Flare + build + Wrangler. Frontend publication failure after backend activation is an explicit partial release, not an atomic transaction. Dashboard Health → Development/Production releases offers contract inspection, backend promotion and rollback on demand, with no polling. It does not publish the frontend.
|
|
84
|
+
|
|
85
|
+
Isolated historical preview environments and a paired frontend/backend version catalogue are not implemented. A saved backend hash is not a preview URL or a Git commit. Use additive contracts when old clients may remain active.
|
|
86
|
+
|
|
87
|
+
## On-demand backups
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
bunx --bun --package @auersperg/flare@next flare backup list --env dev
|
|
91
|
+
bunx --bun --package @auersperg/flare@next flare backup create --env dev --confirm PROJECT_UUID
|
|
92
|
+
bunx --bun --package @auersperg/flare@next flare backup restore --from dev --env prod --id BACKUP_UUID --confirm PROJECT_UUID
|
|
93
|
+
bunx --bun --package @auersperg/flare@next flare backup status --env prod
|
|
94
|
+
bunx --bun --package @auersperg/flare@next flare backup delete --env dev --id BACKUP_UUID --confirm PROJECT_UUID
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Writes require owner access and explicit confirmation (interactive `yes`, or the project UUID in CI). Creation prints its ID before sending the request; after an uncertain response retry explicitly with `--id` and that same ID. No automatic write retry. Restore copies data, IDs, creation timestamps, files and the exact backend build into the **other, empty environment of the same project**. It does not promote a release or overwrite an occupied production database.
|
|
98
|
+
|
|
99
|
+
Pause cron and drain jobs/actions/uploads first. Limits: 10,000 documents / 4 MiB logical data, 100 files / 100 MiB. Secrets, environment settings, schedules and job history are not copied. Interrupted restores stay closed to application calls and deployments; retry the same backup ID. The source backup cannot be deleted while a restore references it. Private R2 backups occupy storage until explicitly deleted; no periodic backup timer or extra service is installed. See the maintenance guide for recovery limitations.
|
|
100
|
+
|
|
101
|
+
## Built-in application login
|
|
102
|
+
|
|
103
|
+
Owner enables authentication/public registration per environment in dashboard → Auth. No external provider is required for native passwords. Applications explicitly decide which functions are public; required remains the default. User role/ownership checks still belong in handlers.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { createClient, createPasswordAuth } from "@auersperg/flare";
|
|
107
|
+
const url = import.meta.env.PUBLIC_FLARE_URL;
|
|
108
|
+
const auth = createPasswordAuth({ url });
|
|
109
|
+
const client = createClient({ url, getToken: auth.getToken });
|
|
110
|
+
const stopAuth = auth.onChange(() => client.refreshAuth());
|
|
111
|
+
await auth.register("my-login", "a long unique passphrase");
|
|
112
|
+
// Later: await auth.login(login, password), auth.me(), auth.changePassword(old, next).
|
|
113
|
+
await auth.logout();
|
|
114
|
+
stopAuth();
|
|
115
|
+
client.dispose();
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Sessions are memory-only and expire after at most 12 hours. Reload requires login again. Password reset/block/revoke in Auth disconnects affected live users. Owner/Access manages accounts; application users and project CLI credentials do not gain dashboard privileges. Public login/registration have bounded rate limits; public registration is not equivalent to public access to private rows. Passwords require 6–128 characters. There is no email verification/recovery or MFA in this minimal version; owner reset is available. Existing short-lived R2 download grants remain valid for their explicit TTL, not until session logout.
|
|
119
|
+
|
|
120
|
+
## License and distribution
|
|
121
|
+
|
|
122
|
+
SDK and CLI are MIT-licensed; see LICENSE. npm name: `@auersperg/flare`.
|
|
123
|
+
The `flare` binary remains unchanged. Preview releases are published under the `next` tag. Core/dashboard are separate non-published
|
|
124
|
+
components and are not included in this package.
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/flare/src/cli/cloud.mjs
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import {
|
|
5
|
+
readFile,
|
|
6
|
+
mkdir,
|
|
7
|
+
writeFile,
|
|
8
|
+
access,
|
|
9
|
+
mkdtemp,
|
|
10
|
+
watch,
|
|
11
|
+
rm
|
|
12
|
+
} from "fs/promises";
|
|
13
|
+
import { resolve, join } from "path";
|
|
14
|
+
var allowedUrl = (value) => {
|
|
15
|
+
const u = new URL(value);
|
|
16
|
+
if (u.protocol !== "https:" || u.username || u.password || u.search || u.hash)
|
|
17
|
+
throw Error("Platform must be an HTTPS management URL");
|
|
18
|
+
return u.href.replace(/\/$/, "");
|
|
19
|
+
};
|
|
20
|
+
async function request(platform, path, body) {
|
|
21
|
+
let token = process.env.FLARE_DEPLOY_TOKEN;
|
|
22
|
+
if (!token) {
|
|
23
|
+
const config = JSON.parse(await readFile(".flare/project.json", "utf8").catch((e) => {
|
|
24
|
+
if (e.code !== "ENOENT")
|
|
25
|
+
throw e;
|
|
26
|
+
return "null";
|
|
27
|
+
}));
|
|
28
|
+
if (config) {
|
|
29
|
+
const { storedToken } = await import("./chunk-x14smx1y.js");
|
|
30
|
+
token = await storedToken(platform, config.project);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (!token)
|
|
34
|
+
throw Error("Sign in with flare dev; CI can set FLARE_DEPLOY_TOKEN");
|
|
35
|
+
const r = await fetch(allowedUrl(platform) + path, {
|
|
36
|
+
method: body === undefined ? "GET" : "POST",
|
|
37
|
+
headers: {
|
|
38
|
+
authorization: `Bearer ${token}`,
|
|
39
|
+
...body === undefined ? {} : { "content-type": "application/json" }
|
|
40
|
+
},
|
|
41
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
42
|
+
redirect: "error",
|
|
43
|
+
signal: AbortSignal.timeout(60000)
|
|
44
|
+
});
|
|
45
|
+
const result = await r.json();
|
|
46
|
+
if (!r.ok) {
|
|
47
|
+
const error = Error(`${result.error ?? "Deployment failed"} (HTTP ${r.status}); writes are not automatically retried`);
|
|
48
|
+
error.status = r.status;
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
async function link(options) {
|
|
54
|
+
if (!options.platform || !options.project || !/^[0-9a-f-]{36}$/.test(options.project))
|
|
55
|
+
throw Error("Required: --platform <management URL> --project <ID>");
|
|
56
|
+
const platform = allowedUrl(options.platform);
|
|
57
|
+
const data = await request(platform, `/api/projects/${options.project}`);
|
|
58
|
+
await mkdir(".flare", { recursive: true });
|
|
59
|
+
await writeFile(".flare/project.json", JSON.stringify({
|
|
60
|
+
platform,
|
|
61
|
+
project: options.project,
|
|
62
|
+
runtime: data.runtime,
|
|
63
|
+
urls: data.urls
|
|
64
|
+
}, null, 2) + `
|
|
65
|
+
`, { mode: 384 });
|
|
66
|
+
return { project: options.project, runtime: data.runtime, urls: data.urls };
|
|
67
|
+
}
|
|
68
|
+
async function deploy(options = {}) {
|
|
69
|
+
if (!options.release) {
|
|
70
|
+
const config = JSON.parse(await readFile(".flare/project.json", "utf8"));
|
|
71
|
+
const { prepare, generateApi } = await import("./chunk-786w6vrj.js");
|
|
72
|
+
const { buildApplication } = await import("./chunk-vx7kmczm.js");
|
|
73
|
+
const source = await prepare(), directory = resolve(".flare/releases/" + randomUUID());
|
|
74
|
+
const env = options.env ?? "prod";
|
|
75
|
+
if (!["dev", "prod"].includes(env))
|
|
76
|
+
throw Error("Environment must be dev or prod");
|
|
77
|
+
if (env === "prod" && options.confirm !== config.project) {
|
|
78
|
+
if (!process.stdin.isTTY)
|
|
79
|
+
throw Error("Production requires --confirm <project ID> in CI");
|
|
80
|
+
const { createInterface } = await import("readline/promises");
|
|
81
|
+
const prompt = createInterface({
|
|
82
|
+
input: process.stdin,
|
|
83
|
+
output: process.stdout
|
|
84
|
+
});
|
|
85
|
+
try {
|
|
86
|
+
if (await prompt.question(`Deploy to production ${config.project}? Type yes: `) !== "yes")
|
|
87
|
+
throw Error("Deployment cancelled");
|
|
88
|
+
} finally {
|
|
89
|
+
prompt.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await buildApplication({ ...source, outdir: directory });
|
|
94
|
+
const result = await deploy({
|
|
95
|
+
env,
|
|
96
|
+
release: directory,
|
|
97
|
+
confirm: env === "prod" ? config.project : undefined,
|
|
98
|
+
cmd: options.cmd
|
|
99
|
+
});
|
|
100
|
+
await generateApi(directory);
|
|
101
|
+
return result;
|
|
102
|
+
} finally {
|
|
103
|
+
await rm(directory, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const config = JSON.parse(await readFile(".flare/project.json", "utf8"));
|
|
107
|
+
const env = options.env ?? "dev";
|
|
108
|
+
if (!["dev", "prod"].includes(env) || !options.release)
|
|
109
|
+
throw Error("Required: --env dev|prod --release <built directory>");
|
|
110
|
+
if (env === "prod" && options.confirm !== config.project)
|
|
111
|
+
throw Error("Production requires --confirm <project ID>");
|
|
112
|
+
const directory = resolve(options.release);
|
|
113
|
+
const read = async (file) => readFile(join(directory, file), "utf8");
|
|
114
|
+
const bundle = {
|
|
115
|
+
release: JSON.parse(await read("release.json")),
|
|
116
|
+
manifest: JSON.parse(await read("manifest.json")),
|
|
117
|
+
schema: JSON.parse(await read("schema.json")),
|
|
118
|
+
facet: await read("facet.js"),
|
|
119
|
+
action: await read("action.js")
|
|
120
|
+
};
|
|
121
|
+
let expected;
|
|
122
|
+
if (options.cmd) {
|
|
123
|
+
const health = await request(config.platform, `/api/projects/${config.project}/${env}/health`);
|
|
124
|
+
expected = health.active;
|
|
125
|
+
const { generateApi } = await import("./chunk-786w6vrj.js");
|
|
126
|
+
await generateApi(directory);
|
|
127
|
+
await frontendBuild(config, env, options.cmd);
|
|
128
|
+
}
|
|
129
|
+
return request(config.platform, `/api/projects/${config.project}/${env}/deploy`, { bundle, confirm: options.confirm, expected });
|
|
130
|
+
}
|
|
131
|
+
async function dev(options = {}) {
|
|
132
|
+
if (options.env && options.env !== "dev")
|
|
133
|
+
throw Error("Watch publishes only to dev; use deploy for production");
|
|
134
|
+
const simple = !options.entry;
|
|
135
|
+
if (simple) {
|
|
136
|
+
const { scaffold } = await import("./chunk-786w6vrj.js");
|
|
137
|
+
await scaffold();
|
|
138
|
+
try {
|
|
139
|
+
await access(".flare/project.json");
|
|
140
|
+
} catch (e) {
|
|
141
|
+
if (e.code !== "ENOENT")
|
|
142
|
+
throw e;
|
|
143
|
+
const { connect } = await import("./chunk-x14smx1y.js");
|
|
144
|
+
await connect();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
await access(".flare/project.json");
|
|
148
|
+
if (simple) {
|
|
149
|
+
const config = JSON.parse(await readFile(".flare/project.json", "utf8"));
|
|
150
|
+
try {
|
|
151
|
+
await request(config.platform, `/api/projects/${config.project}`);
|
|
152
|
+
} catch (e) {
|
|
153
|
+
if (process.env.FLARE_DEPLOY_TOKEN || e.status !== 401 && !e.message.startsWith("Sign in"))
|
|
154
|
+
throw e;
|
|
155
|
+
const { connect } = await import("./chunk-x14smx1y.js");
|
|
156
|
+
await connect();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const { buildApplication } = await import("./chunk-vx7kmczm.js");
|
|
160
|
+
let busy = false, pending = false, timer, hash;
|
|
161
|
+
const publish = async (initial = false) => {
|
|
162
|
+
if (busy) {
|
|
163
|
+
pending = true;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
busy = true;
|
|
167
|
+
try {
|
|
168
|
+
const outdir = resolve(".flare/releases/" + randomUUID());
|
|
169
|
+
try {
|
|
170
|
+
const { prepare, generateApi, frontendEnvironment } = await import("./chunk-786w6vrj.js");
|
|
171
|
+
const source = simple ? await prepare() : { entry: options.entry };
|
|
172
|
+
const release = await buildApplication({ ...source, outdir });
|
|
173
|
+
if (release.hash !== hash) {
|
|
174
|
+
const data = await deploy({ env: "dev", release: outdir });
|
|
175
|
+
if (simple) {
|
|
176
|
+
await generateApi(outdir);
|
|
177
|
+
await frontendEnvironment(data.url);
|
|
178
|
+
}
|
|
179
|
+
hash = release.hash;
|
|
180
|
+
console.log(`Dev release ${hash}
|
|
181
|
+
${data.url}`);
|
|
182
|
+
}
|
|
183
|
+
} finally {
|
|
184
|
+
await rm(outdir, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
} catch (e) {
|
|
187
|
+
if (initial)
|
|
188
|
+
throw e;
|
|
189
|
+
console.error(e.message);
|
|
190
|
+
} finally {
|
|
191
|
+
busy = false;
|
|
192
|
+
if (pending) {
|
|
193
|
+
pending = false;
|
|
194
|
+
publish();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
await publish(true);
|
|
199
|
+
console.log("Watching application code; backend runs in Cloudflare dev. Start your frontend with its own dev server.");
|
|
200
|
+
for await (const event of watch(simple ? resolve("flare") : process.cwd(), {
|
|
201
|
+
recursive: true
|
|
202
|
+
})) {
|
|
203
|
+
const file = event.filename?.toString() ?? "";
|
|
204
|
+
if (/(^|\/)(node_modules|\.flare|\.git|dist|_generated)(\/|$)/.test(file) || !/\.(ts|tsx|js|mjs|sql|json)$/.test(file))
|
|
205
|
+
continue;
|
|
206
|
+
clearTimeout(timer);
|
|
207
|
+
timer = setTimeout(() => void publish(), 400);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function backup(options = {}) {
|
|
211
|
+
const config = JSON.parse(await readFile(".flare/project.json", "utf8"));
|
|
212
|
+
const operation = options.operation ?? "list", env = options.env ?? "prod";
|
|
213
|
+
if (!["dev", "prod"].includes(env) || !["create", "list", "restore", "delete", "status"].includes(operation))
|
|
214
|
+
throw Error("Use backup create|list|restore|delete|status --env dev|prod");
|
|
215
|
+
const path = `/api/projects/${config.project}/${env}/`;
|
|
216
|
+
if (operation === "list")
|
|
217
|
+
return request(config.platform, path + "backups" + (options.cursor ? `?cursor=${encodeURIComponent(options.cursor)}` : ""));
|
|
218
|
+
if (operation === "status")
|
|
219
|
+
return request(config.platform, path + "backup-status");
|
|
220
|
+
const id = options.id ?? (operation === "create" ? randomUUID() : null);
|
|
221
|
+
if (!id || !/^[0-9a-f-]{36}$/.test(id))
|
|
222
|
+
throw Error("Required: --id <backup ID>");
|
|
223
|
+
if (operation === "restore" && (!["dev", "prod"].includes(options.from) || options.from === env))
|
|
224
|
+
throw Error("Restore requires --from dev|prod to a different empty environment of this project");
|
|
225
|
+
if (options.confirm !== config.project) {
|
|
226
|
+
if (!process.stdin.isTTY)
|
|
227
|
+
throw Error("Backup writes require --confirm <project ID> in CI");
|
|
228
|
+
const { createInterface } = await import("readline/promises");
|
|
229
|
+
const prompt = createInterface({
|
|
230
|
+
input: process.stdin,
|
|
231
|
+
output: process.stdout
|
|
232
|
+
});
|
|
233
|
+
try {
|
|
234
|
+
if (await prompt.question(`${operation} backup ${id} \xB7 ${config.project}/${env}? Type yes: `) !== "yes")
|
|
235
|
+
throw Error("Cancelled");
|
|
236
|
+
} finally {
|
|
237
|
+
prompt.close();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
console.error(`Backup operation ${operation}: ${id}`);
|
|
241
|
+
return request(config.platform, path + "backup-" + operation, {
|
|
242
|
+
id,
|
|
243
|
+
sourceEnv: options.from,
|
|
244
|
+
confirm: config.project
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
async function frontendBuild(config, env, command) {
|
|
248
|
+
if (typeof command !== "string" || !command.trim())
|
|
249
|
+
throw Error("Empty build command");
|
|
250
|
+
const settings = await request(config.platform, `/api/projects/${config.project}/${env}/settings`);
|
|
251
|
+
const url = allowedUrl(settings.url);
|
|
252
|
+
const { $ } = await Promise.resolve(globalThis.Bun);
|
|
253
|
+
await $`${{ raw: command }}`.env({
|
|
254
|
+
...process.env,
|
|
255
|
+
FLARE_URL: url,
|
|
256
|
+
PUBLIC_FLARE_URL: url,
|
|
257
|
+
VITE_FLARE_URL: url,
|
|
258
|
+
NEXT_PUBLIC_FLARE_URL: url,
|
|
259
|
+
EXPO_PUBLIC_FLARE_URL: url
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
async function releaseOperation(options, operation) {
|
|
263
|
+
const config = JSON.parse(await readFile(".flare/project.json", "utf8"));
|
|
264
|
+
const env = options.env ?? "prod";
|
|
265
|
+
if (!["dev", "prod"].includes(env) || !/^[a-f0-9]{64}$/.test(options.hash ?? ""))
|
|
266
|
+
throw Error("Required: --hash <64-character release hash> and --env dev|prod");
|
|
267
|
+
if (operation === "promote" && env !== "prod")
|
|
268
|
+
throw Error("Promote targets production only");
|
|
269
|
+
if (options.confirm !== config.project) {
|
|
270
|
+
if (!process.stdin.isTTY)
|
|
271
|
+
throw Error("Release switch requires --confirm <project ID> in CI");
|
|
272
|
+
const { createInterface } = await import("readline/promises");
|
|
273
|
+
const prompt = createInterface({
|
|
274
|
+
input: process.stdin,
|
|
275
|
+
output: process.stdout
|
|
276
|
+
});
|
|
277
|
+
try {
|
|
278
|
+
if (await prompt.question(`${operation} ${options.hash} into ${env}? Type yes: `) !== "yes")
|
|
279
|
+
throw Error("Cancelled");
|
|
280
|
+
} finally {
|
|
281
|
+
prompt.close();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const path = `/api/projects/${config.project}/${env}/`;
|
|
285
|
+
const health = await request(config.platform, path + "health");
|
|
286
|
+
if (options.cmd) {
|
|
287
|
+
const source = operation === "promote" ? "dev" : env;
|
|
288
|
+
const bundle = await request(config.platform, `/api/projects/${config.project}/${source}/release-bundle?hash=${options.hash}`);
|
|
289
|
+
if (bundle.release.hash !== options.hash)
|
|
290
|
+
throw Error("Release hash mismatch");
|
|
291
|
+
const { generateReferences } = await import("./chunk-vx7kmczm.js");
|
|
292
|
+
const { generateApi } = await import("./chunk-786w6vrj.js");
|
|
293
|
+
await mkdir(".flare", { recursive: true });
|
|
294
|
+
const directory = await mkdtemp(resolve(".flare/release-contract-"));
|
|
295
|
+
try {
|
|
296
|
+
await writeFile(join(directory, "api.ts"), await generateReferences(bundle.manifest));
|
|
297
|
+
await writeFile(join(directory, "manifest.json"), JSON.stringify(bundle.manifest));
|
|
298
|
+
await generateApi(directory);
|
|
299
|
+
await frontendBuild(config, env, options.cmd);
|
|
300
|
+
} finally {
|
|
301
|
+
await rm(directory, { recursive: true, force: true });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return request(config.platform, path + "release-" + operation, {
|
|
305
|
+
hash: options.hash,
|
|
306
|
+
expected: health.active,
|
|
307
|
+
confirm: config.project
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
var promote = (options = {}) => releaseOperation(options, "promote");
|
|
311
|
+
var rollback = (options = {}) => releaseOperation(options, "rollback");
|
|
312
|
+
async function releases(options = {}) {
|
|
313
|
+
const config = JSON.parse(await readFile(".flare/project.json", "utf8"));
|
|
314
|
+
const env = options.env ?? "dev";
|
|
315
|
+
if (!["dev", "prod"].includes(env))
|
|
316
|
+
throw Error("Environment must be dev or prod");
|
|
317
|
+
return request(config.platform, `/api/projects/${config.project}/${env}/history`);
|
|
318
|
+
}
|
|
319
|
+
export {
|
|
320
|
+
backup,
|
|
321
|
+
deploy,
|
|
322
|
+
dev,
|
|
323
|
+
link,
|
|
324
|
+
promote,
|
|
325
|
+
releases,
|
|
326
|
+
rollback
|
|
327
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
bundle
|
|
4
|
+
} from "./chunk-xff4b01y.js";
|
|
5
|
+
|
|
6
|
+
// packages/flare/src/cli/project.mjs
|
|
7
|
+
import {
|
|
8
|
+
mkdir,
|
|
9
|
+
readFile,
|
|
10
|
+
writeFile,
|
|
11
|
+
readdir,
|
|
12
|
+
access,
|
|
13
|
+
rm
|
|
14
|
+
} from "fs/promises";
|
|
15
|
+
import { resolve, join } from "path";
|
|
16
|
+
import { pathToFileURL } from "url";
|
|
17
|
+
import { randomUUID } from "crypto";
|
|
18
|
+
async function scaffold() {
|
|
19
|
+
await access("package.json");
|
|
20
|
+
await mkdir("flare", { recursive: true });
|
|
21
|
+
try {
|
|
22
|
+
await writeFile("flare/schema.ts", `import {defineSchema} from "@auersperg/flare/server";
|
|
23
|
+
export default defineSchema({});
|
|
24
|
+
`, { flag: "wx" });
|
|
25
|
+
} catch (e) {
|
|
26
|
+
if (e.code !== "EEXIST")
|
|
27
|
+
throw e;
|
|
28
|
+
}
|
|
29
|
+
for (const [file, text] of [
|
|
30
|
+
[".gitignore", `
|
|
31
|
+
.flare/
|
|
32
|
+
flare/_generated/
|
|
33
|
+
.env.local
|
|
34
|
+
`],
|
|
35
|
+
[".prettierignore", `
|
|
36
|
+
.flare/
|
|
37
|
+
flare/_generated/
|
|
38
|
+
`]
|
|
39
|
+
]) {
|
|
40
|
+
const previous = await readFile(file, "utf8").catch((e) => {
|
|
41
|
+
if (e.code !== "ENOENT")
|
|
42
|
+
throw e;
|
|
43
|
+
return "";
|
|
44
|
+
});
|
|
45
|
+
if (!previous.includes("flare/_generated/"))
|
|
46
|
+
await writeFile(file, previous + text);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function modules(directory, prefix = "") {
|
|
50
|
+
const result = [];
|
|
51
|
+
for (const entry of (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
52
|
+
if (entry.name.startsWith("_") || entry.name.startsWith("."))
|
|
53
|
+
continue;
|
|
54
|
+
const path = join(directory, entry.name), name = prefix + entry.name;
|
|
55
|
+
if (entry.isDirectory())
|
|
56
|
+
result.push(...await modules(path, name + "/"));
|
|
57
|
+
else if (entry.isFile() && entry.name.endsWith(".ts") && name !== "schema.ts" && !name.endsWith(".d.ts"))
|
|
58
|
+
result.push({ path: resolve(path), name: name.slice(0, -3) });
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
async function prepare() {
|
|
63
|
+
await scaffold();
|
|
64
|
+
await mkdir(".flare/build", { recursive: true });
|
|
65
|
+
await mkdir("flare/_generated", { recursive: true });
|
|
66
|
+
await writeFile("flare/_generated/server.ts", `import type schema from "../schema";
|
|
67
|
+
import {createFunctions} from "@auersperg/flare/server";
|
|
68
|
+
export const {query,mutation,action}=createFunctions<typeof schema>();
|
|
69
|
+
`);
|
|
70
|
+
const files = await modules("flare");
|
|
71
|
+
const schema = resolve("flare/schema.ts"), entry = resolve(".flare/build/app.ts");
|
|
72
|
+
const imports = files.map((file, i) => `import * as f${i} from ${JSON.stringify(file.path)};`).join(`
|
|
73
|
+
`);
|
|
74
|
+
const source = `import {defineApp} from "@auersperg/flare/server";
|
|
75
|
+
import schema from ${JSON.stringify(schema)};
|
|
76
|
+
${imports}
|
|
77
|
+
const functions={};
|
|
78
|
+
${files.map((file, i) => `functions[${JSON.stringify(file.name)}]=Object.fromEntries(Object.entries(f${i}).filter(([,fn])=>fn && typeof fn === "object" && ["query","mutation","action"].includes(fn.kind)));`).join(`
|
|
79
|
+
`)}
|
|
80
|
+
const tree=Object.create(null);
|
|
81
|
+
for(const [path,exports] of Object.entries(functions)) {
|
|
82
|
+
const parts=path.split("/");let node=tree;
|
|
83
|
+
for(const part of parts.slice(0,-1))node=node[part]??=Object.create(null);
|
|
84
|
+
const key=parts.at(-1); if(key in node)throw Error("Function namespace conflict: "+path);
|
|
85
|
+
node[key]=exports;
|
|
86
|
+
}
|
|
87
|
+
export default defineApp({schema,functions:tree});
|
|
88
|
+
`;
|
|
89
|
+
await writeFile(entry, source);
|
|
90
|
+
const metadata = resolve(`.flare/build/metadata-${randomUUID()}.mjs`);
|
|
91
|
+
try {
|
|
92
|
+
await bundle({
|
|
93
|
+
entrypoints: [entry],
|
|
94
|
+
outdir: resolve(".flare/build"),
|
|
95
|
+
naming: { entry: metadata.split("/").at(-1) },
|
|
96
|
+
packages: "external",
|
|
97
|
+
target: "bun",
|
|
98
|
+
format: "esm"
|
|
99
|
+
});
|
|
100
|
+
const app = await import(pathToFileURL(metadata).href);
|
|
101
|
+
if (!app.default?.schema)
|
|
102
|
+
throw Error("Schema must export defineSchema(...)");
|
|
103
|
+
const imports = [], declarations = [];
|
|
104
|
+
for (const file of files) {
|
|
105
|
+
const node = file.name.split("/").reduce((tree, name) => tree?.[name], app.default.functions);
|
|
106
|
+
for (const [name, fn] of Object.entries(node ?? {})) {
|
|
107
|
+
if (fn?.kind !== "action")
|
|
108
|
+
continue;
|
|
109
|
+
const symbol = `action${imports.length}`;
|
|
110
|
+
imports.push(`import {${name} as ${symbol}} from ${JSON.stringify(file.path)};`);
|
|
111
|
+
declarations.push(`${JSON.stringify(file.name + "/" + name)}:${symbol}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const actionEntry = resolve(".flare/build/actions.ts");
|
|
115
|
+
await writeFile(actionEntry, `${imports.join(`
|
|
116
|
+
`)}
|
|
117
|
+
export default {functions:{${declarations.join(",")}}};
|
|
118
|
+
`);
|
|
119
|
+
return { entry, actionEntry };
|
|
120
|
+
} finally {
|
|
121
|
+
await rm(metadata, { force: true });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function generateApi(directory) {
|
|
125
|
+
await mkdir("flare/_generated", { recursive: true });
|
|
126
|
+
const flat = await readFile(join(directory, "api.ts"), "utf8");
|
|
127
|
+
const manifest = JSON.parse(await readFile(join(directory, "manifest.json"), "utf8"));
|
|
128
|
+
const paths = Object.keys(manifest.functions);
|
|
129
|
+
const tree = Object.create(null);
|
|
130
|
+
for (const path of paths) {
|
|
131
|
+
let node = tree;
|
|
132
|
+
const parts = path.split("/");
|
|
133
|
+
for (const part of parts.slice(0, -1))
|
|
134
|
+
node = node[part] ??= Object.create(null);
|
|
135
|
+
node[parts.at(-1)] = path;
|
|
136
|
+
}
|
|
137
|
+
const serialize = (value) => typeof value === "string" ? `references[${JSON.stringify(value)}]` : `{${Object.entries(value).map(([key, child]) => `${JSON.stringify(key)}:${serialize(child)}`).join(",")}}`;
|
|
138
|
+
await writeFile("flare/_generated/references.ts", flat);
|
|
139
|
+
await writeFile("flare/_generated/api.ts", `import {api as references} from "./references";
|
|
140
|
+
export const api = ${serialize(tree)} as const;
|
|
141
|
+
`);
|
|
142
|
+
}
|
|
143
|
+
async function frontendEnvironment(url) {
|
|
144
|
+
const pkg = JSON.parse(await readFile("package.json", "utf8"));
|
|
145
|
+
const dependencies = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
146
|
+
const variable = dependencies.astro ? "PUBLIC_FLARE_URL" : dependencies.expo ? "EXPO_PUBLIC_FLARE_URL" : "VITE_FLARE_URL";
|
|
147
|
+
const parsed = new URL(url);
|
|
148
|
+
if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.search || parsed.hash)
|
|
149
|
+
throw Error("Invalid environment URL");
|
|
150
|
+
let previous = await readFile(".env.local", "utf8").catch((e) => {
|
|
151
|
+
if (e.code !== "ENOENT")
|
|
152
|
+
throw e;
|
|
153
|
+
return "";
|
|
154
|
+
});
|
|
155
|
+
const line = variable + "=" + JSON.stringify(parsed.href);
|
|
156
|
+
const pattern = new RegExp("^" + variable + "=.*$", "m");
|
|
157
|
+
const next = pattern.test(previous) ? previous.replace(pattern, () => line) : previous + (previous.endsWith(`
|
|
158
|
+
`) || !previous ? "" : `
|
|
159
|
+
`) + line + `
|
|
160
|
+
`;
|
|
161
|
+
if (next !== previous)
|
|
162
|
+
await writeFile(".env.local", next, { mode: 384 });
|
|
163
|
+
}
|
|
164
|
+
export {
|
|
165
|
+
frontendEnvironment,
|
|
166
|
+
generateApi,
|
|
167
|
+
prepare,
|
|
168
|
+
scaffold
|
|
169
|
+
};
|