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.
Files changed (109) hide show
  1. package/CHANGELOG.md +32 -7
  2. package/LICENSE +33 -21
  3. package/README.md +567 -73
  4. package/node_modules/@fastscript/core-private/BOUNDARY.json +15 -0
  5. package/node_modules/@fastscript/core-private/README.md +5 -0
  6. package/node_modules/@fastscript/core-private/package.json +34 -0
  7. package/node_modules/@fastscript/core-private/src/asset-optimizer.mjs +67 -0
  8. package/node_modules/@fastscript/core-private/src/audit-log.mjs +50 -0
  9. package/node_modules/@fastscript/core-private/src/auth-flows.mjs +29 -0
  10. package/node_modules/@fastscript/core-private/src/auth.mjs +115 -0
  11. package/node_modules/@fastscript/core-private/src/bench.mjs +45 -0
  12. package/node_modules/@fastscript/core-private/src/build.mjs +670 -0
  13. package/node_modules/@fastscript/core-private/src/cache.mjs +248 -0
  14. package/node_modules/@fastscript/core-private/src/check.mjs +22 -0
  15. package/node_modules/@fastscript/core-private/src/cli.mjs +95 -0
  16. package/node_modules/@fastscript/core-private/src/compat.mjs +128 -0
  17. package/node_modules/@fastscript/core-private/src/create.mjs +278 -0
  18. package/node_modules/@fastscript/core-private/src/csp.mjs +26 -0
  19. package/node_modules/@fastscript/core-private/src/db-cli.mjs +185 -0
  20. package/node_modules/@fastscript/core-private/src/db-postgres-collection.mjs +110 -0
  21. package/node_modules/@fastscript/core-private/src/db-postgres.mjs +40 -0
  22. package/node_modules/@fastscript/core-private/src/db.mjs +103 -0
  23. package/node_modules/@fastscript/core-private/src/deploy.mjs +662 -0
  24. package/node_modules/@fastscript/core-private/src/dev.mjs +5 -0
  25. package/node_modules/@fastscript/core-private/src/docs-search.mjs +35 -0
  26. package/node_modules/@fastscript/core-private/src/env.mjs +118 -0
  27. package/node_modules/@fastscript/core-private/src/export.mjs +83 -0
  28. package/node_modules/@fastscript/core-private/src/fs-diagnostics.mjs +70 -0
  29. package/node_modules/@fastscript/core-private/src/fs-error-codes.mjs +141 -0
  30. package/node_modules/@fastscript/core-private/src/fs-formatter.mjs +66 -0
  31. package/node_modules/@fastscript/core-private/src/fs-linter.mjs +274 -0
  32. package/node_modules/@fastscript/core-private/src/fs-normalize.mjs +91 -0
  33. package/node_modules/@fastscript/core-private/src/fs-parser.mjs +980 -0
  34. package/node_modules/@fastscript/core-private/src/generated/docs-search-index.mjs +3182 -0
  35. package/node_modules/@fastscript/core-private/src/i18n.mjs +25 -0
  36. package/node_modules/@fastscript/core-private/src/interop.mjs +16 -0
  37. package/node_modules/@fastscript/core-private/src/jobs.mjs +378 -0
  38. package/node_modules/@fastscript/core-private/src/logger.mjs +27 -0
  39. package/node_modules/@fastscript/core-private/src/metrics.mjs +45 -0
  40. package/node_modules/@fastscript/core-private/src/middleware.mjs +14 -0
  41. package/node_modules/@fastscript/core-private/src/migrate.mjs +81 -0
  42. package/node_modules/@fastscript/core-private/src/migration-wizard.mjs +16 -0
  43. package/node_modules/@fastscript/core-private/src/module-loader.mjs +46 -0
  44. package/node_modules/@fastscript/core-private/src/oauth-providers.mjs +103 -0
  45. package/node_modules/@fastscript/core-private/src/observability.mjs +21 -0
  46. package/node_modules/@fastscript/core-private/src/plugins.mjs +194 -0
  47. package/node_modules/@fastscript/core-private/src/retention.mjs +57 -0
  48. package/node_modules/@fastscript/core-private/src/routes.mjs +178 -0
  49. package/node_modules/@fastscript/core-private/src/scheduler.mjs +104 -0
  50. package/node_modules/@fastscript/core-private/src/security.mjs +233 -0
  51. package/node_modules/@fastscript/core-private/src/server-runtime.mjs +849 -0
  52. package/node_modules/@fastscript/core-private/src/serverless-handler.mjs +20 -0
  53. package/node_modules/@fastscript/core-private/src/session-policy.mjs +38 -0
  54. package/node_modules/@fastscript/core-private/src/start.mjs +10 -0
  55. package/node_modules/@fastscript/core-private/src/storage.mjs +155 -0
  56. package/node_modules/@fastscript/core-private/src/style-primitives.mjs +538 -0
  57. package/node_modules/@fastscript/core-private/src/style-system.mjs +461 -0
  58. package/node_modules/@fastscript/core-private/src/tenant.mjs +55 -0
  59. package/node_modules/@fastscript/core-private/src/typecheck.mjs +1464 -0
  60. package/node_modules/@fastscript/core-private/src/validate.mjs +22 -0
  61. package/node_modules/@fastscript/core-private/src/validation.mjs +88 -0
  62. package/node_modules/@fastscript/core-private/src/webhook.mjs +81 -0
  63. package/node_modules/@fastscript/core-private/src/worker.mjs +24 -0
  64. package/package.json +86 -13
  65. package/src/asset-optimizer.mjs +67 -0
  66. package/src/audit-log.mjs +50 -0
  67. package/src/auth.mjs +1 -115
  68. package/src/bench.mjs +20 -7
  69. package/src/build.mjs +1 -234
  70. package/src/cache.mjs +210 -20
  71. package/src/cli.mjs +29 -5
  72. package/src/compat.mjs +8 -10
  73. package/src/create.mjs +71 -17
  74. package/src/csp.mjs +26 -0
  75. package/src/db-cli.mjs +152 -8
  76. package/src/db-postgres-collection.mjs +110 -0
  77. package/src/deploy.mjs +1 -65
  78. package/src/docs-search.mjs +35 -0
  79. package/src/env.mjs +34 -5
  80. package/src/fs-diagnostics.mjs +70 -0
  81. package/src/fs-error-codes.mjs +126 -0
  82. package/src/fs-formatter.mjs +66 -0
  83. package/src/fs-linter.mjs +274 -0
  84. package/src/fs-normalize.mjs +21 -238
  85. package/src/fs-parser.mjs +1 -0
  86. package/src/generated/docs-search-index.mjs +3220 -0
  87. package/src/i18n.mjs +25 -0
  88. package/src/jobs.mjs +283 -32
  89. package/src/metrics.mjs +45 -0
  90. package/src/migration-wizard.mjs +16 -0
  91. package/src/module-loader.mjs +11 -12
  92. package/src/oauth-providers.mjs +103 -0
  93. package/src/plugins.mjs +194 -0
  94. package/src/retention.mjs +57 -0
  95. package/src/routes.mjs +178 -0
  96. package/src/scheduler.mjs +104 -0
  97. package/src/security.mjs +197 -19
  98. package/src/server-runtime.mjs +1 -339
  99. package/src/serverless-handler.mjs +20 -0
  100. package/src/session-policy.mjs +38 -0
  101. package/src/storage.mjs +1 -56
  102. package/src/style-system.mjs +461 -0
  103. package/src/tenant.mjs +55 -0
  104. package/src/typecheck.mjs +1 -0
  105. package/src/validate.mjs +5 -1
  106. package/src/validation.mjs +14 -5
  107. package/src/webhook.mjs +1 -71
  108. package/src/worker.mjs +23 -4
  109. package/src/language-spec.mjs +0 -58
package/src/create.mjs CHANGED
@@ -1,14 +1,22 @@
1
- import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
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: #050505; color: #fff; }
90
- .nav { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 16px 24px; border-bottom: 1px solid #1f1f1f; }
91
- .nav a { color: #d3d3ff; text-decoration: none; margin-right: 12px; }
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: #9f92ff; font-size: 12px; text-transform: uppercase; letter-spacing: .12em; }
95
- .hero button { padding: 8px 12px; border: 1px solid #333; background: #0f0f0f; color: #fff; border-radius: 8px; cursor: pointer; }
96
- .footer { border-top: 1px solid #1f1f1f; padding: 24px; color: #8a8a8a; }
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.fs"),
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.fs"),
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.fs"),
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
- return ctx.helpers.json({ ok: true, ...put, url: ctx.storage.url(body.key) });
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.fs"),
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.fs"),
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.fs"),
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 db = createFileDatabase({ dir: ".fastscript", name: "appdb" });
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
- const files = readdirSync(MIGRATIONS_DIR).filter((f) => /\.(fs|js|mjs|cjs)$/.test(f)).sort();
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 === "function") {
21
- await fn(db);
22
- count += 1;
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 db = createFileDatabase({ dir: ".fastscript", name: "appdb" });
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
- import { mkdirSync, writeFileSync } from "node:fs";
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 files = [".env", `.env.${mode}`, mode === "production" ? ".env.local" : null].filter(Boolean);
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 schemaPath = join(root, "app", "env.schema.js");
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 = await import(`${pathToFileURL(schemaPath).href}?t=${Date.now()}`);
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
+ }