fastscript 1.0.0 → 3.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 (119) hide show
  1. package/CHANGELOG.md +38 -7
  2. package/LICENSE +33 -21
  3. package/README.md +605 -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 +121 -0
  33. package/node_modules/@fastscript/core-private/src/fs-parser.mjs +1120 -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 +1466 -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 +108 -14
  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/benchmark-discipline.mjs +39 -0
  70. package/src/build.mjs +1 -234
  71. package/src/cache.mjs +210 -20
  72. package/src/cli.mjs +65 -6
  73. package/src/compat.mjs +8 -10
  74. package/src/conversion-manifest.mjs +101 -0
  75. package/src/create.mjs +71 -17
  76. package/src/csp.mjs +26 -0
  77. package/src/db-cli.mjs +152 -8
  78. package/src/db-postgres-collection.mjs +110 -0
  79. package/src/deploy.mjs +1 -65
  80. package/src/diagnostics.mjs +100 -0
  81. package/src/docs-search.mjs +35 -0
  82. package/src/env.mjs +34 -5
  83. package/src/fs-diagnostics.mjs +70 -0
  84. package/src/fs-error-codes.mjs +126 -0
  85. package/src/fs-formatter.mjs +66 -0
  86. package/src/fs-linter.mjs +274 -0
  87. package/src/fs-normalize.mjs +52 -239
  88. package/src/fs-parser.mjs +1 -0
  89. package/src/generated/docs-search-index.mjs +3591 -0
  90. package/src/i18n.mjs +25 -0
  91. package/src/jobs.mjs +283 -32
  92. package/src/metrics.mjs +45 -0
  93. package/src/migrate-rollback.mjs +144 -0
  94. package/src/migrate.mjs +1275 -47
  95. package/src/migration-wizard.mjs +42 -0
  96. package/src/module-loader.mjs +22 -11
  97. package/src/oauth-providers.mjs +103 -0
  98. package/src/permissions-cli.mjs +112 -0
  99. package/src/plugins.mjs +194 -0
  100. package/src/profile.mjs +95 -0
  101. package/src/regression-guard.mjs +245 -0
  102. package/src/retention.mjs +57 -0
  103. package/src/routes.mjs +178 -0
  104. package/src/runtime-permissions.mjs +299 -0
  105. package/src/scheduler.mjs +104 -0
  106. package/src/security.mjs +197 -19
  107. package/src/server-runtime.mjs +1 -339
  108. package/src/serverless-handler.mjs +20 -0
  109. package/src/session-policy.mjs +38 -0
  110. package/src/storage.mjs +1 -56
  111. package/src/style-system.mjs +461 -0
  112. package/src/tenant.mjs +55 -0
  113. package/src/trace.mjs +95 -0
  114. package/src/typecheck.mjs +1 -0
  115. package/src/validate.mjs +13 -1
  116. package/src/validation.mjs +14 -5
  117. package/src/webhook.mjs +1 -71
  118. package/src/worker.mjs +23 -4
  119. package/src/language-spec.mjs +0 -58
@@ -0,0 +1,42 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { createStrictConversionPlan } from "./migrate.mjs";
4
+
5
+ export async function runMigrationWizard(args = []) {
6
+ const target = args[0] || "app";
7
+ const abs = resolve(target);
8
+ if (!existsSync(abs)) throw new Error(`migration wizard: path not found (${abs})`);
9
+
10
+ const prepared = createStrictConversionPlan([abs, "--dry-run"]);
11
+ const { plan } = prepared;
12
+
13
+ console.log("=== strict conversion preview ===");
14
+ console.log(`target: ${abs}`);
15
+ console.log(`rename files: ${plan.renames.length}`);
16
+ console.log(`rewrite files: ${plan.writes.filter((item) => item.kind === "rewrite").length}`);
17
+ console.log(`import rewrites: ${plan.importRewrites.reduce((sum, item) => sum + item.count, 0)}`);
18
+ console.log(`blocked files: ${plan.blockedFiles.length}`);
19
+
20
+ if (plan.renames.length) {
21
+ console.log("--- renames ---");
22
+ for (const item of plan.renames.slice(0, 200)) {
23
+ console.log(`${item.fromRel} -> ${item.toRel}`);
24
+ }
25
+ }
26
+
27
+ if (plan.importRewrites.length) {
28
+ console.log("--- import rewrites ---");
29
+ for (const item of plan.importRewrites.slice(0, 100)) {
30
+ console.log(`${item.file} (${item.count})`);
31
+ }
32
+ }
33
+
34
+ if (plan.blockedFiles.length) {
35
+ console.log("--- blocked ---");
36
+ for (const item of plan.blockedFiles.slice(0, 200)) {
37
+ console.log(`${item.path}: ${item.reason}`);
38
+ }
39
+ }
40
+
41
+ console.log("=== end preview ===");
42
+ }
@@ -1,24 +1,31 @@
1
- import { extname } from "node:path";
1
+ import { extname } from "node:path";
2
2
  import { pathToFileURL } from "node:url";
3
3
  import esbuild from "esbuild";
4
- import {
5
- createFastScriptDiagnosticError,
6
- normalizeFastScriptWithTelemetry,
7
- } from "./fs-normalize.mjs";
4
+ import { normalizeFastScript } from "./fs-normalize.mjs";
5
+ import { assertFastScript } from "./fs-diagnostics.mjs";
6
+ import { createPermissionRuntime } from "./runtime-permissions.mjs";
7
+
8
+ let cachedPermissionRuntime = null;
9
+
10
+ function getPermissionRuntime() {
11
+ if (process.env.FASTSCRIPT_RUNTIME_PERMISSIONS === "0") return null;
12
+ if (!cachedPermissionRuntime) cachedPermissionRuntime = createPermissionRuntime();
13
+ return cachedPermissionRuntime;
14
+ }
8
15
 
9
16
  function fsLoaderPlugin() {
17
+ const compilerMode = (process.env.FASTSCRIPT_COMPILER_MODE || "strict").toLowerCase() === "lenient" ? "lenient" : "strict";
10
18
  return {
11
19
  name: "fastscript-fs-loader",
12
20
  setup(build) {
13
21
  build.onLoad({ filter: /\.fs$/ }, async (args) => {
14
22
  const { readFile } = await import("node:fs/promises");
15
23
  const raw = await readFile(args.path, "utf8");
16
- const result = normalizeFastScriptWithTelemetry(raw, { filename: args.path, strict: false });
17
- const hardErrors = result.diagnostics.filter((diag) => diag.severity === "error");
18
- if (hardErrors.length > 0) {
19
- throw createFastScriptDiagnosticError(hardErrors, { filename: args.path });
20
- }
21
- return { contents: result.code, loader: "js" };
24
+ assertFastScript(raw, { file: args.path, mode: compilerMode });
25
+ return {
26
+ contents: normalizeFastScript(raw, { file: args.path, mode: compilerMode, sourceMap: "inline" }),
27
+ loader: "js",
28
+ };
22
29
  });
23
30
  },
24
31
  };
@@ -26,6 +33,8 @@ function fsLoaderPlugin() {
26
33
 
27
34
  export async function importSourceModule(filePath, { platform = "node" } = {}) {
28
35
  const ext = extname(filePath).toLowerCase();
36
+ const permissionRuntime = getPermissionRuntime();
37
+ permissionRuntime?.assert({ kind: "dynamicImportAccess", resource: filePath, details: { platform } });
29
38
  if (ext !== ".fs") {
30
39
  return import(`${pathToFileURL(filePath).href}?t=${Date.now()}`);
31
40
  }
@@ -41,7 +50,9 @@ export async function importSourceModule(filePath, { platform = "node" } = {}) {
41
50
  plugins: [fsLoaderPlugin()],
42
51
  loader: { ".fs": "js" },
43
52
  });
53
+
44
54
  const code = result.outputFiles[0].text;
45
55
  const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString("base64")}`;
56
+ permissionRuntime?.assert({ kind: "dynamicImportAccess", resource: dataUrl, details: { platform, source: filePath } });
46
57
  return import(dataUrl);
47
58
  }
@@ -0,0 +1,103 @@
1
+ import { buildOAuthAuthorizeUrl, createOAuthState } from "./auth-flows.mjs";
2
+
3
+ const PROVIDERS = {
4
+ github: {
5
+ name: "GitHub",
6
+ authorizeUrl: "https://github.com/login/oauth/authorize",
7
+ tokenUrl: "https://github.com/login/oauth/access_token",
8
+ userUrl: "https://api.github.com/user",
9
+ scope: "read:user user:email",
10
+ },
11
+ google: {
12
+ name: "Google",
13
+ authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
14
+ tokenUrl: "https://oauth2.googleapis.com/token",
15
+ userUrl: "https://www.googleapis.com/oauth2/v3/userinfo",
16
+ scope: "openid email profile",
17
+ },
18
+ microsoft: {
19
+ name: "Microsoft",
20
+ authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
21
+ tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
22
+ userUrl: "https://graph.microsoft.com/v1.0/me",
23
+ scope: "openid profile email User.Read",
24
+ },
25
+ };
26
+
27
+ export function listAuthProviders() {
28
+ return Object.keys(PROVIDERS);
29
+ }
30
+
31
+ export function getAuthProvider(name) {
32
+ const key = String(name || "").toLowerCase();
33
+ const provider = PROVIDERS[key];
34
+ if (!provider) {
35
+ const error = new Error(`Unknown OAuth provider: ${name}`);
36
+ error.status = 400;
37
+ throw error;
38
+ }
39
+ return { id: key, ...provider };
40
+ }
41
+
42
+ export function createOAuthAdapter(name, {
43
+ clientId = process.env[`${String(name || "").toUpperCase()}_CLIENT_ID`],
44
+ clientSecret = process.env[`${String(name || "").toUpperCase()}_CLIENT_SECRET`],
45
+ redirectUri = process.env[`${String(name || "").toUpperCase()}_REDIRECT_URI`],
46
+ } = {}) {
47
+ const provider = getAuthProvider(name);
48
+ return {
49
+ ...provider,
50
+ clientId,
51
+ clientSecret,
52
+ redirectUri,
53
+ authorizeUrl({ state = createOAuthState(), scope = provider.scope } = {}) {
54
+ return {
55
+ state,
56
+ url: buildOAuthAuthorizeUrl({
57
+ authorizeUrl: provider.authorizeUrl,
58
+ clientId,
59
+ redirectUri,
60
+ scope,
61
+ state,
62
+ }),
63
+ };
64
+ },
65
+ async exchangeCode(code) {
66
+ const body = new URLSearchParams({
67
+ grant_type: "authorization_code",
68
+ code,
69
+ client_id: clientId,
70
+ client_secret: clientSecret,
71
+ redirect_uri: redirectUri,
72
+ });
73
+ const response = await fetch(provider.tokenUrl, {
74
+ method: "POST",
75
+ headers: {
76
+ "content-type": "application/x-www-form-urlencoded",
77
+ accept: "application/json",
78
+ },
79
+ body,
80
+ });
81
+ if (!response.ok) {
82
+ const error = new Error(`OAuth token exchange failed (${response.status})`);
83
+ error.status = 502;
84
+ throw error;
85
+ }
86
+ return response.json();
87
+ },
88
+ async fetchUser(accessToken) {
89
+ const response = await fetch(provider.userUrl, {
90
+ headers: {
91
+ authorization: `Bearer ${accessToken}`,
92
+ accept: "application/json",
93
+ },
94
+ });
95
+ if (!response.ok) {
96
+ const error = new Error(`OAuth user fetch failed (${response.status})`);
97
+ error.status = 502;
98
+ throw error;
99
+ }
100
+ return response.json();
101
+ },
102
+ };
103
+ }
@@ -0,0 +1,112 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, relative, resolve } from "node:path";
3
+ import { createPermissionRuntime } from "./runtime-permissions.mjs";
4
+
5
+ function normalize(path) {
6
+ return String(path || "").replace(/\\/g, "/");
7
+ }
8
+
9
+ function parseArgs(args = []) {
10
+ const options = {
11
+ policy: process.env.FASTSCRIPT_PERMISSION_POLICY_PATH || "fastscript.permissions.json",
12
+ mode: "report",
13
+ kind: "",
14
+ resource: "",
15
+ out: "",
16
+ json: false,
17
+ };
18
+
19
+ for (let i = 0; i < args.length; i += 1) {
20
+ const arg = args[i];
21
+ if (arg === "--policy") {
22
+ options.policy = args[i + 1] || options.policy;
23
+ i += 1;
24
+ continue;
25
+ }
26
+ if (arg === "--mode") {
27
+ const next = String(args[i + 1] || "report").toLowerCase();
28
+ options.mode = next === "assert" ? "assert" : "report";
29
+ i += 1;
30
+ continue;
31
+ }
32
+ if (arg === "--kind") {
33
+ options.kind = String(args[i + 1] || "");
34
+ i += 1;
35
+ continue;
36
+ }
37
+ if (arg === "--resource") {
38
+ options.resource = String(args[i + 1] || "");
39
+ i += 1;
40
+ continue;
41
+ }
42
+ if (arg === "--out") {
43
+ options.out = resolve(args[i + 1] || "");
44
+ i += 1;
45
+ continue;
46
+ }
47
+ if (arg === "--json") {
48
+ options.json = true;
49
+ continue;
50
+ }
51
+ }
52
+
53
+ return options;
54
+ }
55
+
56
+ function summarizePolicy(policyRuntime) {
57
+ const policy = policyRuntime.policy;
58
+ return {
59
+ policyPath: normalize(relative(resolve("."), policyRuntime.policyPath)),
60
+ preset: policy.preset,
61
+ boundaries: {
62
+ fileAccess: policy.fileAccess.mode,
63
+ envAccess: policy.envAccess.mode,
64
+ networkAccess: policy.networkAccess.mode,
65
+ subprocessExecution: policy.subprocessExecution.mode,
66
+ dynamicImports: policy.dynamicImports.mode,
67
+ pluginAccess: policy.pluginAccess.mode,
68
+ },
69
+ };
70
+ }
71
+
72
+ export async function runPermissions(args = []) {
73
+ const options = parseArgs(args);
74
+ const runtime = createPermissionRuntime({ policyPath: options.policy });
75
+
76
+ const summary = summarizePolicy(runtime);
77
+ let decision = null;
78
+
79
+ if (options.kind && options.resource) {
80
+ if (options.mode === "assert") {
81
+ decision = runtime.assert({ kind: options.kind, resource: options.resource });
82
+ } else {
83
+ decision = runtime.check({ kind: options.kind, resource: options.resource });
84
+ }
85
+ }
86
+
87
+ const report = {
88
+ generatedAt: new Date().toISOString(),
89
+ summary,
90
+ decision,
91
+ };
92
+
93
+ if (options.out) {
94
+ mkdirSync(dirname(options.out), { recursive: true });
95
+ writeFileSync(options.out, `${JSON.stringify(report, null, 2)}\n`, "utf8");
96
+ }
97
+
98
+ if (options.json) {
99
+ console.log(JSON.stringify(report, null, 2));
100
+ return;
101
+ }
102
+
103
+ console.log(`permissions policy: ${summary.policyPath}`);
104
+ console.log(`preset: ${summary.preset}`);
105
+ for (const [boundary, mode] of Object.entries(summary.boundaries)) {
106
+ console.log(`${boundary}: ${mode}`);
107
+ }
108
+ if (decision) {
109
+ console.log(`decision: ${decision.allowed ? "allow" : "deny"} (${decision.boundary})`);
110
+ console.log(`reason: ${decision.reason}`);
111
+ }
112
+ }
@@ -0,0 +1,194 @@
1
+ import { existsSync } from "node:fs";
2
+ import { extname, join, resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { importSourceModule } from "./module-loader.mjs";
5
+
6
+ export const PLUGIN_API_VERSION = 1;
7
+ const DEFAULT_PLUGIN_TIMEOUT_MS = Number(process.env.PLUGIN_TIMEOUT_MS || 2500);
8
+
9
+ const CONFIG_CANDIDATES = [
10
+ "fastscript.plugins.fs",
11
+ "fastscript.plugins.js",
12
+ "fastscript.plugins.mjs",
13
+ "fastscript.plugins.cjs",
14
+ "app/plugins.fs",
15
+ "app/plugins.js",
16
+ "app/plugins.mjs",
17
+ "app/plugins.cjs",
18
+ ];
19
+
20
+ function asArray(value) {
21
+ if (!value) return [];
22
+ return Array.isArray(value) ? value : [value];
23
+ }
24
+
25
+ function createNullLogger() {
26
+ return {
27
+ info() {},
28
+ warn() {},
29
+ error() {},
30
+ };
31
+ }
32
+
33
+ function withTimeout(task, ms, label) {
34
+ if (!Number.isFinite(ms) || ms <= 0) return task;
35
+ let timer = null;
36
+ const timeout = new Promise((_, reject) => {
37
+ timer = setTimeout(() => {
38
+ const error = new Error(`Plugin hook timeout (${ms}ms): ${label}`);
39
+ error.code = "PLUGIN_HOOK_TIMEOUT";
40
+ reject(error);
41
+ }, ms);
42
+ });
43
+ return Promise.race([task.finally(() => clearTimeout(timer)), timeout]);
44
+ }
45
+
46
+ async function importPluginModule(file) {
47
+ const abs = resolve(file);
48
+ const ext = extname(abs).toLowerCase();
49
+ if (ext === ".fs") return importSourceModule(abs, { platform: "node" });
50
+ return import(`${pathToFileURL(abs).href}?t=${Date.now()}`);
51
+ }
52
+
53
+ function normalizePluginEntries(mod) {
54
+ const entries = [];
55
+ entries.push(...asArray(mod?.plugins));
56
+ entries.push(...asArray(mod?.default));
57
+ return entries;
58
+ }
59
+
60
+ function normalizePlugin(raw, fallbackName) {
61
+ if (!raw) return null;
62
+ if (typeof raw === "function") {
63
+ return { name: fallbackName, apiVersion: PLUGIN_API_VERSION, setup: raw };
64
+ }
65
+ if (typeof raw !== "object") return null;
66
+ const setup = typeof raw.setup === "function" ? raw.setup : null;
67
+ const plugin = {
68
+ name: raw.name || fallbackName,
69
+ apiVersion: Number.isInteger(raw.apiVersion) ? raw.apiVersion : PLUGIN_API_VERSION,
70
+ setup,
71
+ };
72
+ if (typeof raw.middleware === "function") plugin.middleware = raw.middleware;
73
+ if (typeof raw.onBuildStart === "function") plugin.onBuildStart = raw.onBuildStart;
74
+ if (typeof raw.onBuildEnd === "function") plugin.onBuildEnd = raw.onBuildEnd;
75
+ if (typeof raw.onRequestStart === "function") plugin.onRequestStart = raw.onRequestStart;
76
+ if (typeof raw.onRequestEnd === "function") plugin.onRequestEnd = raw.onRequestEnd;
77
+ return plugin;
78
+ }
79
+
80
+ function createRegistry() {
81
+ return {
82
+ middleware: [],
83
+ onBuildStart: [],
84
+ onBuildEnd: [],
85
+ onRequestStart: [],
86
+ onRequestEnd: [],
87
+ };
88
+ }
89
+
90
+ async function runHookList(hooks, ctx, logger, stage) {
91
+ for (const hook of hooks) {
92
+ const label = `${hook.plugin}:${stage}`;
93
+ try {
94
+ await withTimeout(Promise.resolve(hook.fn(ctx)), hook.timeoutMs ?? DEFAULT_PLUGIN_TIMEOUT_MS, label);
95
+ } catch (error) {
96
+ logger.warn("plugin_hook_failed", {
97
+ plugin: hook.plugin,
98
+ stage,
99
+ error: error?.message || String(error),
100
+ });
101
+ }
102
+ }
103
+ }
104
+
105
+ function createHookApi(registry, pluginName) {
106
+ return {
107
+ middleware(fn, opts = {}) {
108
+ if (typeof fn !== "function") return;
109
+ registry.middleware.push({ plugin: pluginName, fn, timeoutMs: opts.timeoutMs });
110
+ },
111
+ onBuildStart(fn, opts = {}) {
112
+ if (typeof fn !== "function") return;
113
+ registry.onBuildStart.push({ plugin: pluginName, fn, timeoutMs: opts.timeoutMs });
114
+ },
115
+ onBuildEnd(fn, opts = {}) {
116
+ if (typeof fn !== "function") return;
117
+ registry.onBuildEnd.push({ plugin: pluginName, fn, timeoutMs: opts.timeoutMs });
118
+ },
119
+ onRequestStart(fn, opts = {}) {
120
+ if (typeof fn !== "function") return;
121
+ registry.onRequestStart.push({ plugin: pluginName, fn, timeoutMs: opts.timeoutMs });
122
+ },
123
+ onRequestEnd(fn, opts = {}) {
124
+ if (typeof fn !== "function") return;
125
+ registry.onRequestEnd.push({ plugin: pluginName, fn, timeoutMs: opts.timeoutMs });
126
+ },
127
+ };
128
+ }
129
+
130
+ function configFiles(root) {
131
+ return CONFIG_CANDIDATES.map((file) => join(root, file)).filter((p) => existsSync(p));
132
+ }
133
+
134
+ export async function createPluginRuntime({ root = process.cwd(), logger = createNullLogger() } = {}) {
135
+ const files = configFiles(root);
136
+ const registry = createRegistry();
137
+ const loaded = [];
138
+
139
+ for (const file of files) {
140
+ let mod;
141
+ try {
142
+ mod = await importPluginModule(file);
143
+ } catch (error) {
144
+ logger.warn("plugin_module_load_failed", { file, error: error?.message || String(error) });
145
+ continue;
146
+ }
147
+ const rawEntries = normalizePluginEntries(mod);
148
+ rawEntries.forEach((raw, idx) => {
149
+ const plugin = normalizePlugin(raw, `${file}#${idx + 1}`);
150
+ if (!plugin) return;
151
+ if (plugin.apiVersion !== PLUGIN_API_VERSION) {
152
+ logger.warn("plugin_api_version_mismatch", {
153
+ plugin: plugin.name,
154
+ expected: PLUGIN_API_VERSION,
155
+ received: plugin.apiVersion,
156
+ });
157
+ return;
158
+ }
159
+
160
+ const hooks = createHookApi(registry, plugin.name);
161
+ try {
162
+ if (plugin.middleware) hooks.middleware(plugin.middleware);
163
+ if (plugin.onBuildStart) hooks.onBuildStart(plugin.onBuildStart);
164
+ if (plugin.onBuildEnd) hooks.onBuildEnd(plugin.onBuildEnd);
165
+ if (plugin.onRequestStart) hooks.onRequestStart(plugin.onRequestStart);
166
+ if (plugin.onRequestEnd) hooks.onRequestEnd(plugin.onRequestEnd);
167
+ if (plugin.setup) plugin.setup({ hooks, apiVersion: PLUGIN_API_VERSION });
168
+ loaded.push({ name: plugin.name, file });
169
+ } catch (error) {
170
+ logger.warn("plugin_setup_failed", { plugin: plugin.name, file, error: error?.message || String(error) });
171
+ }
172
+ });
173
+ }
174
+
175
+ return {
176
+ apiVersion: PLUGIN_API_VERSION,
177
+ loaded,
178
+ middleware() {
179
+ return registry.middleware.map((row) => row.fn);
180
+ },
181
+ async onBuildStart(ctx = {}) {
182
+ await runHookList(registry.onBuildStart, ctx, logger, "onBuildStart");
183
+ },
184
+ async onBuildEnd(ctx = {}) {
185
+ await runHookList(registry.onBuildEnd, ctx, logger, "onBuildEnd");
186
+ },
187
+ async onRequestStart(ctx = {}) {
188
+ await runHookList(registry.onRequestStart, ctx, logger, "onRequestStart");
189
+ },
190
+ async onRequestEnd(ctx = {}) {
191
+ await runHookList(registry.onRequestEnd, ctx, logger, "onRequestEnd");
192
+ },
193
+ };
194
+ }
@@ -0,0 +1,95 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, relative, resolve } from "node:path";
3
+ import { performance } from "node:perf_hooks";
4
+ import { runBuild } from "./build.mjs";
5
+ import { runTypeCheck } from "./typecheck.mjs";
6
+ import { runBench } from "./bench.mjs";
7
+ import { runCompat } from "./compat.mjs";
8
+
9
+ function parseArgs(args = []) {
10
+ const options = {
11
+ command: "build",
12
+ runs: Math.max(1, Number(process.env.FASTSCRIPT_PROFILE_RUNS || 1)),
13
+ out: resolve(".fastscript", "profile-latest.json"),
14
+ };
15
+
16
+ for (let i = 0; i < args.length; i += 1) {
17
+ const arg = args[i];
18
+ if (arg === "--command") {
19
+ options.command = String(args[i + 1] || options.command).toLowerCase();
20
+ i += 1;
21
+ continue;
22
+ }
23
+ if (arg === "--runs") {
24
+ options.runs = Math.max(1, Number(args[i + 1] || options.runs));
25
+ i += 1;
26
+ continue;
27
+ }
28
+ if (arg === "--out") {
29
+ options.out = resolve(args[i + 1] || options.out);
30
+ i += 1;
31
+ continue;
32
+ }
33
+ }
34
+
35
+ return options;
36
+ }
37
+
38
+ function createRunner(command) {
39
+ switch (command) {
40
+ case "build":
41
+ return () => runBuild({ mode: "build" });
42
+ case "typecheck":
43
+ return () => runTypeCheck(["--mode", "pass"]);
44
+ case "bench":
45
+ return () => runBench();
46
+ case "compat":
47
+ return () => runCompat();
48
+ default:
49
+ throw new Error(`profile: unsupported command '${command}'`);
50
+ }
51
+ }
52
+
53
+ export async function runProfile(args = []) {
54
+ const options = parseArgs(args);
55
+ const runner = createRunner(options.command);
56
+ const runs = [];
57
+
58
+ for (let i = 0; i < options.runs; i += 1) {
59
+ const rssBefore = process.memoryUsage().rss;
60
+ const t0 = performance.now();
61
+ await runner();
62
+ const t1 = performance.now();
63
+ const rssAfter = process.memoryUsage().rss;
64
+
65
+ runs.push({
66
+ run: i + 1,
67
+ ms: Number((t1 - t0).toFixed(2)),
68
+ rssBeforeMb: Number((rssBefore / (1024 * 1024)).toFixed(2)),
69
+ rssAfterMb: Number((rssAfter / (1024 * 1024)).toFixed(2)),
70
+ rssDeltaMb: Number(((rssAfter - rssBefore) / (1024 * 1024)).toFixed(2)),
71
+ });
72
+ }
73
+
74
+ const times = runs.map((row) => row.ms).sort((a, b) => a - b);
75
+ const mean = times.reduce((sum, value) => sum + value, 0) / Math.max(1, times.length);
76
+
77
+ const report = {
78
+ generatedAt: new Date().toISOString(),
79
+ command: options.command,
80
+ runs,
81
+ summary: {
82
+ count: runs.length,
83
+ minMs: times[0] || 0,
84
+ medianMs: times[Math.floor(times.length / 2)] || 0,
85
+ maxMs: times[times.length - 1] || 0,
86
+ meanMs: Number(mean.toFixed(2)),
87
+ },
88
+ };
89
+
90
+ mkdirSync(dirname(options.out), { recursive: true });
91
+ writeFileSync(options.out, `${JSON.stringify(report, null, 2)}\n`, "utf8");
92
+
93
+ console.log(`profile complete: command=${options.command}, runs=${runs.length}, mean=${report.summary.meanMs}ms`);
94
+ console.log(`profile report: ${String(relative(resolve("."), options.out)).replace(/\\/g, "/")}`);
95
+ }