@zveltio/cli 3.0.0-beta.4 → 3.0.0-beta.68
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/dist/index.js +1236 -276
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -861,7 +861,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
861
861
|
this._exitCallback = (err) => {
|
|
862
862
|
if (err.code !== "commander.executeSubCommandAsync") {
|
|
863
863
|
throw err;
|
|
864
|
-
}
|
|
864
|
+
}
|
|
865
865
|
};
|
|
866
866
|
}
|
|
867
867
|
return this;
|
|
@@ -1888,7 +1888,7 @@ var require_commander = __commonJS((exports) => {
|
|
|
1888
1888
|
|
|
1889
1889
|
// ../sdk/dist/build/index.js
|
|
1890
1890
|
import { createRequire } from "module";
|
|
1891
|
-
import { existsSync as
|
|
1891
|
+
import { existsSync as existsSync6, readFileSync } from "fs";
|
|
1892
1892
|
import { dirname, join as join5 } from "path";
|
|
1893
1893
|
function escapeRegExp(s) {
|
|
1894
1894
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -1913,7 +1913,7 @@ function resolveSearchPaths(resolveFromDir) {
|
|
|
1913
1913
|
for (let i = 0;i < 6; i++) {
|
|
1914
1914
|
cur = dirname(cur);
|
|
1915
1915
|
paths.push(cur);
|
|
1916
|
-
if (
|
|
1916
|
+
if (existsSync6(join5(cur, "node_modules")))
|
|
1917
1917
|
break;
|
|
1918
1918
|
}
|
|
1919
1919
|
return [...new Set(paths)];
|
|
@@ -1927,7 +1927,7 @@ function resolvePackageRoot(pkg, searchPaths) {
|
|
|
1927
1927
|
}
|
|
1928
1928
|
for (;; ) {
|
|
1929
1929
|
const pkgJsonPath = join5(candidate, "package.json");
|
|
1930
|
-
if (
|
|
1930
|
+
if (existsSync6(pkgJsonPath)) {
|
|
1931
1931
|
const manifest = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
1932
1932
|
if (manifest.name === pkg)
|
|
1933
1933
|
return candidate;
|
|
@@ -1951,7 +1951,7 @@ function resolvePackageEsmEntry(pkg, resolveFromDir) {
|
|
|
1951
1951
|
throw new Error(`Cannot determine ESM entry for package "${pkg}"`);
|
|
1952
1952
|
}
|
|
1953
1953
|
const abs = join5(root, rel.replace(/^\.\//, ""));
|
|
1954
|
-
if (!
|
|
1954
|
+
if (!existsSync6(abs)) {
|
|
1955
1955
|
throw new Error(`ESM entry missing for "${pkg}": ${abs}`);
|
|
1956
1956
|
}
|
|
1957
1957
|
if (abs.endsWith(".d.ts")) {
|
|
@@ -1997,7 +1997,7 @@ var init_build = __esm(() => {
|
|
|
1997
1997
|
});
|
|
1998
1998
|
|
|
1999
1999
|
// src/lib/extension-bundle.ts
|
|
2000
|
-
import { existsSync as
|
|
2000
|
+
import { existsSync as existsSync7, renameSync } from "fs";
|
|
2001
2001
|
import { basename, dirname as dirname2, join as join6 } from "path";
|
|
2002
2002
|
async function bundleExtensionEngine(opts) {
|
|
2003
2003
|
const outdir = dirname2(opts.outfile);
|
|
@@ -2018,7 +2018,7 @@ async function bundleExtensionEngine(opts) {
|
|
|
2018
2018
|
`);
|
|
2019
2019
|
throw new Error(messages || "Bun.build failed");
|
|
2020
2020
|
}
|
|
2021
|
-
if (!
|
|
2021
|
+
if (!existsSync7(builtPath)) {
|
|
2022
2022
|
throw new Error(`Bun.build succeeded but ${builtPath} was not produced`);
|
|
2023
2023
|
}
|
|
2024
2024
|
if (builtPath !== opts.outfile) {
|
|
@@ -2031,6 +2031,18 @@ var init_extension_bundle = __esm(() => {
|
|
|
2031
2031
|
EXTENSION_BUNDLE_CORE_DEPS2 = EXTENSION_BUNDLE_CORE_DEPS;
|
|
2032
2032
|
});
|
|
2033
2033
|
|
|
2034
|
+
// src/lib/pack-isolation.ts
|
|
2035
|
+
function resolvePackIsolation(opts) {
|
|
2036
|
+
let resolved = opts.current;
|
|
2037
|
+
if (opts.firstParty && resolved === "worker" && !opts.keepIsolation) {
|
|
2038
|
+
resolved = undefined;
|
|
2039
|
+
}
|
|
2040
|
+
if (!resolved && opts.communityInject && !opts.firstParty) {
|
|
2041
|
+
resolved = "worker";
|
|
2042
|
+
}
|
|
2043
|
+
return resolved;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2034
2046
|
// src/lib/publisher-tier.ts
|
|
2035
2047
|
function tierAllowsInline(tier) {
|
|
2036
2048
|
return tier === "first-party" || tier === "verified";
|
|
@@ -2069,14 +2081,108 @@ function normalizeTier(v) {
|
|
|
2069
2081
|
// src/commands/extension-pack.ts
|
|
2070
2082
|
var exports_extension_pack = {};
|
|
2071
2083
|
__export(exports_extension_pack, {
|
|
2084
|
+
recordBundledVersions: () => recordBundledVersions,
|
|
2085
|
+
hashEngineSources: () => hashEngineSources,
|
|
2072
2086
|
extensionPackCommand: () => extensionPackCommand
|
|
2073
2087
|
});
|
|
2074
2088
|
import { createHash } from "crypto";
|
|
2075
|
-
import { existsSync as
|
|
2076
|
-
import {
|
|
2089
|
+
import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
2090
|
+
import { homedir } from "os";
|
|
2091
|
+
import { join as join7, relative, resolve } from "path";
|
|
2092
|
+
function scrubBuildPaths(outfile, dir) {
|
|
2093
|
+
const original = readFileSync2(outfile, "utf8");
|
|
2094
|
+
let count = 0;
|
|
2095
|
+
let text = original.replace(/(__dirname|__filename)\s*=\s*"(\/[^"]*)"/g, (_match, name, abs) => {
|
|
2096
|
+
count += 1;
|
|
2097
|
+
const at = abs.lastIndexOf("node_modules/");
|
|
2098
|
+
const tail = at === -1 ? abs.slice(abs.lastIndexOf("/") + 1) : abs.slice(at);
|
|
2099
|
+
return `${name} = "/zveltio-extension/${tail}"`;
|
|
2100
|
+
});
|
|
2101
|
+
text = text.replace(/\.{1,2}\/[^\s"'`]*(?:node_modules|packages)\/[^\s"'`]*/g, (match) => {
|
|
2102
|
+
const store = match.indexOf(".bun/");
|
|
2103
|
+
const at = store === -1 ? Math.min(...[match.lastIndexOf("node_modules/"), match.indexOf("packages/")].filter((i) => i >= 0)) : store;
|
|
2104
|
+
count += 1;
|
|
2105
|
+
return `/zveltio-extension/${match.slice(at)}`;
|
|
2106
|
+
});
|
|
2107
|
+
text = text.replace(/(?<=^\/\/\s*)node_modules\//gm, "/zveltio-extension/node_modules/");
|
|
2108
|
+
const prefix = relative(process.cwd(), dir).replace(/\\/g, "/");
|
|
2109
|
+
if (prefix && !prefix.startsWith("..")) {
|
|
2110
|
+
const re = new RegExp(`(?<=^//\\s*)${prefix.replace(/[.*+?^${}()|[\\]]/g, "\\$&")}/`, "gm");
|
|
2111
|
+
const before = text;
|
|
2112
|
+
text = text.replace(re, "");
|
|
2113
|
+
if (text !== before)
|
|
2114
|
+
count += 1;
|
|
2115
|
+
}
|
|
2116
|
+
const home = homedir();
|
|
2117
|
+
if (home && home.startsWith("/") && home.length > 1 && text.includes(home)) {
|
|
2118
|
+
count += text.split(home).length - 1;
|
|
2119
|
+
text = text.split(home).join("/zveltio-extension");
|
|
2120
|
+
}
|
|
2121
|
+
if (count > 0)
|
|
2122
|
+
writeFileSync(outfile, text, "utf8");
|
|
2123
|
+
return count;
|
|
2124
|
+
}
|
|
2125
|
+
function recordBundledVersions(outfile, dir) {
|
|
2126
|
+
let text = readFileSync2(outfile, "utf8");
|
|
2127
|
+
const recorded = [];
|
|
2128
|
+
for (const dep of CORE_DEPS) {
|
|
2129
|
+
if (!text.includes(`node_modules/${dep}/`) || text.includes(`.bun/${dep}@`))
|
|
2130
|
+
continue;
|
|
2131
|
+
for (let at = resolve(dir);; at = resolve(at, "..")) {
|
|
2132
|
+
const pkg = join7(at, "node_modules", dep, "package.json");
|
|
2133
|
+
if (existsSync8(pkg)) {
|
|
2134
|
+
const { version } = JSON.parse(readFileSync2(pkg, "utf8"));
|
|
2135
|
+
if (version)
|
|
2136
|
+
recorded.push(`${dep}@${version}`);
|
|
2137
|
+
break;
|
|
2138
|
+
}
|
|
2139
|
+
if (at === resolve(at, ".."))
|
|
2140
|
+
break;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
if (recorded.length === 0)
|
|
2144
|
+
return recorded;
|
|
2145
|
+
text = text.replace(/\n*$/, `
|
|
2146
|
+
`);
|
|
2147
|
+
text += recorded.map((r) => `// @zveltio-bundled ${r}
|
|
2148
|
+
`).join("");
|
|
2149
|
+
writeFileSync(outfile, text, "utf8");
|
|
2150
|
+
return recorded;
|
|
2151
|
+
}
|
|
2152
|
+
function hashEngineSources(dir) {
|
|
2153
|
+
const engineDir = join7(dir, "engine");
|
|
2154
|
+
const files = [];
|
|
2155
|
+
const walk = (d) => {
|
|
2156
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
2157
|
+
const full = join7(d, entry.name);
|
|
2158
|
+
if (entry.isDirectory()) {
|
|
2159
|
+
if (entry.name === "migrations" || entry.name === "node_modules")
|
|
2160
|
+
continue;
|
|
2161
|
+
walk(full);
|
|
2162
|
+
continue;
|
|
2163
|
+
}
|
|
2164
|
+
if (!/\.(ts|tsx|js|mjs|json)$/.test(entry.name))
|
|
2165
|
+
continue;
|
|
2166
|
+
if (/\.(test|spec)\.[a-z]+$/.test(entry.name))
|
|
2167
|
+
continue;
|
|
2168
|
+
if (entry.name === "index.js")
|
|
2169
|
+
continue;
|
|
2170
|
+
files.push(full);
|
|
2171
|
+
}
|
|
2172
|
+
};
|
|
2173
|
+
walk(engineDir);
|
|
2174
|
+
const h = createHash("sha256");
|
|
2175
|
+
for (const f of files.sort()) {
|
|
2176
|
+
h.update(f.slice(engineDir.length).replace(/\\/g, "/"));
|
|
2177
|
+
h.update("\x00");
|
|
2178
|
+
h.update(readFileSync2(f));
|
|
2179
|
+
h.update("\x00");
|
|
2180
|
+
}
|
|
2181
|
+
return h.digest("hex");
|
|
2182
|
+
}
|
|
2077
2183
|
function readManifest(dir) {
|
|
2078
2184
|
const path = join7(dir, "manifest.json");
|
|
2079
|
-
if (!
|
|
2185
|
+
if (!existsSync8(path)) {
|
|
2080
2186
|
throw new Error(`No manifest.json at ${path}.`);
|
|
2081
2187
|
}
|
|
2082
2188
|
return JSON.parse(readFileSync2(path, "utf8"));
|
|
@@ -2099,7 +2205,7 @@ async function extensionPackCommand(opts) {
|
|
|
2099
2205
|
const dir = resolve(opts.dir ?? process.cwd());
|
|
2100
2206
|
const entry = join7(dir, "engine", "index.ts");
|
|
2101
2207
|
const outfile = join7(dir, "engine", "index.js");
|
|
2102
|
-
if (!
|
|
2208
|
+
if (!existsSync8(entry)) {
|
|
2103
2209
|
throw new Error(`No engine/index.ts at ${entry}. ` + `Run from the extension root or pass --dir. Extensions without an engine entry don't need pack.`);
|
|
2104
2210
|
}
|
|
2105
2211
|
const manifest = readManifest(dir);
|
|
@@ -2125,6 +2231,14 @@ async function extensionPackCommand(opts) {
|
|
|
2125
2231
|
} catch (err) {
|
|
2126
2232
|
throw new Error(`Bun bundle failed: ${err.message}`);
|
|
2127
2233
|
}
|
|
2234
|
+
const scrubbed = scrubBuildPaths(outfile, dir);
|
|
2235
|
+
if (scrubbed > 0) {
|
|
2236
|
+
console.log(` ${c5.green("\u2713")} ${c5.dim(`${scrubbed} build path(s) neutralised`)}`);
|
|
2237
|
+
}
|
|
2238
|
+
const recorded = recordBundledVersions(outfile, dir);
|
|
2239
|
+
if (recorded.length > 0) {
|
|
2240
|
+
console.log(` ${c5.green("\u2713")} ${c5.dim(`recorded bundled ${recorded.join(", ")}`)}`);
|
|
2241
|
+
}
|
|
2128
2242
|
const bundleBytes = readFileSync2(outfile);
|
|
2129
2243
|
const engineSha256 = createHash("sha256").update(bundleBytes).digest("hex");
|
|
2130
2244
|
const bundleText = bundleBytes.toString("utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|\s)\/\/[^\n]*/g, "$1");
|
|
@@ -2136,21 +2250,29 @@ async function extensionPackCommand(opts) {
|
|
|
2136
2250
|
}
|
|
2137
2251
|
const sizeKb = Math.round(bundleBytes.byteLength / 1024);
|
|
2138
2252
|
console.log(` ${c5.green("\u2713")} ${c5.bold(`engine/index.js`)} ${c5.dim(`(${sizeKb} KB, sha256=${engineSha256.slice(0, 12)}\u2026)`)}`);
|
|
2139
|
-
let
|
|
2140
|
-
if (!
|
|
2253
|
+
let communityInject = false;
|
|
2254
|
+
if (!manifest.engine?.isolation) {
|
|
2141
2255
|
const resolved = await resolvePublisherTier({
|
|
2142
2256
|
firstParty: opts.firstParty,
|
|
2143
2257
|
token: opts.token,
|
|
2144
2258
|
registryUrl: opts.registryUrl
|
|
2145
2259
|
});
|
|
2146
2260
|
if (!resolved.allowsInline) {
|
|
2147
|
-
|
|
2261
|
+
communityInject = true;
|
|
2148
2262
|
console.log(` ${c5.yellow("\u26A0")} ${resolved.tier} publisher \u2014 auto-set ${c5.bold('engine.isolation: "worker"')} ` + `(community extensions can't run inline).`);
|
|
2149
2263
|
if (resolved.source === "default") {
|
|
2150
2264
|
console.log(c5.dim(" Tier defaulted to community (no --first-party flag, no registry token). " + "If you are verified/first-party, pass --first-party or set ZVELTIO_REGISTRY_TOKEN."));
|
|
2151
2265
|
}
|
|
2152
2266
|
}
|
|
2267
|
+
} else if (opts.firstParty && manifest.engine.isolation === "worker" && !opts.keepIsolation) {
|
|
2268
|
+
console.log(` ${c5.yellow("\u26A0")} first-party pack \u2014 clearing sticky ${c5.bold('engine.isolation: "worker"')} ` + `(inline is the first-party default). Pass ${c5.bold("--keep-isolation")} to retain worker.`);
|
|
2153
2269
|
}
|
|
2270
|
+
const resolvedIsolation = resolvePackIsolation({
|
|
2271
|
+
current: manifest.engine?.isolation,
|
|
2272
|
+
firstParty: opts.firstParty,
|
|
2273
|
+
keepIsolation: opts.keepIsolation,
|
|
2274
|
+
communityInject
|
|
2275
|
+
});
|
|
2154
2276
|
if (!opts.noManifestUpdate) {
|
|
2155
2277
|
manifest.engine = {
|
|
2156
2278
|
entry: "engine/index.js",
|
|
@@ -2162,6 +2284,7 @@ async function extensionPackCommand(opts) {
|
|
|
2162
2284
|
};
|
|
2163
2285
|
manifest.integrity = {
|
|
2164
2286
|
engineSha256,
|
|
2287
|
+
sourceSha256: hashEngineSources(dir),
|
|
2165
2288
|
...manifest.integrity?.archiveSha256 ? { archiveSha256: manifest.integrity.archiveSha256 } : {}
|
|
2166
2289
|
};
|
|
2167
2290
|
writeManifest(dir, manifest);
|
|
@@ -3104,10 +3227,13 @@ function findDockerfile(cwd) {
|
|
|
3104
3227
|
return null;
|
|
3105
3228
|
}
|
|
3106
3229
|
async function generateDockerfile(cwd, projectName) {
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3230
|
+
const candidates = [
|
|
3231
|
+
"packages/engine/src/binary-entry.ts",
|
|
3232
|
+
"packages/engine/src/index.ts",
|
|
3233
|
+
"src/binary-entry.ts",
|
|
3234
|
+
"src/index.ts"
|
|
3235
|
+
];
|
|
3236
|
+
const entryPoint = candidates.find((f) => existsSync4(join4(cwd, f))) ?? "index.ts";
|
|
3111
3237
|
await Bun.write(join4(cwd, "Dockerfile"), `# Auto-generated by zveltio deploy
|
|
3112
3238
|
# Edit this file to customise your production image.
|
|
3113
3239
|
FROM oven/bun:1-alpine AS base
|
|
@@ -3140,6 +3266,40 @@ CMD ["./dist/${projectName}"]
|
|
|
3140
3266
|
`);
|
|
3141
3267
|
}
|
|
3142
3268
|
|
|
3269
|
+
// src/lib/engine-modules.ts
|
|
3270
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3271
|
+
import { fileURLToPath } from "url";
|
|
3272
|
+
var CANDIDATES = [
|
|
3273
|
+
"../../../engine/src/db/",
|
|
3274
|
+
"../../engine/src/db/"
|
|
3275
|
+
];
|
|
3276
|
+
|
|
3277
|
+
class EngineModulesUnavailable extends Error {
|
|
3278
|
+
constructor() {
|
|
3279
|
+
super(`this CLI cannot reach the engine\u2019s migration runner.
|
|
3280
|
+
|
|
3281
|
+
` + ` The published @zveltio/cli package does not ship the engine, so the
|
|
3282
|
+
` + ` direct-to-database path only works from a repository checkout.
|
|
3283
|
+
|
|
3284
|
+
` + ` Use one of these instead:
|
|
3285
|
+
` + ` zveltio migrate # the release binary, with DATABASE_URL set
|
|
3286
|
+
` + " zveltio migrate --url http://host # against a running engine");
|
|
3287
|
+
this.name = "EngineModulesUnavailable";
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
async function loadEngineMigrationModules() {
|
|
3291
|
+
for (const base of CANDIDATES) {
|
|
3292
|
+
const dbUrl = new URL(`${base}index.js`, import.meta.url);
|
|
3293
|
+
const path = fileURLToPath(dbUrl);
|
|
3294
|
+
if (!existsSync5(path) && !existsSync5(path.replace(/\.js$/, ".ts")))
|
|
3295
|
+
continue;
|
|
3296
|
+
const db = await import(dbUrl.href);
|
|
3297
|
+
const migrations = await import(new URL(`${base}migrations/index.js`, import.meta.url).href);
|
|
3298
|
+
return { db, migrations };
|
|
3299
|
+
}
|
|
3300
|
+
throw new EngineModulesUnavailable;
|
|
3301
|
+
}
|
|
3302
|
+
|
|
3143
3303
|
// src/commands/migrate.ts
|
|
3144
3304
|
var migrateCommand = new Command("migrate").description("Run pending database migrations").option("--url <url>", "Engine URL", "http://localhost:3000").option("--database-url <url>", "Direct database URL (skip engine)").option("--dry-run", "Show pending migrations without applying").action(async (opts) => {
|
|
3145
3305
|
console.log(`
|
|
@@ -3155,10 +3315,9 @@ var migrateCommand = new Command("migrate").description("Run pending database mi
|
|
|
3155
3315
|
async function runMigrationsDirectly(opts, databaseUrl) {
|
|
3156
3316
|
try {
|
|
3157
3317
|
process.env.DATABASE_URL = databaseUrl;
|
|
3158
|
-
const
|
|
3159
|
-
const
|
|
3160
|
-
const {
|
|
3161
|
-
const { runMigrations, getAppliedMigrations, getLastAppliedMigration } = await import(migrationsPath);
|
|
3318
|
+
const { db: dbModule, migrations } = await loadEngineMigrationModules();
|
|
3319
|
+
const { initDatabase } = dbModule;
|
|
3320
|
+
const { runMigrations, getAppliedMigrations, getLastAppliedMigration } = migrations;
|
|
3162
3321
|
const db = await initDatabase();
|
|
3163
3322
|
if (opts.dryRun) {
|
|
3164
3323
|
const applied = await getAppliedMigrations(db);
|
|
@@ -3229,37 +3388,41 @@ async function runMigrationsViaAPI(opts) {
|
|
|
3229
3388
|
}
|
|
3230
3389
|
|
|
3231
3390
|
// src/commands/extension.ts
|
|
3232
|
-
import { existsSync as
|
|
3391
|
+
import { existsSync as existsSync9 } from "fs";
|
|
3233
3392
|
import { mkdir, writeFile } from "fs/promises";
|
|
3234
3393
|
import { join as join8 } from "path";
|
|
3235
3394
|
async function extensionCommand(action, name, opts) {
|
|
3236
3395
|
switch (action) {
|
|
3237
3396
|
case "create":
|
|
3238
|
-
await createExtension(name, opts.category || "custom");
|
|
3397
|
+
await createExtension(name, opts.category || "custom", opts.codePage === true);
|
|
3239
3398
|
break;
|
|
3240
3399
|
case "build":
|
|
3241
3400
|
await buildExtension(opts);
|
|
3242
3401
|
break;
|
|
3243
|
-
case "dev":
|
|
3244
|
-
await devExtension();
|
|
3245
|
-
break;
|
|
3246
3402
|
}
|
|
3247
3403
|
}
|
|
3248
|
-
async function createExtension(name, category) {
|
|
3404
|
+
async function createExtension(name, category, codePage) {
|
|
3249
3405
|
const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
3406
|
+
const extName = `${category}/${safeName}`;
|
|
3407
|
+
const table = `zv_${safeName.replace(/-/g, "_")}_items`;
|
|
3250
3408
|
const targetDir = join8(process.cwd(), "extensions", category, safeName);
|
|
3251
|
-
if (
|
|
3409
|
+
if (existsSync9(targetDir)) {
|
|
3252
3410
|
console.error(`Extension already exists: ${targetDir}`);
|
|
3253
3411
|
process.exit(1);
|
|
3254
3412
|
}
|
|
3255
3413
|
console.log(`
|
|
3256
|
-
Creating extension: ${
|
|
3414
|
+
Creating extension: ${extName}
|
|
3257
3415
|
`);
|
|
3258
|
-
await mkdir(join8(targetDir, "engine"), { recursive: true });
|
|
3259
|
-
await mkdir(join8(targetDir, "studio", "src", "pages"), { recursive: true });
|
|
3260
3416
|
await mkdir(join8(targetDir, "engine", "migrations"), { recursive: true });
|
|
3417
|
+
await mkdir(join8(targetDir, "studio", "src", "components"), { recursive: true });
|
|
3418
|
+
if (codePage) {
|
|
3419
|
+
await mkdir(join8(targetDir, "studio", "pages"), { recursive: true });
|
|
3420
|
+
} else {
|
|
3421
|
+
await mkdir(join8(targetDir, "studio", "schemas"), { recursive: true });
|
|
3422
|
+
await mkdir(join8(targetDir, "studio", "messages"), { recursive: true });
|
|
3423
|
+
}
|
|
3261
3424
|
await writeFile(join8(targetDir, "manifest.json"), JSON.stringify({
|
|
3262
|
-
name:
|
|
3425
|
+
name: extName,
|
|
3263
3426
|
package: `@zveltio/ext-${safeName}`,
|
|
3264
3427
|
category,
|
|
3265
3428
|
displayName: name,
|
|
@@ -3268,13 +3431,29 @@ Creating extension: ${category}/${safeName}
|
|
|
3268
3431
|
zveltioMinVersion: "1.0.0",
|
|
3269
3432
|
zveltioMaxVersion: "4.0.0",
|
|
3270
3433
|
permissions: ["database"],
|
|
3271
|
-
|
|
3434
|
+
studio: {
|
|
3435
|
+
pages: [
|
|
3436
|
+
{
|
|
3437
|
+
path: `/admin/${safeName}`,
|
|
3438
|
+
label: name,
|
|
3439
|
+
icon: "Puzzle",
|
|
3440
|
+
...codePage ? {} : { schema: `schemas/${safeName}.json` }
|
|
3441
|
+
}
|
|
3442
|
+
],
|
|
3443
|
+
navGroup: "developer"
|
|
3444
|
+
},
|
|
3445
|
+
contributes: {
|
|
3446
|
+
engine: true,
|
|
3447
|
+
studio: true,
|
|
3448
|
+
fieldTypes: [],
|
|
3449
|
+
slots: []
|
|
3450
|
+
}
|
|
3272
3451
|
}, null, 2));
|
|
3273
3452
|
await writeFile(join8(targetDir, "engine", "index.ts"), `import type { ZveltioExtension } from '@zveltio/sdk/extension';
|
|
3274
3453
|
import { join } from 'path';
|
|
3275
3454
|
|
|
3276
3455
|
const extension: ZveltioExtension = {
|
|
3277
|
-
name: '${
|
|
3456
|
+
name: '${extName}',
|
|
3278
3457
|
category: '${category}',
|
|
3279
3458
|
|
|
3280
3459
|
getMigrations() {
|
|
@@ -3284,88 +3463,120 @@ const extension: ZveltioExtension = {
|
|
|
3284
3463
|
},
|
|
3285
3464
|
|
|
3286
3465
|
async register(app, ctx) {
|
|
3287
|
-
//
|
|
3288
|
-
|
|
3466
|
+
// The route the generated page reads. ctx.db resolves the caller's tenant
|
|
3467
|
+
// transaction, so this returns only that tenant's rows.
|
|
3468
|
+
app.get('/items', async (c) => {
|
|
3469
|
+
const data = await ctx.db
|
|
3470
|
+
.selectFrom('${table}')
|
|
3471
|
+
.selectAll()
|
|
3472
|
+
.orderBy('created_at', 'desc')
|
|
3473
|
+
.limit(100)
|
|
3474
|
+
.execute();
|
|
3475
|
+
return c.json({ data });
|
|
3476
|
+
});
|
|
3289
3477
|
},
|
|
3290
3478
|
};
|
|
3291
3479
|
|
|
3292
3480
|
export default extension;
|
|
3293
3481
|
`);
|
|
3294
|
-
await writeFile(join8(targetDir, "engine", "migrations", "001_init.sql"), `-- ${name}
|
|
3295
|
-
--
|
|
3482
|
+
await writeFile(join8(targetDir, "engine", "migrations", "001_init.sql"), `-- ${name} \u2014 initial schema.
|
|
3483
|
+
--
|
|
3484
|
+
-- Tenant isolation is not optional and not something to add later: a table
|
|
3485
|
+
-- without it is readable by every tenant on the instance. The three parts below
|
|
3486
|
+
-- are the whole pattern.
|
|
3296
3487
|
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
--
|
|
3301
|
-
--
|
|
3302
|
-
|
|
3488
|
+
CREATE TABLE IF NOT EXISTS ${table} (
|
|
3489
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3490
|
+
name TEXT NOT NULL,
|
|
3491
|
+
-- 1. The column, defaulting to the tenant of the transaction doing the write,
|
|
3492
|
+
-- so application code never has to name it (and cannot forge it).
|
|
3493
|
+
tenant_id UUID NOT NULL DEFAULT COALESCE(
|
|
3494
|
+
NULLIF(current_setting('zveltio.current_tenant', true), '')::uuid,
|
|
3495
|
+
'00000000-0000-0000-0000-000000000001'::uuid
|
|
3496
|
+
),
|
|
3497
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
3498
|
+
);
|
|
3499
|
+
|
|
3500
|
+
CREATE INDEX IF NOT EXISTS idx_${table}_tenant ON ${table}(tenant_id);
|
|
3501
|
+
|
|
3502
|
+
-- 2. FORCE matters. Without it Postgres lets the table owner bypass the policy,
|
|
3503
|
+
-- and the engine connects as owner on a stock install \u2014 RLS becomes advisory.
|
|
3504
|
+
ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY;
|
|
3505
|
+
ALTER TABLE ${table} FORCE ROW LEVEL SECURITY;
|
|
3506
|
+
|
|
3507
|
+
-- 3. The host's own predicate, not a hand-rolled copy: reads and writes agree
|
|
3508
|
+
-- with the column DEFAULT above, and the engine reconciles every policy
|
|
3509
|
+
-- named \`tenant_isolation_*\` onto it at boot.
|
|
3510
|
+
DROP POLICY IF EXISTS tenant_isolation_${table} ON ${table};
|
|
3511
|
+
CREATE POLICY tenant_isolation_${table} ON ${table}
|
|
3512
|
+
USING (tenant_id = ANY (zveltio_visible_tenants()))
|
|
3513
|
+
WITH CHECK (zveltio_tenant_write_ok(tenant_id));
|
|
3303
3514
|
`);
|
|
3304
|
-
|
|
3515
|
+
if (codePage) {
|
|
3516
|
+
await writeFile(join8(targetDir, "studio", "pages", "+page.svelte"), `<script lang="ts">
|
|
3517
|
+
import { api } from '$lib/api.js';
|
|
3305
3518
|
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
return;
|
|
3311
|
-
}
|
|
3519
|
+
// A code page owns its fetching, loading and error states, and its strings
|
|
3520
|
+
// have to reach i18n by hand. A schema gets all three from the host \u2014 worth
|
|
3521
|
+
// re-checking that this page really needs to be code.
|
|
3522
|
+
let items = $state<Array<{ id: string; name: string }>>([]);
|
|
3312
3523
|
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3524
|
+
$effect(() => {
|
|
3525
|
+
api
|
|
3526
|
+
.get<{ data: Array<{ id: string; name: string }> }>('/ext/${extName}/items')
|
|
3527
|
+
.then((r) => {
|
|
3528
|
+
items = r.data;
|
|
3529
|
+
});
|
|
3319
3530
|
});
|
|
3320
|
-
}
|
|
3321
|
-
`);
|
|
3322
|
-
await writeFile(join8(targetDir, "studio", "src", "pages", "MainPage.svelte"), `<script lang="ts">
|
|
3323
|
-
const engineUrl = (window as any).__ZVELTIO_ENGINE_URL__;
|
|
3324
3531
|
</script>
|
|
3325
3532
|
|
|
3326
3533
|
<div class="space-y-6">
|
|
3327
3534
|
<h1 class="text-2xl font-bold">${name}</h1>
|
|
3328
|
-
|
|
3535
|
+
{#each items as item (item.id)}
|
|
3536
|
+
<div>{item.name}</div>
|
|
3537
|
+
{/each}
|
|
3329
3538
|
</div>
|
|
3330
3539
|
`);
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
})
|
|
3540
|
+
} else {
|
|
3541
|
+
await writeFile(join8(targetDir, "studio", "schemas", `${safeName}.json`), `${JSON.stringify({
|
|
3542
|
+
sduiSchema: 1,
|
|
3543
|
+
title: `${safeName}.title`,
|
|
3544
|
+
subtitle: `${safeName}.subtitle`,
|
|
3545
|
+
resources: [
|
|
3546
|
+
{
|
|
3547
|
+
id: "items",
|
|
3548
|
+
dataSource: `/ext/${extName}/items`,
|
|
3549
|
+
dataPath: "data",
|
|
3550
|
+
search: { fields: ["name"], placeholder: "common.search" },
|
|
3551
|
+
columns: [
|
|
3552
|
+
{ key: "name", label: "common.col.name" },
|
|
3553
|
+
{ key: "created_at", label: "common.col.created", type: "date" }
|
|
3554
|
+
]
|
|
3555
|
+
}
|
|
3556
|
+
]
|
|
3557
|
+
}, null, 2)}
|
|
3558
|
+
`);
|
|
3559
|
+
await writeFile(join8(targetDir, "studio", "messages", "en.json"), `${JSON.stringify({
|
|
3560
|
+
[`${safeName}.title`]: name,
|
|
3561
|
+
[`${safeName}.subtitle`]: `Manage ${name.toLowerCase()} records`
|
|
3562
|
+
}, null, 2)}
|
|
3563
|
+
`);
|
|
3564
|
+
}
|
|
3565
|
+
await writeFile(join8(targetDir, "studio", "src", "contribute.ts.example"), `/**
|
|
3566
|
+
* Rename to contribute.ts to register dashboard/settings slot widgets.
|
|
3567
|
+
* Synced by packages/studio/scripts/sync-extensions.ts \u2014 no studio/dist/ build.
|
|
3568
|
+
*
|
|
3569
|
+
* import { registerContributionSlot } from '$lib/extension-api.svelte.js';
|
|
3570
|
+
* import MyWidget from './components/MyWidget.svelte';
|
|
3571
|
+
*
|
|
3572
|
+
* export function activate(): void {
|
|
3573
|
+
* registerContributionSlot('${extName}', 'dashboard.widgets', {
|
|
3574
|
+
* component: MyWidget,
|
|
3575
|
+
* priority: 100,
|
|
3576
|
+
* });
|
|
3577
|
+
* }
|
|
3578
|
+
*/
|
|
3354
3579
|
`);
|
|
3355
|
-
await writeFile(join8(targetDir, "studio", "package.json"), JSON.stringify({
|
|
3356
|
-
name: `@zveltio/ext-${safeName}-studio`,
|
|
3357
|
-
version: "1.0.0",
|
|
3358
|
-
type: "module",
|
|
3359
|
-
scripts: {
|
|
3360
|
-
build: "vite build",
|
|
3361
|
-
dev: "vite build --watch"
|
|
3362
|
-
},
|
|
3363
|
-
dependencies: { svelte: "^5.0.0" },
|
|
3364
|
-
devDependencies: {
|
|
3365
|
-
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
|
3366
|
-
vite: "^6.0.0"
|
|
3367
|
-
}
|
|
3368
|
-
}, null, 2));
|
|
3369
3580
|
await writeFile(join8(targetDir, ".gitattributes"), `* text=auto eol=lf
|
|
3370
3581
|
|
|
3371
3582
|
# Packed engine bundle: byte-identical across OSes. Manifest
|
|
@@ -3392,57 +3603,60 @@ jobs:
|
|
|
3392
3603
|
- name: Install dependencies
|
|
3393
3604
|
run: bun install
|
|
3394
3605
|
|
|
3606
|
+
# Validate FIRST, on the COMMITTED bundle. It checks the bundle against
|
|
3607
|
+
# manifest integrity.engineSha256 and against the source it was built
|
|
3608
|
+
# from, so a bundle nobody repacked fails here. Running pack before this
|
|
3609
|
+
# would rewrite both hashes and the check could never fail.
|
|
3610
|
+
- name: Validate committed bundle, manifest + structure
|
|
3611
|
+
run: bunx @zveltio/cli extension validate
|
|
3612
|
+
|
|
3395
3613
|
- name: Pack extension (engine/index.ts \u2192 engine/index.js + manifest hash)
|
|
3396
3614
|
run: bunx @zveltio/cli extension pack
|
|
3397
|
-
|
|
3398
|
-
- name: Verify committed bundle matches manifest engineSha256
|
|
3399
|
-
# If pack changed engine/index.js, the committed bytes drift
|
|
3400
|
-
# from manifest.integrity.engineSha256. The publishing pipeline
|
|
3401
|
-
# would reject this \u2014 fail PR before merging.
|
|
3402
|
-
run: |
|
|
3403
|
-
actual=$(sha256sum engine/index.js | awk '{print $1}')
|
|
3404
|
-
declared=$(node -e "console.log(require('./manifest.json').integrity?.engineSha256 ?? '')")
|
|
3405
|
-
if [ "$actual" != "$declared" ]; then
|
|
3406
|
-
echo "::error::Bundle hash $actual \u2260 manifest engineSha256 $declared"
|
|
3407
|
-
echo "::error::Run \\\`bunx @zveltio/cli extension pack\\\` locally and commit the result."
|
|
3408
|
-
exit 1
|
|
3409
|
-
fi
|
|
3410
|
-
|
|
3411
|
-
- name: Validate manifest + structure
|
|
3412
|
-
run: bunx @zveltio/cli extension validate
|
|
3413
3615
|
`);
|
|
3616
|
+
const pageLines = codePage ? ` pages/
|
|
3617
|
+
+page.svelte <- code page (synced into Studio at release)` : ` schemas/
|
|
3618
|
+
${safeName}.json <- THE PAGE (rendered by the host; no build step)
|
|
3619
|
+
messages/
|
|
3620
|
+
en.json <- keys this schema owns; other locales filled from en`;
|
|
3414
3621
|
console.log(`Extension scaffolded at extensions/${category}/${safeName}/
|
|
3415
3622
|
|
|
3416
|
-
Structure:
|
|
3417
3623
|
engine/
|
|
3418
|
-
index.ts <- API routes
|
|
3419
|
-
migrations/ <- SQL
|
|
3624
|
+
index.ts <- API routes (mounted at /ext/${extName}/*)
|
|
3625
|
+
migrations/ <- SQL, with tenant isolation already wired
|
|
3420
3626
|
studio/
|
|
3627
|
+
${pageLines}
|
|
3421
3628
|
src/
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
vite.config.ts
|
|
3629
|
+
components/ <- shared Svelte (optional)
|
|
3630
|
+
contribute.ts.example <- rename to contribute.ts for a dashboard widget
|
|
3425
3631
|
manifest.json
|
|
3426
|
-
.gitattributes
|
|
3427
|
-
.github/workflows/
|
|
3428
|
-
ci.yml <- pack + hash verify + validate on every PR
|
|
3632
|
+
.gitattributes
|
|
3633
|
+
.github/workflows/ci.yml
|
|
3429
3634
|
|
|
3430
3635
|
Next steps:
|
|
3431
|
-
1.
|
|
3432
|
-
2.
|
|
3433
|
-
3.
|
|
3434
|
-
|
|
3636
|
+
1. Put your logic in engine/index.ts \u2014 that is where the truth lives
|
|
3637
|
+
2. ${codePage ? "Write the page in studio/pages/+page.svelte" : `Shape the page in studio/schemas/${safeName}.json`}
|
|
3638
|
+
3. \`bunx @zveltio/cli extension pack\` \u2014 produces engine/index.js + integrity
|
|
3639
|
+
(validate needs this: it rejects a manifest with no engine block)
|
|
3640
|
+
4. \`bunx @zveltio/cli extension validate\`${codePage ? " \u2014 manifest, migrations and bundle" : ` \u2014 checks the schema against the
|
|
3641
|
+
routes you actually serve, and the message keys against what you ship`}
|
|
3642
|
+
5. Local preview: \`cd packages/studio && bun scripts/sync-extensions.ts\`,
|
|
3643
|
+
then \`bun run dev\` \u2014 see developer-guide.md \xA72
|
|
3435
3644
|
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
registry token). Worker mode = Bun.Worker with crash isolation and no
|
|
3441
|
-
direct DB credentials.
|
|
3645
|
+
Where UI goes:
|
|
3646
|
+
a page -> studio/schemas/*.json (almost always)
|
|
3647
|
+
a widget on the dashboard -> studio/src/contribute.ts (occasionally)
|
|
3648
|
+
canvas / chat / map / IDE -> studio/pages/+page.svelte (rarely)
|
|
3442
3649
|
|
|
3443
|
-
|
|
3444
|
-
--first-party. See https://github.com/zveltio-devs/zveltio/blob/master/docs/EXTENSION-DEVELOPER-GUIDE.md#135-isolation-tiers-be-honest-about-what-you-ship
|
|
3650
|
+
Do NOT add studio/vite.config.ts, studio/package.json, or studio/dist/ \u2014 removed in alpha.94/beta.15.
|
|
3445
3651
|
`);
|
|
3652
|
+
if (codePage) {
|
|
3653
|
+
console.warn(`\x1B[33mScaffolded a CODE page. Across the 56 shipped extensions, 61 pages are
|
|
3654
|
+
schemas and seven are code \u2014 and each of those seven sits beside a schema, for
|
|
3655
|
+
the part a schema could not express. On this road you own fetching,
|
|
3656
|
+
loading and error states, i18n by hand, and a Studio release to ship a change.
|
|
3657
|
+
Worth it for a canvas, a chat, a map or an inbox \u2014 for a list, a form or
|
|
3658
|
+
settings, delete studio/pages/ and add \`schema\` back to manifest.studio.pages[0].\x1B[0m`);
|
|
3659
|
+
}
|
|
3446
3660
|
}
|
|
3447
3661
|
async function buildExtension(opts) {
|
|
3448
3662
|
console.warn("\x1B[33m`zveltio extension build` is deprecated \u2014 use `zveltio extension pack`.\x1B[0m");
|
|
@@ -3450,26 +3664,10 @@ async function buildExtension(opts) {
|
|
|
3450
3664
|
const { extensionPackCommand: extensionPackCommand2 } = await Promise.resolve().then(() => (init_extension_pack(), exports_extension_pack));
|
|
3451
3665
|
await extensionPackCommand2({ dir: opts.dir });
|
|
3452
3666
|
}
|
|
3453
|
-
async function devExtension() {
|
|
3454
|
-
console.log(`
|
|
3455
|
-
Starting extension dev mode...
|
|
3456
|
-
`);
|
|
3457
|
-
const studioProc = Bun.spawn(["bun", "run", "dev"], {
|
|
3458
|
-
cwd: "studio",
|
|
3459
|
-
stdout: "inherit",
|
|
3460
|
-
stderr: "inherit"
|
|
3461
|
-
});
|
|
3462
|
-
console.log(" Studio: watching for changes...");
|
|
3463
|
-
process.on("SIGINT", () => {
|
|
3464
|
-
studioProc.kill();
|
|
3465
|
-
process.exit(0);
|
|
3466
|
-
});
|
|
3467
|
-
await studioProc.exited;
|
|
3468
|
-
}
|
|
3469
3667
|
|
|
3470
3668
|
// src/commands/extension-types.ts
|
|
3471
|
-
import { existsSync as
|
|
3472
|
-
import { join as join9, relative } from "path";
|
|
3669
|
+
import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3670
|
+
import { join as join9, relative as relative2 } from "path";
|
|
3473
3671
|
|
|
3474
3672
|
// ../sdk/dist/schema-codegen.js
|
|
3475
3673
|
var NUMERIC_TYPES = new Set([
|
|
@@ -3539,6 +3737,10 @@ function mapPgTypeToTs(pgType) {
|
|
|
3539
3737
|
tsBase = "unknown";
|
|
3540
3738
|
return isArray ? `${tsBase}[]` : tsBase;
|
|
3541
3739
|
}
|
|
3740
|
+
function upSection(sql) {
|
|
3741
|
+
const i = sql.search(/^--\s*DOWN\s*$/im);
|
|
3742
|
+
return i < 0 ? sql : sql.slice(0, i);
|
|
3743
|
+
}
|
|
3542
3744
|
function stripComments(sql) {
|
|
3543
3745
|
let out = "";
|
|
3544
3746
|
let i = 0;
|
|
@@ -3716,13 +3918,12 @@ function parseSchema(sqlChunks) {
|
|
|
3716
3918
|
}
|
|
3717
3919
|
return tables.get(name);
|
|
3718
3920
|
};
|
|
3719
|
-
const allSql = sqlChunks.map(stripComments).join(`
|
|
3921
|
+
const allSql = sqlChunks.map(upSection).map(stripComments).join(`
|
|
3720
3922
|
`);
|
|
3721
3923
|
const createRe = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))\s*\(/gi;
|
|
3722
|
-
|
|
3723
|
-
while ((m = createRe.exec(allSql)) !== null) {
|
|
3924
|
+
for (const m of allSql.matchAll(createRe)) {
|
|
3724
3925
|
const tableName = m[1] ?? m[2];
|
|
3725
|
-
const openIdx =
|
|
3926
|
+
const openIdx = m.index + m[0].length - 1;
|
|
3726
3927
|
const closeIdx = findMatchingParen(allSql, openIdx);
|
|
3727
3928
|
if (closeIdx < 0)
|
|
3728
3929
|
continue;
|
|
@@ -3731,10 +3932,9 @@ function parseSchema(sqlChunks) {
|
|
|
3731
3932
|
const tableCols = ensureTable(tableName);
|
|
3732
3933
|
for (const c6 of columns)
|
|
3733
3934
|
tableCols.set(c6.name, c6);
|
|
3734
|
-
createRe.lastIndex = closeIdx + 1;
|
|
3735
3935
|
}
|
|
3736
3936
|
const alterRe = /ALTER\s+TABLE\s+(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))\s+ADD\s+COLUMN\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))\s+([^;]+?)(?=;|$)/gi;
|
|
3737
|
-
|
|
3937
|
+
for (const m of allSql.matchAll(alterRe)) {
|
|
3738
3938
|
const tableName = m[1] ?? m[2];
|
|
3739
3939
|
const colName = m[3] ?? m[4];
|
|
3740
3940
|
const tail = m[5];
|
|
@@ -3744,6 +3944,10 @@ function parseSchema(sqlChunks) {
|
|
|
3744
3944
|
tableCols.set(col.name, col);
|
|
3745
3945
|
}
|
|
3746
3946
|
}
|
|
3947
|
+
const dropRe = /ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))\s+DROP\s+COLUMN\s+(?:IF\s+EXISTS\s+)?(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))/gi;
|
|
3948
|
+
for (const m of allSql.matchAll(dropRe)) {
|
|
3949
|
+
tables.get(m[1] ?? m[2])?.delete(m[3] ?? m[4]);
|
|
3950
|
+
}
|
|
3747
3951
|
return {
|
|
3748
3952
|
tables: order.map((name) => ({ name, columns: [...tables.get(name).values()] }))
|
|
3749
3953
|
};
|
|
@@ -3793,18 +3997,18 @@ ${c6.bold("Extension types")}
|
|
|
3793
3997
|
`);
|
|
3794
3998
|
console.log(` Extension dir: ${c6.dim(dir)}`);
|
|
3795
3999
|
const manifestPath = join9(dir, "manifest.json");
|
|
3796
|
-
if (!
|
|
4000
|
+
if (!existsSync10(manifestPath)) {
|
|
3797
4001
|
console.error(c6.red(`No manifest.json found in ${dir}`));
|
|
3798
4002
|
console.error(c6.dim(" Run this command from your extension's root folder, or pass --dir."));
|
|
3799
4003
|
process.exit(1);
|
|
3800
4004
|
}
|
|
3801
4005
|
const migrationsDir = join9(dir, "engine", "migrations");
|
|
3802
|
-
if (!
|
|
4006
|
+
if (!existsSync10(migrationsDir)) {
|
|
3803
4007
|
console.error(c6.yellow(`No engine/migrations directory found at ${migrationsDir}`));
|
|
3804
4008
|
console.error(c6.dim(" Nothing to generate. Add SQL migrations first."));
|
|
3805
4009
|
process.exit(0);
|
|
3806
4010
|
}
|
|
3807
|
-
const sqlFiles =
|
|
4011
|
+
const sqlFiles = readdirSync2(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
|
|
3808
4012
|
if (sqlFiles.length === 0) {
|
|
3809
4013
|
console.error(c6.yellow(`No .sql files in ${migrationsDir}`));
|
|
3810
4014
|
process.exit(0);
|
|
@@ -3832,13 +4036,14 @@ ${c6.bold("Extension types")}
|
|
|
3832
4036
|
mkdirSync2(join9(outputPath, ".."), { recursive: true });
|
|
3833
4037
|
writeFileSync2(outputPath, tsBody, "utf8");
|
|
3834
4038
|
console.log(`
|
|
3835
|
-
${c6.green("Written:")} ${
|
|
4039
|
+
${c6.green("Written:")} ${relative2(process.cwd(), outputPath) || outputPath}`);
|
|
3836
4040
|
console.log(c6.dim(" Add `.zveltio/` to your .gitignore. Regenerate with this command after every migration."));
|
|
3837
4041
|
console.log("");
|
|
3838
4042
|
}
|
|
3839
4043
|
|
|
3840
4044
|
// src/commands/extension-validate.ts
|
|
3841
|
-
import {
|
|
4045
|
+
import { createHash as createHash2 } from "crypto";
|
|
4046
|
+
import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync } from "fs";
|
|
3842
4047
|
import { join as join10 } from "path";
|
|
3843
4048
|
|
|
3844
4049
|
// ../sdk/dist/validate/migration-parse.js
|
|
@@ -3855,6 +4060,221 @@ function parseMigrationSql(raw) {
|
|
|
3855
4060
|
return { up, down: downBody.length > 0 ? downBody : null };
|
|
3856
4061
|
}
|
|
3857
4062
|
|
|
4063
|
+
// ../sdk/dist/validate/shared-message-keys.js
|
|
4064
|
+
var SHARED_MESSAGE_KEYS = new Set([
|
|
4065
|
+
"common.accessRestricted",
|
|
4066
|
+
"common.actions",
|
|
4067
|
+
"common.add",
|
|
4068
|
+
"common.added",
|
|
4069
|
+
"common.aiStudio",
|
|
4070
|
+
"common.approve",
|
|
4071
|
+
"common.assign",
|
|
4072
|
+
"common.bulkActions",
|
|
4073
|
+
"common.cancel",
|
|
4074
|
+
"common.cancelEdit",
|
|
4075
|
+
"common.clear",
|
|
4076
|
+
"common.clearSelection",
|
|
4077
|
+
"common.close",
|
|
4078
|
+
"common.col.account",
|
|
4079
|
+
"common.col.active",
|
|
4080
|
+
"common.col.address",
|
|
4081
|
+
"common.col.amount",
|
|
4082
|
+
"common.col.balance",
|
|
4083
|
+
"common.col.bank",
|
|
4084
|
+
"common.col.bcc",
|
|
4085
|
+
"common.col.category",
|
|
4086
|
+
"common.col.cc",
|
|
4087
|
+
"common.col.channel",
|
|
4088
|
+
"common.col.client",
|
|
4089
|
+
"common.col.code",
|
|
4090
|
+
"common.col.collection",
|
|
4091
|
+
"common.col.company",
|
|
4092
|
+
"common.col.created",
|
|
4093
|
+
"common.col.credit",
|
|
4094
|
+
"common.col.currency",
|
|
4095
|
+
"common.col.date",
|
|
4096
|
+
"common.col.days",
|
|
4097
|
+
"common.col.debit",
|
|
4098
|
+
"common.col.department",
|
|
4099
|
+
"common.col.description",
|
|
4100
|
+
"common.col.docNumber",
|
|
4101
|
+
"common.col.dueDate",
|
|
4102
|
+
"common.col.duration",
|
|
4103
|
+
"common.col.email",
|
|
4104
|
+
"common.col.employee",
|
|
4105
|
+
"common.col.enabled",
|
|
4106
|
+
"common.col.end",
|
|
4107
|
+
"common.col.event",
|
|
4108
|
+
"common.col.field",
|
|
4109
|
+
"common.col.file",
|
|
4110
|
+
"common.col.from",
|
|
4111
|
+
"common.col.hireDate",
|
|
4112
|
+
"common.col.host",
|
|
4113
|
+
"common.col.iban",
|
|
4114
|
+
"common.col.index",
|
|
4115
|
+
"common.col.key",
|
|
4116
|
+
"common.col.label",
|
|
4117
|
+
"common.col.leaveType",
|
|
4118
|
+
"common.col.locale",
|
|
4119
|
+
"common.col.message",
|
|
4120
|
+
"common.col.method",
|
|
4121
|
+
"common.col.model",
|
|
4122
|
+
"common.col.name",
|
|
4123
|
+
"common.col.notes",
|
|
4124
|
+
"common.col.number",
|
|
4125
|
+
"common.col.paid",
|
|
4126
|
+
"common.col.parent",
|
|
4127
|
+
"common.col.password",
|
|
4128
|
+
"common.col.path",
|
|
4129
|
+
"common.col.phone",
|
|
4130
|
+
"common.col.port",
|
|
4131
|
+
"common.col.position",
|
|
4132
|
+
"common.col.price",
|
|
4133
|
+
"common.col.priority",
|
|
4134
|
+
"common.col.product",
|
|
4135
|
+
"common.col.project",
|
|
4136
|
+
"common.col.provider",
|
|
4137
|
+
"common.col.quantity",
|
|
4138
|
+
"common.col.reason",
|
|
4139
|
+
"common.col.recipient",
|
|
4140
|
+
"common.col.records",
|
|
4141
|
+
"common.col.required",
|
|
4142
|
+
"common.col.resource",
|
|
4143
|
+
"common.col.role",
|
|
4144
|
+
"common.col.sku",
|
|
4145
|
+
"common.col.slug",
|
|
4146
|
+
"common.col.start",
|
|
4147
|
+
"common.col.status",
|
|
4148
|
+
"common.col.subject",
|
|
4149
|
+
"common.col.taxId",
|
|
4150
|
+
"common.col.template",
|
|
4151
|
+
"common.col.ticket",
|
|
4152
|
+
"common.col.time",
|
|
4153
|
+
"common.col.title",
|
|
4154
|
+
"common.col.to",
|
|
4155
|
+
"common.col.total",
|
|
4156
|
+
"common.col.type",
|
|
4157
|
+
"common.col.unique",
|
|
4158
|
+
"common.col.unit",
|
|
4159
|
+
"common.col.user",
|
|
4160
|
+
"common.col.username",
|
|
4161
|
+
"common.col.validUntil",
|
|
4162
|
+
"common.col.value",
|
|
4163
|
+
"common.col.vat",
|
|
4164
|
+
"common.col.vendor",
|
|
4165
|
+
"common.col.version",
|
|
4166
|
+
"common.col.warehouse",
|
|
4167
|
+
"common.col.year",
|
|
4168
|
+
"common.collectionRequired",
|
|
4169
|
+
"common.confirm",
|
|
4170
|
+
"common.copied",
|
|
4171
|
+
"common.copy",
|
|
4172
|
+
"common.copyShort",
|
|
4173
|
+
"common.create",
|
|
4174
|
+
"common.delete",
|
|
4175
|
+
"common.deleteCount",
|
|
4176
|
+
"common.deleteFailed",
|
|
4177
|
+
"common.discard",
|
|
4178
|
+
"common.dismiss",
|
|
4179
|
+
"common.dismissNotification",
|
|
4180
|
+
"common.done",
|
|
4181
|
+
"common.download",
|
|
4182
|
+
"common.edit",
|
|
4183
|
+
"common.emptyDash",
|
|
4184
|
+
"common.failed",
|
|
4185
|
+
"common.fields",
|
|
4186
|
+
"common.filter.admin",
|
|
4187
|
+
"common.filter.all",
|
|
4188
|
+
"common.filter.allStatuses",
|
|
4189
|
+
"common.filter.auth",
|
|
4190
|
+
"common.filter.data",
|
|
4191
|
+
"common.format",
|
|
4192
|
+
"common.generate",
|
|
4193
|
+
"common.loadFailed",
|
|
4194
|
+
"common.loading",
|
|
4195
|
+
"common.logout",
|
|
4196
|
+
"common.manage",
|
|
4197
|
+
"common.mergeFailed",
|
|
4198
|
+
"common.nameRequired",
|
|
4199
|
+
"common.new",
|
|
4200
|
+
"common.next",
|
|
4201
|
+
"common.no",
|
|
4202
|
+
"common.noResults",
|
|
4203
|
+
"common.notFound",
|
|
4204
|
+
"common.op.delete",
|
|
4205
|
+
"common.op.insert",
|
|
4206
|
+
"common.op.update",
|
|
4207
|
+
"common.open",
|
|
4208
|
+
"common.optional",
|
|
4209
|
+
"common.optionalParen",
|
|
4210
|
+
"common.other",
|
|
4211
|
+
"common.pageOf",
|
|
4212
|
+
"common.post",
|
|
4213
|
+
"common.prev",
|
|
4214
|
+
"common.publish",
|
|
4215
|
+
"common.rangeOf",
|
|
4216
|
+
"common.refresh",
|
|
4217
|
+
"common.reject",
|
|
4218
|
+
"common.remove",
|
|
4219
|
+
"common.rename",
|
|
4220
|
+
"common.reorder",
|
|
4221
|
+
"common.required",
|
|
4222
|
+
"common.resetFailed",
|
|
4223
|
+
"common.retry",
|
|
4224
|
+
"common.revoke",
|
|
4225
|
+
"common.save",
|
|
4226
|
+
"common.saveFailed",
|
|
4227
|
+
"common.saved",
|
|
4228
|
+
"common.saving",
|
|
4229
|
+
"common.search",
|
|
4230
|
+
"common.selectCollection",
|
|
4231
|
+
"common.selectPlaceholder",
|
|
4232
|
+
"common.selected",
|
|
4233
|
+
"common.send",
|
|
4234
|
+
"common.somethingWrong",
|
|
4235
|
+
"common.status.approved",
|
|
4236
|
+
"common.status.cancelled",
|
|
4237
|
+
"common.status.draft",
|
|
4238
|
+
"common.status.pending",
|
|
4239
|
+
"common.status.reimbursed",
|
|
4240
|
+
"common.status.rejected",
|
|
4241
|
+
"common.status.submitted",
|
|
4242
|
+
"common.submit",
|
|
4243
|
+
"common.sync",
|
|
4244
|
+
"common.testConnection",
|
|
4245
|
+
"common.typeToSearch",
|
|
4246
|
+
"common.undo",
|
|
4247
|
+
"common.unexpectedError",
|
|
4248
|
+
"common.unknownError",
|
|
4249
|
+
"common.upload",
|
|
4250
|
+
"common.uploadFailed",
|
|
4251
|
+
"common.uploading",
|
|
4252
|
+
"common.verifyingPermissions",
|
|
4253
|
+
"common.view",
|
|
4254
|
+
"common.viewAll",
|
|
4255
|
+
"common.yes",
|
|
4256
|
+
"ext.approved",
|
|
4257
|
+
"ext.confirm.deleteForm",
|
|
4258
|
+
"ext.confirm.deleteNamed",
|
|
4259
|
+
"ext.confirm.removeSearchIndex",
|
|
4260
|
+
"ext.confirmDelete",
|
|
4261
|
+
"ext.copied",
|
|
4262
|
+
"ext.copyFailed",
|
|
4263
|
+
"ext.created",
|
|
4264
|
+
"ext.deleted",
|
|
4265
|
+
"ext.errorPrefix",
|
|
4266
|
+
"ext.linkCopied",
|
|
4267
|
+
"ext.loadFailed",
|
|
4268
|
+
"ext.rejected",
|
|
4269
|
+
"ext.reveal.copy",
|
|
4270
|
+
"ext.reveal.note",
|
|
4271
|
+
"ext.reveal.title",
|
|
4272
|
+
"ext.saveFailed",
|
|
4273
|
+
"ext.saved",
|
|
4274
|
+
"ext.sent",
|
|
4275
|
+
"ext.submitted"
|
|
4276
|
+
]);
|
|
4277
|
+
|
|
3858
4278
|
// ../sdk/dist/validate/index.js
|
|
3859
4279
|
var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/;
|
|
3860
4280
|
var KNOWN_CATEGORIES = new Set([
|
|
@@ -3890,6 +4310,9 @@ var SAFE_VERSION_RANGE = /^[\d.*^~>=<| -]+$/;
|
|
|
3890
4310
|
function err(code, message, file) {
|
|
3891
4311
|
return file ? { code, message, file } : { code, message };
|
|
3892
4312
|
}
|
|
4313
|
+
function warn(code, message, file) {
|
|
4314
|
+
return { ...err(code, message, file), severity: "warning" };
|
|
4315
|
+
}
|
|
3893
4316
|
function validateManifest(input) {
|
|
3894
4317
|
const out = [];
|
|
3895
4318
|
const m = input.manifest;
|
|
@@ -3917,7 +4340,7 @@ function validateManifest(input) {
|
|
|
3917
4340
|
out.push(err("MANIFEST_BAD_MAX_VERSION", `zveltioMaxVersion "${obj.zveltioMaxVersion}" is not valid semver`));
|
|
3918
4341
|
}
|
|
3919
4342
|
if (typeof obj.category === "string" && !KNOWN_CATEGORIES.has(obj.category)) {
|
|
3920
|
-
out.push(
|
|
4343
|
+
out.push(warn("MANIFEST_UNKNOWN_CATEGORY", `category "${obj.category}" isn't in the known set \u2014 typo? Known: ${[...KNOWN_CATEGORIES].sort().join(", ")}`));
|
|
3921
4344
|
}
|
|
3922
4345
|
return out;
|
|
3923
4346
|
}
|
|
@@ -3949,12 +4372,111 @@ function hasDestructiveDdl(upSql) {
|
|
|
3949
4372
|
return true;
|
|
3950
4373
|
if (/\bDROP\s+COLUMN\b/i.test(s))
|
|
3951
4374
|
return true;
|
|
3952
|
-
if (/\bALTER\s+COLUMN\b[\s\S]*?\bDROP\s+(NOT\s+NULL|DEFAULT)\b/i.test(s))
|
|
3953
|
-
return true;
|
|
3954
4375
|
return false;
|
|
3955
4376
|
}
|
|
4377
|
+
function userRefUuidColumns(upSql) {
|
|
4378
|
+
const s = upSql.replace(/--[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
4379
|
+
const found = new Set;
|
|
4380
|
+
for (const m of s.matchAll(/(\b[a-z_][a-z0-9_]*_by)\s+UUID\b/gi)) {
|
|
4381
|
+
found.add(m[1].toLowerCase());
|
|
4382
|
+
}
|
|
4383
|
+
for (const m of s.matchAll(/ALTER\s+COLUMN\s+(\b[a-z_][a-z0-9_]*_by)\s+TYPE\s+UUID\b/gi)) {
|
|
4384
|
+
found.add(m[1].toLowerCase());
|
|
4385
|
+
}
|
|
4386
|
+
return [...found];
|
|
4387
|
+
}
|
|
4388
|
+
function userRefFixedColumns(upSql) {
|
|
4389
|
+
const s = upSql.replace(/--[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
4390
|
+
return [
|
|
4391
|
+
...new Set([...s.matchAll(/ALTER\s+COLUMN\s+(\b[a-z_][a-z0-9_]*_by)\s+TYPE\s+TEXT\b/gi)].map((m) => m[1].toLowerCase()))
|
|
4392
|
+
];
|
|
4393
|
+
}
|
|
4394
|
+
function createTableColumnDefs(sql) {
|
|
4395
|
+
const out = [];
|
|
4396
|
+
const re = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[^(]*\(/gi;
|
|
4397
|
+
let m;
|
|
4398
|
+
while ((m = re.exec(sql)) !== null) {
|
|
4399
|
+
let depth = 1;
|
|
4400
|
+
let item = "";
|
|
4401
|
+
let i = m.index + m[0].length;
|
|
4402
|
+
for (;i < sql.length && depth > 0; i++) {
|
|
4403
|
+
const ch = sql[i];
|
|
4404
|
+
if (ch === "(")
|
|
4405
|
+
depth++;
|
|
4406
|
+
else if (ch === ")") {
|
|
4407
|
+
depth--;
|
|
4408
|
+
if (depth === 0)
|
|
4409
|
+
break;
|
|
4410
|
+
}
|
|
4411
|
+
if (depth === 1 && ch === ",") {
|
|
4412
|
+
out.push(item);
|
|
4413
|
+
item = "";
|
|
4414
|
+
continue;
|
|
4415
|
+
}
|
|
4416
|
+
item += ch;
|
|
4417
|
+
}
|
|
4418
|
+
out.push(item);
|
|
4419
|
+
re.lastIndex = i;
|
|
4420
|
+
}
|
|
4421
|
+
return out;
|
|
4422
|
+
}
|
|
4423
|
+
function columnLevelUniqueColumns(sql) {
|
|
4424
|
+
const out = [];
|
|
4425
|
+
for (const def of createTableColumnDefs(sql)) {
|
|
4426
|
+
const t = def.trim();
|
|
4427
|
+
const bare = t.replace(/'(?:[^']|'')*'/g, "''");
|
|
4428
|
+
if (!/\bUNIQUE\b(?!\s*\()/i.test(bare))
|
|
4429
|
+
continue;
|
|
4430
|
+
const name = /^([a-z_][a-z0-9_]*)\s+\S/i.exec(t)?.[1];
|
|
4431
|
+
if (!name)
|
|
4432
|
+
continue;
|
|
4433
|
+
if (/^(?:primary|unique|constraint|foreign|check|exclude|like)$/i.test(name))
|
|
4434
|
+
continue;
|
|
4435
|
+
out.push(name);
|
|
4436
|
+
}
|
|
4437
|
+
return out;
|
|
4438
|
+
}
|
|
4439
|
+
function keyColumnSets(upSql) {
|
|
4440
|
+
const s = upSql.replace(/--[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
4441
|
+
const sets = [];
|
|
4442
|
+
const add = (cols) => sets.push(cols.split(",").map((c7) => c7.trim().replace(/["`]/g, "").toLowerCase()).filter(Boolean));
|
|
4443
|
+
for (const m of s.matchAll(/\bUNIQUE\s*\(([^)]+)\)/gi))
|
|
4444
|
+
add(m[1]);
|
|
4445
|
+
for (const m of s.matchAll(/\bPRIMARY\s+KEY\s*\(([^)]+)\)/gi))
|
|
4446
|
+
add(m[1]);
|
|
4447
|
+
for (const m of s.matchAll(/CREATE\s+UNIQUE\s+INDEX[^(]*\(([^)]+)\)/gi))
|
|
4448
|
+
add(m[1]);
|
|
4449
|
+
for (const col of columnLevelUniqueColumns(s))
|
|
4450
|
+
add(col);
|
|
4451
|
+
for (const m of s.matchAll(/\bADD\s+COLUMN\s+(?:IF\s+NOT\s+EXISTS\s+)?([a-z_][a-z0-9_]*)[^,;\n]*?\bUNIQUE\b(?!\s*\()/gi))
|
|
4452
|
+
add(m[1]);
|
|
4453
|
+
return sets;
|
|
4454
|
+
}
|
|
4455
|
+
function narrowKey(parts) {
|
|
4456
|
+
if (parts.length === 0)
|
|
4457
|
+
return null;
|
|
4458
|
+
if (parts.includes("tenant_id"))
|
|
4459
|
+
return null;
|
|
4460
|
+
if (parts.length === 1 && parts[0] === "id")
|
|
4461
|
+
return null;
|
|
4462
|
+
if (parts.some((p) => /token|hash|secret/.test(p)))
|
|
4463
|
+
return null;
|
|
4464
|
+
if (parts.some((p) => /_id$/.test(p)))
|
|
4465
|
+
return null;
|
|
4466
|
+
return [...parts].sort().join(", ");
|
|
4467
|
+
}
|
|
4468
|
+
function widenedKey(parts) {
|
|
4469
|
+
if (!parts.includes("tenant_id"))
|
|
4470
|
+
return null;
|
|
4471
|
+
const rest = parts.filter((p) => p !== "tenant_id").sort();
|
|
4472
|
+
return rest.length > 0 ? rest.join(", ") : null;
|
|
4473
|
+
}
|
|
3956
4474
|
function validateMigrations(input) {
|
|
3957
4475
|
const out = [];
|
|
4476
|
+
const narrowKeys = [];
|
|
4477
|
+
const widened = new Set;
|
|
4478
|
+
const uuidRefs = [];
|
|
4479
|
+
const fixedRefs = new Set;
|
|
3958
4480
|
for (const file of input.files) {
|
|
3959
4481
|
if (file.sql.trim() === "") {
|
|
3960
4482
|
out.push(err("MIGRATION_EMPTY", "migration file is empty", file.filename));
|
|
@@ -3974,6 +4496,25 @@ function validateMigrations(input) {
|
|
|
3974
4496
|
if (input.requireDownForDestructive && hasDestructiveDdl(parsed.up) && !parsed.down) {
|
|
3975
4497
|
out.push(err("MIGRATION_DESTRUCTIVE_NO_DOWN", 'migration contains destructive DDL (DROP TABLE/COLUMN or ALTER ... DROP) but has no "-- DOWN" section. Add one so uninstall --purgeData can roll back.', file.filename));
|
|
3976
4498
|
}
|
|
4499
|
+
narrowKeys.push(...keyColumnSets(parsed.up).map(narrowKey).filter((k) => k !== null).map((cols) => ({ cols, filename: file.filename })));
|
|
4500
|
+
for (const parts of keyColumnSets(parsed.up)) {
|
|
4501
|
+
const wide = widenedKey(parts);
|
|
4502
|
+
if (wide)
|
|
4503
|
+
widened.add(wide);
|
|
4504
|
+
}
|
|
4505
|
+
uuidRefs.push(...userRefUuidColumns(parsed.up).map((column) => ({ column, filename: file.filename })));
|
|
4506
|
+
for (const column of userRefFixedColumns(parsed.up))
|
|
4507
|
+
fixedRefs.add(column);
|
|
4508
|
+
}
|
|
4509
|
+
for (const { column, filename } of uuidRefs) {
|
|
4510
|
+
if (fixedRefs.has(column))
|
|
4511
|
+
continue;
|
|
4512
|
+
out.push(err("SCHEMA_USER_REF_UUID", `"${column}" is declared UUID and its name says it records a user. ` + '`"user".id` is a 32-character nanoid, so Postgres rejects every write with ' + '22P02 "invalid input syntax for type uuid" \u2014 the route then fails for ' + "everybody, always, with an error naming the type rather than the cause. " + "Declare it TEXT.", filename));
|
|
4513
|
+
}
|
|
4514
|
+
for (const { cols, filename } of narrowKeys) {
|
|
4515
|
+
if (widened.has(cols))
|
|
4516
|
+
continue;
|
|
4517
|
+
out.push(err("SCHEMA_UNIQUE_WITHOUT_TENANT", `UNIQUE (${cols}) leaves out tenant_id. Several companies share one ` + "instance, so the second one to use a value is refused it \u2014 its own invoice " + "number, its own product code, its own fiscal year \u2014 and row-level security " + "hides the conflicting row, so it is shown a database error about something " + `it cannot see and cannot resolve. Use UNIQUE (tenant_id, ${cols}) unless ` + "the value must be unique across the whole instance, as a token or a hash is.", filename));
|
|
3977
4518
|
}
|
|
3978
4519
|
return out;
|
|
3979
4520
|
}
|
|
@@ -3997,6 +4538,215 @@ function validateBundleSize(input) {
|
|
|
3997
4538
|
}
|
|
3998
4539
|
return [];
|
|
3999
4540
|
}
|
|
4541
|
+
var SDUI_SHARED_READ_ALLOWLIST = new Set([
|
|
4542
|
+
"/api/collections",
|
|
4543
|
+
"/api/views"
|
|
4544
|
+
]);
|
|
4545
|
+
function normalizeRoute(p) {
|
|
4546
|
+
return p.split("?")[0].replace(/\{[^}]+\}/g, "*").replace(/:[^/]+/g, "*").replace(/\/+$/, "") || "/";
|
|
4547
|
+
}
|
|
4548
|
+
var SDUI_I18N_SLOTS = [
|
|
4549
|
+
"title",
|
|
4550
|
+
"subtitle",
|
|
4551
|
+
"label",
|
|
4552
|
+
"placeholder",
|
|
4553
|
+
"note",
|
|
4554
|
+
"confirm",
|
|
4555
|
+
"description",
|
|
4556
|
+
"emptyText",
|
|
4557
|
+
"emptyTitle",
|
|
4558
|
+
"hint"
|
|
4559
|
+
];
|
|
4560
|
+
var I18N_UNIVERSAL = new Set([
|
|
4561
|
+
"API",
|
|
4562
|
+
"JSON",
|
|
4563
|
+
"NDJSON",
|
|
4564
|
+
"CSV",
|
|
4565
|
+
"SQL",
|
|
4566
|
+
"URL",
|
|
4567
|
+
"UUID",
|
|
4568
|
+
"ID",
|
|
4569
|
+
"HTTP",
|
|
4570
|
+
"HTTPS",
|
|
4571
|
+
"GET",
|
|
4572
|
+
"POST",
|
|
4573
|
+
"PUT",
|
|
4574
|
+
"PATCH",
|
|
4575
|
+
"DELETE",
|
|
4576
|
+
"Zveltio",
|
|
4577
|
+
"OK",
|
|
4578
|
+
"UTC",
|
|
4579
|
+
"AI",
|
|
4580
|
+
"SDK",
|
|
4581
|
+
"CLI",
|
|
4582
|
+
"RLS",
|
|
4583
|
+
"DB",
|
|
4584
|
+
"PDF",
|
|
4585
|
+
"XML",
|
|
4586
|
+
"YAML"
|
|
4587
|
+
]);
|
|
4588
|
+
var I18N_PRODUCT_NAMES = new Set([
|
|
4589
|
+
"HubSpot",
|
|
4590
|
+
"Notion",
|
|
4591
|
+
"Airtable",
|
|
4592
|
+
"Okta",
|
|
4593
|
+
"Entra ID",
|
|
4594
|
+
"Slack",
|
|
4595
|
+
"Stripe",
|
|
4596
|
+
"Salesforce",
|
|
4597
|
+
"ANAF"
|
|
4598
|
+
]);
|
|
4599
|
+
function isProductNameList(s) {
|
|
4600
|
+
const parts = s.split(/[/,\u00B7|]/).map((p) => p.trim()).filter(Boolean);
|
|
4601
|
+
return parts.length > 0 && parts.every((p) => I18N_PRODUCT_NAMES.has(p));
|
|
4602
|
+
}
|
|
4603
|
+
function looksLikeMessageKey(s) {
|
|
4604
|
+
return /^[a-z][a-z0-9]*(\.[a-z0-9_]+)+$/i.test(s);
|
|
4605
|
+
}
|
|
4606
|
+
function looksTranslatable(s) {
|
|
4607
|
+
const t = s.trim();
|
|
4608
|
+
if (t.length < 3)
|
|
4609
|
+
return false;
|
|
4610
|
+
if (I18N_UNIVERSAL.has(t))
|
|
4611
|
+
return false;
|
|
4612
|
+
if (isProductNameList(t))
|
|
4613
|
+
return false;
|
|
4614
|
+
if (!/[a-z]{2}/.test(t))
|
|
4615
|
+
return false;
|
|
4616
|
+
if (/^[a-z0-9_.\-/]+$/.test(t))
|
|
4617
|
+
return false;
|
|
4618
|
+
if (/^[a-z0-9_]+=/.test(t))
|
|
4619
|
+
return false;
|
|
4620
|
+
if (t.startsWith("http"))
|
|
4621
|
+
return false;
|
|
4622
|
+
return /^[A-Z]/.test(t) || t.split(/\s+/).length > 1;
|
|
4623
|
+
}
|
|
4624
|
+
function collectI18nStrings(node, acc) {
|
|
4625
|
+
if (Array.isArray(node)) {
|
|
4626
|
+
for (const x of node)
|
|
4627
|
+
collectI18nStrings(x, acc);
|
|
4628
|
+
return;
|
|
4629
|
+
}
|
|
4630
|
+
if (node && typeof node === "object") {
|
|
4631
|
+
for (const [k, v] of Object.entries(node)) {
|
|
4632
|
+
if (SDUI_I18N_SLOTS.includes(k) && typeof v === "string" && v)
|
|
4633
|
+
acc.push({ slot: k, value: v });
|
|
4634
|
+
else
|
|
4635
|
+
collectI18nStrings(v, acc);
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4638
|
+
}
|
|
4639
|
+
function collectEndpoints(node, acc) {
|
|
4640
|
+
if (Array.isArray(node)) {
|
|
4641
|
+
for (const x of node)
|
|
4642
|
+
collectEndpoints(x, acc);
|
|
4643
|
+
return;
|
|
4644
|
+
}
|
|
4645
|
+
if (node && typeof node === "object") {
|
|
4646
|
+
for (const [k, v] of Object.entries(node)) {
|
|
4647
|
+
if ((k === "dataSource" || k === "endpoint" || k === "saveEndpoint" || k === "catalogDataSource" || k === "loadEndpoint") && typeof v === "string")
|
|
4648
|
+
acc.push(v);
|
|
4649
|
+
else
|
|
4650
|
+
collectEndpoints(v, acc);
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
function validateSduiSchema(input) {
|
|
4655
|
+
const out = [];
|
|
4656
|
+
const { schema, extName, file } = input;
|
|
4657
|
+
if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
|
|
4658
|
+
out.push(err("SDUI_NOT_OBJECT", "schema must be a JSON object", file));
|
|
4659
|
+
return out;
|
|
4660
|
+
}
|
|
4661
|
+
const s = schema;
|
|
4662
|
+
if (typeof s.title !== "string" || !s.title)
|
|
4663
|
+
out.push(err("SDUI_NO_TITLE", 'schema missing "title"', file));
|
|
4664
|
+
if (s.kind === "settings") {
|
|
4665
|
+
if (typeof s.dataSource !== "string" || typeof s.saveEndpoint !== "string")
|
|
4666
|
+
out.push(err("SDUI_SETTINGS_SHAPE", "settings schema needs dataSource + saveEndpoint", file));
|
|
4667
|
+
} else if (!Array.isArray(s.resources) || s.resources.length === 0) {
|
|
4668
|
+
out.push(err("SDUI_NO_RESOURCES", 'page schema needs a non-empty "resources" array', file));
|
|
4669
|
+
}
|
|
4670
|
+
const mount = `/ext/${extName}`;
|
|
4671
|
+
const depMounts = (input.dependencies ?? []).map((d) => `/ext/${d}`);
|
|
4672
|
+
const provided = input.providedRoutes ? new Set(input.providedRoutes.map(normalizeRoute)) : undefined;
|
|
4673
|
+
const inMount = (bare, m) => bare === m || bare.startsWith(`${m}/`);
|
|
4674
|
+
const endpoints = [];
|
|
4675
|
+
collectEndpoints(schema, endpoints);
|
|
4676
|
+
for (const raw of endpoints) {
|
|
4677
|
+
const bare = raw.split("?")[0];
|
|
4678
|
+
if (SDUI_SHARED_READ_ALLOWLIST.has(bare))
|
|
4679
|
+
continue;
|
|
4680
|
+
if (depMounts.some((m) => inMount(bare, m)))
|
|
4681
|
+
continue;
|
|
4682
|
+
if (!inMount(bare, mount)) {
|
|
4683
|
+
out.push(err("SDUI_ENDPOINT_FOREIGN", `endpoint "${raw}" is outside this extension's namespace "${mount}/" (or a declared dependency, or the shared-read allowlist). Schemas may only call their own routes.`, file));
|
|
4684
|
+
continue;
|
|
4685
|
+
}
|
|
4686
|
+
if (provided) {
|
|
4687
|
+
const rel = normalizeRoute(bare.slice(mount.length) || "/");
|
|
4688
|
+
if (!isProvidedRoute(provided, rel)) {
|
|
4689
|
+
out.push(err("SDUI_ENDPOINT_UNKNOWN", `endpoint "${raw}" resolves to "${rel}", which the engine does not serve. Provided routes: ${[...provided].sort().join(", ") || "(none found)"}.`, file));
|
|
4690
|
+
}
|
|
4691
|
+
}
|
|
4692
|
+
}
|
|
4693
|
+
if (input.messageKeys) {
|
|
4694
|
+
const known = new Set(input.messageKeys);
|
|
4695
|
+
const strings = [];
|
|
4696
|
+
collectI18nStrings(schema, strings);
|
|
4697
|
+
for (const { slot, value } of strings) {
|
|
4698
|
+
if (known.has(value))
|
|
4699
|
+
continue;
|
|
4700
|
+
if (looksLikeMessageKey(value)) {
|
|
4701
|
+
out.push(err("SDUI_I18N_KEY_MISSING", `${slot} "${value}" is not a known message key. The host renders an unknown key as literal text, so this reaches the user as-is. Add it to studio/messages/{locale}.json, or use a shared common.* key.`, file));
|
|
4702
|
+
} else if (looksTranslatable(value)) {
|
|
4703
|
+
out.push(warn("SDUI_I18N_HARDCODED", `${slot} "${value}" is hardcoded text, not a message key \u2014 it stays English in all nine locales. Move it to studio/messages/{locale}.json.`, file));
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
return out;
|
|
4708
|
+
}
|
|
4709
|
+
function joinRoutePath(prefix, child) {
|
|
4710
|
+
if (!child || child === "/")
|
|
4711
|
+
return prefix || "/";
|
|
4712
|
+
const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
4713
|
+
return child.startsWith("/") ? `${p}${child}` : `${p}/${child}`;
|
|
4714
|
+
}
|
|
4715
|
+
function isProvidedRoute(provided, rel) {
|
|
4716
|
+
if (provided.has(rel))
|
|
4717
|
+
return true;
|
|
4718
|
+
for (const p of provided) {
|
|
4719
|
+
if (!p.endsWith("/*"))
|
|
4720
|
+
continue;
|
|
4721
|
+
const base = p.slice(0, -2);
|
|
4722
|
+
if (!base || base === "/")
|
|
4723
|
+
continue;
|
|
4724
|
+
if (rel === base || rel.startsWith(`${base}/`))
|
|
4725
|
+
return true;
|
|
4726
|
+
}
|
|
4727
|
+
return false;
|
|
4728
|
+
}
|
|
4729
|
+
function extractEngineRoutes(engineSourceTexts) {
|
|
4730
|
+
const routes = new Set;
|
|
4731
|
+
const methodRe = /\.(?:get|post|put|patch|delete)\(\s*['"`]([^'"`]+)['"`]/g;
|
|
4732
|
+
const mountRe = /\.route\(\s*['"`]([^'"`]+)['"`]/g;
|
|
4733
|
+
for (const text of engineSourceTexts) {
|
|
4734
|
+
let m;
|
|
4735
|
+
while ((m = methodRe.exec(text)) !== null) {
|
|
4736
|
+
if (m[1].startsWith("/"))
|
|
4737
|
+
routes.add(m[1]);
|
|
4738
|
+
}
|
|
4739
|
+
while ((m = mountRe.exec(text)) !== null) {
|
|
4740
|
+
const prefix = m[1];
|
|
4741
|
+
if (!prefix.startsWith("/"))
|
|
4742
|
+
continue;
|
|
4743
|
+
routes.add(prefix);
|
|
4744
|
+
if (prefix !== "/")
|
|
4745
|
+
routes.add(joinRoutePath(prefix, "/*"));
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
return [...routes];
|
|
4749
|
+
}
|
|
4000
4750
|
function validateExtension(input) {
|
|
4001
4751
|
const errors = [
|
|
4002
4752
|
...validateManifest(input.manifest),
|
|
@@ -4006,7 +4756,7 @@ function validateExtension(input) {
|
|
|
4006
4756
|
...input.bundleSize ? validateBundleSize(input.bundleSize) : []
|
|
4007
4757
|
];
|
|
4008
4758
|
return {
|
|
4009
|
-
ok: errors.
|
|
4759
|
+
ok: errors.every((e) => e.severity === "warning"),
|
|
4010
4760
|
errors,
|
|
4011
4761
|
stats: {
|
|
4012
4762
|
tables: input.stats.tables,
|
|
@@ -4017,6 +4767,7 @@ function validateExtension(input) {
|
|
|
4017
4767
|
}
|
|
4018
4768
|
|
|
4019
4769
|
// src/commands/extension-validate.ts
|
|
4770
|
+
init_extension_pack();
|
|
4020
4771
|
var c7 = {
|
|
4021
4772
|
bold: (s) => `\x1B[1m${s}\x1B[0m`,
|
|
4022
4773
|
green: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
@@ -4034,14 +4785,16 @@ var PEER_DEPS_ALLOWLIST = new Set([
|
|
|
4034
4785
|
"@aws-sdk/s3-request-presigner",
|
|
4035
4786
|
"nanoid",
|
|
4036
4787
|
"qrcode",
|
|
4788
|
+
"pdf-lib",
|
|
4789
|
+
"@pdf-lib/fontkit",
|
|
4037
4790
|
"pdfkit",
|
|
4038
4791
|
"graphql"
|
|
4039
4792
|
]);
|
|
4040
4793
|
function recursiveSize(dir) {
|
|
4041
4794
|
let total = 0;
|
|
4042
|
-
if (!
|
|
4795
|
+
if (!existsSync11(dir))
|
|
4043
4796
|
return 0;
|
|
4044
|
-
for (const name of
|
|
4797
|
+
for (const name of readdirSync3(dir)) {
|
|
4045
4798
|
const full = join10(dir, name);
|
|
4046
4799
|
if (name === "node_modules" || name === ".zveltio" || name === "dist")
|
|
4047
4800
|
continue;
|
|
@@ -4058,14 +4811,119 @@ function recursiveSize(dir) {
|
|
|
4058
4811
|
}
|
|
4059
4812
|
return total;
|
|
4060
4813
|
}
|
|
4814
|
+
function readEngineSources(dir) {
|
|
4815
|
+
const bundled = join10(dir, "engine", "index.js");
|
|
4816
|
+
if (existsSync11(bundled)) {
|
|
4817
|
+
try {
|
|
4818
|
+
return [readFileSync4(bundled, "utf8")];
|
|
4819
|
+
} catch {}
|
|
4820
|
+
}
|
|
4821
|
+
const out = [];
|
|
4822
|
+
const walk = (d) => {
|
|
4823
|
+
if (!existsSync11(d))
|
|
4824
|
+
return;
|
|
4825
|
+
for (const name of readdirSync3(d)) {
|
|
4826
|
+
if (name === "migrations" || name === "node_modules")
|
|
4827
|
+
continue;
|
|
4828
|
+
const full = join10(d, name);
|
|
4829
|
+
let st;
|
|
4830
|
+
try {
|
|
4831
|
+
st = statSync(full);
|
|
4832
|
+
} catch {
|
|
4833
|
+
continue;
|
|
4834
|
+
}
|
|
4835
|
+
if (st.isDirectory())
|
|
4836
|
+
walk(full);
|
|
4837
|
+
else if (/\.(ts|js)$/.test(name)) {
|
|
4838
|
+
try {
|
|
4839
|
+
out.push(readFileSync4(full, "utf8"));
|
|
4840
|
+
} catch {}
|
|
4841
|
+
}
|
|
4842
|
+
}
|
|
4843
|
+
};
|
|
4844
|
+
walk(join10(dir, "engine"));
|
|
4845
|
+
return out;
|
|
4846
|
+
}
|
|
4847
|
+
function readOwnMessageKeys(dir) {
|
|
4848
|
+
const path = join10(dir, "studio", "messages", "en.json");
|
|
4849
|
+
if (!existsSync11(path))
|
|
4850
|
+
return;
|
|
4851
|
+
try {
|
|
4852
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
4853
|
+
return new Set(Object.keys(parsed));
|
|
4854
|
+
} catch {
|
|
4855
|
+
return;
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
function readDependencyMessageKeys(dir, extName, deps) {
|
|
4859
|
+
const out = new Set;
|
|
4860
|
+
const depth = extName.split("/").length;
|
|
4861
|
+
const root = join10(dir, ...Array(depth).fill(".."));
|
|
4862
|
+
for (const dep of deps) {
|
|
4863
|
+
const path = join10(root, dep, "studio", "messages", "en.json");
|
|
4864
|
+
if (!existsSync11(path))
|
|
4865
|
+
continue;
|
|
4866
|
+
try {
|
|
4867
|
+
for (const k of Object.keys(JSON.parse(readFileSync4(path, "utf8"))))
|
|
4868
|
+
out.add(k);
|
|
4869
|
+
} catch {}
|
|
4870
|
+
}
|
|
4871
|
+
return out;
|
|
4872
|
+
}
|
|
4061
4873
|
function inferExpectedName(dir) {
|
|
4062
4874
|
const norm = dir.replace(/\\/g, "/");
|
|
4063
|
-
const
|
|
4875
|
+
const marker = "zveltio-extensions/";
|
|
4876
|
+
const idx = norm.lastIndexOf(marker);
|
|
4064
4877
|
if (idx >= 0) {
|
|
4065
|
-
return norm.slice(idx +
|
|
4878
|
+
return norm.slice(idx + marker.length).replace(/\/$/, "");
|
|
4066
4879
|
}
|
|
4067
4880
|
return norm.split("/").filter(Boolean).pop() ?? "";
|
|
4068
4881
|
}
|
|
4882
|
+
function checkBundleIntegrity(dir, manifest) {
|
|
4883
|
+
const bundle = join10(dir, "engine", "index.js");
|
|
4884
|
+
const integrity = manifest?.integrity;
|
|
4885
|
+
if (!integrity)
|
|
4886
|
+
return [];
|
|
4887
|
+
if (!existsSync11(bundle)) {
|
|
4888
|
+
return integrity.engineSha256 && existsSync11(join10(dir, "engine", "index.ts")) ? [
|
|
4889
|
+
{
|
|
4890
|
+
code: "BUNDLE_MISSING",
|
|
4891
|
+
message: "manifest records integrity.engineSha256 but engine/index.js does not exist. " + "Run `zveltio extension pack`.",
|
|
4892
|
+
file: "engine/index.js"
|
|
4893
|
+
}
|
|
4894
|
+
] : [];
|
|
4895
|
+
}
|
|
4896
|
+
const out = [];
|
|
4897
|
+
if (integrity.engineSha256) {
|
|
4898
|
+
const actual = createHash2("sha256").update(readFileSync4(bundle)).digest("hex");
|
|
4899
|
+
if (actual !== integrity.engineSha256) {
|
|
4900
|
+
out.push({
|
|
4901
|
+
code: "BUNDLE_HASH_MISMATCH",
|
|
4902
|
+
message: `engine/index.js (sha256 ${actual.slice(0, 12)}\u2026) does not match ` + `integrity.engineSha256 (${integrity.engineSha256.slice(0, 12)}\u2026). ` + "Re-pack \u2014 the engine refuses a bundle whose hash disagrees with its manifest.",
|
|
4903
|
+
file: "engine/index.js"
|
|
4904
|
+
});
|
|
4905
|
+
}
|
|
4906
|
+
}
|
|
4907
|
+
if (!existsSync11(join10(dir, "engine", "index.ts")))
|
|
4908
|
+
return out;
|
|
4909
|
+
if (!integrity.sourceSha256) {
|
|
4910
|
+
out.push({
|
|
4911
|
+
code: "BUNDLE_SOURCE_UNRECORDED",
|
|
4912
|
+
severity: "warning",
|
|
4913
|
+
message: "manifest has no integrity.sourceSha256, so nothing can tell whether the bundle " + "was built from the committed source. Re-pack to record it.",
|
|
4914
|
+
file: "manifest.json"
|
|
4915
|
+
});
|
|
4916
|
+
return out;
|
|
4917
|
+
}
|
|
4918
|
+
if (hashEngineSources(dir) !== integrity.sourceSha256) {
|
|
4919
|
+
out.push({
|
|
4920
|
+
code: "BUNDLE_OLDER_THAN_SOURCE",
|
|
4921
|
+
message: "engine/index.js was NOT built from the current engine/ sources, so the changes in " + "them do not run anywhere. Run `zveltio extension pack`.",
|
|
4922
|
+
file: "engine/index.js"
|
|
4923
|
+
});
|
|
4924
|
+
}
|
|
4925
|
+
return out;
|
|
4926
|
+
}
|
|
4069
4927
|
async function extensionValidateCommand(opts = {}) {
|
|
4070
4928
|
const dir = opts.dir ?? process.cwd();
|
|
4071
4929
|
console.log(`
|
|
@@ -4074,7 +4932,7 @@ ${c7.bold("Extension validate")}
|
|
|
4074
4932
|
console.log(` Extension dir: ${c7.dim(dir)}`);
|
|
4075
4933
|
const manifestPath = join10(dir, "manifest.json");
|
|
4076
4934
|
let manifest = null;
|
|
4077
|
-
if (
|
|
4935
|
+
if (existsSync11(manifestPath)) {
|
|
4078
4936
|
try {
|
|
4079
4937
|
manifest = JSON.parse(readFileSync4(manifestPath, "utf8"));
|
|
4080
4938
|
} catch (e) {
|
|
@@ -4082,11 +4940,12 @@ ${c7.bold("Extension validate")}
|
|
|
4082
4940
|
manifest = null;
|
|
4083
4941
|
}
|
|
4084
4942
|
}
|
|
4943
|
+
const isStudioOnly = manifest != null && typeof manifest === "object" && manifest?.contributes?.engine === false && !manifest?.engine;
|
|
4085
4944
|
const migrationsDir = join10(dir, "engine", "migrations");
|
|
4086
4945
|
let sqlFiles = [];
|
|
4087
4946
|
let parsedTableCount = 0;
|
|
4088
|
-
if (
|
|
4089
|
-
sqlFiles =
|
|
4947
|
+
if (existsSync11(migrationsDir)) {
|
|
4948
|
+
sqlFiles = readdirSync3(migrationsDir).filter((f) => f.endsWith(".sql")).sort().map((filename) => ({
|
|
4090
4949
|
filename,
|
|
4091
4950
|
sql: readFileSync4(join10(migrationsDir, filename), "utf8")
|
|
4092
4951
|
}));
|
|
@@ -4098,10 +4957,10 @@ ${c7.bold("Extension validate")}
|
|
|
4098
4957
|
}
|
|
4099
4958
|
const paths = {};
|
|
4100
4959
|
for (const rel of ["manifest.json", "engine/index.ts", "engine/index.js"]) {
|
|
4101
|
-
paths[rel] =
|
|
4960
|
+
paths[rel] = existsSync11(join10(dir, rel));
|
|
4102
4961
|
}
|
|
4103
4962
|
const required = ["manifest.json"];
|
|
4104
|
-
if (!(paths["engine/index.ts"] || paths["engine/index.js"])) {
|
|
4963
|
+
if (!isStudioOnly && !(paths["engine/index.ts"] || paths["engine/index.js"])) {
|
|
4105
4964
|
required.push("engine/index.ts");
|
|
4106
4965
|
}
|
|
4107
4966
|
const bundleBytes = recursiveSize(dir);
|
|
@@ -4125,10 +4984,67 @@ ${c7.bold("Extension validate")}
|
|
|
4125
4984
|
},
|
|
4126
4985
|
stats: { tables: parsedTableCount, migrations: sqlFiles.length }
|
|
4127
4986
|
});
|
|
4987
|
+
const sduiErrors = [];
|
|
4988
|
+
{
|
|
4989
|
+
const pages = manifest && typeof manifest === "object" ? manifest?.studio?.pages ?? [] : [];
|
|
4990
|
+
const schemaPages = (Array.isArray(pages) ? pages : []).filter((p) => typeof p?.schema === "string");
|
|
4991
|
+
if (schemaPages.length > 0) {
|
|
4992
|
+
const manifestRecord = manifest && typeof manifest === "object" ? manifest : undefined;
|
|
4993
|
+
const extName = typeof manifestRecord?.name === "string" ? manifestRecord.name : inferExpectedName(dir);
|
|
4994
|
+
const provided = extractEngineRoutes(readEngineSources(dir));
|
|
4995
|
+
const ownMessageKeys = readOwnMessageKeys(dir);
|
|
4996
|
+
const dependencies = Array.isArray(manifest?.dependencies) ? manifest.dependencies.map((d) => typeof d === "string" ? d : d?.name).filter((x) => typeof x === "string") : [];
|
|
4997
|
+
for (const p of schemaPages) {
|
|
4998
|
+
const rel = join10("studio", p.schema).replace(/\\/g, "/");
|
|
4999
|
+
const abs = join10(dir, "studio", p.schema);
|
|
5000
|
+
let parsed = null;
|
|
5001
|
+
if (!existsSync11(abs)) {
|
|
5002
|
+
sduiErrors.push({
|
|
5003
|
+
code: "SDUI_SCHEMA_MISSING",
|
|
5004
|
+
message: "schema file not found",
|
|
5005
|
+
file: rel
|
|
5006
|
+
});
|
|
5007
|
+
continue;
|
|
5008
|
+
}
|
|
5009
|
+
try {
|
|
5010
|
+
parsed = JSON.parse(readFileSync4(abs, "utf8"));
|
|
5011
|
+
} catch (e) {
|
|
5012
|
+
sduiErrors.push({
|
|
5013
|
+
code: "SDUI_SCHEMA_BAD_JSON",
|
|
5014
|
+
message: `not valid JSON: ${e.message}`,
|
|
5015
|
+
file: rel
|
|
5016
|
+
});
|
|
5017
|
+
continue;
|
|
5018
|
+
}
|
|
5019
|
+
sduiErrors.push(...validateSduiSchema({
|
|
5020
|
+
schema: parsed,
|
|
5021
|
+
extName,
|
|
5022
|
+
providedRoutes: provided,
|
|
5023
|
+
dependencies,
|
|
5024
|
+
messageKeys: ownMessageKeys && new Set([
|
|
5025
|
+
...ownMessageKeys,
|
|
5026
|
+
...SHARED_MESSAGE_KEYS,
|
|
5027
|
+
...readDependencyMessageKeys(dir, extName, dependencies)
|
|
5028
|
+
]),
|
|
5029
|
+
file: rel
|
|
5030
|
+
}));
|
|
5031
|
+
}
|
|
5032
|
+
result.errors.push(...sduiErrors);
|
|
5033
|
+
const sduiHard = sduiErrors.filter((e) => e.severity !== "warning");
|
|
5034
|
+
if (sduiHard.length > 0)
|
|
5035
|
+
result.ok = false;
|
|
5036
|
+
const sduiWarn = sduiErrors.length - sduiHard.length;
|
|
5037
|
+
const sduiSummary = sduiHard.length > 0 ? c7.red(`${sduiHard.length} error(s)`) : sduiWarn > 0 ? c7.yellow(`${schemaPages.length} OK, ${sduiWarn} warning(s)`) : c7.green(`${schemaPages.length} OK`);
|
|
5038
|
+
console.log(` SDUI schemas: ${sduiSummary}`);
|
|
5039
|
+
}
|
|
5040
|
+
}
|
|
4128
5041
|
const m = manifest;
|
|
5042
|
+
const studioOnly = isStudioOnly;
|
|
4129
5043
|
const v2Status = (() => {
|
|
4130
5044
|
if (!m)
|
|
4131
5045
|
return "unknown";
|
|
5046
|
+
if (studioOnly)
|
|
5047
|
+
return "studio-only";
|
|
4132
5048
|
if (!m.engine)
|
|
4133
5049
|
return "v1-legacy";
|
|
4134
5050
|
if (m.engine.bundled !== true)
|
|
@@ -4143,34 +5059,39 @@ ${c7.bold("Extension validate")}
|
|
|
4143
5059
|
console.log(` Migrations: ${c7.dim(`${result.stats.migrations} file(s), ${result.stats.tables} table(s) parsed`)}`);
|
|
4144
5060
|
console.log(` Peer deps: ${c7.dim(`${result.stats.peerDeps} declared`)}`);
|
|
4145
5061
|
console.log(` Bundle size: ${c7.dim(`${Math.ceil(bundleBytes / 1024)} KB`)}`);
|
|
4146
|
-
const v2Label = v2Status === "v2-ok" ? c7.green("v2 (bundled)") : v2Status === "v1-legacy" ? c7.yellow("v1 (no engine block \u2014 run `zveltio extension pack`)") : v2Status === "engine-not-bundled" ? c7.red("engine.bundled !== true \u2014 re-pack required") : v2Status === "missing-engine-entry" ? c7.red("engine.entry missing") : v2Status === "missing-engine-sha" ? c7.red("integrity.engineSha256 missing \u2014 re-pack required") : c7.dim("unknown");
|
|
5062
|
+
const v2Label = v2Status === "v2-ok" ? c7.green("v2 (bundled)") : v2Status === "studio-only" ? c7.green("studio/client-only (no engine)") : v2Status === "v1-legacy" ? c7.yellow("v1 (no engine block \u2014 run `zveltio extension pack`)") : v2Status === "engine-not-bundled" ? c7.red("engine.bundled !== true \u2014 re-pack required") : v2Status === "missing-engine-entry" ? c7.red("engine.entry missing") : v2Status === "missing-engine-sha" ? c7.red("integrity.engineSha256 missing \u2014 re-pack required") : c7.dim("unknown");
|
|
4147
5063
|
console.log(` Manifest v2: ${v2Label}`);
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
token: opts.token,
|
|
4152
|
-
registryUrl: opts.registryUrl
|
|
4153
|
-
});
|
|
4154
|
-
const inlineOk = tierAllowsInline(resolved.tier);
|
|
4155
|
-
const tierSrc = resolved.source === "flag" ? "--first-party" : resolved.source === "registry" ? "registry" : "default";
|
|
4156
|
-
if (isolation === "worker") {
|
|
4157
|
-
console.log(` Isolation: ${c7.green("worker (runs in any tier)")}`);
|
|
4158
|
-
} else if (inlineOk) {
|
|
4159
|
-
console.log(` Isolation: ${c7.dim(`inline (${resolved.tier} \u2014 allowed, via ${tierSrc})`)}`);
|
|
5064
|
+
if (studioOnly) {
|
|
5065
|
+
console.log(` Isolation: ${c7.dim("n/a (no engine)")}`);
|
|
5066
|
+
console.log("");
|
|
4160
5067
|
} else {
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
5068
|
+
const isolation = m?.engine?.isolation ?? "inline";
|
|
5069
|
+
const resolved = await resolvePublisherTier({
|
|
5070
|
+
firstParty: opts.firstParty,
|
|
5071
|
+
token: opts.token,
|
|
5072
|
+
registryUrl: opts.registryUrl
|
|
5073
|
+
});
|
|
5074
|
+
const inlineOk = tierAllowsInline(resolved.tier);
|
|
5075
|
+
const tierSrc = resolved.source === "flag" ? "--first-party" : resolved.source === "registry" ? "registry" : "default";
|
|
5076
|
+
if (isolation === "worker") {
|
|
5077
|
+
console.log(` Isolation: ${c7.green("worker (runs in any tier)")}`);
|
|
5078
|
+
} else if (inlineOk) {
|
|
5079
|
+
console.log(` Isolation: ${c7.dim(`inline (${resolved.tier} \u2014 allowed, via ${tierSrc})`)}`);
|
|
5080
|
+
} else {
|
|
5081
|
+
console.log(` Isolation: ${c7.red(`inline \u2014 ${resolved.tier} publishers MUST use 'worker'`)}`);
|
|
5082
|
+
}
|
|
5083
|
+
console.log("");
|
|
5084
|
+
if (isolation !== "worker" && !inlineOk) {
|
|
5085
|
+
console.error(c7.red(`Validation failed: ${resolved.tier} publishers must declare ` + `engine.isolation: "worker" (marketplace-policy.md \xA72).`));
|
|
5086
|
+
console.error(c7.dim(' Add `"isolation": "worker"` to the engine block in manifest.json, ' + "then re-pack. If you are a verified/first-party publisher, pass " + "--first-party or set ZVELTIO_REGISTRY_TOKEN so the tier can be confirmed. " + "See docs/extensions/marketplace-policy.md \xA72."));
|
|
5087
|
+
if (opts.silentExit)
|
|
5088
|
+
throw new Error("Validation failed: community inline isolation");
|
|
5089
|
+
process.exit(1);
|
|
5090
|
+
}
|
|
4170
5091
|
}
|
|
4171
5092
|
if (v2Status === "v1-legacy") {
|
|
4172
5093
|
console.error(c7.red("Validation failed: manifest is v1 (no engine.bundled block)."));
|
|
4173
|
-
console.error(c7.dim(" v1 manifests are unsupported since 1.0.0-beta.1. Run `zveltio extension pack` to " + "generate the v2 engine + integrity blocks. See docs/
|
|
5094
|
+
console.error(c7.dim(" v1 manifests are unsupported since 1.0.0-beta.1. Run `zveltio extension pack` to " + "generate the v2 engine + integrity blocks. See docs/extensions/developer-guide.md \xA74."));
|
|
4174
5095
|
if (opts.silentExit)
|
|
4175
5096
|
throw new Error("Validation failed: v1 manifest unsupported");
|
|
4176
5097
|
process.exit(1);
|
|
@@ -4182,28 +5103,43 @@ ${c7.bold("Extension validate")}
|
|
|
4182
5103
|
throw new Error("Validation failed: incomplete v2 manifest");
|
|
4183
5104
|
process.exit(1);
|
|
4184
5105
|
}
|
|
5106
|
+
const integrityErrors = checkBundleIntegrity(dir, manifest);
|
|
5107
|
+
if (integrityErrors.length > 0) {
|
|
5108
|
+
result.errors.push(...integrityErrors);
|
|
5109
|
+
if (integrityErrors.some((e) => e.severity !== "warning"))
|
|
5110
|
+
result.ok = false;
|
|
5111
|
+
}
|
|
5112
|
+
const warnings = result.errors.filter((e) => e.severity === "warning");
|
|
5113
|
+
const hardErrors = result.errors.filter((e) => e.severity !== "warning");
|
|
5114
|
+
if (warnings.length > 0) {
|
|
5115
|
+
console.log(c7.yellow(`${warnings.length} warning(s):`));
|
|
5116
|
+
for (const e of warnings)
|
|
5117
|
+
printError(e, "warning");
|
|
5118
|
+
console.log("");
|
|
5119
|
+
}
|
|
4185
5120
|
if (result.ok) {
|
|
4186
5121
|
console.log(c7.green("Validation passed."));
|
|
4187
5122
|
console.log(c7.dim(" Run `zveltio extension types` to refresh generated types, then `zveltio extension publish` when ready."));
|
|
4188
5123
|
console.log("");
|
|
4189
5124
|
return;
|
|
4190
5125
|
}
|
|
4191
|
-
console.log(c7.red(`Validation failed with ${
|
|
5126
|
+
console.log(c7.red(`Validation failed with ${hardErrors.length} error(s):`));
|
|
4192
5127
|
console.log("");
|
|
4193
|
-
for (const e of
|
|
4194
|
-
printError(e);
|
|
5128
|
+
for (const e of hardErrors) {
|
|
5129
|
+
printError(e, "error");
|
|
4195
5130
|
}
|
|
4196
5131
|
console.log("");
|
|
4197
5132
|
if (!opts.silentExit)
|
|
4198
5133
|
process.exit(1);
|
|
4199
5134
|
}
|
|
4200
|
-
function printError(e) {
|
|
5135
|
+
function printError(e, severity = "error") {
|
|
4201
5136
|
const loc = e.file ? c7.dim(` (${e.file})`) : "";
|
|
4202
|
-
|
|
5137
|
+
const code = severity === "warning" ? c7.yellow(e.code) : c7.red(e.code);
|
|
5138
|
+
console.log(` ${code} ${e.message}${loc}`);
|
|
4203
5139
|
}
|
|
4204
5140
|
|
|
4205
5141
|
// src/commands/extension-publish.ts
|
|
4206
|
-
import { existsSync as
|
|
5142
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
4207
5143
|
import { join as join12, resolve as resolve2 } from "path";
|
|
4208
5144
|
import { tmpdir } from "os";
|
|
4209
5145
|
|
|
@@ -4272,16 +5208,16 @@ init_extension_pack();
|
|
|
4272
5208
|
|
|
4273
5209
|
// src/commands/keys.ts
|
|
4274
5210
|
import {
|
|
4275
|
-
existsSync as
|
|
5211
|
+
existsSync as existsSync12,
|
|
4276
5212
|
mkdirSync as mkdirSync3,
|
|
4277
|
-
readdirSync as
|
|
5213
|
+
readdirSync as readdirSync4,
|
|
4278
5214
|
readFileSync as readFileSync5,
|
|
4279
5215
|
writeFileSync as writeFileSync3,
|
|
4280
5216
|
chmodSync,
|
|
4281
5217
|
statSync as statSync2
|
|
4282
5218
|
} from "fs";
|
|
4283
5219
|
import { join as join11 } from "path";
|
|
4284
|
-
import { homedir, platform } from "os";
|
|
5220
|
+
import { homedir as homedir2, platform } from "os";
|
|
4285
5221
|
var c8 = {
|
|
4286
5222
|
bold: (s) => `\x1B[1m${s}\x1B[0m`,
|
|
4287
5223
|
green: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
@@ -4290,11 +5226,11 @@ var c8 = {
|
|
|
4290
5226
|
dim: (s) => `\x1B[2m${s}\x1B[0m`
|
|
4291
5227
|
};
|
|
4292
5228
|
function keysDir() {
|
|
4293
|
-
return join11(
|
|
5229
|
+
return join11(homedir2(), ".zveltio", "keys");
|
|
4294
5230
|
}
|
|
4295
5231
|
function ensureKeysDir() {
|
|
4296
5232
|
const dir = keysDir();
|
|
4297
|
-
if (!
|
|
5233
|
+
if (!existsSync12(dir)) {
|
|
4298
5234
|
mkdirSync3(dir, { recursive: true });
|
|
4299
5235
|
if (platform() !== "win32") {
|
|
4300
5236
|
try {
|
|
@@ -4318,7 +5254,7 @@ function writeKeyFile(kp) {
|
|
|
4318
5254
|
}
|
|
4319
5255
|
function readKeyFile(keyId) {
|
|
4320
5256
|
const path = keyPath(keyId);
|
|
4321
|
-
if (!
|
|
5257
|
+
if (!existsSync12(path)) {
|
|
4322
5258
|
throw new Error(`Keypair "${keyId}" not found at ${path}. Run \`zveltio keys generate --id ${keyId}\` first.`);
|
|
4323
5259
|
}
|
|
4324
5260
|
const raw = readFileSync5(path, "utf8");
|
|
@@ -4332,10 +5268,10 @@ function resolveKeyId(explicit) {
|
|
|
4332
5268
|
if (explicit)
|
|
4333
5269
|
return explicit;
|
|
4334
5270
|
const dir = keysDir();
|
|
4335
|
-
if (!
|
|
5271
|
+
if (!existsSync12(dir)) {
|
|
4336
5272
|
throw new Error("No keypairs found. Run `zveltio keys generate` first.");
|
|
4337
5273
|
}
|
|
4338
|
-
const ids =
|
|
5274
|
+
const ids = readdirSync4(dir).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5));
|
|
4339
5275
|
if (ids.length === 0)
|
|
4340
5276
|
throw new Error("No keypairs found. Run `zveltio keys generate` first.");
|
|
4341
5277
|
if (ids.length > 1) {
|
|
@@ -4346,7 +5282,7 @@ function resolveKeyId(explicit) {
|
|
|
4346
5282
|
async function keysGenerateCommand(opts = {}) {
|
|
4347
5283
|
ensureKeysDir();
|
|
4348
5284
|
const kp = await generateKeypair(opts.id);
|
|
4349
|
-
if (
|
|
5285
|
+
if (existsSync12(keyPath(kp.keyId)) && !opts.force) {
|
|
4350
5286
|
console.error(c8.red(`Key "${kp.keyId}" already exists. Pass --force to overwrite.`));
|
|
4351
5287
|
process.exit(1);
|
|
4352
5288
|
}
|
|
@@ -4370,11 +5306,11 @@ ${c8.bold("Public key (share with registry admin):")}
|
|
|
4370
5306
|
}
|
|
4371
5307
|
async function keysListCommand() {
|
|
4372
5308
|
const dir = keysDir();
|
|
4373
|
-
if (!
|
|
5309
|
+
if (!existsSync12(dir)) {
|
|
4374
5310
|
console.log(`${c8.yellow("No keypairs yet.")} Run ${c8.dim("zveltio keys generate")} to create one.`);
|
|
4375
5311
|
return;
|
|
4376
5312
|
}
|
|
4377
|
-
const files =
|
|
5313
|
+
const files = readdirSync4(dir).filter((f) => f.endsWith(".json")).sort();
|
|
4378
5314
|
if (files.length === 0) {
|
|
4379
5315
|
console.log(`${c8.yellow("No keypairs yet.")} Run ${c8.dim("zveltio keys generate")} to create one.`);
|
|
4380
5316
|
return;
|
|
@@ -4411,7 +5347,7 @@ var c9 = {
|
|
|
4411
5347
|
};
|
|
4412
5348
|
function readManifest2(dir) {
|
|
4413
5349
|
const path = join12(dir, "manifest.json");
|
|
4414
|
-
if (!
|
|
5350
|
+
if (!existsSync13(path)) {
|
|
4415
5351
|
throw new Error(`No manifest.json at ${path}. Run from the extension's root or pass --dir.`);
|
|
4416
5352
|
}
|
|
4417
5353
|
const raw = readFileSync6(path, "utf8");
|
|
@@ -4451,7 +5387,7 @@ async function createArchive(dir, outFile) {
|
|
|
4451
5387
|
throw new Error(`tar failed (exit ${code})`);
|
|
4452
5388
|
}
|
|
4453
5389
|
async function runStudioBuild(dir) {
|
|
4454
|
-
if (!
|
|
5390
|
+
if (!existsSync13(join12(dir, "studio")))
|
|
4455
5391
|
return;
|
|
4456
5392
|
const proc = Bun.spawn(["bun", "run", "build"], {
|
|
4457
5393
|
cwd: join12(dir, "studio"),
|
|
@@ -4463,7 +5399,7 @@ async function runStudioBuild(dir) {
|
|
|
4463
5399
|
throw new Error(`Studio build failed (exit ${code})`);
|
|
4464
5400
|
}
|
|
4465
5401
|
async function runEnginePack(dir, tierOpts) {
|
|
4466
|
-
if (!
|
|
5402
|
+
if (!existsSync13(join12(dir, "engine", "index.ts")))
|
|
4467
5403
|
return;
|
|
4468
5404
|
await extensionPackCommand({ dir, ...tierOpts });
|
|
4469
5405
|
}
|
|
@@ -4560,7 +5496,7 @@ ${c9.green("Dry-run complete")} \u2014 skipped archive/sign/publish.
|
|
|
4560
5496
|
console.log(`
|
|
4561
5497
|
${c9.bold("Step 4/6: archive")}`);
|
|
4562
5498
|
const tmpDir = opts.output ? resolve2(opts.output) : join12(tmpdir(), `zveltio-publish-${Date.now()}`);
|
|
4563
|
-
if (!
|
|
5499
|
+
if (!existsSync13(tmpDir))
|
|
4564
5500
|
mkdirSync4(tmpDir, { recursive: true });
|
|
4565
5501
|
const zvextPath = join12(tmpDir, `${artifactBaseName(manifest)}.zvext`);
|
|
4566
5502
|
try {
|
|
@@ -4662,10 +5598,10 @@ ${c9.bold("Step 6/6: upload")}`);
|
|
|
4662
5598
|
}
|
|
4663
5599
|
|
|
4664
5600
|
// src/commands/extension-dev.ts
|
|
4665
|
-
import { existsSync as
|
|
5601
|
+
import { existsSync as existsSync14, statSync as statSync4 } from "fs";
|
|
4666
5602
|
import { watch as fsWatch } from "fs";
|
|
4667
|
-
import { join as join13, resolve as resolve3, relative as
|
|
4668
|
-
import { readdirSync as
|
|
5603
|
+
import { join as join13, resolve as resolve3, relative as relative4 } from "path";
|
|
5604
|
+
import { readdirSync as readdirSync5 } from "fs";
|
|
4669
5605
|
var c10 = {
|
|
4670
5606
|
bold: (s) => `\x1B[1m${s}\x1B[0m`,
|
|
4671
5607
|
green: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
@@ -4678,7 +5614,7 @@ function readManifestName(dir, override) {
|
|
|
4678
5614
|
if (override)
|
|
4679
5615
|
return override;
|
|
4680
5616
|
const manifestPath = join13(dir, "manifest.json");
|
|
4681
|
-
if (!
|
|
5617
|
+
if (!existsSync14(manifestPath)) {
|
|
4682
5618
|
throw new Error(`No manifest.json at ${manifestPath}. Pass --name or run from the extension's root.`);
|
|
4683
5619
|
}
|
|
4684
5620
|
const Bun_ = globalThis.Bun;
|
|
@@ -4694,7 +5630,7 @@ function readManifestName(dir, override) {
|
|
|
4694
5630
|
return m.name;
|
|
4695
5631
|
}
|
|
4696
5632
|
function collectEngineFiles(engineDir) {
|
|
4697
|
-
if (!
|
|
5633
|
+
if (!existsSync14(engineDir))
|
|
4698
5634
|
return [];
|
|
4699
5635
|
const out = [];
|
|
4700
5636
|
const stack = [engineDir];
|
|
@@ -4702,7 +5638,7 @@ function collectEngineFiles(engineDir) {
|
|
|
4702
5638
|
const dir = stack.pop();
|
|
4703
5639
|
let entries;
|
|
4704
5640
|
try {
|
|
4705
|
-
entries =
|
|
5641
|
+
entries = readdirSync5(dir);
|
|
4706
5642
|
} catch {
|
|
4707
5643
|
continue;
|
|
4708
5644
|
}
|
|
@@ -4769,17 +5705,20 @@ ${c10.bold("Extension dev")}
|
|
|
4769
5705
|
process.exit(1);
|
|
4770
5706
|
}
|
|
4771
5707
|
let studioProc = null;
|
|
4772
|
-
|
|
5708
|
+
const studioPkg = join13(dir, "studio", "package.json");
|
|
5709
|
+
if (!opts.noStudio && existsSync14(studioPkg)) {
|
|
4773
5710
|
studioProc = Bun.spawn(["bun", "run", "dev"], {
|
|
4774
5711
|
cwd: join13(dir, "studio"),
|
|
4775
5712
|
stdout: "inherit",
|
|
4776
5713
|
stderr: "inherit"
|
|
4777
5714
|
});
|
|
4778
|
-
console.log(` Studio: ${c10.green("watching (vite dev)")}`);
|
|
5715
|
+
console.log(` Studio: ${c10.green("watching (legacy studio/package.json vite dev)")}`);
|
|
5716
|
+
} else if (!opts.noStudio && existsSync14(join13(dir, "studio"))) {
|
|
5717
|
+
console.log(` Studio: ${c10.dim("tier-3 pages \u2014 run packages/studio dev after sync-extensions (see dev guide \xA72)")}`);
|
|
4779
5718
|
} else {
|
|
4780
5719
|
console.log(` Studio: ${c10.dim(opts.noStudio ? "skipped (--no-studio)" : "no studio/ folder \u2014 skipped")}`);
|
|
4781
5720
|
}
|
|
4782
|
-
if (!
|
|
5721
|
+
if (!existsSync14(engineDir)) {
|
|
4783
5722
|
console.log(` Engine src: ${c10.yellow("no engine/ folder \u2014 engine watch disabled")}`);
|
|
4784
5723
|
} else {
|
|
4785
5724
|
const files = collectEngineFiles(engineDir);
|
|
@@ -4788,7 +5727,7 @@ ${c10.bold("Extension dev")}
|
|
|
4788
5727
|
let lastTrigger = "";
|
|
4789
5728
|
const debounceMs = 250;
|
|
4790
5729
|
const onChange = (filePath) => {
|
|
4791
|
-
lastTrigger =
|
|
5730
|
+
lastTrigger = relative4(dir, filePath);
|
|
4792
5731
|
if (timer)
|
|
4793
5732
|
clearTimeout(timer);
|
|
4794
5733
|
timer = setTimeout(async () => {
|
|
@@ -4890,7 +5829,7 @@ ${c11.bold("Generate Types")}
|
|
|
4890
5829
|
}
|
|
4891
5830
|
|
|
4892
5831
|
// src/commands/install.ts
|
|
4893
|
-
import { existsSync as
|
|
5832
|
+
import { existsSync as existsSync15 } from "fs";
|
|
4894
5833
|
import { readFile, mkdir as mkdir2, cp } from "fs/promises";
|
|
4895
5834
|
import { join as join14 } from "path";
|
|
4896
5835
|
async function installCommand(name, opts) {
|
|
@@ -4905,7 +5844,7 @@ async function installCommand(name, opts) {
|
|
|
4905
5844
|
await installFromCatalog(name, engineUrl);
|
|
4906
5845
|
}
|
|
4907
5846
|
async function installFromPath(name, sourcePath, engineUrl, force) {
|
|
4908
|
-
if (!
|
|
5847
|
+
if (!existsSync15(join14(sourcePath, "manifest.json"))) {
|
|
4909
5848
|
console.error("\u274C No manifest.json found at", sourcePath);
|
|
4910
5849
|
process.exit(1);
|
|
4911
5850
|
}
|
|
@@ -4936,7 +5875,7 @@ async function installFromPath(name, sourcePath, engineUrl, force) {
|
|
|
4936
5875
|
console.log(` Version: ${manifest.version || "?"}`);
|
|
4937
5876
|
console.log(` Category: ${category}`);
|
|
4938
5877
|
const enginePkgPath = join14(process.cwd(), "packages/engine/package.json");
|
|
4939
|
-
if (
|
|
5878
|
+
if (existsSync15(enginePkgPath)) {
|
|
4940
5879
|
const enginePkg = JSON.parse(await readFile(enginePkgPath, "utf-8"));
|
|
4941
5880
|
const engineVersion = enginePkg.version || "unknown";
|
|
4942
5881
|
if (manifest.zveltioMinVersion) {
|
|
@@ -4948,7 +5887,7 @@ async function installFromPath(name, sourcePath, engineUrl, force) {
|
|
|
4948
5887
|
}
|
|
4949
5888
|
const extName = manifest.name;
|
|
4950
5889
|
const targetDir = join14(process.cwd(), "extensions", category, extName);
|
|
4951
|
-
if (
|
|
5890
|
+
if (existsSync15(targetDir) && !force) {
|
|
4952
5891
|
console.error(`\u274C Extension already exists at ${targetDir}`);
|
|
4953
5892
|
console.error(` Use --force to overwrite.`);
|
|
4954
5893
|
process.exit(1);
|
|
@@ -5114,6 +6053,7 @@ async function promptHidden(question) {
|
|
|
5114
6053
|
}
|
|
5115
6054
|
async function createGodCommand(opts) {
|
|
5116
6055
|
const engineUrl = opts.url || process.env.ENGINE_URL || "http://localhost:3000";
|
|
6056
|
+
const recoveryToken = opts.recoveryToken || process.env.RECOVERY_TOKEN || "";
|
|
5117
6057
|
console.log(`
|
|
5118
6058
|
\u26A0\uFE0F SYSTEM RECOVERY OVERRIDE ACCOUNT`);
|
|
5119
6059
|
console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
@@ -5191,28 +6131,29 @@ Creating God (super-admin) user at ${engineUrl}
|
|
|
5191
6131
|
const err2 = await res.json().catch(() => ({ message: res.statusText }));
|
|
5192
6132
|
throw new Error(err2.message || `HTTP ${res.status}`);
|
|
5193
6133
|
}
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
const loginRes = await fetch(`${engineUrl}/api/auth/sign-in/email`, {
|
|
5197
|
-
method: "POST",
|
|
5198
|
-
headers: { "Content-Type": "application/json" },
|
|
5199
|
-
body: JSON.stringify({ email: email.trim(), password })
|
|
5200
|
-
});
|
|
5201
|
-
if (!loginRes.ok) {
|
|
5202
|
-
console.warn("\u26A0\uFE0F Could not auto-grant admin role \u2014 grant manually via permissions API");
|
|
6134
|
+
await res.json();
|
|
6135
|
+
if (!recoveryToken) {
|
|
5203
6136
|
console.log(`
|
|
5204
|
-
\u2705 User created: ${email}
|
|
6137
|
+
\u2705 User created: ${email.trim()}`);
|
|
6138
|
+
console.warn(`
|
|
6139
|
+
\u26A0\uFE0F NOT granted the god role \u2014 no recovery token available.`);
|
|
6140
|
+
console.warn(" `god` is handed out by one endpoint only, and it requires a token:");
|
|
6141
|
+
console.warn(" RECOVERY_TOKEN=<32+ chars> zveltio create-god --email \u2026");
|
|
6142
|
+
console.warn(" zveltio create-god --recovery-token <token> --email \u2026");
|
|
6143
|
+
console.warn(" The engine must be started with the same RECOVERY_TOKEN set.");
|
|
6144
|
+
console.warn(" With direct database access, the release binary does it without a");
|
|
6145
|
+
console.warn(` token: DATABASE_URL=\u2026 zveltio create-god --email \u2026 --password \u2026
|
|
5205
6146
|
`);
|
|
5206
|
-
|
|
6147
|
+
process.exit(1);
|
|
5207
6148
|
}
|
|
5208
|
-
|
|
5209
|
-
const permRes = await fetch(`${engineUrl}/api/permissions/
|
|
6149
|
+
console.log("\uD83D\uDD11 Granting god permissions...");
|
|
6150
|
+
const permRes = await fetch(`${engineUrl}/api/permissions/bootstrap`, {
|
|
5210
6151
|
method: "POST",
|
|
5211
6152
|
headers: {
|
|
5212
6153
|
"Content-Type": "application/json",
|
|
5213
|
-
|
|
6154
|
+
Authorization: `Bearer ${recoveryToken}`
|
|
5214
6155
|
},
|
|
5215
|
-
body: JSON.stringify({
|
|
6156
|
+
body: JSON.stringify({ email: email.trim() })
|
|
5216
6157
|
});
|
|
5217
6158
|
if (permRes.ok) {
|
|
5218
6159
|
console.log(`
|
|
@@ -5220,15 +6161,20 @@ Creating God (super-admin) user at ${engineUrl}
|
|
|
5220
6161
|
console.log(` Email: ${email.trim()}`);
|
|
5221
6162
|
console.log(` Name: ${name.trim()}`);
|
|
5222
6163
|
console.log(` Role: god
|
|
6164
|
+
`);
|
|
6165
|
+
console.log(" The recovery token is now spent. Rotate RECOVERY_TOKEN before the");
|
|
6166
|
+
console.log(` next recovery, and note that any previous god was stood down.
|
|
5223
6167
|
`);
|
|
5224
6168
|
console.log(`\uD83D\uDC49 Log in at: ${engineUrl}/admin
|
|
5225
6169
|
`);
|
|
5226
6170
|
} else {
|
|
6171
|
+
const body = await permRes.json().catch(() => ({}));
|
|
5227
6172
|
console.log(`
|
|
5228
6173
|
\u2705 User created: ${email.trim()}`);
|
|
5229
|
-
console.
|
|
5230
|
-
|
|
6174
|
+
console.error(`
|
|
6175
|
+
\u274C The role was NOT granted: ${body.error ?? `engine returned ${permRes.status}`}
|
|
5231
6176
|
`);
|
|
6177
|
+
process.exit(1);
|
|
5232
6178
|
}
|
|
5233
6179
|
} catch (err2) {
|
|
5234
6180
|
rl2.close();
|
|
@@ -5310,20 +6256,27 @@ var rollbackCommand = new Command("rollback").description("Rollback database mig
|
|
|
5310
6256
|
process.exit(1);
|
|
5311
6257
|
}
|
|
5312
6258
|
process.env.DATABASE_URL = databaseUrl;
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
6259
|
+
let engine;
|
|
6260
|
+
try {
|
|
6261
|
+
engine = await loadEngineMigrationModules();
|
|
6262
|
+
} catch (err2) {
|
|
6263
|
+
console.error(`
|
|
6264
|
+
\u274C ${err2.message}
|
|
6265
|
+
`);
|
|
6266
|
+
process.exit(1);
|
|
6267
|
+
}
|
|
6268
|
+
const { initDatabase } = engine.db;
|
|
6269
|
+
const { getLastAppliedMigration, rollbackMigration } = engine.migrations;
|
|
5317
6270
|
let targetVersion;
|
|
5318
6271
|
if (opts.to !== undefined) {
|
|
5319
6272
|
targetVersion = parseInt(opts.to);
|
|
5320
|
-
if (isNaN(targetVersion)) {
|
|
6273
|
+
if (Number.isNaN(targetVersion)) {
|
|
5321
6274
|
console.error("\u274C --to must be a valid integer");
|
|
5322
6275
|
process.exit(1);
|
|
5323
6276
|
}
|
|
5324
6277
|
} else {
|
|
5325
6278
|
const steps = parseInt(opts.steps);
|
|
5326
|
-
if (isNaN(steps) || steps < 1) {
|
|
6279
|
+
if (Number.isNaN(steps) || steps < 1) {
|
|
5327
6280
|
console.error("\u274C --steps must be a positive integer");
|
|
5328
6281
|
process.exit(1);
|
|
5329
6282
|
}
|
|
@@ -5421,7 +6374,7 @@ var versionCommand = new Command("version").description("Show version informatio
|
|
|
5421
6374
|
});
|
|
5422
6375
|
|
|
5423
6376
|
// src/commands/update.ts
|
|
5424
|
-
import { existsSync as
|
|
6377
|
+
import { existsSync as existsSync16 } from "fs";
|
|
5425
6378
|
import { join as join15 } from "path";
|
|
5426
6379
|
var c12 = {
|
|
5427
6380
|
bold: (s) => `\x1B[1m${s}\x1B[0m`,
|
|
@@ -5439,7 +6392,7 @@ ${c12.bold("Zveltio Update")}
|
|
|
5439
6392
|
const installDir = opts.dir || process.env.ZVELTIO_DIR || join15(process.cwd(), "zveltio");
|
|
5440
6393
|
const metaPath = join15(installDir, ".zveltio-install.json");
|
|
5441
6394
|
let meta = null;
|
|
5442
|
-
if (
|
|
6395
|
+
if (existsSync16(metaPath)) {
|
|
5443
6396
|
try {
|
|
5444
6397
|
meta = JSON.parse(await Bun.file(metaPath).text());
|
|
5445
6398
|
} catch {}
|
|
@@ -5456,8 +6409,8 @@ ${c12.bold("Zveltio Update")}
|
|
|
5456
6409
|
throw new Error(`Registry returned ${res.status}`);
|
|
5457
6410
|
}
|
|
5458
6411
|
versionsData = await res.json();
|
|
5459
|
-
if (!versionsData?.latest) {
|
|
5460
|
-
throw new Error('Invalid versions response (
|
|
6412
|
+
if (!versionsData?.latest && !versionsData?.latest_beta) {
|
|
6413
|
+
throw new Error('Invalid versions response (no "latest" or "latest_beta")');
|
|
5461
6414
|
}
|
|
5462
6415
|
} catch (err2) {
|
|
5463
6416
|
if (opts.check) {
|
|
@@ -5487,6 +6440,13 @@ ${c12.bold("Zveltio Update")}
|
|
|
5487
6440
|
process.exit(1);
|
|
5488
6441
|
}
|
|
5489
6442
|
const targetVersion = opts.version || (opts.channel === "beta" ? versionsData.latest_beta ?? versionsData.latest : versionsData.latest);
|
|
6443
|
+
if (!targetVersion) {
|
|
6444
|
+
console.error(c12.red(`
|
|
6445
|
+
No stable release has been published yet.`));
|
|
6446
|
+
console.error(c12.dim(` Latest beta: v${versionsData.latest_beta} \u2014 run ${c12.cyan("zveltio update --channel beta")}
|
|
6447
|
+
`));
|
|
6448
|
+
process.exit(1);
|
|
6449
|
+
}
|
|
5490
6450
|
console.log(` Target version: ${c12.cyan(`v${targetVersion}`)}`);
|
|
5491
6451
|
if (opts.check) {
|
|
5492
6452
|
const hasUpdate = compareVersions(targetVersion, currentVersion) > 0;
|
|
@@ -5548,7 +6508,7 @@ async function getVersionFromPackageJson() {
|
|
|
5548
6508
|
join15(process.cwd(), "package.json")
|
|
5549
6509
|
];
|
|
5550
6510
|
for (const pkgPath of candidates) {
|
|
5551
|
-
if (!
|
|
6511
|
+
if (!existsSync16(pkgPath))
|
|
5552
6512
|
continue;
|
|
5553
6513
|
try {
|
|
5554
6514
|
const pkg = JSON.parse(await Bun.file(pkgPath).text());
|
|
@@ -5572,14 +6532,14 @@ async function getCurrentVersionFromAPI(port) {
|
|
|
5572
6532
|
}
|
|
5573
6533
|
async function runUpdateScript(targetVersion, installDir, meta, force) {
|
|
5574
6534
|
const envPath = join15(installDir, ".env");
|
|
5575
|
-
if (
|
|
6535
|
+
if (existsSync16(envPath)) {
|
|
5576
6536
|
const backupPath = `${envPath}.backup.${Date.now()}`;
|
|
5577
6537
|
const envContent = await Bun.file(envPath).text();
|
|
5578
6538
|
await Bun.write(backupPath, envContent);
|
|
5579
6539
|
console.log(` Backed up .env \u2192 ${backupPath}`);
|
|
5580
6540
|
}
|
|
5581
6541
|
const installScript = join15(installDir, "install.sh");
|
|
5582
|
-
if (
|
|
6542
|
+
if (existsSync16(installScript)) {
|
|
5583
6543
|
const args = [
|
|
5584
6544
|
"bash",
|
|
5585
6545
|
installScript,
|
|
@@ -5598,7 +6558,7 @@ async function runUpdateScript(targetVersion, installDir, meta, force) {
|
|
|
5598
6558
|
if (code !== 0) {
|
|
5599
6559
|
console.error(`
|
|
5600
6560
|
Update failed. Your previous version is still running.`);
|
|
5601
|
-
if (
|
|
6561
|
+
if (existsSync16(envPath)) {
|
|
5602
6562
|
console.log(` Restore .env: copy ${envPath}.backup.* to ${envPath}`);
|
|
5603
6563
|
}
|
|
5604
6564
|
process.exit(1);
|
|
@@ -5763,14 +6723,14 @@ ${c13.bold("Zveltio Status")}
|
|
|
5763
6723
|
console.log("");
|
|
5764
6724
|
}
|
|
5765
6725
|
async function getLocalVersion() {
|
|
5766
|
-
const { existsSync:
|
|
6726
|
+
const { existsSync: existsSync17 } = await import("fs");
|
|
5767
6727
|
const { join: join16 } = await import("path");
|
|
5768
6728
|
const candidates = [
|
|
5769
6729
|
join16(process.cwd(), "packages/engine/package.json"),
|
|
5770
6730
|
join16(process.cwd(), "package.json")
|
|
5771
6731
|
];
|
|
5772
6732
|
for (const p of candidates) {
|
|
5773
|
-
if (!
|
|
6733
|
+
if (!existsSync17(p))
|
|
5774
6734
|
continue;
|
|
5775
6735
|
try {
|
|
5776
6736
|
const pkg = JSON.parse(await Bun.file(p).text());
|
|
@@ -5783,7 +6743,7 @@ async function getLocalVersion() {
|
|
|
5783
6743
|
// package.json
|
|
5784
6744
|
var package_default = {
|
|
5785
6745
|
name: "@zveltio/cli",
|
|
5786
|
-
version: "3.0.0-beta.
|
|
6746
|
+
version: "3.0.0-beta.68",
|
|
5787
6747
|
type: "module",
|
|
5788
6748
|
bin: {
|
|
5789
6749
|
zveltio: "./dist/index.js"
|
|
@@ -5798,8 +6758,8 @@ var package_default = {
|
|
|
5798
6758
|
dev: "bun run --watch src/index.ts"
|
|
5799
6759
|
},
|
|
5800
6760
|
dependencies: {
|
|
5801
|
-
"@zveltio/sdk": "3.0.0-beta.
|
|
5802
|
-
commander: "^12.
|
|
6761
|
+
"@zveltio/sdk": "3.0.0-beta.68",
|
|
6762
|
+
commander: "^12.1.0"
|
|
5803
6763
|
},
|
|
5804
6764
|
devDependencies: {
|
|
5805
6765
|
"bun-types": "latest",
|
|
@@ -5823,7 +6783,7 @@ program2.addCommand(rollbackCommand);
|
|
|
5823
6783
|
program2.command("status").description("Show system status (DB, cache, uptime, version)").option("--url <url>", "Engine URL", "http://localhost:3000").option("--json", "Output as JSON").action(statusCommand);
|
|
5824
6784
|
program2.addCommand(versionCommand);
|
|
5825
6785
|
program2.addCommand(updateCommand);
|
|
5826
|
-
program2.command("create-god").description("Create the first super-admin (god) user interactively").option("--url <url>", "Engine URL", "http://localhost:3000").option("--email <email>", "Admin email (skip prompt)").option("--name <name>", "Admin name (skip prompt)").action(createGodCommand);
|
|
6786
|
+
program2.command("create-god").description("Create the first super-admin (god) user interactively").option("--url <url>", "Engine URL", "http://localhost:3000").option("--email <email>", "Admin email (skip prompt)").option("--name <name>", "Admin name (skip prompt)").option("--recovery-token <token>", "RECOVERY_TOKEN the engine was started with \u2014 required to grant the god role (env: RECOVERY_TOKEN)").action(createGodCommand);
|
|
5827
6787
|
var generate = program2.command("generate").description("Code generation utilities");
|
|
5828
6788
|
generate.command("types [collection]").description("Generate TypeScript types for collections (writes to ./types/zveltio.d.ts)").option("-o, --output <path>", "Output file path", "./types/zveltio.d.ts").option("--url <url>", "Engine URL", "http://localhost:3000").action(generateTypesCommand);
|
|
5829
6789
|
program2.command("generate-types [collection]").description("Generate TypeScript types for your collections").option("-o, --output <path>", "Output file path", "./types/zveltio.d.ts").option("--url <url>", "Engine URL", "http://localhost:3000").action(generateTypesCommand);
|
|
@@ -5851,10 +6811,10 @@ extensions.command("disable <name>").description("Disable an active extension").
|
|
|
5851
6811
|
console.log(`${name} disabled.`);
|
|
5852
6812
|
});
|
|
5853
6813
|
var ext = program2.command("extension").description('Manage Zveltio extensions (use "extensions" for new commands)');
|
|
5854
|
-
ext.command("create <name>").description("Scaffold a new extension").option("--category <category>", "Extension category", "custom").action((name, opts) => extensionCommand("create", name, opts));
|
|
6814
|
+
ext.command("create <name>").description("Scaffold a new extension (admin page as an SDUI schema)").option("--category <category>", "Extension category", "custom").option("--code-page", "Scaffold a Svelte page instead of a schema \u2014 for UI a schema cannot express (canvas, chat, map, inbox)").action((name, opts) => extensionCommand("create", name, opts));
|
|
5855
6815
|
ext.command("build").description("[deprecated] Alias for `extension pack` \u2014 produces the v2 engine bundle").option("--dir <dir>", "Extension root directory (defaults to cwd)").action((opts) => extensionCommand("build", "", opts));
|
|
5856
6816
|
ext.command("dev").description("Watch engine/ + studio/, hot-reload the running engine on changes (S4-03)").option("--dir <dir>", "Extension root directory (defaults to cwd)").option("--url <url>", "Engine URL (env: ZVELTIO_ENGINE_URL)", "http://localhost:3000").option("--name <name>", "Extension name (default: read from manifest.json)").option("--no-studio", "Skip the Studio dev process (engine watch only)").action((opts) => extensionDevCommand(opts));
|
|
5857
|
-
ext.command("pack").description("Bundle engine/index.ts \u2192 engine/index.js (target=bun, ESM) and write engine + integrity blocks into manifest.json. Required before publish on v2 channels.").option("--dir <dir>", "Extension root directory (defaults to cwd)").option("--sourcemap", "Emit engine/index.js.map alongside the bundle").option("--no-manifest-update", "Build the bundle but do not patch manifest.json").option("--first-party", "Vendor / monorepo build \u2014 keep inline isolation, skip worker auto-inject").option("--token <token>", "Registry token for the publisher-tier lookup (env: ZVELTIO_REGISTRY_TOKEN)").option("--registry-url <url>", "Registry base URL (env: ZVELTIO_REGISTRY_URL)").action(async (opts) => {
|
|
6817
|
+
ext.command("pack").description("Bundle engine/index.ts \u2192 engine/index.js (target=bun, ESM) and write engine + integrity blocks into manifest.json. Required before publish on v2 channels.").option("--dir <dir>", "Extension root directory (defaults to cwd)").option("--sourcemap", "Emit engine/index.js.map alongside the bundle").option("--no-manifest-update", "Build the bundle but do not patch manifest.json").option("--first-party", "Vendor / monorepo build \u2014 keep inline isolation, skip worker auto-inject").option("--keep-isolation", 'With --first-party, retain engine.isolation: "worker" if already set (default: clear sticky worker)').option("--token <token>", "Registry token for the publisher-tier lookup (env: ZVELTIO_REGISTRY_TOKEN)").option("--registry-url <url>", "Registry base URL (env: ZVELTIO_REGISTRY_URL)").action(async (opts) => {
|
|
5858
6818
|
const { extensionPackCommand: extensionPackCommand2 } = await Promise.resolve().then(() => (init_extension_pack(), exports_extension_pack));
|
|
5859
6819
|
return extensionPackCommand2(opts);
|
|
5860
6820
|
});
|