fastscript 0.1.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/.github/workflows/ci.yml +17 -0
- package/CHANGELOG.md +5 -0
- package/Dockerfile +9 -0
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/app/api/auth.js +10 -0
- package/app/api/hello.js +3 -0
- package/app/api/upload.js +9 -0
- package/app/api/webhook.js +10 -0
- package/app/db/migrations/001_init.js +6 -0
- package/app/db/seed.js +5 -0
- package/app/env.schema.js +6 -0
- package/app/middleware.fs +7 -0
- package/app/pages/404.fs +3 -0
- package/app/pages/_layout.fs +17 -0
- package/app/pages/benchmarks.fs +15 -0
- package/app/pages/docs/index.fs +16 -0
- package/app/pages/index.fs +22 -0
- package/app/pages/private.fs +12 -0
- package/app/pages/showcase.fs +14 -0
- package/app/styles.css +14 -0
- package/docs/AI_CONTEXT_PACK_V1.md +25 -0
- package/docs/DEPLOY_GUIDE.md +14 -0
- package/docs/INCIDENT_PLAYBOOK.md +18 -0
- package/docs/INTEROP_RULES.md +7 -0
- package/docs/PLUGIN_API_CONTRACT.md +22 -0
- package/ecosystem.config.cjs +1 -0
- package/examples/fullstack/README.md +10 -0
- package/examples/fullstack/app/api/orders.js +8 -0
- package/examples/fullstack/app/db/migrations/001_init.js +3 -0
- package/examples/fullstack/app/db/seed.js +3 -0
- package/examples/fullstack/app/jobs/send-order-email.js +4 -0
- package/examples/fullstack/app/pages/_layout.fs +1 -0
- package/examples/fullstack/app/pages/index.fs +3 -0
- package/examples/startup-mvp/README.md +8 -0
- package/examples/startup-mvp/app/api/cart.js +9 -0
- package/examples/startup-mvp/app/api/checkout.js +8 -0
- package/examples/startup-mvp/app/db/migrations/001_products.js +6 -0
- package/examples/startup-mvp/app/jobs/send-receipt.js +4 -0
- package/examples/startup-mvp/app/pages/_layout.fs +3 -0
- package/examples/startup-mvp/app/pages/dashboard/index.fs +9 -0
- package/examples/startup-mvp/app/pages/index.fs +18 -0
- package/package.json +50 -0
- package/scripts/bench-report.mjs +36 -0
- package/scripts/release.mjs +21 -0
- package/scripts/smoke-dev.mjs +78 -0
- package/scripts/smoke-start.mjs +41 -0
- package/scripts/test-auth.mjs +26 -0
- package/scripts/test-db.mjs +31 -0
- package/scripts/test-jobs.mjs +15 -0
- package/scripts/test-middleware.mjs +37 -0
- package/scripts/test-roundtrip.mjs +44 -0
- package/scripts/test-validation.mjs +17 -0
- package/scripts/test-webhook-storage.mjs +22 -0
- package/spec/FASTSCRIPT_1000_BUILD_LIST.md +1090 -0
- package/src/auth-flows.mjs +29 -0
- package/src/auth.mjs +115 -0
- package/src/bench.mjs +46 -0
- package/src/build.mjs +222 -0
- package/src/cache.mjs +58 -0
- package/src/check.mjs +22 -0
- package/src/cli.mjs +71 -0
- package/src/compat.mjs +122 -0
- package/src/create.mjs +190 -0
- package/src/db-cli.mjs +45 -0
- package/src/db-postgres.mjs +40 -0
- package/src/db.mjs +103 -0
- package/src/deploy.mjs +65 -0
- package/src/dev.mjs +5 -0
- package/src/env.mjs +89 -0
- package/src/export.mjs +83 -0
- package/src/fs-normalize.mjs +100 -0
- package/src/interop.mjs +16 -0
- package/src/jobs.mjs +127 -0
- package/src/logger.mjs +27 -0
- package/src/middleware.mjs +14 -0
- package/src/migrate.mjs +81 -0
- package/src/observability.mjs +21 -0
- package/src/security.mjs +55 -0
- package/src/server-runtime.mjs +339 -0
- package/src/start.mjs +10 -0
- package/src/storage.mjs +56 -0
- package/src/validate.mjs +18 -0
- package/src/validation.mjs +79 -0
- package/src/webhook.mjs +71 -0
- package/src/worker.mjs +5 -0
- package/vercel.json +15 -0
- package/vscode/fastscript-language/README.md +12 -0
- package/vscode/fastscript-language/extension.js +24 -0
- package/vscode/fastscript-language/language-configuration.json +6 -0
- package/vscode/fastscript-language/lsp/server.cjs +27 -0
- package/vscode/fastscript-language/lsp/smoke-test.cjs +1 -0
- package/vscode/fastscript-language/package.json +36 -0
- package/vscode/fastscript-language/snippets/fastscript.code-snippets +24 -0
- package/vscode/fastscript-language/syntaxes/fastscript.tmLanguage.json +21 -0
- package/wrangler.toml +5 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { gzipSync } from "node:zlib";
|
|
4
|
+
import { performance } from "node:perf_hooks";
|
|
5
|
+
import { runBuild } from "../src/build.mjs";
|
|
6
|
+
|
|
7
|
+
function gzipSize(path) {
|
|
8
|
+
if (!existsSync(path)) return 0;
|
|
9
|
+
return gzipSync(readFileSync(path), { level: 9 }).byteLength;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function kb(n) {
|
|
13
|
+
return (n / 1024).toFixed(2);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const t0 = performance.now();
|
|
17
|
+
await runBuild();
|
|
18
|
+
const t1 = performance.now();
|
|
19
|
+
|
|
20
|
+
const dist = resolve('dist');
|
|
21
|
+
const manifest = JSON.parse(readFileSync(join(dist, 'fastscript-manifest.json'), 'utf8'));
|
|
22
|
+
const jsAssets = [join(dist, 'router.js')];
|
|
23
|
+
if (manifest.layout) jsAssets.push(join(dist, manifest.layout.replace(/^\.\//, '')));
|
|
24
|
+
const home = manifest.routes.find((r) => r.path === '/');
|
|
25
|
+
if (home?.module) jsAssets.push(join(dist, home.module.replace(/^\.\//, '')));
|
|
26
|
+
const cssAssets = [join(dist, 'styles.css')];
|
|
27
|
+
|
|
28
|
+
const js = jsAssets.reduce((s, p) => s + gzipSize(p), 0);
|
|
29
|
+
const css = cssAssets.reduce((s, p) => s + gzipSize(p), 0);
|
|
30
|
+
|
|
31
|
+
const md = `# FastScript Benchmark Report\n\n- Build time: ${(t1 - t0).toFixed(2)}ms\n- Routes: ${manifest.routes.length}\n- API routes: ${manifest.apiRoutes?.length ?? 0}\n- JS first-load gzip: ${kb(js)}KB\n- CSS first-load gzip: ${kb(css)}KB\n\n## Budgets\n- JS budget (30KB): ${js <= 30 * 1024 ? 'PASS' : 'FAIL'}\n- CSS budget (10KB): ${css <= 10 * 1024 ? 'PASS' : 'FAIL'}\n`;
|
|
32
|
+
|
|
33
|
+
const outDir = resolve('benchmarks');
|
|
34
|
+
mkdirSync(outDir, { recursive: true });
|
|
35
|
+
writeFileSync(join(outDir, 'latest-report.md'), md, 'utf8');
|
|
36
|
+
console.log('bench report written: benchmarks/latest-report.md');
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const pkgPath = "package.json";
|
|
4
|
+
const changelogPath = "CHANGELOG.md";
|
|
5
|
+
|
|
6
|
+
const level = process.argv[2] || "patch";
|
|
7
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
8
|
+
const [maj, min, pat] = pkg.version.split(".").map(Number);
|
|
9
|
+
let next = [maj, min, pat];
|
|
10
|
+
if (level === "major") next = [maj + 1, 0, 0];
|
|
11
|
+
else if (level === "minor") next = [maj, min + 1, 0];
|
|
12
|
+
else next = [maj, min, pat + 1];
|
|
13
|
+
|
|
14
|
+
pkg.version = next.join(".");
|
|
15
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
16
|
+
|
|
17
|
+
const entry = `## v${pkg.version} - ${new Date().toISOString().slice(0, 10)}\n- release prep\n\n`;
|
|
18
|
+
const prev = existsSync(changelogPath) ? readFileSync(changelogPath, "utf8") : "# Changelog\n\n";
|
|
19
|
+
writeFileSync(changelogPath, prev + entry, "utf8");
|
|
20
|
+
|
|
21
|
+
console.log(`version bumped to ${pkg.version}`);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
3
|
+
|
|
4
|
+
async function waitFor(url, ms = 20000) {
|
|
5
|
+
const start = Date.now();
|
|
6
|
+
let lastErr = null;
|
|
7
|
+
while (Date.now() - start < ms) {
|
|
8
|
+
try {
|
|
9
|
+
const r = await fetch(url);
|
|
10
|
+
if (r.status >= 200 && r.status < 600) return r;
|
|
11
|
+
} catch (e) {
|
|
12
|
+
lastErr = e;
|
|
13
|
+
}
|
|
14
|
+
await sleep(300);
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`Timeout waiting for ${url}${lastErr ? `: ${lastErr.message}` : ""}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const proc = spawn(process.execPath, ["./src/cli.mjs", "dev"], {
|
|
20
|
+
cwd: process.cwd(),
|
|
21
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
22
|
+
env: { ...process.env },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
let output = "";
|
|
26
|
+
proc.stdout.on("data", (d) => (output += d.toString()));
|
|
27
|
+
proc.stderr.on("data", (d) => (output += d.toString()));
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await waitFor("http://localhost:4173/");
|
|
31
|
+
|
|
32
|
+
const home = await fetch("http://localhost:4173/");
|
|
33
|
+
if (home.status !== 200) throw new Error(`Home status ${home.status}`);
|
|
34
|
+
const html = await home.text();
|
|
35
|
+
if (!html.includes("FastScript")) throw new Error("Home SSR did not include FastScript text");
|
|
36
|
+
|
|
37
|
+
const api = await fetch("http://localhost:4173/api/hello");
|
|
38
|
+
if (api.status !== 200) throw new Error(`API status ${api.status}`);
|
|
39
|
+
const apiJson = await api.json();
|
|
40
|
+
if (!apiJson.ok) throw new Error("API JSON did not return ok=true");
|
|
41
|
+
|
|
42
|
+
const privateRes = await fetch("http://localhost:4173/private", { redirect: "manual" });
|
|
43
|
+
if (!(privateRes.status === 302 || privateRes.status === 307)) {
|
|
44
|
+
throw new Error(`Expected redirect on /private, got ${privateRes.status}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const login = await fetch("http://localhost:4173/api/auth", {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: { "content-type": "application/json" },
|
|
50
|
+
body: JSON.stringify({}),
|
|
51
|
+
redirect: "manual",
|
|
52
|
+
});
|
|
53
|
+
if (login.status !== 200) throw new Error(`Auth login failed: ${login.status}`);
|
|
54
|
+
const setCookie = login.headers.get("set-cookie");
|
|
55
|
+
if (!setCookie || !setCookie.includes("fs_session=")) throw new Error("Missing fs_session cookie on login");
|
|
56
|
+
|
|
57
|
+
const bad = await fetch("http://localhost:4173/api/auth", {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
60
|
+
body: "{bad",
|
|
61
|
+
});
|
|
62
|
+
if (bad.status !== 400) throw new Error(`Expected 400 on invalid JSON, got ${bad.status}`);
|
|
63
|
+
|
|
64
|
+
const upload = await fetch("http://localhost:4173/api/upload", {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
67
|
+
body: JSON.stringify({ key: "smoke/one.txt", content: "hello" }),
|
|
68
|
+
});
|
|
69
|
+
if (upload.status !== 200) throw new Error(`Upload failed: ${upload.status}`);
|
|
70
|
+
const up = await upload.json();
|
|
71
|
+
const blob = await fetch(`http://localhost:4173${up.url}`);
|
|
72
|
+
if (blob.status !== 200) throw new Error(`Uploaded blob fetch failed: ${blob.status}`);
|
|
73
|
+
|
|
74
|
+
console.log("smoke-dev pass: SSR, API, middleware redirect, auth cookie, validation, upload");
|
|
75
|
+
} finally {
|
|
76
|
+
proc.kill("SIGTERM");
|
|
77
|
+
await sleep(400);
|
|
78
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
3
|
+
|
|
4
|
+
async function waitFor(url, ms = 20000) {
|
|
5
|
+
const start = Date.now();
|
|
6
|
+
while (Date.now() - start < ms) {
|
|
7
|
+
try {
|
|
8
|
+
const r = await fetch(url);
|
|
9
|
+
if (r.status >= 200) return;
|
|
10
|
+
} catch {}
|
|
11
|
+
await sleep(300);
|
|
12
|
+
}
|
|
13
|
+
throw new Error(`Timeout waiting for ${url}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const build = spawn(process.execPath, ["./src/cli.mjs", "build"], { cwd: process.cwd(), stdio: "inherit" });
|
|
17
|
+
await new Promise((resolve, reject) => {
|
|
18
|
+
build.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`build failed: ${code}`))));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const proc = spawn(process.execPath, ["./src/cli.mjs", "start"], {
|
|
22
|
+
cwd: process.cwd(),
|
|
23
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
24
|
+
env: { ...process.env, PORT: "4173", NODE_ENV: "production" },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await waitFor("http://localhost:4173/");
|
|
29
|
+
const home = await fetch("http://localhost:4173/");
|
|
30
|
+
if (home.status !== 200) throw new Error(`start home status ${home.status}`);
|
|
31
|
+
const reqId = home.headers.get("x-request-id");
|
|
32
|
+
if (!reqId) throw new Error("missing x-request-id header");
|
|
33
|
+
|
|
34
|
+
const api = await fetch("http://localhost:4173/api/hello", { headers: { accept: "application/json" } });
|
|
35
|
+
if (api.status !== 200) throw new Error(`start api status ${api.status}`);
|
|
36
|
+
|
|
37
|
+
console.log("smoke-start pass: production adapter serving SSR/API with request IDs");
|
|
38
|
+
} finally {
|
|
39
|
+
proc.kill("SIGTERM");
|
|
40
|
+
await sleep(300);
|
|
41
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { rmSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { createSessionManager, parseCookies, serializeCookie } from "../src/auth.mjs";
|
|
5
|
+
|
|
6
|
+
const dir = resolve('.tmp-auth-tests');
|
|
7
|
+
rmSync(dir, { recursive: true, force: true });
|
|
8
|
+
|
|
9
|
+
const sm = createSessionManager({ dir, cookieName: 't' });
|
|
10
|
+
const token = sm.create({ id: 'u1' }, 60);
|
|
11
|
+
assert.ok(token.split('.').length === 3);
|
|
12
|
+
assert.equal(sm.read(token).user.id, 'u1');
|
|
13
|
+
|
|
14
|
+
const rotated = sm.rotate(token, 60);
|
|
15
|
+
assert.ok(rotated);
|
|
16
|
+
assert.equal(sm.read(token), null);
|
|
17
|
+
assert.equal(sm.read(rotated).user.id, 'u1');
|
|
18
|
+
|
|
19
|
+
sm.delete(rotated);
|
|
20
|
+
assert.equal(sm.read(rotated), null);
|
|
21
|
+
|
|
22
|
+
const c = serializeCookie('a', 'b', { path: '/', maxAge: 10, httpOnly: true });
|
|
23
|
+
assert.ok(c.includes('a=b'));
|
|
24
|
+
assert.equal(parseCookies('a=b; x=y').x, 'y');
|
|
25
|
+
|
|
26
|
+
console.log('test-auth pass');
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { rmSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { createFileDatabase } from "../src/db.mjs";
|
|
5
|
+
|
|
6
|
+
const dir = resolve('.tmp-db-tests');
|
|
7
|
+
rmSync(dir, { recursive: true, force: true });
|
|
8
|
+
|
|
9
|
+
const db = createFileDatabase({ dir, name: 'test' });
|
|
10
|
+
const users = db.collection('users');
|
|
11
|
+
users.set('u1', { id: 'u1', role: 'admin', active: true });
|
|
12
|
+
users.upsert('u2', () => ({ id: 'u2', role: 'user', active: false }));
|
|
13
|
+
|
|
14
|
+
assert.equal(users.get('u1').role, 'admin');
|
|
15
|
+
assert.equal(users.first((u) => u.id === 'u2').id, 'u2');
|
|
16
|
+
assert.equal(users.where({ role: 'admin' }).length, 1);
|
|
17
|
+
assert.equal(db.where('users', { active: false }).length, 1);
|
|
18
|
+
|
|
19
|
+
let rolledBack = false;
|
|
20
|
+
try {
|
|
21
|
+
db.transaction((tx) => {
|
|
22
|
+
tx.collection('users').set('u3', { id: 'u3' });
|
|
23
|
+
throw new Error('boom');
|
|
24
|
+
});
|
|
25
|
+
} catch {
|
|
26
|
+
rolledBack = true;
|
|
27
|
+
}
|
|
28
|
+
assert.equal(rolledBack, true);
|
|
29
|
+
assert.equal(users.get('u3'), null);
|
|
30
|
+
|
|
31
|
+
console.log('test-db pass');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { rmSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { createJobQueue } from "../src/jobs.mjs";
|
|
5
|
+
|
|
6
|
+
const dir = resolve('.tmp-jobs-tests');
|
|
7
|
+
rmSync(dir, { recursive: true, force: true });
|
|
8
|
+
const q = createJobQueue({ dir });
|
|
9
|
+
const job = q.enqueue('a', { n: 1 }, { delayMs: 0, maxAttempts: 2, backoffMs: 1 });
|
|
10
|
+
assert.equal(q.peekReady(10).length >= 1, true);
|
|
11
|
+
q.fail(job);
|
|
12
|
+
assert.equal(q.list().length, 1);
|
|
13
|
+
q.fail(job);
|
|
14
|
+
assert.equal(q.list().length, 0);
|
|
15
|
+
console.log('test-jobs pass');
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { composeMiddleware } from "../src/middleware.mjs";
|
|
3
|
+
|
|
4
|
+
const calls = [];
|
|
5
|
+
const run = composeMiddleware([
|
|
6
|
+
async (ctx, next) => {
|
|
7
|
+
calls.push("m1:before");
|
|
8
|
+
ctx.x = 1;
|
|
9
|
+
const r = await next();
|
|
10
|
+
calls.push("m1:after");
|
|
11
|
+
return r;
|
|
12
|
+
},
|
|
13
|
+
async (ctx, next) => {
|
|
14
|
+
calls.push("m2:before");
|
|
15
|
+
ctx.y = ctx.x + 1;
|
|
16
|
+
const r = await next();
|
|
17
|
+
calls.push("m2:after");
|
|
18
|
+
return r;
|
|
19
|
+
},
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const result = await run({}, async () => {
|
|
23
|
+
calls.push("handler");
|
|
24
|
+
return { ok: true };
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
assert.equal(result.ok, true);
|
|
28
|
+
assert.deepEqual(calls, ["m1:before", "m2:before", "handler", "m2:after", "m1:after"]);
|
|
29
|
+
|
|
30
|
+
const short = composeMiddleware([
|
|
31
|
+
async () => ({ status: 401 }),
|
|
32
|
+
async () => ({ status: 200 }),
|
|
33
|
+
]);
|
|
34
|
+
const shortRes = await short({}, async () => ({ status: 204 }));
|
|
35
|
+
assert.equal(shortRes.status, 401);
|
|
36
|
+
|
|
37
|
+
console.log("test-middleware pass");
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { cpSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, existsSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { runMigrate } from "../src/migrate.mjs";
|
|
6
|
+
import { runExport } from "../src/export.mjs";
|
|
7
|
+
|
|
8
|
+
const root = resolve('.tmp-roundtrip');
|
|
9
|
+
const pages = join(root, 'pages');
|
|
10
|
+
rmSync(root, { recursive: true, force: true });
|
|
11
|
+
mkdirSync(pages, { recursive: true });
|
|
12
|
+
|
|
13
|
+
writeFileSync(join(pages, 'index.ts'), `
|
|
14
|
+
import type { X } from './types'
|
|
15
|
+
state n = 1
|
|
16
|
+
function add(a: number, b: number): number { return a + b }
|
|
17
|
+
export default function Home(){ return '<h1>' + String(add(n,2)) + '</h1>' }
|
|
18
|
+
`, 'utf8');
|
|
19
|
+
|
|
20
|
+
await runMigrate(root);
|
|
21
|
+
assert.equal(existsSync(join(pages, 'index.fs')), true);
|
|
22
|
+
assert.equal(existsSync(join(pages, 'index.ts')), false);
|
|
23
|
+
|
|
24
|
+
const appDir = resolve('app');
|
|
25
|
+
const restoreDir = mkdtempSync(join(tmpdir(), 'fs-app-backup-'));
|
|
26
|
+
const backupApp = join(restoreDir, 'app');
|
|
27
|
+
|
|
28
|
+
if (existsSync(appDir)) renameSync(appDir, backupApp);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
mkdirSync(join(appDir, 'pages'), { recursive: true });
|
|
32
|
+
writeFileSync(join(appDir, 'pages', 'index.fs'), readFileSync(join(pages, 'index.fs'), 'utf8'), 'utf8');
|
|
33
|
+
|
|
34
|
+
await runExport(['--to', 'js', '--out', '.tmp-export-js']);
|
|
35
|
+
await runExport(['--to', 'ts', '--out', '.tmp-export-ts']);
|
|
36
|
+
assert.equal(existsSync(resolve('.tmp-export-js/pages/index.js')), true);
|
|
37
|
+
assert.equal(existsSync(resolve('.tmp-export-ts/pages/index.ts')), true);
|
|
38
|
+
} finally {
|
|
39
|
+
rmSync(appDir, { recursive: true, force: true });
|
|
40
|
+
if (existsSync(backupApp)) renameSync(backupApp, appDir);
|
|
41
|
+
rmSync(restoreDir, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log('test-roundtrip pass');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { validateShape } from "../src/validation.mjs";
|
|
3
|
+
|
|
4
|
+
const q = validateShape({ page: "int", search: "string?" }, { page: "2" }, "query");
|
|
5
|
+
assert.equal(q.value.page, 2);
|
|
6
|
+
assert.equal(q.value.search, undefined);
|
|
7
|
+
|
|
8
|
+
let failed = false;
|
|
9
|
+
try {
|
|
10
|
+
validateShape({ page: "int" }, { page: "x" }, "query");
|
|
11
|
+
} catch (e) {
|
|
12
|
+
failed = true;
|
|
13
|
+
assert.equal(e.status, 400);
|
|
14
|
+
}
|
|
15
|
+
assert.equal(failed, true);
|
|
16
|
+
|
|
17
|
+
console.log("test-validation pass");
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { rmSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { signPayload, verifySignature, isReplay } from "../src/webhook.mjs";
|
|
5
|
+
import { createLocalStorage } from "../src/storage.mjs";
|
|
6
|
+
|
|
7
|
+
const payload = Buffer.from('{"ok":true}');
|
|
8
|
+
const secret = 'topsecret';
|
|
9
|
+
const sig = signPayload(payload, secret);
|
|
10
|
+
assert.equal(verifySignature(payload, sig, secret), true);
|
|
11
|
+
assert.equal(verifySignature(payload, sig, 'bad'), false);
|
|
12
|
+
assert.equal(isReplay(Math.floor(Date.now() / 1000), 300), false);
|
|
13
|
+
|
|
14
|
+
const dir = resolve('.tmp-storage-tests');
|
|
15
|
+
rmSync(dir, { recursive: true, force: true });
|
|
16
|
+
const store = createLocalStorage({ dir });
|
|
17
|
+
store.put('a/b.txt', Buffer.from('hello'));
|
|
18
|
+
assert.equal(String(store.get('a/b.txt')), 'hello');
|
|
19
|
+
store.delete('a/b.txt');
|
|
20
|
+
assert.equal(store.get('a/b.txt'), null);
|
|
21
|
+
|
|
22
|
+
console.log('test-webhook-storage pass');
|