fastscript 1.0.0 → 2.0.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.
- package/CHANGELOG.md +32 -7
- package/LICENSE +33 -21
- package/README.md +567 -73
- package/node_modules/@fastscript/core-private/BOUNDARY.json +15 -0
- package/node_modules/@fastscript/core-private/README.md +5 -0
- package/node_modules/@fastscript/core-private/package.json +34 -0
- package/node_modules/@fastscript/core-private/src/asset-optimizer.mjs +67 -0
- package/node_modules/@fastscript/core-private/src/audit-log.mjs +50 -0
- package/node_modules/@fastscript/core-private/src/auth-flows.mjs +29 -0
- package/node_modules/@fastscript/core-private/src/auth.mjs +115 -0
- package/node_modules/@fastscript/core-private/src/bench.mjs +45 -0
- package/node_modules/@fastscript/core-private/src/build.mjs +670 -0
- package/node_modules/@fastscript/core-private/src/cache.mjs +248 -0
- package/node_modules/@fastscript/core-private/src/check.mjs +22 -0
- package/node_modules/@fastscript/core-private/src/cli.mjs +95 -0
- package/node_modules/@fastscript/core-private/src/compat.mjs +128 -0
- package/node_modules/@fastscript/core-private/src/create.mjs +278 -0
- package/node_modules/@fastscript/core-private/src/csp.mjs +26 -0
- package/node_modules/@fastscript/core-private/src/db-cli.mjs +185 -0
- package/node_modules/@fastscript/core-private/src/db-postgres-collection.mjs +110 -0
- package/node_modules/@fastscript/core-private/src/db-postgres.mjs +40 -0
- package/node_modules/@fastscript/core-private/src/db.mjs +103 -0
- package/node_modules/@fastscript/core-private/src/deploy.mjs +662 -0
- package/node_modules/@fastscript/core-private/src/dev.mjs +5 -0
- package/node_modules/@fastscript/core-private/src/docs-search.mjs +35 -0
- package/node_modules/@fastscript/core-private/src/env.mjs +118 -0
- package/node_modules/@fastscript/core-private/src/export.mjs +83 -0
- package/node_modules/@fastscript/core-private/src/fs-diagnostics.mjs +70 -0
- package/node_modules/@fastscript/core-private/src/fs-error-codes.mjs +141 -0
- package/node_modules/@fastscript/core-private/src/fs-formatter.mjs +66 -0
- package/node_modules/@fastscript/core-private/src/fs-linter.mjs +274 -0
- package/node_modules/@fastscript/core-private/src/fs-normalize.mjs +91 -0
- package/node_modules/@fastscript/core-private/src/fs-parser.mjs +980 -0
- package/node_modules/@fastscript/core-private/src/generated/docs-search-index.mjs +3182 -0
- package/node_modules/@fastscript/core-private/src/i18n.mjs +25 -0
- package/node_modules/@fastscript/core-private/src/interop.mjs +16 -0
- package/node_modules/@fastscript/core-private/src/jobs.mjs +378 -0
- package/node_modules/@fastscript/core-private/src/logger.mjs +27 -0
- package/node_modules/@fastscript/core-private/src/metrics.mjs +45 -0
- package/node_modules/@fastscript/core-private/src/middleware.mjs +14 -0
- package/node_modules/@fastscript/core-private/src/migrate.mjs +81 -0
- package/node_modules/@fastscript/core-private/src/migration-wizard.mjs +16 -0
- package/node_modules/@fastscript/core-private/src/module-loader.mjs +46 -0
- package/node_modules/@fastscript/core-private/src/oauth-providers.mjs +103 -0
- package/node_modules/@fastscript/core-private/src/observability.mjs +21 -0
- package/node_modules/@fastscript/core-private/src/plugins.mjs +194 -0
- package/node_modules/@fastscript/core-private/src/retention.mjs +57 -0
- package/node_modules/@fastscript/core-private/src/routes.mjs +178 -0
- package/node_modules/@fastscript/core-private/src/scheduler.mjs +104 -0
- package/node_modules/@fastscript/core-private/src/security.mjs +233 -0
- package/node_modules/@fastscript/core-private/src/server-runtime.mjs +849 -0
- package/node_modules/@fastscript/core-private/src/serverless-handler.mjs +20 -0
- package/node_modules/@fastscript/core-private/src/session-policy.mjs +38 -0
- package/node_modules/@fastscript/core-private/src/start.mjs +10 -0
- package/node_modules/@fastscript/core-private/src/storage.mjs +155 -0
- package/node_modules/@fastscript/core-private/src/style-primitives.mjs +538 -0
- package/node_modules/@fastscript/core-private/src/style-system.mjs +461 -0
- package/node_modules/@fastscript/core-private/src/tenant.mjs +55 -0
- package/node_modules/@fastscript/core-private/src/typecheck.mjs +1464 -0
- package/node_modules/@fastscript/core-private/src/validate.mjs +22 -0
- package/node_modules/@fastscript/core-private/src/validation.mjs +88 -0
- package/node_modules/@fastscript/core-private/src/webhook.mjs +81 -0
- package/node_modules/@fastscript/core-private/src/worker.mjs +24 -0
- package/package.json +86 -13
- package/src/asset-optimizer.mjs +67 -0
- package/src/audit-log.mjs +50 -0
- package/src/auth.mjs +1 -115
- package/src/bench.mjs +20 -7
- package/src/build.mjs +1 -234
- package/src/cache.mjs +210 -20
- package/src/cli.mjs +29 -5
- package/src/compat.mjs +8 -10
- package/src/create.mjs +71 -17
- package/src/csp.mjs +26 -0
- package/src/db-cli.mjs +152 -8
- package/src/db-postgres-collection.mjs +110 -0
- package/src/deploy.mjs +1 -65
- package/src/docs-search.mjs +35 -0
- package/src/env.mjs +34 -5
- package/src/fs-diagnostics.mjs +70 -0
- package/src/fs-error-codes.mjs +126 -0
- package/src/fs-formatter.mjs +66 -0
- package/src/fs-linter.mjs +274 -0
- package/src/fs-normalize.mjs +21 -238
- package/src/fs-parser.mjs +1 -0
- package/src/generated/docs-search-index.mjs +3220 -0
- package/src/i18n.mjs +25 -0
- package/src/jobs.mjs +283 -32
- package/src/metrics.mjs +45 -0
- package/src/migration-wizard.mjs +16 -0
- package/src/module-loader.mjs +11 -12
- package/src/oauth-providers.mjs +103 -0
- package/src/plugins.mjs +194 -0
- package/src/retention.mjs +57 -0
- package/src/routes.mjs +178 -0
- package/src/scheduler.mjs +104 -0
- package/src/security.mjs +197 -19
- package/src/server-runtime.mjs +1 -339
- package/src/serverless-handler.mjs +20 -0
- package/src/session-policy.mjs +38 -0
- package/src/storage.mjs +1 -56
- package/src/style-system.mjs +461 -0
- package/src/tenant.mjs +55 -0
- package/src/typecheck.mjs +1 -0
- package/src/validate.mjs +5 -1
- package/src/validation.mjs +14 -5
- package/src/webhook.mjs +1 -71
- package/src/worker.mjs +23 -4
- package/src/language-spec.mjs +0 -58
package/src/create.mjs
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
function ensureDir(path) {
|
|
5
5
|
if (!existsSync(path)) mkdirSync(path, { recursive: true });
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export async function createApp(target = "app") {
|
|
8
|
+
export async function createApp(target = "app", { template = "default" } = {}) {
|
|
9
9
|
const root = process.cwd();
|
|
10
10
|
const appRoot = join(root, target);
|
|
11
11
|
const pagesRoot = join(appRoot, "pages");
|
|
12
|
+
const templateDir = join(root, "examples", template, "app");
|
|
13
|
+
|
|
14
|
+
if (template !== "default" && existsSync(templateDir)) {
|
|
15
|
+
ensureDir(appRoot);
|
|
16
|
+
cpSync(templateDir, appRoot, { recursive: true });
|
|
17
|
+
console.log(`created ${target} from template: ${template}`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
12
20
|
|
|
13
21
|
ensureDir(pagesRoot);
|
|
14
22
|
ensureDir(join(appRoot, "api"));
|
|
@@ -86,25 +94,70 @@ export async function GET(ctx) {
|
|
|
86
94
|
path: join(appRoot, "styles.css"),
|
|
87
95
|
content: `:root { color-scheme: dark; }
|
|
88
96
|
* { box-sizing: border-box; }
|
|
89
|
-
body { margin: 0; font: 16px/1.6 ui-sans-serif, system-ui; background:
|
|
90
|
-
.nav { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 16px 24px; border-bottom: 1px solid
|
|
91
|
-
.nav a { color:
|
|
97
|
+
body { margin: 0; font: 16px/1.6 ui-sans-serif, system-ui; background: var(--fs-color-bg); color: var(--fs-color-text); }
|
|
98
|
+
.nav { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 16px 24px; border-bottom: 1px solid var(--fs-color-border); }
|
|
99
|
+
.nav a { color: var(--fs-color-accentSoft); text-decoration: none; margin-right: 12px; }
|
|
92
100
|
.page { max-width: 980px; margin: 0 auto; padding: 40px 24px; }
|
|
93
101
|
.hero h1 { font-size: clamp(2rem, 6vw, 4rem); line-height: 1.05; margin: 0 0 10px; }
|
|
94
|
-
.eyebrow { color:
|
|
95
|
-
.hero button { padding: 8px 12px; border: 1px solid
|
|
96
|
-
.footer { border-top: 1px solid
|
|
102
|
+
.eyebrow { color: var(--fs-color-accent); font-size: 12px; text-transform: uppercase; letter-spacing: .12em; }
|
|
103
|
+
.hero button { padding: 8px 12px; border: 1px solid var(--fs-color-border); background: var(--fs-color-surface); color: var(--fs-color-text); border-radius: 8px; cursor: pointer; }
|
|
104
|
+
.footer { border-top: 1px solid var(--fs-color-border); padding: 24px; color: var(--fs-color-muted); }
|
|
105
|
+
`,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
path: join(appRoot, "design", "tokens.json"),
|
|
109
|
+
content: `{
|
|
110
|
+
"color": {
|
|
111
|
+
"bg": "#050505",
|
|
112
|
+
"surface": "#090909",
|
|
113
|
+
"text": "#ffffff",
|
|
114
|
+
"muted": "#8a8a8a",
|
|
115
|
+
"border": "#1f1f1f",
|
|
116
|
+
"accent": "#9f92ff",
|
|
117
|
+
"accentSoft": "#d3d3ff"
|
|
118
|
+
},
|
|
119
|
+
"space": {
|
|
120
|
+
"1": "4px",
|
|
121
|
+
"2": "8px",
|
|
122
|
+
"3": "12px",
|
|
123
|
+
"4": "16px",
|
|
124
|
+
"5": "20px",
|
|
125
|
+
"6": "24px",
|
|
126
|
+
"8": "32px",
|
|
127
|
+
"10": "40px",
|
|
128
|
+
"12": "48px"
|
|
129
|
+
},
|
|
130
|
+
"radius": {
|
|
131
|
+
"sm": "8px",
|
|
132
|
+
"md": "12px",
|
|
133
|
+
"lg": "16px"
|
|
134
|
+
},
|
|
135
|
+
"shadow": {
|
|
136
|
+
"soft": "0 10px 40px rgba(0,0,0,0.22)"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
`,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
path: join(appRoot, "design", "class-allowlist.json"),
|
|
143
|
+
content: `[
|
|
144
|
+
"nav",
|
|
145
|
+
"page",
|
|
146
|
+
"footer",
|
|
147
|
+
"hero",
|
|
148
|
+
"eyebrow"
|
|
149
|
+
]
|
|
97
150
|
`,
|
|
98
151
|
},
|
|
99
152
|
{
|
|
100
|
-
path: join(appRoot, "api", "hello.
|
|
153
|
+
path: join(appRoot, "api", "hello.js"),
|
|
101
154
|
content: `export async function GET() {
|
|
102
155
|
return { status: 200, json: { ok: true, message: "Hello from FastScript API" } };
|
|
103
156
|
}
|
|
104
157
|
`,
|
|
105
158
|
},
|
|
106
159
|
{
|
|
107
|
-
path: join(appRoot, "api", "auth.
|
|
160
|
+
path: join(appRoot, "api", "auth.js"),
|
|
108
161
|
content: `export const schemas = {
|
|
109
162
|
POST: { name: "string?" }
|
|
110
163
|
};
|
|
@@ -123,20 +176,21 @@ export async function DELETE(ctx) {
|
|
|
123
176
|
`,
|
|
124
177
|
},
|
|
125
178
|
{
|
|
126
|
-
path: join(appRoot, "api", "upload.
|
|
179
|
+
path: join(appRoot, "api", "upload.js"),
|
|
127
180
|
content: `export const schemas = {
|
|
128
|
-
POST: { key: "string", content: "string" }
|
|
181
|
+
POST: { key: "string", content: "string", acl: "string?" }
|
|
129
182
|
};
|
|
130
183
|
|
|
131
184
|
export async function POST(ctx) {
|
|
132
185
|
const body = await ctx.input.validateBody(schemas.POST);
|
|
133
|
-
const put = ctx.storage.put(body.key, Buffer.from(body.content, "utf8"));
|
|
134
|
-
|
|
186
|
+
const put = ctx.storage.put(body.key, Buffer.from(body.content, "utf8"), { acl: body.acl || "public" });
|
|
187
|
+
const signedUrl = ctx.storage.signedUrl ? ctx.storage.signedUrl(body.key, { action: "get", expiresInSec: 900 }) : null;
|
|
188
|
+
return ctx.helpers.json({ ok: true, ...put, url: ctx.storage.url(body.key), signedUrl });
|
|
135
189
|
}
|
|
136
190
|
`,
|
|
137
191
|
},
|
|
138
192
|
{
|
|
139
|
-
path: join(appRoot, "api", "webhook.
|
|
193
|
+
path: join(appRoot, "api", "webhook.js"),
|
|
140
194
|
content: `import { verifyWebhookRequest } from "../../src/webhook.mjs";
|
|
141
195
|
|
|
142
196
|
export async function POST(ctx) {
|
|
@@ -161,7 +215,7 @@ export async function POST(ctx) {
|
|
|
161
215
|
`,
|
|
162
216
|
},
|
|
163
217
|
{
|
|
164
|
-
path: join(appRoot, "db", "migrations", "001_init.
|
|
218
|
+
path: join(appRoot, "db", "migrations", "001_init.js"),
|
|
165
219
|
content: `export async function up(db) {
|
|
166
220
|
const users = db.collection("users");
|
|
167
221
|
if (!users.get("u_1")) {
|
|
@@ -171,7 +225,7 @@ export async function POST(ctx) {
|
|
|
171
225
|
`,
|
|
172
226
|
},
|
|
173
227
|
{
|
|
174
|
-
path: join(appRoot, "db", "seed.
|
|
228
|
+
path: join(appRoot, "db", "seed.js"),
|
|
175
229
|
content: `export async function seed(db) {
|
|
176
230
|
db.transaction((tx) => {
|
|
177
231
|
tx.collection("posts").set("hello", { id: "hello", title: "First Post", published: true });
|
package/src/csp.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const BASE_DIRECTIVES = {
|
|
2
|
+
"default-src": ["'self'"],
|
|
3
|
+
"img-src": ["'self'", "data:", "https:"],
|
|
4
|
+
"style-src": ["'self'", "'unsafe-inline'"],
|
|
5
|
+
"font-src": ["'self'", "data:", "https:"],
|
|
6
|
+
"connect-src": ["'self'"],
|
|
7
|
+
"script-src": ["'self'"],
|
|
8
|
+
"frame-ancestors": ["'none'"],
|
|
9
|
+
"base-uri": ["'self'"],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function generateCspPolicy({ target = "node", mode = process.env.NODE_ENV || "development", nonce = "" } = {}) {
|
|
13
|
+
const directives = structuredClone(BASE_DIRECTIVES);
|
|
14
|
+
if (target === "vercel" || target === "cloudflare") {
|
|
15
|
+
directives["connect-src"].push("https://*.vercel.app", "https://*.workers.dev");
|
|
16
|
+
}
|
|
17
|
+
if (mode !== "production") {
|
|
18
|
+
directives["connect-src"].push("ws:", "wss:");
|
|
19
|
+
directives["script-src"].push("'unsafe-eval'");
|
|
20
|
+
}
|
|
21
|
+
if (nonce) directives["script-src"].push(`'nonce-${nonce}'`);
|
|
22
|
+
|
|
23
|
+
return Object.entries(directives)
|
|
24
|
+
.map(([key, values]) => `${key} ${[...new Set(values)].join(" ")}`)
|
|
25
|
+
.join("; ");
|
|
26
|
+
}
|
package/src/db-cli.mjs
CHANGED
|
@@ -1,41 +1,185 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { createFileDatabase } from "./db.mjs";
|
|
4
|
+
import { createPostgresCollectionDatabase } from "./db-postgres-collection.mjs";
|
|
4
5
|
import { importSourceModule } from "./module-loader.mjs";
|
|
5
6
|
|
|
7
|
+
const FASTSCRIPT_DIR = resolve(".fastscript");
|
|
6
8
|
const MIGRATIONS_DIR = resolve("app/db/migrations");
|
|
9
|
+
const MIGRATION_LEDGER = join(FASTSCRIPT_DIR, "migrations.json");
|
|
7
10
|
const SEED_FILES = [resolve("app/db/seed.fs"), resolve("app/db/seed.js"), resolve("app/db/seed.mjs"), resolve("app/db/seed.cjs")];
|
|
8
11
|
|
|
12
|
+
function readLedgerFile() {
|
|
13
|
+
if (!existsSync(MIGRATION_LEDGER)) return { applied: [] };
|
|
14
|
+
try {
|
|
15
|
+
const raw = JSON.parse(readFileSync(MIGRATION_LEDGER, "utf8"));
|
|
16
|
+
return { applied: Array.isArray(raw.applied) ? raw.applied : [] };
|
|
17
|
+
} catch {
|
|
18
|
+
return { applied: [] };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeLedgerFile(applied) {
|
|
23
|
+
mkdirSync(FASTSCRIPT_DIR, { recursive: true });
|
|
24
|
+
writeFileSync(MIGRATION_LEDGER, JSON.stringify({ applied: [...new Set(applied)].sort() }, null, 2), "utf8");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function createLedgerAdapter(driver = (process.env.DB_DRIVER || "file").toLowerCase()) {
|
|
28
|
+
if (driver !== "postgres") {
|
|
29
|
+
return {
|
|
30
|
+
type: "file",
|
|
31
|
+
async listApplied() {
|
|
32
|
+
return readLedgerFile().applied;
|
|
33
|
+
},
|
|
34
|
+
async markApplied(id) {
|
|
35
|
+
const current = new Set(readLedgerFile().applied);
|
|
36
|
+
current.add(id);
|
|
37
|
+
writeLedgerFile([...current]);
|
|
38
|
+
},
|
|
39
|
+
async markRolledBack(id) {
|
|
40
|
+
const current = new Set(readLedgerFile().applied);
|
|
41
|
+
current.delete(id);
|
|
42
|
+
writeLedgerFile([...current]);
|
|
43
|
+
},
|
|
44
|
+
async close() {},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const { Client } = await import("pg");
|
|
50
|
+
const client = new Client({ connectionString: process.env.DATABASE_URL });
|
|
51
|
+
await client.connect();
|
|
52
|
+
await client.query(`
|
|
53
|
+
CREATE TABLE IF NOT EXISTS fs_migrations_ledger (
|
|
54
|
+
id text PRIMARY KEY,
|
|
55
|
+
applied_at timestamptz NOT NULL DEFAULT now()
|
|
56
|
+
);
|
|
57
|
+
`);
|
|
58
|
+
return {
|
|
59
|
+
type: "postgres",
|
|
60
|
+
async listApplied() {
|
|
61
|
+
const rows = (await client.query("SELECT id FROM fs_migrations_ledger ORDER BY applied_at ASC")).rows;
|
|
62
|
+
return rows.map((row) => row.id);
|
|
63
|
+
},
|
|
64
|
+
async markApplied(id) {
|
|
65
|
+
await client.query(
|
|
66
|
+
"INSERT INTO fs_migrations_ledger(id) VALUES($1) ON CONFLICT(id) DO NOTHING",
|
|
67
|
+
[id],
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
async markRolledBack(id) {
|
|
71
|
+
await client.query("DELETE FROM fs_migrations_ledger WHERE id=$1", [id]);
|
|
72
|
+
},
|
|
73
|
+
async close() {
|
|
74
|
+
await client.end();
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
} catch {
|
|
78
|
+
return createLedgerAdapter("file");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function createMigrationDatabase(driver = (process.env.DB_DRIVER || "file").toLowerCase()) {
|
|
83
|
+
if (driver !== "postgres") return createFileDatabase({ dir: ".fastscript", name: "appdb" });
|
|
84
|
+
try {
|
|
85
|
+
return await createPostgresCollectionDatabase({ connectionString: process.env.DATABASE_URL });
|
|
86
|
+
} catch {
|
|
87
|
+
return createFileDatabase({ dir: ".fastscript", name: "appdb" });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function migrationFiles() {
|
|
92
|
+
if (!existsSync(MIGRATIONS_DIR)) return [];
|
|
93
|
+
return readdirSync(MIGRATIONS_DIR).filter((file) => /\.(fs|js|mjs|cjs)$/.test(file)).sort();
|
|
94
|
+
}
|
|
95
|
+
|
|
9
96
|
export async function runDbMigrate() {
|
|
10
|
-
const
|
|
97
|
+
const driver = (process.env.DB_DRIVER || "file").toLowerCase();
|
|
98
|
+
const db = await createMigrationDatabase(driver);
|
|
99
|
+
const ledger = await createLedgerAdapter(driver);
|
|
11
100
|
if (!existsSync(MIGRATIONS_DIR)) {
|
|
12
101
|
console.log("db migrate: no app/db/migrations directory");
|
|
13
102
|
return;
|
|
14
103
|
}
|
|
15
|
-
|
|
104
|
+
|
|
105
|
+
const done = new Set(await ledger.listApplied());
|
|
106
|
+
const files = migrationFiles();
|
|
16
107
|
let count = 0;
|
|
108
|
+
|
|
17
109
|
for (const file of files) {
|
|
110
|
+
if (done.has(file)) {
|
|
111
|
+
console.log(`db migrate: skipped ${file} (already applied)`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
18
114
|
const mod = await importSourceModule(join(MIGRATIONS_DIR, file), { platform: "node" });
|
|
19
115
|
const fn = mod.up ?? mod.default;
|
|
20
|
-
if (typeof fn
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
console.log(`db migrate: applied ${file}`);
|
|
116
|
+
if (typeof fn !== "function") {
|
|
117
|
+
console.log(`db migrate: skipped ${file} (missing up/default export)`);
|
|
118
|
+
continue;
|
|
24
119
|
}
|
|
120
|
+
await fn(db);
|
|
121
|
+
await ledger.markApplied(file);
|
|
122
|
+
done.add(file);
|
|
123
|
+
count += 1;
|
|
124
|
+
console.log(`db migrate: applied ${file}`);
|
|
25
125
|
}
|
|
126
|
+
|
|
127
|
+
if (db?.flush) await db.flush();
|
|
128
|
+
if (db?.close) await db.close();
|
|
129
|
+
await ledger.close();
|
|
130
|
+
|
|
26
131
|
console.log(`db migrate complete: ${count} migration(s)`);
|
|
27
132
|
}
|
|
28
133
|
|
|
134
|
+
export async function runDbRollback(args = []) {
|
|
135
|
+
const driver = (process.env.DB_DRIVER || "file").toLowerCase();
|
|
136
|
+
const db = await createMigrationDatabase(driver);
|
|
137
|
+
const ledger = await createLedgerAdapter(driver);
|
|
138
|
+
const countFlag = args.indexOf("--count");
|
|
139
|
+
const count = Math.max(1, Number(countFlag >= 0 ? args[countFlag + 1] || 1 : 1));
|
|
140
|
+
const applied = await ledger.listApplied();
|
|
141
|
+
const target = applied.slice(-count).reverse();
|
|
142
|
+
|
|
143
|
+
if (!target.length) {
|
|
144
|
+
console.log("db rollback: nothing to rollback");
|
|
145
|
+
if (db?.close) await db.close();
|
|
146
|
+
await ledger.close();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let rolledBack = 0;
|
|
151
|
+
for (const id of target) {
|
|
152
|
+
const mod = await importSourceModule(join(MIGRATIONS_DIR, id), { platform: "node" });
|
|
153
|
+
const fn = mod.down;
|
|
154
|
+
if (typeof fn !== "function") {
|
|
155
|
+
throw new Error(`db rollback: migration ${id} does not export down(db)`);
|
|
156
|
+
}
|
|
157
|
+
await fn(db);
|
|
158
|
+
await ledger.markRolledBack(id);
|
|
159
|
+
rolledBack += 1;
|
|
160
|
+
console.log(`db rollback: reverted ${id}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (db?.flush) await db.flush();
|
|
164
|
+
if (db?.close) await db.close();
|
|
165
|
+
await ledger.close();
|
|
166
|
+
console.log(`db rollback complete: ${rolledBack} migration(s)`);
|
|
167
|
+
}
|
|
168
|
+
|
|
29
169
|
export async function runDbSeed() {
|
|
30
|
-
const
|
|
170
|
+
const driver = (process.env.DB_DRIVER || "file").toLowerCase();
|
|
171
|
+
const db = await createMigrationDatabase(driver);
|
|
31
172
|
const seedFile = SEED_FILES.find((p) => existsSync(p));
|
|
32
173
|
if (!seedFile) {
|
|
33
174
|
console.log("db seed: no app/db/seed file");
|
|
175
|
+
if (db?.close) await db.close();
|
|
34
176
|
return;
|
|
35
177
|
}
|
|
36
178
|
const mod = await importSourceModule(seedFile, { platform: "node" });
|
|
37
179
|
const fn = mod.seed ?? mod.default;
|
|
38
180
|
if (typeof fn !== "function") throw new Error("app/db/seed must export seed(db) or default(db)");
|
|
39
181
|
await fn(db);
|
|
182
|
+
if (db?.flush) await db.flush();
|
|
183
|
+
if (db?.close) await db.close();
|
|
40
184
|
console.log("db seed complete");
|
|
41
185
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export async function createPostgresCollectionDatabase({ connectionString = process.env.DATABASE_URL } = {}) {
|
|
2
|
+
const { Client } = await import("pg");
|
|
3
|
+
const client = new Client({ connectionString });
|
|
4
|
+
await client.connect();
|
|
5
|
+
await client.query(`
|
|
6
|
+
CREATE TABLE IF NOT EXISTS fs_records (
|
|
7
|
+
collection text NOT NULL,
|
|
8
|
+
id text NOT NULL,
|
|
9
|
+
data jsonb NOT NULL,
|
|
10
|
+
PRIMARY KEY(collection, id)
|
|
11
|
+
);
|
|
12
|
+
`);
|
|
13
|
+
|
|
14
|
+
const state = { collections: {} };
|
|
15
|
+
const rows = (await client.query("SELECT collection, id, data FROM fs_records")).rows;
|
|
16
|
+
for (const row of rows) {
|
|
17
|
+
if (!state.collections[row.collection]) state.collections[row.collection] = {};
|
|
18
|
+
state.collections[row.collection][row.id] = row.data;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const pending = new Set();
|
|
22
|
+
function enqueue(op) {
|
|
23
|
+
const p = Promise.resolve().then(op).catch(() => {}).finally(() => pending.delete(p));
|
|
24
|
+
pending.add(p);
|
|
25
|
+
return p;
|
|
26
|
+
}
|
|
27
|
+
function ensureCollection(collection) {
|
|
28
|
+
if (!state.collections[collection]) state.collections[collection] = {};
|
|
29
|
+
return state.collections[collection];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const db = {
|
|
33
|
+
collection(name) {
|
|
34
|
+
return {
|
|
35
|
+
get(id) {
|
|
36
|
+
const col = ensureCollection(name);
|
|
37
|
+
return col[id] ?? null;
|
|
38
|
+
},
|
|
39
|
+
set(id, value) {
|
|
40
|
+
const col = ensureCollection(name);
|
|
41
|
+
col[id] = value;
|
|
42
|
+
enqueue(() => client.query(
|
|
43
|
+
"INSERT INTO fs_records(collection, id, data) VALUES($1,$2,$3::jsonb) ON CONFLICT(collection,id) DO UPDATE SET data=excluded.data",
|
|
44
|
+
[name, id, JSON.stringify(value)],
|
|
45
|
+
));
|
|
46
|
+
return col[id];
|
|
47
|
+
},
|
|
48
|
+
delete(id) {
|
|
49
|
+
const col = ensureCollection(name);
|
|
50
|
+
delete col[id];
|
|
51
|
+
enqueue(() => client.query("DELETE FROM fs_records WHERE collection=$1 AND id=$2", [name, id]));
|
|
52
|
+
},
|
|
53
|
+
all() {
|
|
54
|
+
const col = ensureCollection(name);
|
|
55
|
+
return Object.values(col);
|
|
56
|
+
},
|
|
57
|
+
upsert(id, updater) {
|
|
58
|
+
const col = ensureCollection(name);
|
|
59
|
+
const prev = col[id] ?? null;
|
|
60
|
+
const next = typeof updater === "function" ? updater(prev) : updater;
|
|
61
|
+
col[id] = next;
|
|
62
|
+
enqueue(() => client.query(
|
|
63
|
+
"INSERT INTO fs_records(collection, id, data) VALUES($1,$2,$3::jsonb) ON CONFLICT(collection,id) DO UPDATE SET data=excluded.data",
|
|
64
|
+
[name, id, JSON.stringify(next)],
|
|
65
|
+
));
|
|
66
|
+
return next;
|
|
67
|
+
},
|
|
68
|
+
first(predicate) {
|
|
69
|
+
const col = ensureCollection(name);
|
|
70
|
+
return Object.values(col).find(predicate) ?? null;
|
|
71
|
+
},
|
|
72
|
+
where(filters) {
|
|
73
|
+
const col = ensureCollection(name);
|
|
74
|
+
if (typeof filters === "function") return Object.values(col).filter(filters);
|
|
75
|
+
return Object.values(col).filter((row) =>
|
|
76
|
+
Object.entries(filters || {}).every(([k, v]) => row?.[k] === v),
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
transaction(fn) {
|
|
82
|
+
// In-memory atomic mutation, writes are enqueued asynchronously.
|
|
83
|
+
return fn(db);
|
|
84
|
+
},
|
|
85
|
+
query(collection, predicate) {
|
|
86
|
+
const col = ensureCollection(collection);
|
|
87
|
+
return Object.values(col).filter(predicate);
|
|
88
|
+
},
|
|
89
|
+
first(collection, predicate) {
|
|
90
|
+
const col = ensureCollection(collection);
|
|
91
|
+
return Object.values(col).find(predicate) ?? null;
|
|
92
|
+
},
|
|
93
|
+
where(collection, filters) {
|
|
94
|
+
const col = ensureCollection(collection);
|
|
95
|
+
if (typeof filters === "function") return Object.values(col).filter(filters);
|
|
96
|
+
return Object.values(col).filter((row) =>
|
|
97
|
+
Object.entries(filters || {}).every(([k, v]) => row?.[k] === v),
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
async flush() {
|
|
101
|
+
await Promise.all([...pending]);
|
|
102
|
+
},
|
|
103
|
+
async close() {
|
|
104
|
+
await Promise.all([...pending]);
|
|
105
|
+
await client.end();
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return db;
|
|
110
|
+
}
|
package/src/deploy.mjs
CHANGED
|
@@ -1,65 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { resolve, join } from "node:path";
|
|
3
|
-
|
|
4
|
-
export async function runDeploy(args = []) {
|
|
5
|
-
let target = "node";
|
|
6
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
7
|
-
if (args[i] === "--target") target = (args[i + 1] || "node").toLowerCase();
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const root = resolve(process.cwd());
|
|
11
|
-
|
|
12
|
-
if (target === "node" || target === "pm2") {
|
|
13
|
-
const file = join(root, "ecosystem.config.cjs");
|
|
14
|
-
writeFileSync(
|
|
15
|
-
file,
|
|
16
|
-
`module.exports = { apps: [{ name: "fastscript-app", script: "node", args: "./src/cli.mjs start", env: { NODE_ENV: "production", PORT: 4173 } }] };\n`,
|
|
17
|
-
"utf8",
|
|
18
|
-
);
|
|
19
|
-
writeFileSync(
|
|
20
|
-
join(root, "Dockerfile"),
|
|
21
|
-
`FROM node:20-alpine\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci --omit=dev\nCOPY . .\nRUN npm run build\nENV NODE_ENV=production\nEXPOSE 4173\nCMD [\"node\",\"./src/cli.mjs\",\"start\"]\n`,
|
|
22
|
-
"utf8",
|
|
23
|
-
);
|
|
24
|
-
console.log("deploy adapter ready: ecosystem.config.cjs (PM2/Node)");
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (target === "vercel") {
|
|
29
|
-
const file = join(root, "vercel.json");
|
|
30
|
-
writeFileSync(
|
|
31
|
-
file,
|
|
32
|
-
JSON.stringify(
|
|
33
|
-
{
|
|
34
|
-
version: 2,
|
|
35
|
-
builds: [{ src: "package.json", use: "@vercel/node" }],
|
|
36
|
-
routes: [{ src: "/(.*)", dest: "/src/cli.mjs" }],
|
|
37
|
-
},
|
|
38
|
-
null,
|
|
39
|
-
2,
|
|
40
|
-
),
|
|
41
|
-
"utf8",
|
|
42
|
-
);
|
|
43
|
-
console.log("deploy adapter ready: vercel.json");
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (target === "cloudflare") {
|
|
48
|
-
const wrangler = join(root, "wrangler.toml");
|
|
49
|
-
writeFileSync(
|
|
50
|
-
wrangler,
|
|
51
|
-
`name = "fastscript-app"\nmain = "dist/worker.js"\ncompatibility_date = "2026-01-01"\n[assets]\ndirectory = "dist"\n`,
|
|
52
|
-
"utf8",
|
|
53
|
-
);
|
|
54
|
-
mkdirSync(join(root, "dist"), { recursive: true });
|
|
55
|
-
writeFileSync(
|
|
56
|
-
join(root, "dist", "worker.js"),
|
|
57
|
-
`export default { async fetch(req, env) { return env.ASSETS.fetch(req); } };\n`,
|
|
58
|
-
"utf8",
|
|
59
|
-
);
|
|
60
|
-
console.log("deploy adapter ready: wrangler.toml + dist/worker.js");
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
throw new Error(`Unknown deploy target: ${target}. Use node|pm2|vercel|cloudflare`);
|
|
65
|
-
}
|
|
1
|
+
export * from "@fastscript/core-private/deploy";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
function tokenize(text) {
|
|
5
|
+
return String(text || "")
|
|
6
|
+
.toLowerCase()
|
|
7
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
8
|
+
.split(/\s+/)
|
|
9
|
+
.filter(Boolean);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function rankDocs(index = [], query = "", { limit = 20 } = {}) {
|
|
13
|
+
const q = tokenize(query);
|
|
14
|
+
if (!q.length) return index.slice(0, limit);
|
|
15
|
+
return index
|
|
16
|
+
.map((item) => {
|
|
17
|
+
const terms = item.terms || {};
|
|
18
|
+
let score = 0;
|
|
19
|
+
for (const token of q) score += Number(terms[token] || 0);
|
|
20
|
+
return { ...item, score };
|
|
21
|
+
})
|
|
22
|
+
.filter((item) => item.score > 0)
|
|
23
|
+
.sort((a, b) => b.score - a.score || a.title.localeCompare(b.title))
|
|
24
|
+
.slice(0, limit);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function loadDocsIndex(path = "docs/search-index.json") {
|
|
28
|
+
const full = resolve(path);
|
|
29
|
+
if (!existsSync(full)) return [];
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(full, "utf8"));
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/env.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { importSourceModule } from "./module-loader.mjs";
|
|
4
5
|
|
|
5
6
|
function parseDotEnv(raw) {
|
|
6
7
|
const out = {};
|
|
@@ -18,15 +19,28 @@ function parseDotEnv(raw) {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export function loadEnv({ root = process.cwd(), mode = process.env.NODE_ENV || "development" } = {}) {
|
|
21
|
-
const
|
|
22
|
+
const profile = process.env.FASTSCRIPT_PROFILE || mode;
|
|
23
|
+
const files = [
|
|
24
|
+
".env",
|
|
25
|
+
`.env.${mode}`,
|
|
26
|
+
profile !== mode ? `.env.${profile}` : null,
|
|
27
|
+
mode === "production" ? ".env.local" : null,
|
|
28
|
+
].filter(Boolean);
|
|
22
29
|
const merged = {};
|
|
30
|
+
const configPath = join(root, "fastscript.config.json");
|
|
31
|
+
if (existsSync(configPath)) {
|
|
32
|
+
try {
|
|
33
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
34
|
+
Object.assign(merged, config?.profiles?.[profile] || {});
|
|
35
|
+
} catch {}
|
|
36
|
+
}
|
|
23
37
|
for (const file of files) {
|
|
24
38
|
const path = join(root, file);
|
|
25
39
|
if (!existsSync(path)) continue;
|
|
26
40
|
Object.assign(merged, parseDotEnv(readFileSync(path, "utf8")));
|
|
27
41
|
}
|
|
28
42
|
for (const [k, v] of Object.entries(merged)) if (process.env[k] === undefined) process.env[k] = v;
|
|
29
|
-
return { mode, values: merged };
|
|
43
|
+
return { mode, profile, values: merged };
|
|
30
44
|
}
|
|
31
45
|
|
|
32
46
|
export function validateEnv(schema = {}, env = process.env) {
|
|
@@ -68,9 +82,13 @@ export function validateEnv(schema = {}, env = process.env) {
|
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
export async function validateAppEnv({ root = process.cwd() } = {}) {
|
|
71
|
-
const
|
|
85
|
+
const fsSchema = join(root, "app", "env.schema.fs");
|
|
86
|
+
const jsSchema = join(root, "app", "env.schema.js");
|
|
87
|
+
const schemaPath = existsSync(fsSchema) ? fsSchema : jsSchema;
|
|
72
88
|
if (!existsSync(schemaPath)) return null;
|
|
73
|
-
const mod =
|
|
89
|
+
const mod = schemaPath.endsWith('.fs')
|
|
90
|
+
? await importSourceModule(schemaPath, { platform: "node" })
|
|
91
|
+
: await import(`${pathToFileURL(schemaPath).href}?t=${Date.now()}`);
|
|
74
92
|
const schema = mod.schema || mod.default || {};
|
|
75
93
|
return validateEnv(schema, process.env);
|
|
76
94
|
}
|
|
@@ -86,4 +104,15 @@ export function appendEnvIfMissing(key, value) {
|
|
|
86
104
|
const current = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
87
105
|
if (current.includes(`${key}=`)) return;
|
|
88
106
|
appendFileSync(path, `${key}=${value}\n`, "utf8");
|
|
89
|
-
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function listConfigProfiles({ root = process.cwd() } = {}) {
|
|
110
|
+
const path = join(root, "fastscript.config.json");
|
|
111
|
+
if (!existsSync(path)) return [];
|
|
112
|
+
try {
|
|
113
|
+
const config = JSON.parse(readFileSync(path, "utf8"));
|
|
114
|
+
return Object.keys(config.profiles || {});
|
|
115
|
+
} catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|