@svgrid/enterprise 3.1.17 → 3.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/SvGridEditPanel.svelte +14 -1
  2. package/dist/SvGridScheduler.svelte +15 -6
  3. package/dist/SvSheet.svelte +2 -0
  4. package/dist/cdn/svgrid-enterprise.svelte-external.js +6582 -6501
  5. package/dist/designer/assets/{index-C1Ujg62Z.js → index-DKudY-XD.js} +357 -346
  6. package/dist/designer/assets/index-WCxj6WfJ.css +1 -0
  7. package/dist/designer/index.html +2 -2
  8. package/dist/fields/SvBase.svelte +36 -33
  9. package/dist/fields/base-connect.js +50 -9
  10. package/dist/fields/base-sql.d.ts +8 -0
  11. package/dist/fields/base-sql.js +12 -3
  12. package/dist/install.js +3 -3
  13. package/dist/node/studio.js +205 -60
  14. package/dist/sheet-enable.d.ts +8 -2
  15. package/dist/sheet-enable.js +21 -9
  16. package/dist/sources/introspect-supabase.d.ts +6 -3
  17. package/dist/sources/introspect-supabase.js +29 -11
  18. package/dist/sources/supabase.d.ts +10 -0
  19. package/dist/sources/supabase.js +11 -2
  20. package/dist/studio/cli.d.ts +9 -0
  21. package/dist/studio/cli.js +41 -7
  22. package/dist/studio/emit-project.d.ts +3 -1
  23. package/dist/studio/emit-project.js +84 -33
  24. package/dist/studio/emit-schema.js +9 -2
  25. package/dist/studio/index.d.ts +1 -1
  26. package/dist/studio/init-flow.d.ts +7 -0
  27. package/dist/studio/init-flow.js +23 -2
  28. package/dist/studio/introspect-db.d.ts +15 -1
  29. package/dist/studio/introspect-db.js +29 -5
  30. package/dist/sveltekit/sql-source.d.ts +24 -0
  31. package/dist/sveltekit/sql-source.js +34 -1
  32. package/dist/sveltekit/sql.js +14 -2
  33. package/dist/version.d.ts +2 -2
  34. package/dist/version.js +2 -2
  35. package/dist/wc/GridFooter-BgoA77TJ.js +145 -0
  36. package/dist/wc/{GridMenus-q9qYpzKh.js → GridMenus-DVGuqp2l.js} +430 -430
  37. package/dist/wc/{PinSplitter-D-qtDhSM.js → PinSplitter-Btco0txr.js} +24 -24
  38. package/dist/wc/{SvChartMenu-CQNrQGT2.js → SvChartMenu-Ue-HFR5c.js} +58 -58
  39. package/dist/wc/{SvDateRangeInput-BYNOfYxD.js → SvDateRangeInput-CJZohgBe.js} +43 -43
  40. package/dist/wc/{SvDateTimePicker-Bq9N0Oiy.js → SvDateTimePicker-DmK_QkkU.js} +241 -241
  41. package/dist/wc/{SvField-B_kurx_P.js → SvField-C-OodMMv.js} +71 -71
  42. package/dist/wc/SvGridCellEditor-C7ios4i_.js +582 -0
  43. package/dist/wc/{SvGridChart-DrmqEen2.js → SvGridChart-DWRqlhCV.js} +549 -549
  44. package/dist/wc/{SvGridChartBuilder-XFvW1BdT.js → SvGridChartBuilder-BXW3yUR0.js} +337 -337
  45. package/dist/wc/{SvGridChartPanel-oJJtUKG6.js → SvGridChartPanel-28n4tomx.js} +354 -354
  46. package/dist/wc/{SvGridChartView-Cetuj70H.js → SvGridChartView-CbG_WrU-.js} +7 -7
  47. package/dist/wc/{SvGridDropdown-45WsObLL.js → SvGridDropdown-BVnpNU_I.js} +130 -130
  48. package/dist/wc/{SvModal-DrfOYYyX.js → SvModal-DLppAQED.js} +91 -91
  49. package/dist/wc/{client-DQIzjDBG.js → client-GWAV79ON.js} +479 -476
  50. package/dist/wc/{date-format-DS-z_h9v.js → date-format-4_QoS4zZ.js} +393 -393
  51. package/dist/wc/{grid-extension.svelte-Bufq4B4C.js → grid-extension.svelte-CucBS4FL.js} +1 -1
  52. package/dist/wc/sv-sheet-element.js +10474 -10460
  53. package/dist/wc/{svelte-virtualizer.svelte-CkZaFakp.js → svelte-virtualizer.svelte-BDcouWqw.js} +3 -3
  54. package/package.json +17 -4
  55. package/dist/designer/assets/index-DdmxO6Dd.css +0 -1
  56. package/dist/wc/GridFooter-4T29pfok.js +0 -145
  57. package/dist/wc/SvGridCellEditor-Nnl8C3Yj.js +0 -582
@@ -1151,7 +1151,7 @@ function mapSqlType(raw) {
1151
1151
  if (/json/.test(t)) return "json";
1152
1152
  return "text";
1153
1153
  }
1154
- function fieldFrom(name, rawType, pk, notNull) {
1154
+ function fieldFrom(name, rawType, pk, notNull, generated = false) {
1155
1155
  const field = {
1156
1156
  field: name,
1157
1157
  type: mapSqlType(rawType)
@@ -1159,12 +1159,26 @@ function fieldFrom(name, rawType, pk, notNull) {
1159
1159
  if (pk) {
1160
1160
  field.primaryKey = true;
1161
1161
  field.readonly = true;
1162
- } else if (notNull) field.required = true;
1162
+ } else if (generated) field.readonly = true;
1163
+ else if (notNull) field.required = true;
1163
1164
  return field;
1164
1165
  }
1165
- /** Map catalog rows (aliased name/type/nullable/pk) from information_schema dialects. */
1166
+ /**
1167
+ * A Base table's order column stays in the schema (base-connect finds it there
1168
+ * and reads in its order) but is never shown or written by a generated app.
1169
+ */
1170
+ function hideBaseOrderColumn(field) {
1171
+ if (field.field !== "svgrid_order") return field;
1172
+ const { required: _required, ...rest } = field;
1173
+ return {
1174
+ ...rest,
1175
+ readonly: true,
1176
+ hidden: true
1177
+ };
1178
+ }
1179
+ /** Map catalog rows (aliased name/type/nullable/pk/generated) from information_schema dialects. */
1166
1180
  function infoSchemaFields(rows) {
1167
- return rows.map((r) => fieldFrom(String(r.name), String(r.type ?? ""), truthy(r.pk), !truthy(r.nullable)));
1181
+ return rows.map((r) => hideBaseOrderColumn(fieldFrom(String(r.name), String(r.type ?? ""), truthy(r.pk), !truthy(r.nullable), truthy(r.generated))));
1168
1182
  }
1169
1183
  /** Map SQLite `PRAGMA table_info` rows (name/type/notnull/pk-as-index). */
1170
1184
  function sqliteFields(rows) {
@@ -1191,7 +1205,8 @@ var DIALECTS$1 = {
1191
1205
  columns: (table) => ({
1192
1206
  sql: `SELECT c.column_name AS name, c.data_type AS type,
1193
1207
  CASE WHEN c.is_nullable = 'YES' THEN 1 ELSE 0 END AS nullable,
1194
- CASE WHEN pk.column_name IS NOT NULL THEN 1 ELSE 0 END AS pk
1208
+ CASE WHEN pk.column_name IS NOT NULL THEN 1 ELSE 0 END AS pk,
1209
+ CASE WHEN (c.is_identity = 'YES' AND c.identity_generation = 'ALWAYS') OR c.is_generated = 'ALWAYS' THEN 1 ELSE 0 END AS generated
1195
1210
  FROM information_schema.columns c
1196
1211
  LEFT JOIN (
1197
1212
  SELECT kcu.column_name
@@ -1226,7 +1241,8 @@ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = $1`,
1226
1241
  columns: (table) => ({
1227
1242
  sql: `SELECT COLUMN_NAME AS name, DATA_TYPE AS type,
1228
1243
  CASE WHEN IS_NULLABLE = 'YES' THEN 1 ELSE 0 END AS nullable,
1229
- CASE WHEN COLUMN_KEY = 'PRI' THEN 1 ELSE 0 END AS pk
1244
+ CASE WHEN COLUMN_KEY = 'PRI' THEN 1 ELSE 0 END AS pk,
1245
+ CASE WHEN EXTRA LIKE '%GENERATED%' THEN 1 ELSE 0 END AS generated
1230
1246
  FROM information_schema.columns
1231
1247
  WHERE TABLE_NAME = ? AND TABLE_SCHEMA = DATABASE()
1232
1248
  ORDER BY ORDINAL_POSITION`,
@@ -16215,7 +16231,11 @@ function layoutFile(nav, opts = {}) {
16215
16231
  contents: `<script lang="ts">
16216
16232
  import '../app.css'
16217
16233
  import '../custom.css'
16218
- import { page } from '$app/state'${opts.access ? `\n import { currentRole, canScreen } from '$lib/access'` : ""}${opts.i18n ? `\n import { t, currentLocale, locales } from '$lib/i18n'` : ""}${opts.auth ? `\n import type { LayoutData } from './$types'` : ""}${opts.supabaseAuth ? `\n import { SvAuthGate, type SupabaseAuthClientLike } from '@svgrid/enterprise'\n import { supabaseClient } from '$lib/connections'` : ""}
16234
+ import { page } from '$app/state'
16235
+ import { setLicenseKey } from '@svgrid/enterprise'${opts.access ? `\n import { currentRole, canScreen } from '$lib/access'` : ""}${opts.i18n ? `\n import { t, currentLocale, locales } from '$lib/i18n'` : ""}${opts.auth ? `\n import type { LayoutData } from './$types'` : ""}${opts.supabaseAuth ? `\n import { SvAuthGate, type SupabaseAuthClientLike } from '@svgrid/enterprise'\n import { supabaseClient } from '$lib/connections'` : ""}
16236
+
16237
+ // VITE_SVPRO_KEY in .env is your SvGrid license key; it removes the unlicensed watermark.
16238
+ if (import.meta.env.VITE_SVPRO_KEY) setLicenseKey(import.meta.env.VITE_SVPRO_KEY)
16219
16239
 
16220
16240
  let { children${opts.auth ? ", data" : ""} }${opts.auth ? ": { children: import('svelte').Snippet; data: LayoutData }" : ""} = $props()
16221
16241
  const nav = ${JSON.stringify(links)}
@@ -16430,11 +16450,18 @@ function emitStudioApp(schemas) {
16430
16450
  label: n.label
16431
16451
  };
16432
16452
  });
16453
+ const customCss = {
16454
+ path: "src/custom.css",
16455
+ description: "Your custom styles (imported after app.css so it overrides).",
16456
+ userOwned: true,
16457
+ contents: "/* Your custom styles. Imported after app.css, so these rules override the defaults. */\n"
16458
+ };
16433
16459
  return [
16434
16460
  ...files,
16435
16461
  ...prepared.map((s) => entityScreenPage(s)),
16436
16462
  layoutFile(nav),
16437
- homeFile(nav)
16463
+ homeFile(nav),
16464
+ customCss
16438
16465
  ];
16439
16466
  }
16440
16467
  //#endregion
@@ -16457,7 +16484,7 @@ function hasFieldConditions(schema) {
16457
16484
  *
16458
16485
  * `version.test.ts` asserts this matches `package.json`, so the two cannot drift.
16459
16486
  */
16460
- var SVGRID_VERSION = "3.1.17";
16487
+ var SVGRID_VERSION = "3.1.18";
16461
16488
  /**
16462
16489
  * The version of `@svgrid/grid`, which releases on its own line: enterprise is
16463
16490
  * on 3.1.x while the grid is on 3.0.x.
@@ -16470,7 +16497,7 @@ var SVGRID_VERSION = "3.1.17";
16470
16497
  *
16471
16498
  * `version.test.ts` asserts this matches packages/grid/package.json.
16472
16499
  */
16473
- var GRID_VERSION = "3.0.18";
16500
+ var GRID_VERSION = "3.0.19";
16474
16501
  //#endregion
16475
16502
  //#region src/studio/emit-project.ts
16476
16503
  var has = (blocks, kind) => blocks.some((b) => b.config.kind === kind);
@@ -16501,15 +16528,24 @@ function columnFormatExpr(fmt) {
16501
16528
  }
16502
16529
  }
16503
16530
  /** The visible grid columns: configured order, per-column header/width/align overrides, editability per mode. */
16504
- function gridColumnsExpr(schemaVar, block, typeName) {
16531
+ function gridColumnsExpr(schemaVar, block, typeName, relDisplay) {
16505
16532
  if (block.config.kind !== "grid") return `schemaToColumns(${schemaVar})`;
16506
16533
  const cfg = block.config;
16507
16534
  const idSafe = block.id.replace(/-/g, "_");
16508
- const visible = cfg.columns.filter((c) => c.show);
16535
+ const shown = cfg.columns.filter((c) => c.show);
16536
+ const shownFields = new Set(shown.map((c) => c.field));
16537
+ const seen = /* @__PURE__ */ new Set();
16538
+ const visible = shown.map((c) => {
16539
+ const d = relDisplay?.get(c.field);
16540
+ return {
16541
+ c,
16542
+ col: d && !shownFields.has(d) ? d : c.field
16543
+ };
16544
+ }).filter((v) => !seen.has(v.col) && !!seen.add(v.col));
16509
16545
  const editablePart = cfg.editing === "inline" ? "" : ", editable: false";
16510
16546
  if (visible.length === 0) return editablePart ? `schemaToColumns(${schemaVar}).map((c) => ({ ...c${editablePart} }))` : `schemaToColumns(${schemaVar})`;
16511
- const order = visible.map((c) => `'${c.field}'`);
16512
- const ovEntries = visible.map((c) => {
16547
+ const order = visible.map((v) => `'${v.col}'`);
16548
+ const ovEntries = visible.map(({ c, col }) => {
16513
16549
  const parts = [];
16514
16550
  if (c.header) parts.push(`header: ${JSON.stringify(c.header)}`);
16515
16551
  if (c.width != null) parts.push(`width: ${c.width}`);
@@ -16518,7 +16554,7 @@ function gridColumnsExpr(schemaVar, block, typeName) {
16518
16554
  if (cfg.treeData && c.field === cfg.treeData.labelField && typeName) parts.push(`cell: (ctx: CellContext<${typeName}>) => renderSnippet(treeCell_${idSafe}, { value: ctx.getValue(), row: ctx.row.original })`);
16519
16555
  else if (c.cellType && typeName) parts.push(`cell: (ctx: CellContext<${typeName}>) => renderSnippet(${cellSnippetName(idSafe, c.field)}, { value: ctx.getValue() })`);
16520
16556
  else if (c.format) parts.push(`format: ${columnFormatExpr(c.format)}`);
16521
- return parts.length ? `'${c.field}': { ${parts.join(", ")} }` : null;
16557
+ return parts.length ? `'${col}': { ${parts.join(", ")} }` : null;
16522
16558
  }).filter((x) => !!x);
16523
16559
  const ovDecl = ovEntries.length ? ` const ov: Record<string, Partial<(typeof all)[number]>> = { ${ovEntries.join(", ")} };` : "";
16524
16560
  const ovPart = ovEntries.length ? ", ...(ov[String(c.field)] ?? {})" : "";
@@ -18240,7 +18276,7 @@ function screenPage(schema, rawSchema, screen, resolve, rawResolve, accessEnable
18240
18276
  if (blocks.some((b) => b.config.kind === "grid" && !!b.config.export?.print)) parts.push(PRINT_HELPER);
18241
18277
  for (const b of blocks) if (b.config.kind === "grid") {
18242
18278
  const idSafe = b.id.replace(/-/g, "_");
18243
- const colExpr = gridColumnsExpr(n.schemaVar, b, n.type);
18279
+ const colExpr = gridColumnsExpr(n.schemaVar, b, n.type, relationDisplayFields(rawSchema, resolve));
18244
18280
  let colValue = i18nEnabled ? `localizeCols(${colExpr}, ${JSON.stringify(schema.name)}, $t)` : colExpr;
18245
18281
  const actions = (b.config.rowActions ?? []).filter((a) => a.kind !== "edit" || wantsForm);
18246
18282
  if (actions.length) {
@@ -18550,7 +18586,7 @@ function attrs(f) {
18550
18586
  return out.length ? " " + out.join(" ") : "";
18551
18587
  }
18552
18588
  /** The `+page.server.ts` + SSR `+page.svelte` for a single-grid CRUD screen. */
18553
- function emitSsrGridScreen(schema, screen, routed, accessEnabled, screenIds, byName) {
18589
+ function emitSsrGridScreen(schema, screen, routed, accessEnabled, screenIds, byName, rawSchema = schema) {
18554
18590
  const n = namesFor(schema);
18555
18591
  const isSql = routed(schema.name);
18556
18592
  const relSchemaOf = (f) => byName.get(f.relation.entity);
@@ -18567,18 +18603,23 @@ function emitSsrGridScreen(schema, screen, routed, accessEnabled, screenIds, byN
18567
18603
  const normType = (t) => t === "number" ? "number" : t === "boolean" ? "boolean" : "text";
18568
18604
  const fieldTypesLit = `{ ${formFields.map((f) => `${jsStr(f.field)}: ${jsStr(normType(f.type))}`).join(", ")} }`;
18569
18605
  const isRel = (f) => f.type === "relation" && !!f.relation && byName.has(f.relation.entity);
18606
+ const relSourceOf = (rs) => routed(rs.name) ? `createKitDataSource<Record<string, unknown>>({ endpoint: ${jsStr("/api/" + namesFor(rs).route)}, fetch })` : namesFor(rs).sourceVar;
18607
+ const relDisplay = relationDisplayFields(rawSchema, (name) => byName.get(name));
18608
+ const labelFields = isSql ? schema.fields.filter((f) => isRel(f) && schema.fields.some((x) => x.field === relDisplay.get(f.field))) : [];
18570
18609
  const relFields = formFields.filter(isRel);
18571
18610
  const relRouted = relFields.some((f) => routed(relSchemaOf(f).name));
18572
- const relSourceVars = [...new Set(relFields.filter((f) => !routed(relSchemaOf(f).name)).map((f) => namesFor(relSchemaOf(f)).sourceVar))];
18611
+ const relSourceVars = [...new Set([...relFields, ...labelFields].filter((f) => !routed(relSchemaOf(f).name)).map((f) => namesFor(relSchemaOf(f)).sourceVar))];
18573
18612
  const relPrefetch = relFields.map((f) => {
18574
18613
  const rs = relSchemaOf(f);
18575
- const rel = namesFor(rs);
18576
18614
  const relId = relIdOf(rs);
18577
18615
  const labelF = f.relation.labelField ?? relId;
18578
- const relSource = routed(rs.name) ? `createKitDataSource<Record<string, unknown>>({ endpoint: ${jsStr("/api/" + rel.route)}, fetch })` : rel.sourceVar;
18579
- return ` const ${f.field}Options = (await ${relSource}.getRows({ startRow: 0, endRow: 100, pageIndex: 0, pageSize: 100, sortModel: [], filterModel: {} })).rows.map((r: Record<string, unknown>) => ({ value: String(r[${jsStr(relId)}] ?? ''), label: String(r[${jsStr(labelF)}] ?? '') }))`;
18616
+ return ` const ${f.field}Options = (await ${relSourceOf(rs)}.getRows({ startRow: 0, endRow: 100, pageIndex: 0, pageSize: 100, sortModel: [], filterModel: {} })).rows.map((r: Record<string, unknown>) => ({ value: String(r[${jsStr(relId)}] ?? ''), label: String(r[${jsStr(labelF)}] ?? '') }))`;
18580
18617
  }).join("\n");
18581
18618
  const relReturn = relFields.map((f) => `, ${f.field}Options`).join("");
18619
+ const labelSpecs = labelFields.map((f) => {
18620
+ const rs = relSchemaOf(f);
18621
+ return `{ fk: ${jsStr(f.field)}, as: ${jsStr(relDisplay.get(f.field))}, lookup: createRelationLookup({ source: ${relSourceOf(rs)}, valueField: ${jsStr(relIdOf(rs))}, labelField: ${jsStr(f.relation.labelField ?? relIdOf(rs))} }) }`;
18622
+ });
18582
18623
  const src = isSql ? "source(fetch)" : n.sourceVar;
18583
18624
  const fetchArg = isSql ? ", fetch" : "";
18584
18625
  const loadFetchArg = isSql || relRouted ? ", fetch" : "";
@@ -18587,7 +18628,8 @@ function emitSsrGridScreen(schema, screen, routed, accessEnabled, screenIds, byN
18587
18628
  const enterpriseImports = [
18588
18629
  "validateAll",
18589
18630
  ...conditional ? ["stripHiddenValues"] : [],
18590
- ...isSql || relRouted ? ["createKitDataSource"] : []
18631
+ ...isSql || relRouted ? ["createKitDataSource"] : [],
18632
+ ...labelSpecs.length ? ["createRelationLookup", "withRelationLabels"] : []
18591
18633
  ].join(", ");
18592
18634
  const dataImports = [.../* @__PURE__ */ new Set([...isSql ? [] : [n.sourceVar], ...relSourceVars])];
18593
18635
  const sourceImport = dataImports.length ? `import { ${dataImports.join(", ")} } from '$lib/data'\n` : "";
@@ -18625,7 +18667,7 @@ function formToValues(fd: FormData): Record<string, unknown> {
18625
18667
 
18626
18668
  export const load: PageServerLoad = async ({ url${loadFetchArg}${localsArg} }) => {
18627
18669
  ${readGuard} const plan = planFromSearchParams(url, ${pageSize})
18628
- const { rows, rowCount } = await ${src}.getRows(plan)
18670
+ const { rows, rowCount } = await ${labelSpecs.length ? `withRelationLabels(${src}, [\n ${labelSpecs.join(",\n ")},\n ])` : src}.getRows(plan)
18629
18671
  ${relPrefetch ? relPrefetch + "\n" : ""} return { rows, total: rowCount, page: plan.pageIndex, size: plan.pageSize, sort: plan.sortModel${relReturn} }
18630
18672
  }
18631
18673
 
@@ -18981,7 +19023,7 @@ function emitStudioProject(project) {
18981
19023
  const schema = byName.get(screen.entity);
18982
19024
  if (!schema) throw new Error(`emitStudioProject: screen "${screen.title}" references missing entity "${screen.entity}"`);
18983
19025
  if (isSsrScreen(project, screen)) {
18984
- if (ssrScreenShape(project, screen) === "grid") pages.push(...emitSsrGridScreen(schema, screen, routed, accessEnabled, screensByEntity.get(screen.entity) ?? [], byName));
19026
+ if (ssrScreenShape(project, screen) === "grid") pages.push(...emitSsrGridScreen(schema, screen, routed, accessEnabled, screensByEntity.get(screen.entity) ?? [], byName, rawByName.get(screen.entity) ?? schema));
18985
19027
  else pages.push(ssrReadServerFile(schema, screen, routed, accessEnabled, screensByEntity.get(screen.entity) ?? [], resolve), screenPage(schema, rawByName.get(screen.entity) ?? schema, screen, resolve, (name) => rawByName.get(name), accessEnabled, i18nEnabled, routeById, drillEnabled, true));
18986
19028
  continue;
18987
19029
  }
@@ -21106,7 +21148,7 @@ jobs:
21106
21148
  - uses: actions/checkout@v4
21107
21149
  - uses: actions/setup-node@v4
21108
21150
  with:
21109
- node-version: 20
21151
+ node-version: 22
21110
21152
  - run: npm install
21111
21153
  - run: npm run build
21112
21154
  - run: npm test
@@ -21125,7 +21167,7 @@ jobs:
21125
21167
  - uses: actions/checkout@v4
21126
21168
  - uses: actions/setup-node@v4
21127
21169
  with:
21128
- node-version: 20
21170
+ node-version: 22
21129
21171
  - run: npm install
21130
21172
  ${steps}`;
21131
21173
  }
@@ -21207,7 +21249,7 @@ function deployPlan(project) {
21207
21249
  files: [{
21208
21250
  path: "Dockerfile",
21209
21251
  description: "Container image (multi-stage: build then run `node build`).",
21210
- contents: `# Build stage\nFROM node:20-alpine AS build\nWORKDIR /app\nCOPY package*.json ./\nRUN npm install\nCOPY . .\nRUN npm run build\nRUN npm prune --omit=dev\n\n# Run stage\nFROM node:20-alpine\nWORKDIR /app\nENV NODE_ENV=production\nCOPY --from=build /app/build ./build\nCOPY --from=build /app/node_modules ./node_modules\nCOPY --from=build /app/package.json ./package.json\nEXPOSE 3000\nCMD ["node", "build"]\n`
21252
+ contents: `# Build stage\nFROM node:22-alpine AS build\nWORKDIR /app\nCOPY package*.json ./\nRUN npm install\nCOPY . .\nRUN npm run build\nRUN npm prune --omit=dev\n\n# Run stage\nFROM node:22-alpine\nWORKDIR /app\nENV NODE_ENV=production\nCOPY --from=build /app/build ./build\nCOPY --from=build /app/node_modules ./node_modules\nCOPY --from=build /app/package.json ./package.json\nEXPOSE 3000\nCMD ["node", "build"]\n`
21211
21253
  }, {
21212
21254
  path: ".dockerignore",
21213
21255
  description: "Files kept out of the image.",
@@ -21243,13 +21285,21 @@ function studioDeployInfo(project) {
21243
21285
  }
21244
21286
  /** DEPLOY.md - the runbook: required env vars, then Git-integration / CI / CLI paths. */
21245
21287
  function deployDocs(project, plan, envKeys, hasMigrations, opts = {}) {
21246
- const envBlock = envKeys.length ? `## Environment variables\n\nSet these on your host (and locally in \`.env\` - copy \`.env.example\`):\n\n${envKeys.map((k) => `- \`${k}\``).join("\n")}\n\nOptional: \`VITE_SVPRO_KEY\` (your SvGrid license key) removes the unlicensed watermark - call \`setLicenseKey(import.meta.env.VITE_SVPRO_KEY)\` once at startup (see the docs).\n\n` : "";
21288
+ const envBlock = envKeys.length ? `## Environment variables\n\nSet these on your host (and locally in \`.env\` - copy \`.env.example\`):\n\n${envKeys.map((k) => `- \`${k}\``).join("\n")}\n\nOptional: \`VITE_SVPRO_KEY\` (your SvGrid license key) removes the unlicensed watermark - the root layout passes it to \`setLicenseKey\`. It is read at build time, so set it before \`npm run build\`.\n\n` : "";
21247
21289
  const migrations = hasMigrations ? `## Database migrations\n\nThis app has a typed Drizzle schema (\`src/lib/server/db/schema.ts\`). Create + apply the tables:\n\n\`\`\`bash\nnpm run db:generate # SQL migrations from the schema\nnpm run db:migrate # apply them to DATABASE_URL\n${opts.hasSeedScript ? "npm run db:seed # optional: insert the sample rows\n" : ""}\`\`\`\n\n(Run these against your production database as part of your release step.)\n\n` : "";
21248
21290
  const ddl = opts.hasDdl ? `## Database tables\n\nThis app's SQL entities expect their tables to exist. Run \`db/schema.sql\` against the database your \`DATABASE_URL\` points at (psql / mysql / sqlite3 / sqlcmd, or your provider's SQL console) before first use - and optionally \`db/seed.sql\` for sample rows. Tip: enable the Drizzle data layer in Studio for real, versioned migrations instead.\n\n` : "";
21249
- const gitIntegration = plan.dashboard ? `## Option A - Git integration (simplest, no secrets)\n\n1. Push this repo to GitHub.\n2. Import it at <${plan.dashboard}> - the SvelteKit build is auto-detected.\n3. Add the environment variables above in the host's dashboard.\n\nEvery push to \`main\` then redeploys automatically.\n\n` : "";
21250
- const ci = plan.deployWorkflow ? `## Option B - GitHub Actions (included)\n\n\`.github/workflows/deploy.yml\` deploys on every push to \`main\`. Add these repository secrets to enable it (Settings -> Secrets and variables -> Actions):\n\n${plan.secrets.map((s) => `- \`${s}\``).join("\n")}\n\nUntil the secrets are set the deploy step is skipped, so CI stays green. Prefer Option A? Delete \`deploy.yml\`.\n\n` : "";
21291
+ const letters = [
21292
+ "A",
21293
+ "B",
21294
+ "C"
21295
+ ];
21296
+ const gitLetter = plan.dashboard ? letters.shift() : "";
21297
+ const ciLetter = plan.deployWorkflow ? letters.shift() : "";
21298
+ const cliLetter = letters.shift();
21299
+ const gitIntegration = plan.dashboard ? `## Option ${gitLetter} - Git integration (simplest, no CI secrets)\n\n1. Push this repo to GitHub.\n2. Import it at <${plan.dashboard}> - the SvelteKit build is auto-detected.\n${envKeys.length ? "3. Add the environment variables above in the host's dashboard.\n" : ""}\nEvery push to \`main\` then redeploys automatically.\n\n` : "";
21300
+ const ci = plan.deployWorkflow ? `## Option ${ciLetter} - GitHub Actions (included)\n\n\`.github/workflows/deploy.yml\` deploys on every push to \`main\`. Add these repository secrets to enable it (Settings -> Secrets and variables -> Actions):\n\n${plan.secrets.map((s) => `- \`${s}\``).join("\n")}\n\nUntil the secrets are set the deploy step is skipped, so CI stays green.${gitLetter ? ` Prefer Option ${gitLetter}? Delete \`deploy.yml\`.` : ""}\n\n` : "";
21251
21301
  const dockerNote = project.deploy === "node" ? `## Container\n\nA \`Dockerfile\` is included:\n\n\`\`\`bash\ndocker build -t ${appSlug(project.title)} .\ndocker run -p 3000:3000 --env-file .env ${appSlug(project.title)}\n\`\`\`\n\nThe server listens on \`PORT\` (default 3000).\n\n` : "";
21252
- const cli = `## Option C - Deploy from your machine\n\n\`\`\`bash\nnpm run deploy\n\`\`\`\n\n(runs \`${plan.deployScript}\`)\n`;
21302
+ const cli = `## Option ${cliLetter} - Deploy from your machine\n\n\`\`\`bash\nnpm run deploy\n\`\`\`\n\n(runs \`${plan.deployScript}\`)\n`;
21253
21303
  return `# Deploying ${project.title}\n\nConfigured for **${plan.label}** (SvelteKit \`${plan.adapterModule}\`). A CI workflow (\`.github/workflows/ci.yml\`) builds + tests every push.\n\n${envBlock}${ddl}${migrations}${gitIntegration}${ci}${dockerNote}${cli}`;
21254
21304
  }
21255
21305
  /** The env-var keys the generated app reads (for DEPLOY.md). */
@@ -21288,13 +21338,20 @@ function supabaseConnOf(project) {
21288
21338
  * random `SESSION_SECRET` filled in, so the app is secure out of the box (no
21289
21339
  * hardcoded dev secret), and the Supabase URL + anon key the model already
21290
21340
  * holds, so a Supabase app runs as generated instead of failing on an empty
21291
- * URL. Only emitted when there is something to fill in. */
21292
- function envDotFile(allSource, project) {
21341
+ * URL. Only emitted when there is something to fill in.
21342
+ *
21343
+ * `databaseUrl` is the connection the app was built from (`studio init --db
21344
+ * sqlite --url ./shop.db`). Only a SQLite file path is written: it is not a
21345
+ * secret, and without it the app opens an empty data.db. Server URLs carry a
21346
+ * password, so those stay for the user to paste. */
21347
+ function envDotFile(allSource, project, databaseUrl) {
21293
21348
  const example = envExample(allSource);
21294
21349
  if (!example) return null;
21295
21350
  const supa = allSource.includes("PUBLIC_SUPABASE_URL") ? supabaseConnOf(project) : null;
21296
- if (!allSource.includes("SESSION_SECRET") && !supa) return null;
21351
+ const sqliteFile = databaseUrl && allSource.includes("import Database from 'better-sqlite3'") && /^DATABASE_URL=$/m.test(example) ? databaseUrl : null;
21352
+ if (!allSource.includes("SESSION_SECRET") && !supa && !sqliteFile) return null;
21297
21353
  let env = example.replace("SESSION_SECRET=change-me-to-a-long-random-string", `SESSION_SECRET=${randomSecret()}`);
21354
+ if (sqliteFile) env = env.replace(/^DATABASE_URL=$/m, `DATABASE_URL=${sqliteFile}`);
21298
21355
  if (supa) env = env.replace(/^PUBLIC_SUPABASE_URL=$/m, `PUBLIC_SUPABASE_URL=${supa.url}`).replace(/^PUBLIC_SUPABASE_ANON_KEY=$/m, `PUBLIC_SUPABASE_ANON_KEY=${supa.key}`);
21299
21356
  return env;
21300
21357
  }
@@ -21376,7 +21433,7 @@ function runtimeDeps(project, allSource) {
21376
21433
  if (allSource.includes("import pg from 'pg'")) dependencies["pg"] = "^8.11.0";
21377
21434
  if (allSource.includes("from 'mysql2/promise'")) dependencies["mysql2"] = "^3.9.0";
21378
21435
  if (allSource.includes("import mssql from 'mssql'")) dependencies["mssql"] = "^10.0.0";
21379
- if (allSource.includes("import Database from 'better-sqlite3'")) dependencies["better-sqlite3"] = "^11.0.0";
21436
+ if (allSource.includes("import Database from 'better-sqlite3'")) dependencies["better-sqlite3"] = BETTER_SQLITE3_RANGE;
21380
21437
  if (allSource.includes("from '@libsql/client'")) dependencies["@libsql/client"] = "^0.14.0";
21381
21438
  if (allSource.includes("from '@electric-sql/pglite'")) dependencies["@electric-sql/pglite"] = "^0.5.0";
21382
21439
  if (/from ['"]hyperformula['"]/.test(allSource)) dependencies["hyperformula"] = "^3.3.0";
@@ -21476,12 +21533,20 @@ import { ${imports.join(", ")} } from './schemas'
21476
21533
  ${blocks.join("\n\n")}
21477
21534
  `;
21478
21535
  }
21536
+ /** The better-sqlite3 range a generated app installs. Mirrored in the studio
21537
+ * package's designer-server.ts RUNTIME_DEPS. */
21538
+ var BETTER_SQLITE3_RANGE = "^12.1.0 || ^13.0.0";
21479
21539
  var VITEST_CONFIG = `import { defineConfig } from 'vitest/config'
21540
+ import { svelte } from '@sveltejs/vite-plugin-svelte'
21480
21541
 
21481
- // Node-only test runner for the generated smoke tests (no Svelte/DOM needed).
21482
- // Kept separate from vite.config so the SvelteKit plugin doesn't load here.
21542
+ // Test runner for the generated smoke tests, in Node (no DOM). Kept separate from
21543
+ // vite.config so the SvelteKit plugin doesn't load here. The tests import the
21544
+ // @svgrid/enterprise root, which re-exports .svelte components: the svelte plugin
21545
+ // compiles them, and inlining @svgrid/* routes them through it instead of Node's
21546
+ // own loader (which rejects the .svelte extension).
21483
21547
  export default defineConfig({
21484
- test: { environment: 'node', include: ['src/**/*.test.ts'] },
21548
+ plugins: [svelte()],
21549
+ test: { environment: 'node', include: ['src/**/*.test.ts'], server: { deps: { inline: [/@svgrid\\//] } } },
21485
21550
  })
21486
21551
  `;
21487
21552
  /**
@@ -21546,12 +21611,14 @@ jobs:
21546
21611
  ${steps}`
21547
21612
  }];
21548
21613
  }
21549
- function emitStudioAppBundle(project) {
21614
+ function emitStudioAppBundle(project, opts = {}) {
21550
21615
  const generated = emitStudioProject(project);
21551
21616
  const allSource = generated.map((f) => f.contents).join("\n");
21617
+ const dotEnv = envDotFile(allSource, project, opts.databaseUrl);
21618
+ const readmeConnections = `SQL entities read their connection from \`.env\` (\`DATABASE_URL\`)${generated.some((f) => f.path === "src/lib/connections.ts") ? ";\nSupabase entities use the shared client in `src/lib/connections.ts`, which reads\n`PUBLIC_SUPABASE_URL` and `PUBLIC_SUPABASE_ANON_KEY` from `.env`" : ""}.\n`;
21552
21619
  const plan = deployPlan(project);
21553
21620
  const deploySteps = plan.dashboard ? `1. Push this folder to a Git repo (GitHub / GitLab / Bitbucket).\n2. Import it at <${plan.dashboard}> - build settings are detected automatically.\n\nOr deploy straight from your machine with the CLI:\n\n\`\`\`bash\n${plan.cli}\n\`\`\`` : `Build and run the server:\n\n\`\`\`bash\n${plan.cli}\n\`\`\``;
21554
- const readme = `# ${project.title}\n\nGenerated with SvGrid Studio.\n\n\`\`\`bash\nnpm install\nnpm run dev\n\`\`\`\n\nSQL / Supabase entities read their connection from \`.env\` (\`DATABASE_URL\`) or\n\`src/lib/connections.ts\`. Everything else runs on seeded in-memory data.\n\n## Deploy (${plan.label})\n\nThis app is configured for **${plan.label}** (SvelteKit \`${plan.adapterModule}\`).\n\n${deploySteps}\n\nTo target a different host, pick another **Deploy target** in the designer and\nre-generate, or swap the adapter in \`svelte.config.js\`. Entities on **Local\ndatabase** (PGlite) or **In-memory** need no server env; SQL / Supabase entities\nneed their connection set in the host's environment variables.\n\n## Round-tripping back into the designer\n\nThis app ships its own design model in \`studio.config.json\`. To keep editing\nvisually, open the SvGrid Studio designer and **Load** that file - your entities,\nscreens, blocks, theme, RBAC, i18n, etc. come back exactly as generated, and you\ncan re-generate from there.\n\nThe designer regenerates the files under \`src/routes\` and \`src/lib\` from the\nmodel, so **keep your own custom code in new files/modules and import it**, rather\nthan editing the generated screens in place - that way a re-generate never\nclobbers your work. (The CLI workflow, \`npx @svgrid/studio add\`, is the\nalternative: it wraps generated code in \`svgrid:managed\` markers and preserves\nanything you write outside them.)\n`;
21621
+ const readme = `# ${project.title}\n\nGenerated with SvGrid Studio.\n\n\`\`\`bash\nnpm install\nnpm run dev\n\`\`\`\n\n${readmeConnections}Everything else runs on seeded in-memory data.\n\n## Deploy (${plan.label})\n\nThis app is configured for **${plan.label}** (SvelteKit \`${plan.adapterModule}\`).\n\n${deploySteps}\n\nTo target a different host, pick another **Deploy target** in the designer and\nre-generate, or swap the adapter in \`svelte.config.js\`. Entities on **Local\ndatabase** (PGlite) or **In-memory** need no server env; SQL / Supabase entities\nneed their connection set in the host's environment variables.\n\n## Round-tripping back into the designer\n\nThis app ships its own design model in \`studio.config.json\`. To keep editing\nvisually, open the SvGrid Studio designer and **Load** that file - your entities,\nscreens, blocks, theme, RBAC, i18n, etc. come back exactly as generated, and you\ncan re-generate from there.\n\nThe designer regenerates the files under \`src/routes\` and \`src/lib\` from the\nmodel, so **keep your own custom code in new files/modules and import it**, rather\nthan editing the generated screens in place - that way a re-generate never\nclobbers your work. (The CLI workflow, \`npx @svgrid/studio add\`, is the\nalternative: it wraps generated code in \`svgrid:managed\` markers and preserves\nanything you write outside them.)\n`;
21555
21622
  const bundle = [...[
21556
21623
  {
21557
21624
  path: "package.json",
@@ -21582,10 +21649,10 @@ function emitStudioAppBundle(project) {
21582
21649
  description: "Environment variables the app reads (copy to .env and fill in).",
21583
21650
  contents: envExample(allSource)
21584
21651
  }] : [],
21585
- ...envDotFile(allSource, project) ? [{
21652
+ ...dotEnv ? [{
21586
21653
  path: ".env",
21587
- description: "Local env (git-ignored): a real random SESSION_SECRET and the Supabase connection the design holds are pre-filled, so the app runs as generated; fill in the rest.",
21588
- contents: envDotFile(allSource, project)
21654
+ description: "Local env (git-ignored): a real random SESSION_SECRET, the Supabase connection the design holds and a SQLite file path are pre-filled, so the app runs as generated; fill in the rest.",
21655
+ contents: dotEnv
21589
21656
  }] : [],
21590
21657
  {
21591
21658
  path: "src/app.css",
@@ -41261,23 +41328,64 @@ async function resolveSchemas(from, io) {
41261
41328
  if (src == null) throw new Error(`runStudioAddApp: schema file not found: ${from}`);
41262
41329
  return isPrismaSource(from, src) ? introspectPrismaAll(src) : introspectDrizzleAll(src);
41263
41330
  }
41264
- /** Merge-write a set of generated files, preserving user edits outside managed regions. */
41331
+ /**
41332
+ * Where to park a generated file next to `path`: `<path>.new`, unless that holds
41333
+ * something else (an earlier run's copy the user may be merging), then the first
41334
+ * free `<path>.new.1`, `.new.2`, ... A slot already holding these exact contents
41335
+ * is reused, so re-running does not pile up copies.
41336
+ */
41337
+ async function freeNewPath(path, contents, io) {
41338
+ for (let i = 0;; i++) {
41339
+ const candidate = i === 0 ? `${path}.new` : `${path}.new.${i}`;
41340
+ const there = await io.readFile(candidate);
41341
+ if (there == null || there === contents) return candidate;
41342
+ }
41343
+ }
41344
+ /**
41345
+ * Merge-write a set of generated files, preserving user edits outside managed
41346
+ * regions.
41347
+ *
41348
+ * A file already on disk with no managed markers is somebody's own code (the
41349
+ * create-studio starter's nav layout, a hand-written page), and a merge would
41350
+ * replace it whole. Those get the new version beside them as `<file>.new`, the
41351
+ * same convention the designer uses for a hand-edited file, and come back in
41352
+ * `conflicts`. Unlike the designer, an existing `.new` with other contents is
41353
+ * kept: the copy goes to `.new.1` and up (see freeNewPath). A folder `init` generated (it has studio.config.json) is the
41354
+ * exception: its files are Studio's own output, and regenerating them is the
41355
+ * point.
41356
+ */
41265
41357
  async function writeAll$1(files, io) {
41266
41358
  const written = [];
41359
+ const conflicts = [];
41360
+ const studioApp = await io.readFile("studio.config.json") != null;
41267
41361
  for (const file of files) {
41268
41362
  const existing = await io.readFile(file.path);
41269
41363
  if (skipUserOwned(file, existing != null)) continue;
41364
+ if (existing != null && !studioApp && existing !== file.contents && !existing.includes("// svgrid:managed:start")) {
41365
+ const parkedAs = await freeNewPath(file.path, file.contents, io);
41366
+ await io.writeFile(parkedAs, file.contents);
41367
+ conflicts.push({
41368
+ path: file.path,
41369
+ parkedAs
41370
+ });
41371
+ continue;
41372
+ }
41270
41373
  await io.writeFile(file.path, mergeManaged(existing, file.contents));
41271
41374
  written.push(file.path);
41272
41375
  }
41273
- return written;
41376
+ return {
41377
+ written,
41378
+ conflicts
41379
+ };
41274
41380
  }
41275
41381
  async function runStudioAdd(options, io) {
41276
41382
  const schema = await resolveSchema(options, io);
41277
41383
  const { files } = scaffold(schema, options);
41384
+ const { written, conflicts } = await writeAll$1(files, io);
41278
41385
  return {
41279
41386
  schema,
41280
- written: await writeAll$1(files, io),
41387
+ written,
41388
+ conflicts,
41281
41389
  verify: await verifyScaffold(files)
41282
41390
  };
41283
41391
  }
@@ -41326,8 +41434,10 @@ async function appShellDefaults(options, io) {
41326
41434
  */
41327
41435
  async function runStudioAddApp(schemas, options, io) {
41328
41436
  const { files } = scaffoldApp(schemas, await appShellDefaults(options, io));
41437
+ const { written, conflicts } = await writeAll$1(files, io);
41329
41438
  return {
41330
- written: await writeAll$1(files, io),
41439
+ written,
41440
+ conflicts,
41331
41441
  verify: await verifyScaffold(files)
41332
41442
  };
41333
41443
  }
@@ -41423,29 +41533,41 @@ async function viaSample(o, base) {
41423
41533
  }));
41424
41534
  }
41425
41535
  /** List a Supabase project's tables from its PostgREST OpenAPI doc (URL + anon key).
41426
- * Returns the exposed table names (sorted), or `[]` when the doc can't be read
41427
- * (CORS-blocked, bad key). Lets the Studio wizard offer a table picker instead of
41428
- * making the user hand-type the table name. */
41536
+ * Returns the exposed table names (sorted). `[]` means the project answered and
41537
+ * lists none, or answered 401/403 without naming the key as invalid (the doc is
41538
+ * restricted for this key; each table can still be introspected by name).
41539
+ * Throws, naming the URL, when the project cannot be reached (wrong URL, CORS),
41540
+ * answers with another error status, says the key is invalid, or sends something
41541
+ * that is not an API description. */
41429
41542
  async function listSupabaseTables(url, key, fetchFn = globalThis.fetch) {
41430
- const base = url.replace(/\/+$/, "");
41543
+ const at = `${url.replace(/\/+$/, "")}/rest/v1/`;
41431
41544
  let res;
41432
41545
  try {
41433
- res = await fetchFn(`${base}/rest/v1/`, { headers: {
41546
+ res = await fetchFn(at, { headers: {
41434
41547
  apikey: key,
41435
41548
  Authorization: `Bearer ${key}`,
41436
41549
  Accept: "application/json"
41437
41550
  } });
41438
- } catch {
41439
- return [];
41551
+ } catch (e) {
41552
+ throw new Error(`Could not reach ${at} (${e instanceof Error ? e.message : String(e)})`, { cause: e });
41553
+ }
41554
+ if (!res.ok) {
41555
+ let body = "";
41556
+ try {
41557
+ body = await res.text();
41558
+ } catch {}
41559
+ const badKey = /invalid api key/i.test(body);
41560
+ if ((res.status === 401 || res.status === 403) && !badKey) return [];
41561
+ throw new Error(`${at} answered ${res.status}${badKey ? " (invalid API key)" : ""}`);
41440
41562
  }
41441
- if (!res.ok) return [];
41442
41563
  let spec;
41443
41564
  try {
41444
41565
  spec = await res.json();
41445
41566
  } catch {
41446
- return [];
41567
+ spec = null;
41447
41568
  }
41448
- const defs = spec?.definitions ?? spec?.components?.schemas ?? {};
41569
+ if (!spec || typeof spec !== "object" || !(spec.definitions || spec.components || spec.paths)) throw new Error(`${at} did not answer with an API description: is this a Supabase project URL?`);
41570
+ const defs = spec.definitions ?? spec.components?.schemas ?? {};
41449
41571
  return Object.keys(defs).sort((a, b) => a.localeCompare(b));
41450
41572
  }
41451
41573
  /** Introspect a Supabase/PostgREST table into an EntitySchema, or null. */
@@ -41457,7 +41579,11 @@ async function introspectSupabaseTable(options) {
41457
41579
  const base = o.url.replace(/\/+$/, "");
41458
41580
  const columns = await viaOpenApi(o, base) ?? await viaSample(o, base) ?? await viaCsv(o, base);
41459
41581
  if (!columns || !columns.length) return null;
41460
- return buildEntitySchema(o.table, columns);
41582
+ const schema = buildEntitySchema(o.table, columns);
41583
+ return {
41584
+ ...schema,
41585
+ fields: schema.fields.map((f) => hideBaseOrderColumn(f))
41586
+ };
41461
41587
  }
41462
41588
  //#endregion
41463
41589
  //#region src/studio/user-error.ts
@@ -41719,7 +41845,8 @@ async function gatherFromDatabase(flags, prompts, db) {
41719
41845
  schemas,
41720
41846
  sources,
41721
41847
  seed: {},
41722
- kind: "sql"
41848
+ kind: "sql",
41849
+ ...isFileDialect(dialect) ? { sqliteFile: url } : {}
41723
41850
  };
41724
41851
  }
41725
41852
  /**
@@ -41896,6 +42023,24 @@ async function askTheme(flags, prompts) {
41896
42023
  }
41897
42024
  /** Prefix a path with the output folder (`.` writes into the current one). */
41898
42025
  var inDir = (outDir, path) => (!outDir || outDir === "." ? "" : outDir.replace(/[\\/]+$/, "") + "/") + path;
42026
+ /**
42027
+ * A file path given relative to where init ran, re-expressed relative to the
42028
+ * app folder, which is where the app runs (`npm run dev` in `outDir`). Absolute
42029
+ * paths pass through. An absolute or `..` out dir has no relative answer without
42030
+ * the cwd, so the path is made absolute when the runtime has one.
42031
+ */
42032
+ function fileFromApp(file, outDir) {
42033
+ const fileUrl = /^file:(?![\\/])(.+)$/.exec(file);
42034
+ if (fileUrl) return `file:${fileFromApp(fileUrl[1], outDir)}`;
42035
+ if (!outDir || outDir === "." || file === ":memory:" || /^([a-zA-Z]:[\\/]|[\\/]|file:)/.test(file)) return file;
42036
+ const rel = file.replace(/^\.[\\/]/, "");
42037
+ const segments = outDir.split(/[\\/]+/).filter((s) => s && s !== ".");
42038
+ if (/^([a-zA-Z]:[\\/]|[\\/])/.test(outDir) || segments.includes("..")) {
42039
+ const cwd = globalThis.process?.cwd?.();
42040
+ return cwd ? `${cwd.replace(/\\/g, "/").replace(/\/+$/, "")}/${rel.replace(/\\/g, "/")}` : file;
42041
+ }
42042
+ return "../".repeat(segments.length) + rel;
42043
+ }
41899
42044
  /** Which marker files are present in `outDir` right now. */
41900
42045
  async function markersIn(outDir, io) {
41901
42046
  const found = /* @__PURE__ */ new Set();
@@ -41965,7 +42110,7 @@ async function runStudioInit(flags, prompts, db, io, fetchText = null) {
41965
42110
  ...theme ? { theme } : {}
41966
42111
  });
41967
42112
  const project = flags.kit === 3 ? setKitVersion(built, 3) : built;
41968
- const written = await writeAll(emitStudioAppBundle(project), outDir, io);
42113
+ const written = await writeAll(emitStudioAppBundle(project, data.sqliteFile ? { databaseUrl: fileFromApp(data.sqliteFile, outDir) } : {}), outDir, io);
41969
42114
  const configPath = inDir(outDir, "studio.config.json");
41970
42115
  await io.writeFile(configPath, serializeProject(project));
41971
42116
  written.push(configPath);
@@ -1,6 +1,12 @@
1
+ /**
2
+ * Register the keymap and the formula-aware fill, without the spreadsheet
3
+ * nudge. {@link installEnterprise} calls this: an app that wires the Pro API
4
+ * for export or pivot has not opened a spreadsheet, and must not get a card
5
+ * naming one.
6
+ */
7
+ export declare function registerSheetKeymap(): void;
1
8
  /**
2
9
  * Register the Excel keymap. Idempotent - safe to call from every component
3
- * that wants it, or once at app start. Also invoked by {@link installEnterprise}
4
- * so wiring the Pro API turns the shortcuts on too.
10
+ * that wants it, or once at app start.
5
11
  */
6
12
  export declare function enableSheet(): void;
@@ -7,8 +7,8 @@
7
7
  * The grid ships the `registerGridShortcuts` seam for free and interprets a key
8
8
  * itself only when nothing claims it; the commands are here. Evaluated like
9
9
  * the rest of Enterprise, and part of the Suite edition: it works without a
10
- * license key, and under a Grid-edition key, but the grid shows the unlicensed
11
- * watermark and a one-time console nudge in both cases.
10
+ * license key, but the grid shows the unlicensed watermark and a one-time
11
+ * console nudge.
12
12
  *
13
13
  * ```ts
14
14
  * import { setLicenseKey, enableSheet } from '@svgrid/enterprise'
@@ -24,16 +24,17 @@ import { nudgeEnterpriseFor } from './license.js';
24
24
  import { handleSheetKey } from './sheet/shortcuts.js';
25
25
  import { setFillTranslator } from './sheet/commands.js';
26
26
  import { translateFormula } from './sheet/refs.js';
27
- let enabled = false;
27
+ let registered = false;
28
28
  /**
29
- * Register the Excel keymap. Idempotent - safe to call from every component
30
- * that wants it, or once at app start. Also invoked by {@link installEnterprise}
31
- * so wiring the Pro API turns the shortcuts on too.
29
+ * Register the keymap and the formula-aware fill, without the spreadsheet
30
+ * nudge. {@link installEnterprise} calls this: an app that wires the Pro API
31
+ * for export or pivot has not opened a spreadsheet, and must not get a card
32
+ * naming one.
32
33
  */
33
- export function enableSheet() {
34
- if (enabled)
34
+ export function registerSheetKeymap() {
35
+ if (registered)
35
36
  return;
36
- enabled = true;
37
+ registered = true;
37
38
  // Priority 100 so the sheet keymap sits above anything an app registers of
38
39
  // its own without asking for a priority.
39
40
  registerGridShortcuts(handleSheetKey, { id: 'svgrid-sheet', priority: 100 });
@@ -41,5 +42,16 @@ export function enableSheet() {
41
42
  // literal text into every row; with it the relative half of each reference
42
43
  // shifts and the absolute half does not, the way Excel fills.
43
44
  setFillTranslator((value, delta) => translateFormula(value, delta.rows, delta.cols));
45
+ }
46
+ let enabled = false;
47
+ /**
48
+ * Register the Excel keymap. Idempotent - safe to call from every component
49
+ * that wants it, or once at app start.
50
+ */
51
+ export function enableSheet() {
52
+ registerSheetKeymap();
53
+ if (enabled)
54
+ return;
55
+ enabled = true;
44
56
  nudgeEnterpriseFor('spreadsheet', 'Spreadsheet');
45
57
  }
@@ -22,9 +22,12 @@ export type IntrospectSupabaseOptions = {
22
22
  fetch?: typeof fetch;
23
23
  };
24
24
  /** List a Supabase project's tables from its PostgREST OpenAPI doc (URL + anon key).
25
- * Returns the exposed table names (sorted), or `[]` when the doc can't be read
26
- * (CORS-blocked, bad key). Lets the Studio wizard offer a table picker instead of
27
- * making the user hand-type the table name. */
25
+ * Returns the exposed table names (sorted). `[]` means the project answered and
26
+ * lists none, or answered 401/403 without naming the key as invalid (the doc is
27
+ * restricted for this key; each table can still be introspected by name).
28
+ * Throws, naming the URL, when the project cannot be reached (wrong URL, CORS),
29
+ * answers with another error status, says the key is invalid, or sends something
30
+ * that is not an API description. */
28
31
  export declare function listSupabaseTables(url: string, key: string, fetchFn?: typeof fetch): Promise<string[]>;
29
32
  /** Introspect a Supabase/PostgREST table into an EntitySchema, or null. */
30
33
  export declare function introspectSupabaseTable<TData extends RowData = RowData>(options: IntrospectSupabaseOptions): Promise<EntitySchema<TData> | null>;