@svgrid/create-studio 0.1.4 → 0.2.1
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/index.mjs +26 -38
- package/package.json +6 -2
- package/templates/default/src/lib/data.ts +10 -1
- package/templates/default/src/lib/schemas.ts +27 -0
- package/templates/default/src/routes/+layout.svelte +1 -0
- package/templates/default/src/routes/+page.svelte +1 -0
- package/templates/default/src/routes/products/+page.svelte +8 -0
package/index.mjs
CHANGED
|
@@ -163,7 +163,13 @@ async function applyTheme(destDir, choice) {
|
|
|
163
163
|
|
|
164
164
|
/**
|
|
165
165
|
* Generate the app from a studio.config.json (the visual designer's export):
|
|
166
|
-
*
|
|
166
|
+
* the COMPLETE runnable bundle - same code path as the designer's own "Download
|
|
167
|
+
* .zip" - so the deploy target picked in the designer (Vercel / Netlify /
|
|
168
|
+
* Cloudflare / Node) gets its real adapter + config, and every driver dependency
|
|
169
|
+
* the generated code needs (pg / mysql2 / mssql / better-sqlite3 / libsql /
|
|
170
|
+
* pglite / supabase-js) is detected and added, instead of layering a partial
|
|
171
|
+
* set of routes on top of the static template's fixed adapter-auto/package.json.
|
|
172
|
+
* No static template involved for this path - the bundle is self-sufficient.
|
|
167
173
|
*/
|
|
168
174
|
async function applyProject(destDir, projectPath) {
|
|
169
175
|
const abs = resolve(process.cwd(), projectPath)
|
|
@@ -178,43 +184,13 @@ async function applyProject(destDir, projectPath) {
|
|
|
178
184
|
}
|
|
179
185
|
|
|
180
186
|
const project = studio.parseProject(json)
|
|
181
|
-
const files = studio.
|
|
182
|
-
|
|
183
|
-
for (const ex of ['customers', 'orders']) {
|
|
184
|
-
await rm(join(destDir, 'src', 'routes', ex), { recursive: true, force: true })
|
|
185
|
-
}
|
|
187
|
+
const files = studio.emitStudioAppBundle(project)
|
|
186
188
|
for (const f of files) {
|
|
187
189
|
const full = join(destDir, f.path)
|
|
188
190
|
await mkdir(dirname(full), { recursive: true })
|
|
189
191
|
await writeFile(full, f.contents)
|
|
190
192
|
}
|
|
191
|
-
|
|
192
|
-
// drivers) so the app installs + runs turnkey.
|
|
193
|
-
const allSource = files.map((f) => f.contents).join('\n')
|
|
194
|
-
const DEPS = [
|
|
195
|
-
["from '@supabase/supabase-js'", '@supabase/supabase-js', '^2.45.0'],
|
|
196
|
-
["import pg from 'pg'", 'pg', '^8.11.0'],
|
|
197
|
-
["from 'mysql2/promise'", 'mysql2', '^3.9.0'],
|
|
198
|
-
["import mssql from 'mssql'", 'mssql', '^10.0.0'],
|
|
199
|
-
["import Database from 'better-sqlite3'", 'better-sqlite3', '^11.0.0'],
|
|
200
|
-
]
|
|
201
|
-
for (const [needle, dep, version] of DEPS) {
|
|
202
|
-
if (allSource.includes(needle)) await addDependency(destDir, dep, version)
|
|
203
|
-
}
|
|
204
|
-
return project.screens.map((s) => s.title)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** Add a runtime dependency to the generated app's package.json (idempotent). */
|
|
208
|
-
async function addDependency(destDir, name, version) {
|
|
209
|
-
const pkgPath = join(destDir, 'package.json')
|
|
210
|
-
if (!existsSync(pkgPath)) return
|
|
211
|
-
try {
|
|
212
|
-
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
213
|
-
pkg.dependencies = { ...pkg.dependencies, [name]: version }
|
|
214
|
-
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
215
|
-
} catch {
|
|
216
|
-
// leave deps as-is if package.json isn't valid JSON
|
|
217
|
-
}
|
|
193
|
+
return { titles: project.screens.map((s) => s.title), deployLabel: studio.studioDeployInfo(project).label }
|
|
218
194
|
}
|
|
219
195
|
|
|
220
196
|
/**
|
|
@@ -349,18 +325,27 @@ async function main() {
|
|
|
349
325
|
const themeChoice = args.project ? null : await promptTheme(args, ask, interactive)
|
|
350
326
|
if (rl) rl.close()
|
|
351
327
|
|
|
352
|
-
// 3. Scaffold.
|
|
328
|
+
// 3. Scaffold. --project ships a complete, self-sufficient bundle (its own
|
|
329
|
+
// package.json/adapter config/theme) - skip the static template for that path
|
|
330
|
+
// so its deploy-target choice isn't immediately overwritten by the template's
|
|
331
|
+
// fixed adapter-auto. --from and the default (seeded example) path still
|
|
332
|
+
// build on the template.
|
|
353
333
|
await mkdir(destDir, { recursive: true })
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
334
|
+
if (!args.project) {
|
|
335
|
+
await copyTemplate(TEMPLATE_DIR, destDir)
|
|
336
|
+
await setProjectName(destDir, projectName)
|
|
337
|
+
await applyTheme(destDir, themeChoice)
|
|
338
|
+
}
|
|
357
339
|
|
|
358
340
|
// 3b. Generate from a designer project (--project) or a schema file (--from).
|
|
359
341
|
let entities = null
|
|
360
342
|
let source = null
|
|
343
|
+
let deployLabel = null
|
|
361
344
|
try {
|
|
362
345
|
if (args.project) {
|
|
363
|
-
|
|
346
|
+
const result = await applyProject(destDir, args.project)
|
|
347
|
+
entities = result.titles
|
|
348
|
+
deployLabel = result.deployLabel
|
|
364
349
|
source = args.project
|
|
365
350
|
} else if (args.from) {
|
|
366
351
|
entities = await applyFromSchema(destDir, args.from)
|
|
@@ -374,6 +359,9 @@ async function main() {
|
|
|
374
359
|
// 4. Next steps.
|
|
375
360
|
const rel = isAbsolute(target) || target.startsWith('.') ? target : `./${target}`
|
|
376
361
|
stdout.write(`\n${color('green', '✔')} Scaffolded ${color('bold', projectName)} into ${rel}\n`)
|
|
362
|
+
if (deployLabel) {
|
|
363
|
+
stdout.write(` ${color('dim', 'deploy')} ${deployLabel}\n`)
|
|
364
|
+
}
|
|
377
365
|
if (themeChoice) {
|
|
378
366
|
stdout.write(` ${color('dim', 'theme')} ${themeChoice.name}${themeChoice.dark ? ' (dark)' : ''}\n`)
|
|
379
367
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svgrid/create-studio",
|
|
3
|
-
"
|
|
3
|
+
"funding": {
|
|
4
|
+
"type": "commercial",
|
|
5
|
+
"url": "https://svgrid.com/pricing"
|
|
6
|
+
},
|
|
7
|
+
"version": "0.2.1",
|
|
4
8
|
"description": "Scaffold a runnable SvGrid Studio data app in one command: npm create @svgrid/studio@latest",
|
|
5
9
|
"type": "module",
|
|
6
10
|
"license": "MIT",
|
|
@@ -37,6 +41,6 @@
|
|
|
37
41
|
"node": ">=18"
|
|
38
42
|
},
|
|
39
43
|
"dependencies": {
|
|
40
|
-
"@svgrid/enterprise": "^2.
|
|
44
|
+
"@svgrid/enterprise": "^2.2.1"
|
|
41
45
|
}
|
|
42
46
|
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
createRelationLookup,
|
|
11
11
|
type ServerDataSource,
|
|
12
12
|
} from '@svgrid/enterprise'
|
|
13
|
-
import { customerSchema, orderSchema, type Customer, type Order } from './schemas'
|
|
13
|
+
import { customerSchema, orderSchema, productSchema, type Customer, type Order, type Product } from './schemas'
|
|
14
14
|
|
|
15
15
|
const customerSeed: Customer[] = [
|
|
16
16
|
{ id: 'c1', name: 'Ada Lovelace', email: 'ada@analytic.io', tier: 'enterprise', mrr: 1200, active: true },
|
|
@@ -27,8 +27,17 @@ const orderSeed: Order[] = [
|
|
|
27
27
|
{ id: 'o4', ref: 'INV-1004', customerId: 'c3', amount: 980, status: 'refunded' },
|
|
28
28
|
]
|
|
29
29
|
|
|
30
|
+
const productSeed: Product[] = [
|
|
31
|
+
{ id: 'p1', name: 'Analytics Seat', sku: 'SW-ANL-01', category: 'software', price: 49, inStock: true },
|
|
32
|
+
{ id: 'p2', name: 'Edge Gateway', sku: 'HW-EGW-02', category: 'hardware', price: 899, inStock: true },
|
|
33
|
+
{ id: 'p3', name: 'Onboarding', sku: 'SV-ONB-03', category: 'service', price: 1500, inStock: true },
|
|
34
|
+
{ id: 'p4', name: 'Sensor Kit', sku: 'HW-SNS-04', category: 'hardware', price: 249, inStock: false },
|
|
35
|
+
]
|
|
36
|
+
|
|
30
37
|
export const customersSource = createInMemoryDataSource(customerSeed, customerSchema)
|
|
31
38
|
|
|
39
|
+
export const productsSource = createInMemoryDataSource(productSeed, productSchema)
|
|
40
|
+
|
|
32
41
|
// Orders reference a customer. Enrich every method's rows with the customer
|
|
33
42
|
// NAME so the grid shows it (the form stores the id via the lookup below).
|
|
34
43
|
const nameById = () => new Map((customersSource.rows() as Customer[]).map((c) => [c.id, c.name]))
|
|
@@ -23,6 +23,15 @@ export type Order = {
|
|
|
23
23
|
status: string
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export type Product = {
|
|
27
|
+
id: string
|
|
28
|
+
name: string
|
|
29
|
+
sku: string
|
|
30
|
+
category: string
|
|
31
|
+
price: number
|
|
32
|
+
inStock: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
export const customerSchema: EntitySchema<Customer> = {
|
|
27
36
|
name: 'customers',
|
|
28
37
|
label: 'Customer',
|
|
@@ -59,3 +68,21 @@ export const orderSchema: EntitySchema<Order> = {
|
|
|
59
68
|
] },
|
|
60
69
|
],
|
|
61
70
|
}
|
|
71
|
+
|
|
72
|
+
export const productSchema: EntitySchema<Product> = {
|
|
73
|
+
name: 'products',
|
|
74
|
+
label: 'Product',
|
|
75
|
+
idField: 'id',
|
|
76
|
+
fields: [
|
|
77
|
+
{ field: 'id', type: 'text', primaryKey: true, readonly: true, hidden: { form: true } },
|
|
78
|
+
{ field: 'name', type: 'text', required: true, minLength: 2 },
|
|
79
|
+
{ field: 'sku', type: 'text', label: 'SKU', required: true },
|
|
80
|
+
{ field: 'category', type: 'enum', options: [
|
|
81
|
+
{ value: 'hardware', label: 'Hardware' },
|
|
82
|
+
{ value: 'software', label: 'Software' },
|
|
83
|
+
{ value: 'service', label: 'Service' },
|
|
84
|
+
] },
|
|
85
|
+
{ field: 'price', type: 'number', label: 'Price ($)', min: 0 },
|
|
86
|
+
{ field: 'inStock', type: 'boolean', label: 'In stock' },
|
|
87
|
+
],
|
|
88
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const entities = [
|
|
3
3
|
{ href: '/customers', label: 'Customers', desc: 'Tiers, MRR, and status. Sort, filter, edit inline in a modal.' },
|
|
4
4
|
{ href: '/orders', label: 'Orders', desc: 'Linked to a customer via a searchable lookup field.' },
|
|
5
|
+
{ href: '/products', label: 'Products', desc: 'SKU, category, price, and stock. A standalone catalog screen.' },
|
|
5
6
|
]
|
|
6
7
|
</script>
|
|
7
8
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import EntityScreen from '$lib/EntityScreen.svelte'
|
|
3
|
+
import { productSchema } from '$lib/schemas'
|
|
4
|
+
import { productsSource, nextId } from '$lib/data'
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<h1 class="st__title">Products</h1>
|
|
8
|
+
<EntityScreen schema={productSchema} source={productsSource} newId={() => nextId('p')} />
|