From ac19d9935a3d0b3f8ea6debc38cfcfd6f92ad097 Mon Sep 17 00:00:00 2001 From: Amen <2668333+amenophis1er@users.noreply.github.com> Date: Sun, 13 Sep 2026 21:20:56 -0400 Subject: [PATCH] feat: scaffold and migrate for Trokky 3.0, where the site mounts the Studio 'trokky create' emits server.ts that imports studioRouter from '@trokky/studio/express' and mounts it before any catch-all, forwards the top-level structure, and writes a trokky.config.ts whose studio block holds only branding. All generated projects pin the @trokky packages to ^3.0.0. 'trokky migrate' pins to ^3.0.0 and gains a studio-mount warning: the pre-3.0 idioms (studioPath in mount(), getMountedStudioPath(), info.studioPath, enabled/path/requireAuth/apiUrl or structure inside the studio block, the dist/server/assets.js import) are reported with the replacement. They are not rewritten: the fix is two lines whose right place, before the site's own catch-all, a text rewrite cannot know, and the server refuses the old shape at boot so nothing can slip through. --- cmd/migrate.go | 17 ++++--- internal/migrate/e2e_test.go | 8 ++-- internal/migrate/packagejson.go | 4 +- internal/migrate/packagejson_test.go | 38 +++++++-------- internal/migrate/run_test.go | 8 ++-- internal/migrate/warnings.go | 50 +++++++++++++++++++- internal/migrate/warnings_test.go | 58 ++++++++++++++++++++++- internal/scaffold/scaffold_test.go | 39 ++++++++++++---- internal/scaffold/templates.go | 70 +++++++++++++--------------- 9 files changed, 205 insertions(+), 87 deletions(-) diff --git a/cmd/migrate.go b/cmd/migrate.go index 190d737..bf01106 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -14,7 +14,7 @@ import ( func newMigrateCmd() *cobra.Command { cmd := &cobra.Command{ Use: "migrate", - Short: "Upgrade a Trokky project from v0.1.x packages to v2", + Short: "Upgrade a Trokky project to the current packages (v0.1.x split packages, or a pre-3.0 Studio mount)", Long: `Rewrite a Trokky site project from the old split v0.1.x npm packages to the consolidated v2 packages. @@ -25,13 +25,18 @@ project's JS/TS/Astro sources (@trokky/core -> @trokky/trokky, @trokky/trokky/adapters/*, @trokky/mail-adapter-* -> @trokky/trokky/mail/*, @trokky/fields -> @trokky/studio, ...) and updates the dependency sections of every package.json, pinning @trokky/trokky, @trokky/studio and @trokky/client -to ^2.0.0. node_modules, dist, build, .git, .astro and symlinks are never +to ^3.0.0. node_modules, dist, build, .git, .astro and symlinks are never touched. -It also reports what it cannot fix by itself: imports of package internals, -Astro's build-time inlining of import.meta.env.TROKKY_API_URL, and — most -importantly — structure entries marked type: 'singleton' whose schema does not -set singleton: true. That divergence is a data-loss trap: 'trokky restore' +It also reports what it cannot fix by itself: the pre-3.0 Studio mounting +(studio.enabled/path/structure in the config, studioPath in mount(), the +dist/server/assets.js import), which since 3.0 the server refuses at boot and +which is replaced by two lines in server.ts — import { studioRouter } from +'@trokky/studio/express' and app.use('/studio', studioRouter({ apiPath: '/api' })) +placed before your own catch-all; imports of package internals; Astro's +build-time inlining of import.meta.env.TROKKY_API_URL; and — most importantly — +structure entries marked type: 'singleton' whose schema does not set +singleton: true. That divergence is a data-loss trap: 'trokky restore' POSTs such a document instead of doing an id-preserving PUT, which regenerates the document id and orphans the structure entry. Fix those warnings before you restore into the migrated site. diff --git a/internal/migrate/e2e_test.go b/internal/migrate/e2e_test.go index 77349ef..7cabefa 100644 --- a/internal/migrate/e2e_test.go +++ b/internal/migrate/e2e_test.go @@ -191,9 +191,9 @@ func TestE2EWrite(t *testing.T) { t.Fatalf("rewritten package.json is not valid JSON: %v\n%s", err, pkgSrc) } wantDeps := map[string]string{ - "@trokky/trokky": "^2.0.0", - "@trokky/studio": "^2.0.0", - "@trokky/client": "^2.0.0", + "@trokky/trokky": "^3.0.0", + "@trokky/studio": "^3.0.0", + "@trokky/client": "^3.0.0", "express": "^4.19.0", } for k, v := range wantDeps { @@ -217,7 +217,7 @@ func TestE2EWrite(t *testing.T) { strings.Index(pkgSrc, `"scripts"`), strings.Index(pkgSrc, `"dependencies"`); !(i < j && j < k && k < d) { t.Errorf("key order changed (name=%d type=%d scripts=%d dependencies=%d):\n%s", i, j, k, d, pkgSrc) } - if !strings.Contains(pkgSrc, "\n \"@trokky/trokky\": \"^2.0.0\",") { + if !strings.Contains(pkgSrc, "\n \"@trokky/trokky\": \"^3.0.0\",") { t.Errorf("2-space indentation was not preserved:\n%s", pkgSrc) } diff --git a/internal/migrate/packagejson.go b/internal/migrate/packagejson.go index 8baa34a..487c3fc 100644 --- a/internal/migrate/packagejson.go +++ b/internal/migrate/packagejson.go @@ -8,8 +8,8 @@ import ( "strings" ) -// TargetVersion is the version range the consolidated v2 packages are pinned to. -const TargetVersion = "^2.0.0" +// TargetVersion is the version range the consolidated packages are pinned to. +const TargetVersion = "^3.0.0" // consolidatedPackages are the packages that survive into v2 and must be moved // to TargetVersion wherever they appear. diff --git a/internal/migrate/packagejson_test.go b/internal/migrate/packagejson_test.go index 927dcc5..59d8e6f 100644 --- a/internal/migrate/packagejson_test.go +++ b/internal/migrate/packagejson_test.go @@ -46,8 +46,8 @@ func TestRewritePackageJSONBasic(t *testing.T) { }, "dependencies": { "express": "^4.19.2", - "@trokky/trokky": "^2.0.0", - "@trokky/studio": "^2.0.0", + "@trokky/trokky": "^3.0.0", + "@trokky/studio": "^3.0.0", "zod": "^3.23.8" }, "devDependencies": { @@ -66,9 +66,9 @@ func TestRewritePackageJSONBasic(t *testing.T) { wantChanges := []string{ "removed @trokky/core", - "added @trokky/trokky ^2.0.0", + "added @trokky/trokky ^3.0.0", "removed @trokky/express", - "@trokky/studio -> ^2.0.0", + "@trokky/studio -> ^3.0.0", "removed @trokky/types", } if len(changes) != len(wantChanges) { @@ -99,7 +99,7 @@ func TestRewritePackageJSONInsertsAtFirstRemoval(t *testing.T) { want := `{ "dependencies": { "a": "1", - "@trokky/trokky": "^2.0.0", + "@trokky/trokky": "^3.0.0", "b": "2", "c": "3" } @@ -128,7 +128,7 @@ func TestRewritePackageJSONNoAddWhenNothingRemoved(t *testing.T) { want := `{ "name": "web", "dependencies": { - "@trokky/client": "^2.0.0", + "@trokky/client": "^3.0.0", "astro": "^4.15.0" } } @@ -136,7 +136,7 @@ func TestRewritePackageJSONNoAddWhenNothingRemoved(t *testing.T) { if string(out) != want { t.Errorf("got:\n%s\nwant:\n%s", out, want) } - if len(changes) != 1 || changes[0] != "@trokky/client -> ^2.0.0" { + if len(changes) != 1 || changes[0] != "@trokky/client -> ^3.0.0" { t.Errorf("changes = %v, want only the client bump", changes) } if strings.Contains(string(out), "@trokky/trokky") { @@ -164,11 +164,11 @@ func TestRewritePackageJSONAddsToDevDependenciesOnly(t *testing.T) { } want := `{ "dependencies": { - "@trokky/client": "^2.0.0", + "@trokky/client": "^3.0.0", "astro": "^4.15.0" }, "devDependencies": { - "@trokky/trokky": "^2.0.0", + "@trokky/trokky": "^3.0.0", "typescript": "^5.5.0" } } @@ -176,7 +176,7 @@ func TestRewritePackageJSONAddsToDevDependenciesOnly(t *testing.T) { if string(out) != want { t.Errorf("got:\n%s\nwant:\n%s", out, want) } - wantChanges := []string{"@trokky/client -> ^2.0.0", "removed @trokky/types", "added @trokky/trokky ^2.0.0"} + wantChanges := []string{"@trokky/client -> ^3.0.0", "removed @trokky/types", "added @trokky/trokky ^3.0.0"} if len(changes) != len(wantChanges) { t.Fatalf("changes = %v, want %v", changes, wantChanges) } @@ -205,7 +205,7 @@ func TestRewritePackageJSONAddsOnceWhenBothSectionsRemove(t *testing.T) { } want := `{ "dependencies": { - "@trokky/trokky": "^2.0.0", + "@trokky/trokky": "^3.0.0", "express": "^4.19.2" }, "devDependencies": { @@ -238,7 +238,7 @@ func TestRewritePackageJSONNoAddWhenTrokkyAlreadyPresentElsewhere(t *testing.T) want := `{ "dependencies": {}, "devDependencies": { - "@trokky/trokky": "^2.0.0" + "@trokky/trokky": "^3.0.0" } } ` @@ -290,7 +290,7 @@ func TestRewritePackageJSONCollapsesEmptiedSection(t *testing.T) { want := `{ "name": "site", "dependencies": { - "@trokky/trokky": "^2.0.0" + "@trokky/trokky": "^3.0.0" }, "devDependencies": {}, "private": true @@ -320,7 +320,7 @@ func TestRewritePackageJSONCollapsesLastSectionWithoutComma(t *testing.T) { } want := "{\n" + " \"dependencies\": {\n" + - " \"@trokky/trokky\": \"^2.0.0\"\n" + + " \"@trokky/trokky\": \"^3.0.0\"\n" + " },\n" + " \"peerDependencies\": {}\n" + "}\n" @@ -344,14 +344,14 @@ func TestRewritePackageJSONExistingTrokkyBumped(t *testing.T) { } want := `{ "dependencies": { - "@trokky/trokky": "^2.0.0" + "@trokky/trokky": "^3.0.0" } } ` if string(out) != want { t.Errorf("got:\n%s\nwant:\n%s", out, want) } - if len(changes) != 2 || changes[0] != "@trokky/trokky -> ^2.0.0" || changes[1] != "removed @trokky/core" { + if len(changes) != 2 || changes[0] != "@trokky/trokky -> ^3.0.0" || changes[1] != "removed @trokky/core" { t.Errorf("changes = %v", changes) } } @@ -395,7 +395,7 @@ func TestRewritePackageJSONPreservesIndentation(t *testing.T) { want := "{\n" + " \"name\": \"x\",\n" + " \"dependencies\": {\n" + - " \"@trokky/trokky\": \"^2.0.0\",\n" + + " \"@trokky/trokky\": \"^3.0.0\",\n" + " \"express\": \"^4.19.2\"\n" + " },\n" + " \"private\": true\n" + @@ -422,7 +422,7 @@ func TestRewritePackageJSONPreservesIndentation(t *testing.T) { want := "{\n" + "\t\"name\": \"x\",\n" + "\t\"dependencies\": {\n" + - "\t\t\"@trokky/trokky\": \"^2.0.0\",\n" + + "\t\t\"@trokky/trokky\": \"^3.0.0\",\n" + "\t\t\"express\": \"^4.19.2\"\n" + "\t},\n" + "\t\"private\": true\n" + @@ -511,7 +511,7 @@ func TestRewritePackageJSONNoTrailingNewline(t *testing.T) { if err != nil { t.Fatal(err) } - want := "{\n \"dependencies\": {\n \"@trokky/trokky\": \"^2.0.0\"\n }\n}" + want := "{\n \"dependencies\": {\n \"@trokky/trokky\": \"^3.0.0\"\n }\n}" if string(out) != want { t.Errorf("got %q, want %q", out, want) } diff --git a/internal/migrate/run_test.go b/internal/migrate/run_test.go index ff0b4cb..08a067f 100644 --- a/internal/migrate/run_test.go +++ b/internal/migrate/run_test.go @@ -186,8 +186,8 @@ export const app = createTrokky({ adapter: filesystemData(), mail: mailConsole() wantPkg := `{ "name": "site", "dependencies": { - "@trokky/trokky": "^2.0.0", - "@trokky/studio": "^2.0.0", + "@trokky/trokky": "^3.0.0", + "@trokky/studio": "^3.0.0", "express": "^4.19.2" } } @@ -269,7 +269,7 @@ func TestResultReport(t *testing.T) { res := &Result{ Files: []FileChange{ {Path: "src/server.ts", Kind: KindSource, Hits: map[string]int{"@trokky/core": 2, "@trokky/express": 1}}, - {Path: "package.json", Kind: KindPackageJSON, Changes: []string{"removed @trokky/core", "added @trokky/trokky ^2.0.0"}}, + {Path: "package.json", Kind: KindPackageJSON, Changes: []string{"removed @trokky/core", "added @trokky/trokky ^3.0.0"}}, }, Hits: map[string]int{"@trokky/core": 2, "@trokky/express": 1}, Scanned: 2, @@ -287,7 +287,7 @@ func TestResultReport(t *testing.T) { "src/server.ts", "@trokky/express x1, @trokky/core x2", "package.json", - "removed @trokky/core, added @trokky/trokky ^2.0.0", + "removed @trokky/core, added @trokky/trokky ^3.0.0", "Specifiers rewritten by mapping:", "@trokky/express", "-> @trokky/trokky", diff --git a/internal/migrate/warnings.go b/internal/migrate/warnings.go index 3f37568..633e180 100644 --- a/internal/migrate/warnings.go +++ b/internal/migrate/warnings.go @@ -15,8 +15,8 @@ import ( // look at. Everything here comes from a text scan of the sources: we never // execute or type-check the project. type Warning struct { - // Kind is one of "internal-import", "astro-env", "singleton-divergence" - // or "singleton-near-miss". + // Kind is one of "internal-import", "astro-env", "singleton-divergence", + // "singleton-near-miss" or "studio-mount". Kind string // File is relative to the scanned root, slash-separated. It is empty when // the warning is not tied to a file. @@ -499,6 +499,51 @@ func warnIsSchemaFile(rel, src string) bool { // ScanWarnings walks root and returns every warning found, sorted by file and // line. Skipped directories (node_modules, .git, dist, build, .astro) and // symlinks are not visited. +// --------------------------------------------------------------------------- +// studio mounting (Trokky 3.0) +// --------------------------------------------------------------------------- + +// studioMountPatterns are the pre-3.0 ways a project told the server to serve the +// Studio. Since 3.0 the server refuses them at boot, so they are reported here +// with the replacement rather than rewritten: the fix is two lines in the server +// file whose right place (before the site's own catch-all) a text rewrite cannot +// know. +var studioMountPatterns = []struct { + re *regexp.Regexp + message string +}{ + {regexp.MustCompile(`studioPath\s*:`), + "mount() no longer takes studioPath. Mount the Studio yourself: import { studioRouter } from '@trokky/studio/express' and app.use('/studio', studioRouter({ apiPath: '/api' })) before any catch-all"}, + {regexp.MustCompile(`getMountedStudioPath\s*\(`), + "getMountedStudioPath() is gone; the Studio path is wherever you app.use() studioRouter()"}, + {regexp.MustCompile(`\.\s*studioPath\b`), + "getInfo()/getMountedPaths() no longer report studioPath; keep your own constant for the mount path"}, + {regexp.MustCompile(`studio\s*:\s*\{[^}]*\b(enabled|path|requireAuth|apiUrl)\s*:`), + "the studio config block may only hold branding, fields, settings and session; remove enabled/path/requireAuth/apiUrl (serving the Studio is server.ts's job now)"}, + {regexp.MustCompile(`studio\s*:\s*\{[^}]*\bstructure\b`), + "move structure out of the studio block to the top level of the config: the server enforces singletons from it"}, + {regexp.MustCompile(`@trokky/studio/dist/server/assets\.js`), + "'@trokky/studio/dist/server/assets.js' is no longer exported; use studioRouter() from '@trokky/studio/express'"}, +} + +// ScanStudioMount reports every pre-3.0 Studio mounting idiom in src. +func ScanStudioMount(rel, src string) []Warning { + var out []Warning + for _, p := range studioMountPatterns { + loc := p.re.FindStringIndex(src) + if loc == nil { + continue + } + out = append(out, Warning{ + Kind: "studio-mount", + File: rel, + Line: 1 + strings.Count(src[:loc[0]], "\n"), + Message: p.message, + }) + } + return out +} + // ScanSummary records what the singleton check actually examined, so that a clean run can // be told apart from a run that found nothing to look at. Reporting only "no warnings" makes // those two identical, and the second one loses documents on restore. @@ -550,6 +595,7 @@ func ScanWarningsSummary(root string) ([]Warning, ScanSummary, error) { src := string(data) out = append(out, ScanInternalImports(rel, src)...) + out = append(out, ScanStudioMount(rel, src)...) if astro { out = append(out, ScanAstroEnv(rel, src)...) } diff --git a/internal/migrate/warnings_test.go b/internal/migrate/warnings_test.go index 4c9146c..9041b07 100644 --- a/internal/migrate/warnings_test.go +++ b/internal/migrate/warnings_test.go @@ -141,7 +141,7 @@ func TestIsAstroProject(t *testing.T) { {"astro file", map[string]string{"src/pages/index.astro": "---\n---\n

\n"}, true}, {"package.json dep", map[string]string{"package.json": `{"dependencies":{"astro":"^4.0.0"}}`}, true}, {"package.json devDep", map[string]string{"package.json": `{"devDependencies":{"astro":"^4.0.0"}}`}, true}, - {"plain node project", map[string]string{"package.json": `{"dependencies":{"trokky":"^2.0.0"}}`, "server.ts": "//\n"}, false}, + {"plain node project", map[string]string{"package.json": `{"dependencies":{"trokky":"^3.0.0"}}`, "server.ts": "//\n"}, false}, {"astro only in node_modules", map[string]string{"node_modules/x/a.astro": "x"}, false}, } @@ -175,7 +175,7 @@ func TestScanWarningsAstroEnvOnlyForAstroProjects(t *testing.T) { // Same source, no astro markers anywhere: not our problem. plainRoot := warnWriteTree(t, map[string]string{ - "package.json": `{"dependencies":{"trokky":"^2.0.0"}}`, + "package.json": `{"dependencies":{"trokky":"^3.0.0"}}`, "src/page.ts": warnAstroPage, }) got, err = ScanWarnings(plainRoot) @@ -384,3 +384,57 @@ func TestScanWarningsMissingRoot(t *testing.T) { t.Fatal("expected an error for a missing root") } } + +func TestScanStudioMountReportsEveryPre30Idiom(t *testing.T) { + src := `import { startServer } from '@trokky/trokky/express' +const server = await startServer(config) +trokky.mount(app, { apiPath: '/api', studioPath: '/studio' }) +console.log(info.studioPath) +const p = trokky.getMountedStudioPath() +const { getStudioHTML } = await import('@trokky/studio/dist/server/assets.js') +` + got := ScanStudioMount("server.ts", src) + kinds := map[string]bool{} + for _, w := range got { + if w.Kind != "studio-mount" { + t.Errorf("kind = %q, want studio-mount", w.Kind) + } + if w.File != "server.ts" || w.Line == 0 { + t.Errorf("bad location %s:%d", w.File, w.Line) + } + kinds[w.Message[:20]] = true + } + if len(got) != 4 { + t.Fatalf("expected 4 warnings (studioPath, getMountedStudioPath, .studioPath, dist import), got %d: %+v", len(got), got) + } + if got[0].Line != 3 { + t.Errorf("first warning line = %d, want 3 (the studioPath mount option)", got[0].Line) + } +} + +func TestScanStudioMountFlagsOldConfigKeys(t *testing.T) { + cfg := `export default { + studio: { + enabled: true, + path: '/studio', + structure, + }, +}` + got := ScanStudioMount("trokky.config.ts", cfg) + if len(got) != 2 { + t.Fatalf("expected the removed-keys warning and the structure warning, got %d: %+v", len(got), got) + } + if !strings.Contains(got[0].Message, "enabled/path/requireAuth/apiUrl") || !strings.Contains(got[1].Message, "top level") { + t.Errorf("unexpected messages: %+v", got) + } +} + +func TestScanStudioMountIsQuietOnTheNewShape(t *testing.T) { + src := `import { studioRouter } from '@trokky/studio/express' +server.app.use('/studio', studioRouter({ apiPath: '/api' })) +export default { structure, studio: { branding: { title: 'CMS' } } } +` + if got := ScanStudioMount("server.ts", src); len(got) != 0 { + t.Errorf("new shape must not warn: %+v", got) + } +} diff --git a/internal/scaffold/scaffold_test.go b/internal/scaffold/scaffold_test.go index b43b2cd..5259545 100644 --- a/internal/scaffold/scaffold_test.go +++ b/internal/scaffold/scaffold_test.go @@ -119,8 +119,8 @@ func TestScaffoldTemplates(t *testing.T) { // (b) package.json pins the v2 packages pkg := files["scaffold:package.json"] - if !strings.Contains(pkg, `"@trokky/trokky": "^2.0.0"`) { - t.Errorf("package.json missing trokky ^2.0.0:\n%s", pkg) + if !strings.Contains(pkg, `"@trokky/trokky": "`+TrokkyVersion+`"`) { + t.Errorf("package.json missing trokky %s:\n%s", TrokkyVersion, pkg) } if !strings.Contains(pkg, "@trokky/client") { t.Errorf("package.json missing @trokky/client:\n%s", pkg) @@ -158,11 +158,30 @@ func TestScaffoldTemplates(t *testing.T) { t.Errorf("server.ts has wrong data adapter import %q:\n%s", unwantData, server) } - if cfg.Studio == StudioNone && strings.Contains(server, "studioPath:") { - t.Errorf("server.ts mounts studio for studio=none:\n%s", server) + // Since 3.0 the site mounts Studio itself; the server never takes a studioPath. + if strings.Contains(server, "studioPath:") || strings.Contains(server, "studio.enabled") { + t.Errorf("server.ts uses the pre-3.0 studio mounting:\n%s", server) } - if cfg.Studio != StudioNone && !strings.Contains(server, "studioPath: '/studio'") { - t.Errorf("server.ts missing studio mount:\n%s", server) + hasRouter := strings.Contains(server, "from '@trokky/studio/express'") && strings.Contains(server, "app.use(studioPath, studioRouter(") + if cfg.Studio == StudioEmbedded && !hasRouter { + t.Errorf("server.ts does not mount studioRouter for an embedded studio:\n%s", server) + } + if cfg.Studio != StudioEmbedded && hasRouter { + t.Errorf("server.ts mounts studioRouter for studio=%s:\n%s", cfg.Studio, server) + } + if cfg.Studio != StudioNone && !strings.Contains(server, "structure: trokkyConfig.structure") { + t.Errorf("server.ts does not forward the top-level structure:\n%s", server) + } + // Only the studio block itself may not carry the removed keys; other + // sections legitimately use "enabled:". + if cfgText := files["scaffold:trokky.config.ts"]; strings.Contains(cfgText, "studio: {") { + block := cfgText[strings.Index(cfgText, "studio: {"):] + block = block[:strings.Index(block, "},")+2] + for _, old := range []string{"enabled:", "path:", "requireAuth:", "apiUrl:", "structure,"} { + if strings.Contains(block, old) { + t.Errorf("trokky.config.ts studio block still has pre-3.0 key %q:\n%s", old, block) + } + } } // trokky.config.ts uses the v2 type import @@ -209,11 +228,11 @@ func TestGeneratePackageJSONIsValidJSON(t *testing.T) { if out.Name != cfg.Name { t.Errorf("package.json name = %q, want %q", out.Name, cfg.Name) } - if out.Dependencies["@trokky/trokky"] != "^2.0.0" { - t.Errorf("dependencies.@trokky/trokky = %q, want ^2.0.0", out.Dependencies["@trokky/trokky"]) + if out.Dependencies["@trokky/trokky"] != TrokkyVersion { + t.Errorf("dependencies.@trokky/trokky = %q, want %s", out.Dependencies["@trokky/trokky"], TrokkyVersion) } - if out.Dependencies["@trokky/client"] != "^2.0.0" { - t.Errorf("dependencies[@trokky/client] = %q, want ^2.0.0", out.Dependencies["@trokky/client"]) + if out.Dependencies["@trokky/client"] != TrokkyVersion { + t.Errorf("dependencies[@trokky/client] = %q, want %s", out.Dependencies["@trokky/client"], TrokkyVersion) } }) } diff --git a/internal/scaffold/templates.go b/internal/scaffold/templates.go index 772d5ca..1e565fe 100644 --- a/internal/scaffold/templates.go +++ b/internal/scaffold/templates.go @@ -7,12 +7,15 @@ import ( ) // GeneratePackageJSON generates package.json based on project config. +// TrokkyVersion is the range every generated project pins the @trokky packages to. +const TrokkyVersion = "^3.0.0" + func GeneratePackageJSON(cfg ProjectConfig) string { - // Trokky v2 ships the server, mail, i18n and all adapters in the single + // Trokky ships the server, mail, i18n and all adapters in the single // `trokky` package; adapters are enabled via side-effect imports. deps := map[string]string{ - "@trokky/trokky": "^2.0.0", - "@trokky/client": "^2.0.0", + "@trokky/trokky": TrokkyVersion, + "@trokky/client": TrokkyVersion, "express": "^4.18.2", "dotenv": "^16.3.1", "sharp": "^0.33.0", @@ -24,7 +27,7 @@ func GeneratePackageJSON(cfg ProjectConfig) string { } if cfg.Studio != StudioNone { - deps["@trokky/studio"] = "^2.0.0" + deps["@trokky/studio"] = TrokkyVersion } pkg := map[string]interface{}{ @@ -59,12 +62,21 @@ func GenerateServerTS(cfg ProjectConfig) string { dataAdapterImport = "import '@trokky/trokky/adapters/postgres-data'" } - // Studio is only mounted when the project embeds or proxies it. + // Studio is a router the site mounts itself (since Trokky 3.0). Only an + // embedded Studio is mounted here; a separate deployment serves its own. + studioImport := "" studioMount := "" studioLog := "" + studioConfig := "" + structureConfig := "" if cfg.Studio != StudioNone { - studioMount = "\n studioPath: '/studio'," - studioLog = fmt.Sprintf("\n console.log(%sStudio: http://localhost:${port}${paths.studioPath}%s)", bt, bt) + structureConfig = "\n\n // Navigation structure (the server enforces singletons from it)\n structure: trokkyConfig.structure," + studioConfig = "\n\n // What the Studio reads over the API (branding, settings, session)\n studio: trokkyConfig.studio," + } + if cfg.Studio == StudioEmbedded { + studioImport = "\nimport { studioRouter } from '@trokky/studio/express'" + studioMount = "\n\n // Studio: mount it before any catch-all of your own.\n const studioPath = '/studio'\n app.use(studioPath, studioRouter({ apiPath: paths.apiPath, branding: trokkyConfig.studio?.branding }))" + studioLog = fmt.Sprintf("\n console.log(%sStudio: http://localhost:${port}${studioPath}%s)", bt, bt) } // Optional config sections are only forwarded when trokky.config.ts declares @@ -90,7 +102,7 @@ func GenerateServerTS(cfg ProjectConfig) string { */ import express, { type Express } from 'express' -import { TrokkyExpress } from '@trokky/trokky/express' +import { TrokkyExpress } from '@trokky/trokky/express'%s %s import '@trokky/trokky/adapters/filesystem-media' import trokkyConfig from './trokky.config.js' @@ -114,17 +126,12 @@ async function startServer() { security: trokkyConfig.security, // HTTP server options - server: trokkyConfig.server, - - // Studio integration - studio: trokkyConfig.studio,%s + server: trokkyConfig.server,%s%s%s }) - trokky.mount(app, { - apiPath: '/api',%s - }) + trokky.mount(app, { apiPath: '/api' }) - const paths = trokky.getMountedPaths() + const paths = trokky.getMountedPaths()%s app.get('/health', (_req, res) => { res.json({ @@ -150,7 +157,7 @@ startServer().catch(error => { console.error('Server startup failed:', error) process.exit(1) }) -`, cfg.Name, dataAdapterImport, extraOptions, studioMount, +`, cfg.Name, studioImport, dataAdapterImport, structureConfig, studioConfig, extraOptions, studioMount, bt, cfg.Name, bt, bt, bt, bt, bt, studioLog, bt, bt, cfg.Name) } @@ -265,30 +272,18 @@ const mailFromName = process.env.EMAIL_FROM_NAME || '%s' } : undefined,` } - // Studio config + // Structure (a core concern: singleton enforcement reads it) and the Studio + // config the UI fetches over the API. Where the Studio is served is decided + // in server.ts, not here. structureImport := "" studioConfig := "" - if cfg.Studio == StudioEmbedded { - structureImport = "import { structure } from './structure.js'" - studioConfig = ` - studio: { - enabled: true, - path: '/studio', - structure, - },` - } else if cfg.Studio == StudioSeparate { + if cfg.Studio != StudioNone { structureImport = "import { structure } from './structure.js'" - studioConfig = ` - studio: { - enabled: false, - apiUrl: process.env.API_URL, - structure, - },` - } else { - studioConfig = ` + studioConfig = fmt.Sprintf(` + structure, studio: { - enabled: false, - },` + branding: { title: %q }, + },`, cfg.Name) } // i18n config @@ -622,4 +617,3 @@ export const schemas = [ ] ` } -