@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.
- package/dist/SvGridEditPanel.svelte +14 -1
- package/dist/SvGridScheduler.svelte +15 -6
- package/dist/SvSheet.svelte +2 -0
- package/dist/cdn/svgrid-enterprise.svelte-external.js +6582 -6501
- package/dist/designer/assets/{index-C1Ujg62Z.js → index-DKudY-XD.js} +357 -346
- package/dist/designer/assets/index-WCxj6WfJ.css +1 -0
- package/dist/designer/index.html +2 -2
- package/dist/fields/SvBase.svelte +36 -33
- package/dist/fields/base-connect.js +50 -9
- package/dist/fields/base-sql.d.ts +8 -0
- package/dist/fields/base-sql.js +12 -3
- package/dist/install.js +3 -3
- package/dist/node/studio.js +205 -60
- package/dist/sheet-enable.d.ts +8 -2
- package/dist/sheet-enable.js +21 -9
- package/dist/sources/introspect-supabase.d.ts +6 -3
- package/dist/sources/introspect-supabase.js +29 -11
- package/dist/sources/supabase.d.ts +10 -0
- package/dist/sources/supabase.js +11 -2
- package/dist/studio/cli.d.ts +9 -0
- package/dist/studio/cli.js +41 -7
- package/dist/studio/emit-project.d.ts +3 -1
- package/dist/studio/emit-project.js +84 -33
- package/dist/studio/emit-schema.js +9 -2
- package/dist/studio/index.d.ts +1 -1
- package/dist/studio/init-flow.d.ts +7 -0
- package/dist/studio/init-flow.js +23 -2
- package/dist/studio/introspect-db.d.ts +15 -1
- package/dist/studio/introspect-db.js +29 -5
- package/dist/sveltekit/sql-source.d.ts +24 -0
- package/dist/sveltekit/sql-source.js +34 -1
- package/dist/sveltekit/sql.js +14 -2
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/wc/GridFooter-BgoA77TJ.js +145 -0
- package/dist/wc/{GridMenus-q9qYpzKh.js → GridMenus-DVGuqp2l.js} +430 -430
- package/dist/wc/{PinSplitter-D-qtDhSM.js → PinSplitter-Btco0txr.js} +24 -24
- package/dist/wc/{SvChartMenu-CQNrQGT2.js → SvChartMenu-Ue-HFR5c.js} +58 -58
- package/dist/wc/{SvDateRangeInput-BYNOfYxD.js → SvDateRangeInput-CJZohgBe.js} +43 -43
- package/dist/wc/{SvDateTimePicker-Bq9N0Oiy.js → SvDateTimePicker-DmK_QkkU.js} +241 -241
- package/dist/wc/{SvField-B_kurx_P.js → SvField-C-OodMMv.js} +71 -71
- package/dist/wc/SvGridCellEditor-C7ios4i_.js +582 -0
- package/dist/wc/{SvGridChart-DrmqEen2.js → SvGridChart-DWRqlhCV.js} +549 -549
- package/dist/wc/{SvGridChartBuilder-XFvW1BdT.js → SvGridChartBuilder-BXW3yUR0.js} +337 -337
- package/dist/wc/{SvGridChartPanel-oJJtUKG6.js → SvGridChartPanel-28n4tomx.js} +354 -354
- package/dist/wc/{SvGridChartView-Cetuj70H.js → SvGridChartView-CbG_WrU-.js} +7 -7
- package/dist/wc/{SvGridDropdown-45WsObLL.js → SvGridDropdown-BVnpNU_I.js} +130 -130
- package/dist/wc/{SvModal-DrfOYYyX.js → SvModal-DLppAQED.js} +91 -91
- package/dist/wc/{client-DQIzjDBG.js → client-GWAV79ON.js} +479 -476
- package/dist/wc/{date-format-DS-z_h9v.js → date-format-4_QoS4zZ.js} +393 -393
- package/dist/wc/{grid-extension.svelte-Bufq4B4C.js → grid-extension.svelte-CucBS4FL.js} +1 -1
- package/dist/wc/sv-sheet-element.js +10474 -10460
- package/dist/wc/{svelte-virtualizer.svelte-CkZaFakp.js → svelte-virtualizer.svelte-BDcouWqw.js} +3 -3
- package/package.json +17 -4
- package/dist/designer/assets/index-DdmxO6Dd.css +0 -1
- package/dist/wc/GridFooter-4T29pfok.js +0 -145
- package/dist/wc/SvGridCellEditor-Nnl8C3Yj.js +0 -582
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildEntitySchema } from './schema-from-columns.js';
|
|
2
|
+
import { hideBaseOrderColumn } from '../studio/introspect-db.js';
|
|
2
3
|
const mapOpenApiType = (prop) => {
|
|
3
4
|
if (Array.isArray(prop.enum))
|
|
4
5
|
return 'enum';
|
|
@@ -98,28 +99,44 @@ async function viaSample(o, base) {
|
|
|
98
99
|
}));
|
|
99
100
|
}
|
|
100
101
|
/** List a Supabase project's tables from its PostgREST OpenAPI doc (URL + anon key).
|
|
101
|
-
* Returns the exposed table names (sorted)
|
|
102
|
-
*
|
|
103
|
-
*
|
|
102
|
+
* Returns the exposed table names (sorted). `[]` means the project answered and
|
|
103
|
+
* lists none, or answered 401/403 without naming the key as invalid (the doc is
|
|
104
|
+
* restricted for this key; each table can still be introspected by name).
|
|
105
|
+
* Throws, naming the URL, when the project cannot be reached (wrong URL, CORS),
|
|
106
|
+
* answers with another error status, says the key is invalid, or sends something
|
|
107
|
+
* that is not an API description. */
|
|
104
108
|
export async function listSupabaseTables(url, key, fetchFn = globalThis.fetch) {
|
|
105
109
|
const base = url.replace(/\/+$/, '');
|
|
110
|
+
const at = `${base}/rest/v1/`;
|
|
106
111
|
let res;
|
|
107
112
|
try {
|
|
108
|
-
res = await fetchFn(
|
|
113
|
+
res = await fetchFn(at, { headers: { apikey: key, Authorization: `Bearer ${key}`, Accept: 'application/json' } });
|
|
109
114
|
}
|
|
110
|
-
catch {
|
|
111
|
-
|
|
115
|
+
catch (e) {
|
|
116
|
+
throw new Error(`Could not reach ${at} (${e instanceof Error ? e.message : String(e)})`, { cause: e });
|
|
117
|
+
}
|
|
118
|
+
if (!res.ok) {
|
|
119
|
+
let body = '';
|
|
120
|
+
try {
|
|
121
|
+
body = await res.text();
|
|
122
|
+
}
|
|
123
|
+
catch { /* no body */ }
|
|
124
|
+
const badKey = /invalid api key/i.test(body);
|
|
125
|
+
if ((res.status === 401 || res.status === 403) && !badKey)
|
|
126
|
+
return [];
|
|
127
|
+
throw new Error(`${at} answered ${res.status}${badKey ? ' (invalid API key)' : ''}`);
|
|
112
128
|
}
|
|
113
|
-
if (!res.ok)
|
|
114
|
-
return [];
|
|
115
129
|
let spec;
|
|
116
130
|
try {
|
|
117
131
|
spec = await res.json();
|
|
118
132
|
}
|
|
119
133
|
catch {
|
|
120
|
-
|
|
134
|
+
spec = null;
|
|
121
135
|
}
|
|
122
|
-
|
|
136
|
+
if (!spec || typeof spec !== 'object' || !(spec.definitions || spec.components || spec.paths)) {
|
|
137
|
+
throw new Error(`${at} did not answer with an API description: is this a Supabase project URL?`);
|
|
138
|
+
}
|
|
139
|
+
const defs = spec.definitions ?? spec.components?.schemas ?? {};
|
|
123
140
|
// PostgREST also lists RPCs under `paths` as `/rpc/...`; `definitions` are the tables/views.
|
|
124
141
|
return Object.keys(defs).sort((a, b) => a.localeCompare(b));
|
|
125
142
|
}
|
|
@@ -133,5 +150,6 @@ export async function introspectSupabaseTable(options) {
|
|
|
133
150
|
const columns = (await viaOpenApi(o, base)) ?? (await viaSample(o, base)) ?? (await viaCsv(o, base));
|
|
134
151
|
if (!columns || !columns.length)
|
|
135
152
|
return null;
|
|
136
|
-
|
|
153
|
+
const schema = buildEntitySchema(o.table, columns);
|
|
154
|
+
return { ...schema, fields: schema.fields.map((f) => hideBaseOrderColumn(f)) };
|
|
137
155
|
}
|
|
@@ -38,5 +38,15 @@ export type SupabaseDataSourceConfig<TData extends RowData> = {
|
|
|
38
38
|
* pivoted request is answered as plain grouping, with a warning once.
|
|
39
39
|
*/
|
|
40
40
|
pivot?: (request: ServerRequest) => Promise<ServerResult<TData>>;
|
|
41
|
+
/**
|
|
42
|
+
* How rows come back when the request has no sort, e.g. creation order.
|
|
43
|
+
* Default: a `created_at` / `inserted_at` field when the schema has one.
|
|
44
|
+
* The primary key is appended to every leaf order as the tie-breaker, so
|
|
45
|
+
* paging with `range` is deterministic.
|
|
46
|
+
*/
|
|
47
|
+
defaultOrder?: ReadonlyArray<{
|
|
48
|
+
field: string;
|
|
49
|
+
desc?: boolean;
|
|
50
|
+
}>;
|
|
41
51
|
};
|
|
42
52
|
export declare function createSupabaseDataSource<TData extends RowData>(config: SupabaseDataSourceConfig<TData>): WritableDataSource<TData>;
|
package/dist/sources/supabase.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveIdField } from '../schema.js';
|
|
2
|
+
import { unsortedOrder } from '../sveltekit/sql-source.js';
|
|
2
3
|
import { nudgeEnterpriseFor } from '../license.js';
|
|
3
4
|
import { normalizeFilters } from './filters.js';
|
|
4
5
|
const sanitize = (term) => term.replace(/[%,()]/g, '');
|
|
@@ -9,6 +10,8 @@ export function createSupabaseDataSource(config) {
|
|
|
9
10
|
const searchColumns = config.searchColumns ??
|
|
10
11
|
schema.fields.filter((f) => f.type === 'text' || f.type === 'enum').map((f) => String(f.field));
|
|
11
12
|
let warnedPivot = false;
|
|
13
|
+
const key = schema.fields.some((f) => f.field === idField) ? idField : null;
|
|
14
|
+
const unsorted = unsortedOrder(schema, config.defaultOrder);
|
|
12
15
|
return {
|
|
13
16
|
async getRows(request) {
|
|
14
17
|
// ---- Server-side pivot ----------------------------------------------
|
|
@@ -112,8 +115,14 @@ export function createSupabaseDataSource(config) {
|
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
117
|
else {
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
// Without an order PostgREST pages in no fixed order. Unsorted rows go
|
|
119
|
+
// in creation order where it is known; the key breaks ties.
|
|
120
|
+
const sorted = request.sortModel.map((s) => ({ field: s.id, desc: !!s.desc }));
|
|
121
|
+
const order = sorted.length ? sorted : unsorted;
|
|
122
|
+
for (const o of order)
|
|
123
|
+
q = q.order(o.field, { ascending: !o.desc });
|
|
124
|
+
if (key && !order.some((o) => o.field === key))
|
|
125
|
+
q = q.order(key, { ascending: true });
|
|
117
126
|
}
|
|
118
127
|
const { data, count, error } = await q.range(request.startRow, request.endRow - 1);
|
|
119
128
|
if (error)
|
package/dist/studio/cli.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export type AddResult = {
|
|
|
35
35
|
schema: EntitySchema;
|
|
36
36
|
/** Paths written, in order. */
|
|
37
37
|
written: string[];
|
|
38
|
+
/** Existing files without managed markers, left alone; the new version went to `parkedAs`. */
|
|
39
|
+
conflicts: ParkedFile[];
|
|
38
40
|
verify: VerifyResult;
|
|
39
41
|
};
|
|
40
42
|
/** Resolve an EntitySchema from whichever input was provided. */
|
|
@@ -44,9 +46,16 @@ export declare function resolveSchema(options: AddOptions, io: StudioIO): Promis
|
|
|
44
46
|
* --from` path), linked so foreign keys become working relation lookups.
|
|
45
47
|
*/
|
|
46
48
|
export declare function resolveSchemas(from: string, io: StudioIO): Promise<EntitySchema[]>;
|
|
49
|
+
/** A generated file that was not written over an existing one, and where it went instead. */
|
|
50
|
+
export type ParkedFile = {
|
|
51
|
+
path: string;
|
|
52
|
+
parkedAs: string;
|
|
53
|
+
};
|
|
47
54
|
export declare function runStudioAdd(options: AddOptions, io: StudioIO): Promise<AddResult>;
|
|
48
55
|
export type AddAppResult = {
|
|
49
56
|
written: string[];
|
|
57
|
+
/** Existing files without managed markers, left alone; the new version went to `parkedAs`. */
|
|
58
|
+
conflicts: ParkedFile[];
|
|
50
59
|
verify: VerifyResult;
|
|
51
60
|
};
|
|
52
61
|
/**
|
package/dist/studio/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { introspectDrizzle, introspectDrizzleAll, introspectJson } from './introspect.js';
|
|
2
2
|
import { introspectPrisma, introspectPrismaAll } from './introspect-prisma.js';
|
|
3
|
-
import { mergeManaged, scaffold, skipUserOwned } from './scaffold.js';
|
|
3
|
+
import { MANAGED_START, mergeManaged, scaffold, skipUserOwned } from './scaffold.js';
|
|
4
4
|
import { scaffoldApp } from './scaffold-app.js';
|
|
5
5
|
import { verifyScaffold } from './verify.js';
|
|
6
6
|
/** A `.prisma` file (by extension or a `model X {` block) parses as Prisma; otherwise Drizzle. */
|
|
@@ -34,9 +34,37 @@ export async function resolveSchemas(from, io) {
|
|
|
34
34
|
throw new Error(`runStudioAddApp: schema file not found: ${from}`);
|
|
35
35
|
return isPrismaSource(from, src) ? introspectPrismaAll(src) : introspectDrizzleAll(src);
|
|
36
36
|
}
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Where to park a generated file next to `path`: `<path>.new`, unless that holds
|
|
39
|
+
* something else (an earlier run's copy the user may be merging), then the first
|
|
40
|
+
* free `<path>.new.1`, `.new.2`, ... A slot already holding these exact contents
|
|
41
|
+
* is reused, so re-running does not pile up copies.
|
|
42
|
+
*/
|
|
43
|
+
async function freeNewPath(path, contents, io) {
|
|
44
|
+
for (let i = 0;; i++) {
|
|
45
|
+
const candidate = i === 0 ? `${path}.new` : `${path}.new.${i}`;
|
|
46
|
+
const there = await io.readFile(candidate);
|
|
47
|
+
if (there == null || there === contents)
|
|
48
|
+
return candidate;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Merge-write a set of generated files, preserving user edits outside managed
|
|
53
|
+
* regions.
|
|
54
|
+
*
|
|
55
|
+
* A file already on disk with no managed markers is somebody's own code (the
|
|
56
|
+
* create-studio starter's nav layout, a hand-written page), and a merge would
|
|
57
|
+
* replace it whole. Those get the new version beside them as `<file>.new`, the
|
|
58
|
+
* same convention the designer uses for a hand-edited file, and come back in
|
|
59
|
+
* `conflicts`. Unlike the designer, an existing `.new` with other contents is
|
|
60
|
+
* kept: the copy goes to `.new.1` and up (see freeNewPath). A folder `init` generated (it has studio.config.json) is the
|
|
61
|
+
* exception: its files are Studio's own output, and regenerating them is the
|
|
62
|
+
* point.
|
|
63
|
+
*/
|
|
38
64
|
async function writeAll(files, io) {
|
|
39
65
|
const written = [];
|
|
66
|
+
const conflicts = [];
|
|
67
|
+
const studioApp = (await io.readFile('studio.config.json')) != null;
|
|
40
68
|
for (const file of files) {
|
|
41
69
|
const existing = await io.readFile(file.path);
|
|
42
70
|
// User-owned companions (a screen's `handlers.ts`) carry no managed markers -
|
|
@@ -45,18 +73,24 @@ async function writeAll(files, io) {
|
|
|
45
73
|
// HANDLERS-DESIGN.md (the round-trip contract).
|
|
46
74
|
if (skipUserOwned(file, existing != null))
|
|
47
75
|
continue;
|
|
76
|
+
if (existing != null && !studioApp && existing !== file.contents && !existing.includes(MANAGED_START)) {
|
|
77
|
+
const parkedAs = await freeNewPath(file.path, file.contents, io);
|
|
78
|
+
await io.writeFile(parkedAs, file.contents);
|
|
79
|
+
conflicts.push({ path: file.path, parkedAs });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
48
82
|
// Regenerating? Replace only the managed region, keep the user's edits.
|
|
49
83
|
await io.writeFile(file.path, mergeManaged(existing, file.contents));
|
|
50
84
|
written.push(file.path);
|
|
51
85
|
}
|
|
52
|
-
return written;
|
|
86
|
+
return { written, conflicts };
|
|
53
87
|
}
|
|
54
88
|
export async function runStudioAdd(options, io) {
|
|
55
89
|
const schema = await resolveSchema(options, io);
|
|
56
90
|
const { files } = scaffold(schema, options);
|
|
57
|
-
const written = await writeAll(files, io);
|
|
91
|
+
const { written, conflicts } = await writeAll(files, io);
|
|
58
92
|
const verify = await verifyScaffold(files);
|
|
59
|
-
return { schema, written, verify };
|
|
93
|
+
return { schema, written, conflicts, verify };
|
|
60
94
|
}
|
|
61
95
|
/** The stylesheets an `init`-generated layout imports, in cascade order. */
|
|
62
96
|
const APP_STYLESHEETS = ['app.css', 'custom.css'];
|
|
@@ -118,7 +152,7 @@ async function appShellDefaults(options, io) {
|
|
|
118
152
|
*/
|
|
119
153
|
export async function runStudioAddApp(schemas, options, io) {
|
|
120
154
|
const { files } = scaffoldApp(schemas, await appShellDefaults(options, io));
|
|
121
|
-
const written = await writeAll(files, io);
|
|
155
|
+
const { written, conflicts } = await writeAll(files, io);
|
|
122
156
|
const verify = await verifyScaffold(files);
|
|
123
|
-
return { written, verify };
|
|
157
|
+
return { written, conflicts, verify };
|
|
124
158
|
}
|
|
@@ -65,7 +65,9 @@ export declare function studioDeployInfo(project: StudioProject): {
|
|
|
65
65
|
/** The runtime npm deps the generated source imports, keyed by package -> range.
|
|
66
66
|
* One source of truth for package.json AND the fragment-mode dependency report. */
|
|
67
67
|
export declare function runtimeDeps(project: StudioProject, allSource: string): Record<string, string>;
|
|
68
|
-
export declare function emitStudioAppBundle(project: StudioProject
|
|
68
|
+
export declare function emitStudioAppBundle(project: StudioProject, opts?: {
|
|
69
|
+
databaseUrl?: string;
|
|
70
|
+
}): GeneratedFile[];
|
|
69
71
|
/**
|
|
70
72
|
* Emit ONLY the app content (`src/routes` + `src/lib` + `db`) to drop into an
|
|
71
73
|
* EXISTING SvelteKit app - not a whole new app. Everything emitStudioProject
|
|
@@ -38,20 +38,31 @@ function columnFormatExpr(fmt) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
/** The visible grid columns: configured order, per-column header/width/align overrides, editability per mode. */
|
|
41
|
-
function gridColumnsExpr(schemaVar, block, typeName) {
|
|
41
|
+
function gridColumnsExpr(schemaVar, block, typeName, relDisplay) {
|
|
42
42
|
if (block.config.kind !== 'grid')
|
|
43
43
|
return `schemaToColumns(${schemaVar})`;
|
|
44
44
|
const cfg = block.config;
|
|
45
45
|
const idSafe = block.id.replace(/-/g, '_');
|
|
46
|
-
const
|
|
46
|
+
const shown = cfg.columns.filter((c) => c.show);
|
|
47
|
+
// The prepared schema hides a relation's FK from the grid and shows its label
|
|
48
|
+
// column instead (`customer_id` -> `customer`), so a configured FK column maps
|
|
49
|
+
// to the label column - unless that one is configured too.
|
|
50
|
+
const shownFields = new Set(shown.map((c) => c.field));
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
const visible = shown
|
|
53
|
+
.map((c) => {
|
|
54
|
+
const d = relDisplay?.get(c.field);
|
|
55
|
+
return { c, col: d && !shownFields.has(d) ? d : c.field };
|
|
56
|
+
})
|
|
57
|
+
.filter((v) => !seen.has(v.col) && !!seen.add(v.col));
|
|
47
58
|
// Only inline editing keeps cells editable; form / read-only grids are not editable in place.
|
|
48
59
|
const editablePart = cfg.editing === 'inline' ? '' : ', editable: false';
|
|
49
60
|
if (visible.length === 0) {
|
|
50
61
|
return editablePart ? `schemaToColumns(${schemaVar}).map((c) => ({ ...c${editablePart} }))` : `schemaToColumns(${schemaVar})`;
|
|
51
62
|
}
|
|
52
|
-
const order = visible.map((
|
|
63
|
+
const order = visible.map((v) => `'${v.col}'`);
|
|
53
64
|
const ovEntries = visible
|
|
54
|
-
.map((c) => {
|
|
65
|
+
.map(({ c, col }) => {
|
|
55
66
|
const parts = [];
|
|
56
67
|
if (c.header)
|
|
57
68
|
parts.push(`header: ${JSON.stringify(c.header)}`);
|
|
@@ -70,7 +81,7 @@ function gridColumnsExpr(schemaVar, block, typeName) {
|
|
|
70
81
|
parts.push(`cell: (ctx: CellContext<${typeName}>) => renderSnippet(${cellSnippetName(idSafe, c.field)}, { value: ctx.getValue() })`);
|
|
71
82
|
else if (c.format)
|
|
72
83
|
parts.push(`format: ${columnFormatExpr(c.format)}`);
|
|
73
|
-
return parts.length ? `'${
|
|
84
|
+
return parts.length ? `'${col}': { ${parts.join(', ')} }` : null;
|
|
74
85
|
})
|
|
75
86
|
.filter((x) => !!x);
|
|
76
87
|
const ovDecl = ovEntries.length ? ` const ov: Record<string, Partial<(typeof all)[number]>> = { ${ovEntries.join(', ')} };` : '';
|
|
@@ -2151,7 +2162,7 @@ function screenPage(schema, rawSchema, screen, resolve, rawResolve, accessEnable
|
|
|
2151
2162
|
for (const b of blocks) {
|
|
2152
2163
|
if (b.config.kind === 'grid') {
|
|
2153
2164
|
const idSafe = b.id.replace(/-/g, '_');
|
|
2154
|
-
const colExpr = gridColumnsExpr(n.schemaVar, b, n.type);
|
|
2165
|
+
const colExpr = gridColumnsExpr(n.schemaVar, b, n.type, relationDisplayFields(rawSchema, resolve));
|
|
2155
2166
|
let colValue = i18nEnabled ? `localizeCols(${colExpr}, ${JSON.stringify(schema.name)}, $t)` : colExpr;
|
|
2156
2167
|
// An "edit" action needs the form; drop it if this grid has no edit modal.
|
|
2157
2168
|
const actions = (b.config.rowActions ?? []).filter((a) => a.kind !== 'edit' || wantsForm);
|
|
@@ -2532,7 +2543,7 @@ function attrs(f) {
|
|
|
2532
2543
|
return out.length ? ' ' + out.join(' ') : '';
|
|
2533
2544
|
}
|
|
2534
2545
|
/** The `+page.server.ts` + SSR `+page.svelte` for a single-grid CRUD screen. */
|
|
2535
|
-
function emitSsrGridScreen(schema, screen, routed, accessEnabled, screenIds, byName) {
|
|
2546
|
+
function emitSsrGridScreen(schema, screen, routed, accessEnabled, screenIds, byName, rawSchema = schema) {
|
|
2536
2547
|
const n = namesFor(schema);
|
|
2537
2548
|
// Routed (SQL, or in-memory rows held on the server): a same-origin client over
|
|
2538
2549
|
// the entity's /api route. Otherwise REST, whose source is imported from
|
|
@@ -2561,24 +2572,35 @@ function emitSsrGridScreen(schema, screen, routed, accessEnabled, screenIds, byN
|
|
|
2561
2572
|
const fieldTypesLit = `{ ${formFields.map((f) => `${jsStr(f.field)}: ${jsStr(normType(f.type))}`).join(', ')} }`;
|
|
2562
2573
|
// Relation fields render as a native <select> whose options are prefetched in
|
|
2563
2574
|
// load - from the related entity's /api route when it has one, else from its
|
|
2564
|
-
// in-process source in $lib/data (see
|
|
2575
|
+
// in-process source in $lib/data (see relSourceOf below).
|
|
2565
2576
|
const isRel = (f) => f.type === 'relation' && !!f.relation && byName.has(f.relation.entity);
|
|
2577
|
+
const relSourceOf = (rs) => routed(rs.name)
|
|
2578
|
+
? `createKitDataSource<Record<string, unknown>>({ endpoint: ${jsStr('/api/' + namesFor(rs).route)}, fetch })`
|
|
2579
|
+
: namesFor(rs).sourceVar;
|
|
2580
|
+
// The grid shows each relation as its label column (`customer_id` -> `customer`).
|
|
2581
|
+
// The $lib/data sources fill it with withRelationLabels; rows read through the
|
|
2582
|
+
// /api route come back without it, so load wraps that client the same way:
|
|
2583
|
+
// one batched `id IN (...)` lookup per relation for the ids on this page only.
|
|
2584
|
+
// Names come from the RAW schema, as everywhere else that has to match
|
|
2585
|
+
// withRelationLabels.
|
|
2586
|
+
const relDisplay = relationDisplayFields(rawSchema, (name) => byName.get(name));
|
|
2587
|
+
const labelFields = isSql ? schema.fields.filter((f) => isRel(f) && schema.fields.some((x) => x.field === relDisplay.get(f.field))) : [];
|
|
2566
2588
|
const relFields = formFields.filter(isRel);
|
|
2567
2589
|
const relRouted = relFields.some((f) => routed(relSchemaOf(f).name));
|
|
2568
|
-
const relSourceVars = [...new Set(relFields.filter((f) => !routed(relSchemaOf(f).name)).map((f) => namesFor(relSchemaOf(f)).sourceVar))];
|
|
2590
|
+
const relSourceVars = [...new Set([...relFields, ...labelFields].filter((f) => !routed(relSchemaOf(f).name)).map((f) => namesFor(relSchemaOf(f)).sourceVar))];
|
|
2569
2591
|
const relPrefetch = relFields
|
|
2570
2592
|
.map((f) => {
|
|
2571
2593
|
const rs = relSchemaOf(f);
|
|
2572
|
-
const rel = namesFor(rs);
|
|
2573
2594
|
const relId = relIdOf(rs);
|
|
2574
2595
|
const labelF = f.relation.labelField ?? relId;
|
|
2575
|
-
const
|
|
2576
|
-
? `createKitDataSource<Record<string, unknown>>({ endpoint: ${jsStr('/api/' + rel.route)}, fetch })`
|
|
2577
|
-
: rel.sourceVar;
|
|
2578
|
-
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)}] ?? '') }))`;
|
|
2596
|
+
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)}] ?? '') }))`;
|
|
2579
2597
|
})
|
|
2580
2598
|
.join('\n');
|
|
2581
2599
|
const relReturn = relFields.map((f) => `, ${f.field}Options`).join('');
|
|
2600
|
+
const labelSpecs = labelFields.map((f) => {
|
|
2601
|
+
const rs = relSchemaOf(f);
|
|
2602
|
+
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))} }) }`;
|
|
2603
|
+
});
|
|
2582
2604
|
// Source acquisition:
|
|
2583
2605
|
// - routed: build a same-origin client over the entity's /api/<entity> route
|
|
2584
2606
|
// with SvelteKit's event.fetch, so validation / RBAC / triggers / audit stay
|
|
@@ -2596,6 +2618,7 @@ function emitSsrGridScreen(schema, screen, routed, accessEnabled, screenIds, byN
|
|
|
2596
2618
|
'validateAll',
|
|
2597
2619
|
...(conditional ? ['stripHiddenValues'] : []),
|
|
2598
2620
|
...(isSql || relRouted ? ['createKitDataSource'] : []),
|
|
2621
|
+
...(labelSpecs.length ? ['createRelationLookup', 'withRelationLabels'] : []),
|
|
2599
2622
|
].join(', ');
|
|
2600
2623
|
const dataImports = [...new Set([...(isSql ? [] : [n.sourceVar]), ...relSourceVars])];
|
|
2601
2624
|
const sourceImport = dataImports.length ? `import { ${dataImports.join(', ')} } from '$lib/data'\n` : '';
|
|
@@ -2637,7 +2660,7 @@ function formToValues(fd: FormData): Record<string, unknown> {
|
|
|
2637
2660
|
|
|
2638
2661
|
export const load: PageServerLoad = async ({ url${loadFetchArg}${localsArg} }) => {
|
|
2639
2662
|
${readGuard} const plan = planFromSearchParams(url, ${pageSize})
|
|
2640
|
-
const { rows, rowCount } = await ${src}.getRows(plan)
|
|
2663
|
+
const { rows, rowCount } = await ${labelSpecs.length ? `withRelationLabels(${src}, [\n ${labelSpecs.join(',\n ')},\n ])` : src}.getRows(plan)
|
|
2641
2664
|
${relPrefetch ? relPrefetch + '\n' : ''} return { rows, total: rowCount, page: plan.pageIndex, size: plan.pageSize, sort: plan.sortModel${relReturn} }
|
|
2642
2665
|
}
|
|
2643
2666
|
|
|
@@ -3050,7 +3073,7 @@ export function emitStudioProject(project) {
|
|
|
3050
3073
|
// load-only server file, and the SAME page markup renders from data.*.
|
|
3051
3074
|
if (isSsrScreen(project, screen)) {
|
|
3052
3075
|
if (ssrScreenShape(project, screen) === 'grid') {
|
|
3053
|
-
pages.push(...emitSsrGridScreen(schema, screen, routed, accessEnabled, screensByEntity.get(screen.entity) ?? [], byName));
|
|
3076
|
+
pages.push(...emitSsrGridScreen(schema, screen, routed, accessEnabled, screensByEntity.get(screen.entity) ?? [], byName, rawByName.get(screen.entity) ?? schema));
|
|
3054
3077
|
}
|
|
3055
3078
|
else {
|
|
3056
3079
|
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));
|
|
@@ -5005,7 +5028,7 @@ jobs:
|
|
|
5005
5028
|
- uses: actions/checkout@v4
|
|
5006
5029
|
- uses: actions/setup-node@v4
|
|
5007
5030
|
with:
|
|
5008
|
-
node-version:
|
|
5031
|
+
node-version: 22
|
|
5009
5032
|
- run: npm install
|
|
5010
5033
|
- run: npm run build
|
|
5011
5034
|
- run: npm test
|
|
@@ -5024,7 +5047,7 @@ jobs:
|
|
|
5024
5047
|
- uses: actions/checkout@v4
|
|
5025
5048
|
- uses: actions/setup-node@v4
|
|
5026
5049
|
with:
|
|
5027
|
-
node-version:
|
|
5050
|
+
node-version: 22
|
|
5028
5051
|
- run: npm install
|
|
5029
5052
|
${steps}`;
|
|
5030
5053
|
}
|
|
@@ -5097,7 +5120,7 @@ function deployPlan(project) {
|
|
|
5097
5120
|
adapterModule: '@sveltejs/adapter-node',
|
|
5098
5121
|
adapterDep: ['@sveltejs/adapter-node', '^5.2.0'],
|
|
5099
5122
|
files: [
|
|
5100
|
-
{ path: 'Dockerfile', description: 'Container image (multi-stage: build then run `node build`).', contents: `# Build stage\nFROM node:
|
|
5123
|
+
{ path: 'Dockerfile', description: 'Container image (multi-stage: build then run `node build`).', 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` },
|
|
5101
5124
|
{ path: '.dockerignore', description: 'Files kept out of the image.', contents: `node_modules\n.svelte-kit\nbuild\n.env\n.env.*\n.git\n.github\n` },
|
|
5102
5125
|
],
|
|
5103
5126
|
cli: 'npm run build && node build',
|
|
@@ -5126,7 +5149,7 @@ export function studioDeployInfo(project) {
|
|
|
5126
5149
|
/** DEPLOY.md - the runbook: required env vars, then Git-integration / CI / CLI paths. */
|
|
5127
5150
|
function deployDocs(project, plan, envKeys, hasMigrations, opts = {}) {
|
|
5128
5151
|
const envBlock = envKeys.length
|
|
5129
|
-
? `## 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 -
|
|
5152
|
+
? `## 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`
|
|
5130
5153
|
: '';
|
|
5131
5154
|
const migrations = hasMigrations
|
|
5132
5155
|
? `## 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`
|
|
@@ -5134,16 +5157,22 @@ function deployDocs(project, plan, envKeys, hasMigrations, opts = {}) {
|
|
|
5134
5157
|
const ddl = opts.hasDdl
|
|
5135
5158
|
? `## 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`
|
|
5136
5159
|
: '';
|
|
5160
|
+
// Letter the options that exist, so a target without a dashboard or a deploy
|
|
5161
|
+
// workflow does not skip from A to C.
|
|
5162
|
+
const letters = ['A', 'B', 'C'];
|
|
5163
|
+
const gitLetter = plan.dashboard ? letters.shift() : '';
|
|
5164
|
+
const ciLetter = plan.deployWorkflow ? letters.shift() : '';
|
|
5165
|
+
const cliLetter = letters.shift();
|
|
5137
5166
|
const gitIntegration = plan.dashboard
|
|
5138
|
-
? `## Option
|
|
5167
|
+
? `## 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`
|
|
5139
5168
|
: '';
|
|
5140
5169
|
const ci = plan.deployWorkflow
|
|
5141
|
-
? `## Option
|
|
5170
|
+
? `## 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`
|
|
5142
5171
|
: '';
|
|
5143
5172
|
const dockerNote = project.deploy === 'node'
|
|
5144
5173
|
? `## 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`
|
|
5145
5174
|
: '';
|
|
5146
|
-
const cli = `## Option
|
|
5175
|
+
const cli = `## Option ${cliLetter} - Deploy from your machine\n\n\`\`\`bash\nnpm run deploy\n\`\`\`\n\n(runs \`${plan.deployScript}\`)\n`;
|
|
5147
5176
|
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}`;
|
|
5148
5177
|
}
|
|
5149
5178
|
/** The env-var keys the generated app reads (for DEPLOY.md). */
|
|
@@ -5190,15 +5219,23 @@ function supabaseConnOf(project) {
|
|
|
5190
5219
|
* random `SESSION_SECRET` filled in, so the app is secure out of the box (no
|
|
5191
5220
|
* hardcoded dev secret), and the Supabase URL + anon key the model already
|
|
5192
5221
|
* holds, so a Supabase app runs as generated instead of failing on an empty
|
|
5193
|
-
* URL. Only emitted when there is something to fill in.
|
|
5194
|
-
|
|
5222
|
+
* URL. Only emitted when there is something to fill in.
|
|
5223
|
+
*
|
|
5224
|
+
* `databaseUrl` is the connection the app was built from (`studio init --db
|
|
5225
|
+
* sqlite --url ./shop.db`). Only a SQLite file path is written: it is not a
|
|
5226
|
+
* secret, and without it the app opens an empty data.db. Server URLs carry a
|
|
5227
|
+
* password, so those stay for the user to paste. */
|
|
5228
|
+
function envDotFile(allSource, project, databaseUrl) {
|
|
5195
5229
|
const example = envExample(allSource);
|
|
5196
5230
|
if (!example)
|
|
5197
5231
|
return null;
|
|
5198
5232
|
const supa = allSource.includes('PUBLIC_SUPABASE_URL') ? supabaseConnOf(project) : null;
|
|
5199
|
-
|
|
5233
|
+
const sqliteFile = databaseUrl && allSource.includes("import Database from 'better-sqlite3'") && /^DATABASE_URL=$/m.test(example) ? databaseUrl : null;
|
|
5234
|
+
if (!allSource.includes('SESSION_SECRET') && !supa && !sqliteFile)
|
|
5200
5235
|
return null;
|
|
5201
5236
|
let env = example.replace('SESSION_SECRET=change-me-to-a-long-random-string', `SESSION_SECRET=${randomSecret()}`);
|
|
5237
|
+
if (sqliteFile)
|
|
5238
|
+
env = env.replace(/^DATABASE_URL=$/m, `DATABASE_URL=${sqliteFile}`);
|
|
5202
5239
|
// The anon key is public / browser-safe; access is governed by RLS.
|
|
5203
5240
|
if (supa)
|
|
5204
5241
|
env = env.replace(/^PUBLIC_SUPABASE_URL=$/m, `PUBLIC_SUPABASE_URL=${supa.url}`).replace(/^PUBLIC_SUPABASE_ANON_KEY=$/m, `PUBLIC_SUPABASE_ANON_KEY=${supa.key}`);
|
|
@@ -5296,8 +5333,11 @@ export function runtimeDeps(project, allSource) {
|
|
|
5296
5333
|
dependencies['mysql2'] = '^3.9.0';
|
|
5297
5334
|
if (allSource.includes("import mssql from 'mssql'"))
|
|
5298
5335
|
dependencies['mssql'] = '^10.0.0';
|
|
5336
|
+
// 11.x and 12.0.0 ship no Node 24 prebuild, so npm falls back to node-gyp and
|
|
5337
|
+
// the install fails without a C++ toolchain. 12.1+ has one; 13.x bundles N-API
|
|
5338
|
+
// prebuilds that load on any Node >= 22.
|
|
5299
5339
|
if (allSource.includes("import Database from 'better-sqlite3'"))
|
|
5300
|
-
dependencies['better-sqlite3'] =
|
|
5340
|
+
dependencies['better-sqlite3'] = BETTER_SQLITE3_RANGE;
|
|
5301
5341
|
if (allSource.includes("from '@libsql/client'"))
|
|
5302
5342
|
dependencies['@libsql/client'] = '^0.14.0';
|
|
5303
5343
|
if (allSource.includes("from '@electric-sql/pglite'"))
|
|
@@ -5415,12 +5455,20 @@ import { ${imports.join(', ')} } from './schemas'
|
|
|
5415
5455
|
${blocks.join('\n\n')}
|
|
5416
5456
|
`;
|
|
5417
5457
|
}
|
|
5458
|
+
/** The better-sqlite3 range a generated app installs. Mirrored in the studio
|
|
5459
|
+
* package's designer-server.ts RUNTIME_DEPS. */
|
|
5460
|
+
const BETTER_SQLITE3_RANGE = '^12.1.0 || ^13.0.0';
|
|
5418
5461
|
const VITEST_CONFIG = `import { defineConfig } from 'vitest/config'
|
|
5462
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
|
5419
5463
|
|
|
5420
|
-
//
|
|
5421
|
-
//
|
|
5464
|
+
// Test runner for the generated smoke tests, in Node (no DOM). Kept separate from
|
|
5465
|
+
// vite.config so the SvelteKit plugin doesn't load here. The tests import the
|
|
5466
|
+
// @svgrid/enterprise root, which re-exports .svelte components: the svelte plugin
|
|
5467
|
+
// compiles them, and inlining @svgrid/* routes them through it instead of Node's
|
|
5468
|
+
// own loader (which rejects the .svelte extension).
|
|
5422
5469
|
export default defineConfig({
|
|
5423
|
-
|
|
5470
|
+
plugins: [svelte()],
|
|
5471
|
+
test: { environment: 'node', include: ['src/**/*.test.ts'], server: { deps: { inline: [/@svgrid\\//] } } },
|
|
5424
5472
|
})
|
|
5425
5473
|
`;
|
|
5426
5474
|
/**
|
|
@@ -5486,14 +5534,17 @@ jobs:
|
|
|
5486
5534
|
${steps}`,
|
|
5487
5535
|
}];
|
|
5488
5536
|
}
|
|
5489
|
-
export function emitStudioAppBundle(project) {
|
|
5537
|
+
export function emitStudioAppBundle(project, opts = {}) {
|
|
5490
5538
|
const generated = emitStudioProject(project);
|
|
5491
5539
|
const allSource = generated.map((f) => f.contents).join('\n');
|
|
5540
|
+
const dotEnv = envDotFile(allSource, project, opts.databaseUrl);
|
|
5541
|
+
const hasConnections = generated.some((f) => f.path === 'src/lib/connections.ts');
|
|
5542
|
+
const readmeConnections = `SQL entities read their connection from \`.env\` (\`DATABASE_URL\`)${hasConnections ? ';\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`;
|
|
5492
5543
|
const plan = deployPlan(project);
|
|
5493
5544
|
const deploySteps = plan.dashboard
|
|
5494
5545
|
? `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\`\`\``
|
|
5495
5546
|
: `Build and run the server:\n\n\`\`\`bash\n${plan.cli}\n\`\`\``;
|
|
5496
|
-
const readme = `# ${project.title}\n\nGenerated with SvGrid Studio.\n\n\`\`\`bash\nnpm install\nnpm run dev\n\`\`\`\n\
|
|
5547
|
+
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`;
|
|
5497
5548
|
const scaffold = [
|
|
5498
5549
|
{ path: 'package.json', description: 'Dependencies + scripts (npm install, npm run dev).', contents: packageJson(project, allSource) },
|
|
5499
5550
|
{ path: 'svelte.config.js', description: `SvelteKit config (${plan.adapterModule}).`, contents: svelteConfig(plan) },
|
|
@@ -5504,7 +5555,7 @@ export function emitStudioAppBundle(project) {
|
|
|
5504
5555
|
rootLayoutModule(project),
|
|
5505
5556
|
...SCAFFOLD_STATIC,
|
|
5506
5557
|
...(envExample(allSource) ? [{ path: '.env.example', description: 'Environment variables the app reads (copy to .env and fill in).', contents: envExample(allSource) }] : []),
|
|
5507
|
-
...(
|
|
5558
|
+
...(dotEnv ? [{ path: '.env', 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.', contents: dotEnv }] : []),
|
|
5508
5559
|
{ path: 'src/app.css', description: 'App theme tokens + page styles.', contents: appCss(project) },
|
|
5509
5560
|
// Your styles: a dedicated CSS file the layout imports AFTER app.css, so its rules
|
|
5510
5561
|
// win. Edited in the designer's Styles panel; regenerated from the model.
|
|
@@ -986,7 +986,11 @@ export function layoutFile(nav, opts = {}) {
|
|
|
986
986
|
contents: `<script lang="ts">
|
|
987
987
|
import '../app.css'
|
|
988
988
|
import '../custom.css'
|
|
989
|
-
import { page } from '$app/state'
|
|
989
|
+
import { page } from '$app/state'
|
|
990
|
+
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'` : ''}
|
|
991
|
+
|
|
992
|
+
// VITE_SVPRO_KEY in .env is your SvGrid license key; it removes the unlicensed watermark.
|
|
993
|
+
if (import.meta.env.VITE_SVPRO_KEY) setLicenseKey(import.meta.env.VITE_SVPRO_KEY)
|
|
990
994
|
|
|
991
995
|
let { children${opts.auth ? ', data' : ''} }${opts.auth ? ": { children: import('svelte').Snippet; data: LayoutData }" : ''} = $props()
|
|
992
996
|
const nav = ${JSON.stringify(links)}
|
|
@@ -1201,5 +1205,8 @@ export function emitStudioApp(schemas) {
|
|
|
1201
1205
|
const n = namesFor(s);
|
|
1202
1206
|
return { href: `/${n.route}`, label: n.label };
|
|
1203
1207
|
});
|
|
1204
|
-
|
|
1208
|
+
// The layout imports ../custom.css after app.css; emitStudioAppBundle writes it
|
|
1209
|
+
// too, so an app built here needs its own copy or the build cannot resolve it.
|
|
1210
|
+
const customCss = { path: 'src/custom.css', description: 'Your custom styles (imported after app.css so it overrides).', userOwned: true, contents: '/* Your custom styles. Imported after app.css, so these rules override the defaults. */\n' };
|
|
1211
|
+
return [...files, ...prepared.map((s) => entityScreenPage(s)), layoutFile(nav), homeFile(nav), customCss];
|
|
1205
1212
|
}
|
package/dist/studio/index.d.ts
CHANGED
|
@@ -31,6 +31,6 @@ export { verifyScaffold, summarizeVerify, type VerifyResult, type VerifyIssue, t
|
|
|
31
31
|
export { UI_COMPONENT_REGISTRY, uiComponentSpec, gridPropSurface, gridApiSettableProps, gridIconGroups, GRID_CURATED_PROPS, STANDARD_UI_EVENTS, type UiPropType, type UiPropGroup, type UiComponentEvent, type UiComponentProp, type UiComponentSpec, } from './ui-components.js';
|
|
32
32
|
export { buildCopilotMessages, projectFromModelText, type CopilotMessages } from './copilot-core.js';
|
|
33
33
|
export { resolveDeployTarget, deployCommands, missingEnvKeys, type DeployProvider, type DeployResolution, type DeployPlanCommands, } from './deploy-cli.js';
|
|
34
|
-
export { runStudioAdd, runStudioAddApp, resolveSchema, resolveSchemas, type StudioIO, type AddOptions, type AddResult, type AddAppResult, } from './cli.js';
|
|
34
|
+
export { runStudioAdd, runStudioAddApp, resolveSchema, resolveSchemas, type StudioIO, type AddOptions, type AddResult, type AddAppResult, type ParkedFile, } from './cli.js';
|
|
35
35
|
export { resolveIdField, schemaToColumns, schemaToFormFields, isFieldHidden, linkRelationLabels, pickLabelField, type EntitySchema, type EntityField, type EntityFieldType, } from '../schema.js';
|
|
36
36
|
export { checkLicenseKey, type LicenseInfo, type LicenseStatus } from '../license-core.js';
|
|
@@ -59,6 +59,13 @@ export type InitResult = {
|
|
|
59
59
|
};
|
|
60
60
|
/** Parse "all", "none", a comma list of names, or a list of 1-based numbers. */
|
|
61
61
|
export declare function parseSelection(answer: string, items: string[]): string[];
|
|
62
|
+
/**
|
|
63
|
+
* A file path given relative to where init ran, re-expressed relative to the
|
|
64
|
+
* app folder, which is where the app runs (`npm run dev` in `outDir`). Absolute
|
|
65
|
+
* paths pass through. An absolute or `..` out dir has no relative answer without
|
|
66
|
+
* the cwd, so the path is made absolute when the runtime has one.
|
|
67
|
+
*/
|
|
68
|
+
export declare function fileFromApp(file: string, outDir: string): string;
|
|
62
69
|
/**
|
|
63
70
|
* The whole guided flow: ask, build, write. Returns the project and what was
|
|
64
71
|
* written so the caller can print next steps (or assert them in a test).
|