@decocms/start 0.19.0

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 (185) hide show
  1. package/.cursor/skills/deco-api-call-dedup/SKILL.md +443 -0
  2. package/.cursor/skills/deco-apps-architecture/SKILL.md +255 -0
  3. package/.cursor/skills/deco-apps-architecture/app-pattern.md +288 -0
  4. package/.cursor/skills/deco-apps-architecture/commerce-types.md +239 -0
  5. package/.cursor/skills/deco-apps-architecture/new-app-guide.md +268 -0
  6. package/.cursor/skills/deco-apps-architecture/scripts-codegen.md +148 -0
  7. package/.cursor/skills/deco-apps-architecture/shared-utils.md +181 -0
  8. package/.cursor/skills/deco-apps-architecture/vtex-deep-structure.md +253 -0
  9. package/.cursor/skills/deco-apps-architecture/website-app.md +169 -0
  10. package/.cursor/skills/deco-apps-vtex-porting/SKILL.md +189 -0
  11. package/.cursor/skills/deco-apps-vtex-porting/adaptation-patterns.md +335 -0
  12. package/.cursor/skills/deco-apps-vtex-porting/commerce-porting.md +155 -0
  13. package/.cursor/skills/deco-apps-vtex-porting/cookie-auth-patterns.md +148 -0
  14. package/.cursor/skills/deco-apps-vtex-porting/structure-map.md +234 -0
  15. package/.cursor/skills/deco-apps-vtex-porting/transform-mapping.md +99 -0
  16. package/.cursor/skills/deco-apps-vtex-porting/website-porting.md +194 -0
  17. package/.cursor/skills/deco-apps-vtex-review/SKILL.md +234 -0
  18. package/.cursor/skills/deco-async-rendering-architecture/SKILL.md +270 -0
  19. package/.cursor/skills/deco-async-rendering-site-guide/SKILL.md +417 -0
  20. package/.cursor/skills/deco-cms-layout-caching/SKILL.md +293 -0
  21. package/.cursor/skills/deco-cms-route-config/SKILL.md +388 -0
  22. package/.cursor/skills/deco-core-architecture/SKILL.md +185 -0
  23. package/.cursor/skills/deco-core-architecture/blocks.md +196 -0
  24. package/.cursor/skills/deco-core-architecture/deco-vs-deco-start.md +191 -0
  25. package/.cursor/skills/deco-core-architecture/engine.md +220 -0
  26. package/.cursor/skills/deco-core-architecture/hooks-components.md +157 -0
  27. package/.cursor/skills/deco-core-architecture/plugins-clients.md +136 -0
  28. package/.cursor/skills/deco-core-architecture/runtime.md +116 -0
  29. package/.cursor/skills/deco-core-architecture/site-usage.md +165 -0
  30. package/.cursor/skills/deco-e2e-testing/SKILL.md +372 -0
  31. package/.cursor/skills/deco-e2e-testing/discovery.md +337 -0
  32. package/.cursor/skills/deco-e2e-testing/scripts/scaffold.sh +81 -0
  33. package/.cursor/skills/deco-e2e-testing/selectors.md +175 -0
  34. package/.cursor/skills/deco-e2e-testing/templates/package.json +18 -0
  35. package/.cursor/skills/deco-e2e-testing/templates/playwright.config.ts +65 -0
  36. package/.cursor/skills/deco-e2e-testing/templates/scripts/baseline.ts +279 -0
  37. package/.cursor/skills/deco-e2e-testing/templates/scripts/run-e2e.ts +194 -0
  38. package/.cursor/skills/deco-e2e-testing/templates/specs/ecommerce-flow.spec.ts +612 -0
  39. package/.cursor/skills/deco-e2e-testing/templates/tsconfig.json +12 -0
  40. package/.cursor/skills/deco-e2e-testing/templates/utils/metrics-collector.ts +918 -0
  41. package/.cursor/skills/deco-e2e-testing/troubleshooting.md +602 -0
  42. package/.cursor/skills/deco-edge-caching/SKILL.md +316 -0
  43. package/.cursor/skills/deco-full-analysis/SKILL.md +898 -0
  44. package/.cursor/skills/deco-full-analysis/checklists/asset-optimization.md +251 -0
  45. package/.cursor/skills/deco-full-analysis/checklists/bug-fix.md +189 -0
  46. package/.cursor/skills/deco-full-analysis/checklists/cache-strategy.md +144 -0
  47. package/.cursor/skills/deco-full-analysis/checklists/dependency-update.md +150 -0
  48. package/.cursor/skills/deco-full-analysis/checklists/hydration-fix.md +191 -0
  49. package/.cursor/skills/deco-full-analysis/checklists/image-optimization.md +180 -0
  50. package/.cursor/skills/deco-full-analysis/checklists/loader-optimization.md +165 -0
  51. package/.cursor/skills/deco-full-analysis/checklists/seo-fix.md +183 -0
  52. package/.cursor/skills/deco-full-analysis/checklists/site-cleanup.md +281 -0
  53. package/.cursor/skills/deco-full-analysis/discovery.md +548 -0
  54. package/.cursor/skills/deco-incident-debugging/SKILL.md +378 -0
  55. package/.cursor/skills/deco-incident-debugging/headless-mode.md +510 -0
  56. package/.cursor/skills/deco-incident-debugging/learnings-index.md +227 -0
  57. package/.cursor/skills/deco-incident-debugging/triage-workflow.md +312 -0
  58. package/.cursor/skills/deco-islands-migration/SKILL.md +251 -0
  59. package/.cursor/skills/deco-loader-n-plus-1-detector/SKILL.md +275 -0
  60. package/.cursor/skills/deco-performance-audit/SKILL.md +530 -0
  61. package/.cursor/skills/deco-performance-audit/tools-reference.md +428 -0
  62. package/.cursor/skills/deco-performance-audit/workflow.md +457 -0
  63. package/.cursor/skills/deco-server-functions-invoke/SKILL.md +92 -0
  64. package/.cursor/skills/deco-server-functions-invoke/architecture.md +166 -0
  65. package/.cursor/skills/deco-server-functions-invoke/generator.md +122 -0
  66. package/.cursor/skills/deco-server-functions-invoke/problem.md +98 -0
  67. package/.cursor/skills/deco-server-functions-invoke/troubleshooting.md +110 -0
  68. package/.cursor/skills/deco-site-deployment/SKILL.md +396 -0
  69. package/.cursor/skills/deco-site-memory-debugging/SKILL.md +121 -0
  70. package/.cursor/skills/deco-site-memory-debugging/cdp-connection.md +222 -0
  71. package/.cursor/skills/deco-site-memory-debugging/memory-analysis.md +362 -0
  72. package/.cursor/skills/deco-site-patterns/SKILL.md +124 -0
  73. package/.cursor/skills/deco-site-patterns/app-composition.md +337 -0
  74. package/.cursor/skills/deco-site-patterns/client-patterns.md +341 -0
  75. package/.cursor/skills/deco-site-patterns/cms-wiring.md +230 -0
  76. package/.cursor/skills/deco-site-patterns/section-patterns.md +340 -0
  77. package/.cursor/skills/deco-site-scaling-tuning/SKILL.md +240 -0
  78. package/.cursor/skills/deco-site-scaling-tuning/analysis-scripts.md +267 -0
  79. package/.cursor/skills/deco-start-architecture/SKILL.md +218 -0
  80. package/.cursor/skills/deco-start-architecture/admin-protocol.md +156 -0
  81. package/.cursor/skills/deco-start-architecture/cms-resolution.md +201 -0
  82. package/.cursor/skills/deco-start-architecture/code-quality.md +158 -0
  83. package/.cursor/skills/deco-start-architecture/gap-analysis.md +129 -0
  84. package/.cursor/skills/deco-start-architecture/sdk-utilities.md +197 -0
  85. package/.cursor/skills/deco-start-architecture/worker-entry-caching.md +154 -0
  86. package/.cursor/skills/deco-startup-analysis/SKILL.md +248 -0
  87. package/.cursor/skills/deco-storefront-test-checklist/SKILL.md +369 -0
  88. package/.cursor/skills/deco-tanstack-hydration-fixes/SKILL.md +468 -0
  89. package/.cursor/skills/deco-tanstack-navigation/SKILL.md +681 -0
  90. package/.cursor/skills/deco-tanstack-search/SKILL.md +411 -0
  91. package/.cursor/skills/deco-tanstack-storefront-patterns/SKILL.md +1013 -0
  92. package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +518 -0
  93. package/.cursor/skills/deco-to-tanstack-migration/references/codemod-commands.md +174 -0
  94. package/.cursor/skills/deco-to-tanstack-migration/references/commerce/README.md +78 -0
  95. package/.cursor/skills/deco-to-tanstack-migration/references/deco-framework/README.md +128 -0
  96. package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +719 -0
  97. package/.cursor/skills/deco-to-tanstack-migration/references/imports/README.md +70 -0
  98. package/.cursor/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +154 -0
  99. package/.cursor/skills/deco-to-tanstack-migration/references/signals/README.md +220 -0
  100. package/.cursor/skills/deco-to-tanstack-migration/references/vite-config/README.md +78 -0
  101. package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +55 -0
  102. package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +110 -0
  103. package/.cursor/skills/deco-to-tanstack-migration/templates/router.md +96 -0
  104. package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +167 -0
  105. package/.cursor/skills/deco-to-tanstack-migration/templates/vite-config.md +122 -0
  106. package/.cursor/skills/deco-to-tanstack-migration/templates/worker-entry.md +67 -0
  107. package/.cursor/skills/deco-typescript-fixes/SKILL.md +178 -0
  108. package/.cursor/skills/deco-typescript-fixes/common-fixes.md +330 -0
  109. package/.cursor/skills/deco-typescript-fixes/strategy.md +148 -0
  110. package/.cursor/skills/deco-variant-selection-perf/SKILL.md +272 -0
  111. package/.cursor/skills/deco-vtex-fetch-cache/SKILL.md +225 -0
  112. package/.cursor/skills/find-skills/SKILL.md +133 -0
  113. package/.cursor/skills/incident-report/SKILL.md +179 -0
  114. package/.cursor/skills/incident-report/references/5-whys.md +75 -0
  115. package/.cursor/skills/incident-report/templates/client-report.md +187 -0
  116. package/.cursor/skills/incident-report/templates/internal-report.md +206 -0
  117. package/.cursor/skills/template-skill/SKILL.md +38 -0
  118. package/.github/workflows/release.yml +32 -0
  119. package/.releaserc.json +25 -0
  120. package/CLAUDE.md +135 -0
  121. package/GAP_ANALYSIS.md +224 -0
  122. package/GAP_ANALYSIS_V2.md +1013 -0
  123. package/biome.json +39 -0
  124. package/knip.json +5 -0
  125. package/package.json +87 -0
  126. package/scripts/generate-blocks.ts +69 -0
  127. package/scripts/generate-invoke.ts +378 -0
  128. package/scripts/generate-schema.ts +657 -0
  129. package/src/admin/cors.ts +29 -0
  130. package/src/admin/decofile.ts +72 -0
  131. package/src/admin/index.ts +24 -0
  132. package/src/admin/invoke.ts +163 -0
  133. package/src/admin/liveControls.ts +29 -0
  134. package/src/admin/meta.ts +70 -0
  135. package/src/admin/render.ts +205 -0
  136. package/src/admin/schema.ts +686 -0
  137. package/src/admin/setup.ts +44 -0
  138. package/src/cms/index.ts +59 -0
  139. package/src/cms/loader.ts +180 -0
  140. package/src/cms/registry.ts +162 -0
  141. package/src/cms/resolve.ts +1005 -0
  142. package/src/cms/sectionLoaders.ts +294 -0
  143. package/src/hooks/DecoPageRenderer.tsx +444 -0
  144. package/src/hooks/LazySection.tsx +109 -0
  145. package/src/hooks/LiveControls.tsx +108 -0
  146. package/src/hooks/SectionErrorFallback.tsx +85 -0
  147. package/src/hooks/index.ts +8 -0
  148. package/src/index.ts +5 -0
  149. package/src/matchers/builtins.ts +184 -0
  150. package/src/matchers/posthog.ts +154 -0
  151. package/src/middleware/decoState.ts +55 -0
  152. package/src/middleware/healthMetrics.ts +131 -0
  153. package/src/middleware/index.ts +80 -0
  154. package/src/middleware/liveness.ts +21 -0
  155. package/src/middleware/observability.ts +205 -0
  156. package/src/routes/adminRoutes.ts +83 -0
  157. package/src/routes/cmsRoute.ts +302 -0
  158. package/src/routes/components.tsx +34 -0
  159. package/src/routes/index.ts +15 -0
  160. package/src/sdk/analytics.ts +72 -0
  161. package/src/sdk/cacheHeaders.ts +268 -0
  162. package/src/sdk/cachedLoader.ts +206 -0
  163. package/src/sdk/clx.ts +3 -0
  164. package/src/sdk/cookie.ts +39 -0
  165. package/src/sdk/createInvoke.ts +57 -0
  166. package/src/sdk/csp.ts +59 -0
  167. package/src/sdk/env.ts +27 -0
  168. package/src/sdk/index.ts +63 -0
  169. package/src/sdk/instrumentedFetch.ts +137 -0
  170. package/src/sdk/invoke.ts +133 -0
  171. package/src/sdk/mergeCacheControl.ts +150 -0
  172. package/src/sdk/redirects.ts +217 -0
  173. package/src/sdk/requestContext.ts +184 -0
  174. package/src/sdk/serverTimings.ts +68 -0
  175. package/src/sdk/signal.ts +41 -0
  176. package/src/sdk/sitemap.ts +143 -0
  177. package/src/sdk/urlUtils.ts +117 -0
  178. package/src/sdk/useDevice.ts +82 -0
  179. package/src/sdk/useId.ts +7 -0
  180. package/src/sdk/useScript.ts +101 -0
  181. package/src/sdk/workerEntry.ts +703 -0
  182. package/src/sdk/wrapCaughtErrors.ts +107 -0
  183. package/src/types/index.ts +39 -0
  184. package/src/types/widgets.ts +13 -0
  185. package/tsconfig.json +13 -0
package/biome.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.0/schema.json",
3
+ "assist": {
4
+ "actions": {
5
+ "source": {
6
+ "organizeImports": "on"
7
+ }
8
+ }
9
+ },
10
+ "linter": {
11
+ "enabled": true,
12
+ "rules": {
13
+ "recommended": true,
14
+ "correctness": {
15
+ "noUnusedVariables": "warn",
16
+ "noUnusedImports": "warn"
17
+ },
18
+ "suspicious": {
19
+ "noExplicitAny": "off",
20
+ "noArrayIndexKey": "off"
21
+ },
22
+ "security": {
23
+ "noDangerouslySetInnerHtml": "off"
24
+ },
25
+ "style": {
26
+ "noNonNullAssertion": "off"
27
+ }
28
+ }
29
+ },
30
+ "formatter": {
31
+ "enabled": true,
32
+ "indentStyle": "space",
33
+ "indentWidth": 2,
34
+ "lineWidth": 100
35
+ },
36
+ "files": {
37
+ "includes": ["src/**", "scripts/**"]
38
+ }
39
+ }
package/knip.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "project": ["src/**/*.{ts,tsx}", "scripts/**/*.ts"],
3
+ "ignoreBinaries": ["semantic-release"],
4
+ "ignore": ["src/admin/setup.ts"]
5
+ }
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@decocms/start",
3
+ "version": "0.19.0",
4
+ "type": "module",
5
+ "description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
6
+ "main": "./src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./cms": "./src/cms/index.ts",
10
+ "./admin": "./src/admin/index.ts",
11
+ "./hooks": "./src/hooks/index.ts",
12
+ "./types": "./src/types/index.ts",
13
+ "./sdk": "./src/sdk/index.ts",
14
+ "./sdk/useScript": "./src/sdk/useScript.ts",
15
+ "./sdk/signal": "./src/sdk/signal.ts",
16
+ "./sdk/clx": "./src/sdk/clx.ts",
17
+ "./sdk/useId": "./src/sdk/useId.ts",
18
+ "./sdk/cookie": "./src/sdk/cookie.ts",
19
+ "./sdk/analytics": "./src/sdk/analytics.ts",
20
+ "./sdk/cachedLoader": "./src/sdk/cachedLoader.ts",
21
+ "./sdk/serverTimings": "./src/sdk/serverTimings.ts",
22
+ "./sdk/cacheHeaders": "./src/sdk/cacheHeaders.ts",
23
+ "./sdk/invoke": "./src/sdk/invoke.ts",
24
+ "./sdk/instrumentedFetch": "./src/sdk/instrumentedFetch.ts",
25
+ "./sdk/workerEntry": "./src/sdk/workerEntry.ts",
26
+ "./sdk/redirects": "./src/sdk/redirects.ts",
27
+ "./sdk/sitemap": "./src/sdk/sitemap.ts",
28
+ "./sdk/useDevice": "./src/sdk/useDevice.ts",
29
+ "./middleware": "./src/middleware/index.ts",
30
+ "./middleware/healthMetrics": "./src/middleware/healthMetrics.ts",
31
+ "./sdk/wrapCaughtErrors": "./src/sdk/wrapCaughtErrors.ts",
32
+ "./sdk/csp": "./src/sdk/csp.ts",
33
+ "./sdk/urlUtils": "./src/sdk/urlUtils.ts",
34
+ "./sdk/mergeCacheControl": "./src/sdk/mergeCacheControl.ts",
35
+ "./sdk/requestContext": "./src/sdk/requestContext.ts",
36
+ "./sdk/createInvoke": "./src/sdk/createInvoke.ts",
37
+ "./matchers/posthog": "./src/matchers/posthog.ts",
38
+ "./matchers/builtins": "./src/matchers/builtins.ts",
39
+ "./types/widgets": "./src/types/widgets.ts",
40
+ "./routes": "./src/routes/index.ts",
41
+ "./scripts/generate-blocks": "./scripts/generate-blocks.ts",
42
+ "./scripts/generate-schema": "./scripts/generate-schema.ts",
43
+ "./scripts/generate-invoke": "./scripts/generate-invoke.ts"
44
+ },
45
+ "scripts": {
46
+ "build": "tsc",
47
+ "typecheck": "tsc --noEmit",
48
+ "lint": "biome check src/ scripts/",
49
+ "lint:fix": "biome check --write src/ scripts/",
50
+ "lint:unused": "knip",
51
+ "format": "biome format src/ scripts/",
52
+ "format:fix": "biome format --write src/ scripts/",
53
+ "check": "npm run typecheck && npm run lint && npm run lint:unused"
54
+ },
55
+ "keywords": [
56
+ "deco",
57
+ "tanstack",
58
+ "cms",
59
+ "framework"
60
+ ],
61
+ "author": "deco.cx",
62
+ "license": "MIT",
63
+ "repository": {
64
+ "type": "git",
65
+ "url": "https://github.com/decocms/deco-start.git"
66
+ },
67
+ "publishConfig": {
68
+ "registry": "https://registry.npmjs.org",
69
+ "access": "public"
70
+ },
71
+ "peerDependencies": {
72
+ "@tanstack/react-start": ">=1.0.0",
73
+ "@tanstack/store": ">=0.7.0",
74
+ "react": "^19.0.0",
75
+ "react-dom": "^19.0.0"
76
+ },
77
+ "devDependencies": {
78
+ "@biomejs/biome": "^2.4.6",
79
+ "@semantic-release/git": "^10.0.1",
80
+ "@tanstack/store": "^0.9.1",
81
+ "@types/react": "^19.0.0",
82
+ "@types/react-dom": "^19.0.0",
83
+ "knip": "^5.86.0",
84
+ "ts-morph": "^27.0.0",
85
+ "typescript": "^5.9.0"
86
+ }
87
+ }
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Reads .deco/blocks/*.json and emits a TypeScript barrel.
4
+ *
5
+ * Usage (from site root):
6
+ * npx tsx node_modules/@decocms/start/scripts/generate-blocks.ts
7
+ *
8
+ * Env / CLI:
9
+ * --blocks-dir override input (default: .deco/blocks)
10
+ * --out-file override output (default: src/server/cms/blocks.gen.ts)
11
+ */
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+
15
+ const args = process.argv.slice(2);
16
+ function arg(name: string, fallback: string): string {
17
+ const idx = args.indexOf(`--${name}`);
18
+ return idx !== -1 && args[idx + 1] ? args[idx + 1] : fallback;
19
+ }
20
+
21
+ const blocksDir = path.resolve(process.cwd(), arg("blocks-dir", ".deco/blocks"));
22
+ const outFile = path.resolve(process.cwd(), arg("out-file", "src/server/cms/blocks.gen.ts"));
23
+
24
+ function decodeBlockName(filename: string): string {
25
+ return decodeURIComponent(decodeURIComponent(filename)).replace(/\.json$/, "");
26
+ }
27
+
28
+ if (!fs.existsSync(blocksDir)) {
29
+ console.warn(`Blocks directory not found: ${blocksDir} — generating empty barrel.`);
30
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
31
+ fs.writeFileSync(
32
+ outFile,
33
+ `// Auto-generated — no blocks found\nexport const blocks: Record<string, any> = {};\n`,
34
+ );
35
+ process.exit(0);
36
+ }
37
+
38
+ const files = fs.readdirSync(blocksDir).filter((f) => f.endsWith(".json"));
39
+
40
+ // Deduplicate: prefer the non-URL-encoded filename when both exist
41
+ const blockFiles: Record<string, string> = {};
42
+ for (const file of files) {
43
+ const name = decodeBlockName(file);
44
+ const isEncoded = file !== `${name}.json`;
45
+ if (blockFiles[name] && !isEncoded) {
46
+ // Plain filename wins over URL-encoded variant
47
+ } else if (blockFiles[name] && isEncoded) {
48
+ continue;
49
+ }
50
+ blockFiles[name] = file;
51
+ }
52
+
53
+ const blocks: Record<string, unknown> = {};
54
+ for (const [name, file] of Object.entries(blockFiles)) {
55
+ try {
56
+ const content = fs.readFileSync(path.join(blocksDir, file), "utf-8");
57
+ blocks[name] = JSON.parse(content);
58
+ } catch (e) {
59
+ console.warn(`Failed to parse ${file}:`, e);
60
+ }
61
+ }
62
+
63
+ const output = `// Auto-generated from .deco/blocks/*.json\n// Do not edit manually.\n\nexport const blocks: Record<string, any> = ${JSON.stringify(blocks, null, 2)};\n`;
64
+
65
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
66
+ fs.writeFileSync(outFile, output);
67
+ console.log(
68
+ `Generated ${Object.keys(blocks).length} blocks → ${path.relative(process.cwd(), outFile)}`,
69
+ );
@@ -0,0 +1,378 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Scans @decocms/apps vtex/invoke.ts and generates a site-local invoke file
4
+ * with top-level createServerFn declarations.
5
+ *
6
+ * TanStack Start's compiler only transforms createServerFn().handler() when
7
+ * the call is at module top-level (assigned to a const). The factory pattern
8
+ * used in @decocms/apps/vtex/invoke.ts causes the "fast path" in the compiler
9
+ * to skip the .handler() calls because they're inside a function body.
10
+ *
11
+ * This script generates an equivalent file where each server function is a
12
+ * top-level const, which the compiler can correctly transform into RPC stubs.
13
+ *
14
+ * Usage (from site root):
15
+ * npx tsx node_modules/@decocms/start/scripts/generate-invoke.ts
16
+ *
17
+ * Env / CLI:
18
+ * --out-file override output (default: src/server/invoke.gen.ts)
19
+ * --apps-dir override @decocms/apps location (default: auto-resolve from node_modules)
20
+ */
21
+ import fs from "node:fs";
22
+ import path from "node:path";
23
+ import { Project, type PropertyAssignment, SyntaxKind } from "ts-morph";
24
+
25
+ const args = process.argv.slice(2);
26
+ function arg(name: string, fallback: string): string {
27
+ const idx = args.indexOf(`--${name}`);
28
+ return idx !== -1 && args[idx + 1] ? args[idx + 1] : fallback;
29
+ }
30
+
31
+ const cwd = process.cwd();
32
+ const outFile = path.resolve(cwd, arg("out-file", "src/server/invoke.gen.ts"));
33
+
34
+ function resolveAppsDir(): string {
35
+ const explicit = arg("apps-dir", "");
36
+ if (explicit) return path.resolve(cwd, explicit);
37
+
38
+ // Try common locations
39
+ const candidates = [
40
+ path.resolve(cwd, "node_modules/@decocms/apps"),
41
+ path.resolve(cwd, "../apps-start"),
42
+ ];
43
+ for (const c of candidates) {
44
+ if (fs.existsSync(path.join(c, "vtex/invoke.ts"))) return c;
45
+ }
46
+ throw new Error("Could not find @decocms/apps. Use --apps-dir to specify its location.");
47
+ }
48
+
49
+ const appsDir = resolveAppsDir();
50
+ const invokeFile = path.join(appsDir, "vtex/invoke.ts");
51
+
52
+ if (!fs.existsSync(invokeFile)) {
53
+ console.error(`invoke.ts not found at: ${invokeFile}`);
54
+ process.exit(1);
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Parse the source invoke.ts to extract action definitions
59
+ // ---------------------------------------------------------------------------
60
+
61
+ interface ActionDef {
62
+ name: string;
63
+ /** The import source for the action function (e.g., "@decocms/apps/vtex/actions/checkout") */
64
+ importSource: string;
65
+ /** The imported function name (e.g., "addItemsToCart") */
66
+ importedFn: string;
67
+ /** The input type as a string (e.g., "{ orderFormId: string; ... }") */
68
+ inputType: string;
69
+ /** The return type as a string (e.g., "OrderForm") */
70
+ returnType: string;
71
+ /** Whether to unwrap VtexFetchResult */
72
+ unwrap: boolean;
73
+ /** The body of the action call (e.g., "addItemsToCart(input.orderFormId, input.orderItems)") */
74
+ callBody: string;
75
+ }
76
+
77
+ const project = new Project({ compilerOptions: { strict: true } });
78
+ const sourceFile = project.addSourceFileAtPath(invokeFile);
79
+
80
+ // Collect all imports to know which functions come from where
81
+ const importMap = new Map<string, { source: string; importedName: string }>();
82
+ for (const imp of sourceFile.getImportDeclarations()) {
83
+ const source = imp.getModuleSpecifierValue();
84
+ for (const named of imp.getNamedImports()) {
85
+ const localName = named.getName();
86
+ const importedName = named.getAliasNode()?.getText() || localName;
87
+ importMap.set(localName, {
88
+ source: source.startsWith("./") ? `@decocms/apps/vtex/${source.slice(2)}` : source,
89
+ importedName: localName,
90
+ });
91
+ }
92
+ }
93
+
94
+ // Collect type imports
95
+ const typeImportMap = new Map<string, { source: string; importedName: string }>();
96
+ for (const imp of sourceFile.getImportDeclarations()) {
97
+ if (!imp.isTypeOnly()) {
98
+ for (const named of imp.getNamedImports()) {
99
+ if (named.isTypeOnly()) {
100
+ const localName = named.getName();
101
+ const source = imp.getModuleSpecifierValue();
102
+ typeImportMap.set(localName, {
103
+ source: source.startsWith("./") ? `@decocms/apps/vtex/${source.slice(2)}` : source,
104
+ importedName: localName,
105
+ });
106
+ }
107
+ }
108
+ }
109
+ if (imp.isTypeOnly()) {
110
+ const source = imp.getModuleSpecifierValue();
111
+ for (const named of imp.getNamedImports()) {
112
+ const localName = named.getName();
113
+ typeImportMap.set(localName, {
114
+ source: source.startsWith("./") ? `@decocms/apps/vtex/${source.slice(2)}` : source,
115
+ importedName: localName,
116
+ });
117
+ }
118
+ }
119
+ }
120
+
121
+ // Find the invoke const and extract actions
122
+ const invokeVar = sourceFile.getVariableDeclaration("invoke");
123
+ if (!invokeVar) {
124
+ console.error("Could not find 'export const invoke' in invoke.ts");
125
+ process.exit(1);
126
+ }
127
+
128
+ const actions: ActionDef[] = [];
129
+ const invokeInit = invokeVar.getInitializer();
130
+ if (!invokeInit) {
131
+ console.error("invoke variable has no initializer");
132
+ process.exit(1);
133
+ }
134
+
135
+ // Navigate: invoke → .vtex → .actions → each property
136
+ const vtexProp = invokeInit
137
+ .asKindOrThrow(SyntaxKind.AsExpression)
138
+ .getExpression()
139
+ .asKindOrThrow(SyntaxKind.ObjectLiteralExpression)
140
+ .getProperty("vtex");
141
+
142
+ if (!vtexProp) {
143
+ console.error("Could not find 'vtex' property in invoke object");
144
+ process.exit(1);
145
+ }
146
+
147
+ const vtexObj = (vtexProp as PropertyAssignment)
148
+ .getInitializer()!
149
+ .asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
150
+
151
+ const actionsProp = vtexObj.getProperty("actions");
152
+ if (!actionsProp) {
153
+ console.error("Could not find 'actions' property in vtex object");
154
+ process.exit(1);
155
+ }
156
+
157
+ const actionsObj = (actionsProp as PropertyAssignment)
158
+ .getInitializer()!
159
+ .asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
160
+
161
+ for (const prop of actionsObj.getProperties()) {
162
+ if (prop.getKind() !== SyntaxKind.PropertyAssignment) continue;
163
+ const pa = prop as PropertyAssignment;
164
+ const name = pa.getName();
165
+ const initText = pa.getInitializer()!.getText();
166
+
167
+ // Check if it uses createInvokeFn with unwrap
168
+ const unwrap = initText.includes("unwrap: true");
169
+
170
+ // Extract the arrow function body from createInvokeFn((input: ...) => ...)
171
+ // We'll parse the call expression to get the action call
172
+ const callExpr = pa.getInitializer()!;
173
+ let inputType = "any";
174
+ let callBody = "";
175
+
176
+ // Navigate through potential type assertion (as ...)
177
+ let createInvokeFnCall = callExpr;
178
+ if (callExpr.getKind() === SyntaxKind.AsExpression) {
179
+ createInvokeFnCall = callExpr.asKindOrThrow(SyntaxKind.AsExpression).getExpression();
180
+ }
181
+
182
+ // Now we have createInvokeFn(...) call
183
+ if (createInvokeFnCall.getKind() === SyntaxKind.CallExpression) {
184
+ const callArgs = createInvokeFnCall.asKindOrThrow(SyntaxKind.CallExpression).getArguments();
185
+ if (callArgs.length >= 1) {
186
+ const arrowFn = callArgs[0];
187
+ if (arrowFn.getKind() === SyntaxKind.ArrowFunction) {
188
+ const arrow = arrowFn.asKindOrThrow(SyntaxKind.ArrowFunction);
189
+ const params = arrow.getParameters();
190
+ if (params.length >= 1) {
191
+ const paramType = params[0].getTypeNode()?.getText() || "any";
192
+ inputType = paramType;
193
+ }
194
+ // Get the body (the actual action call)
195
+ const body = arrow.getBody();
196
+ callBody = body.getText();
197
+
198
+ // If body is a block, extract the expression
199
+ if (callBody.startsWith("{")) {
200
+ // It's a block body — skip for now, use simplified version
201
+ callBody = "";
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ // Determine which function is being called
208
+ let importedFn = "";
209
+ let importSource = "";
210
+ for (const [fnName, info] of importMap.entries()) {
211
+ if (callBody.includes(`${fnName}(`)) {
212
+ importedFn = fnName;
213
+ importSource = info.source;
214
+ break;
215
+ }
216
+ }
217
+
218
+ // Extract the return type from the "as" assertion if present
219
+ let returnType = "any";
220
+ if (callExpr.getKind() === SyntaxKind.AsExpression) {
221
+ const asExpr = callExpr.asKindOrThrow(SyntaxKind.AsExpression);
222
+ const typeText = asExpr.getTypeNode()?.getText() || "";
223
+ // Extract Promise<X> from (ctx: {...}) => Promise<X>
224
+ const promiseMatch = typeText.match(/Promise<(.+)>$/s);
225
+ if (promiseMatch) {
226
+ returnType = promiseMatch[1].trim();
227
+ }
228
+ }
229
+
230
+ actions.push({
231
+ name,
232
+ importSource,
233
+ importedFn,
234
+ inputType,
235
+ returnType,
236
+ unwrap,
237
+ callBody,
238
+ });
239
+ }
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // Generate the output file
243
+ // ---------------------------------------------------------------------------
244
+
245
+ // Collect unique imports needed
246
+ const fnImports = new Map<string, Set<string>>();
247
+ const typeImports = new Map<string, Set<string>>();
248
+
249
+ for (const action of actions) {
250
+ if (action.importSource && action.importedFn) {
251
+ if (!fnImports.has(action.importSource)) {
252
+ fnImports.set(action.importSource, new Set());
253
+ }
254
+ fnImports.get(action.importSource)!.add(action.importedFn);
255
+ }
256
+ }
257
+
258
+ // Add type imports referenced in inputType or returnType
259
+ for (const action of actions) {
260
+ const allText = action.inputType + action.returnType + action.callBody;
261
+ for (const [typeName, info] of typeImportMap.entries()) {
262
+ if (allText.includes(typeName)) {
263
+ if (!typeImports.has(info.source)) {
264
+ typeImports.set(info.source, new Set());
265
+ }
266
+ typeImports.get(info.source)!.add(typeName);
267
+ }
268
+ }
269
+ // Also check value imports that appear in the types (like SimulationItem)
270
+ for (const [fnName, info] of importMap.entries()) {
271
+ if (action.inputType.includes(fnName) && !fnImports.get(info.source)?.has(fnName)) {
272
+ if (!typeImports.has(info.source)) {
273
+ typeImports.set(info.source, new Set());
274
+ }
275
+ typeImports.get(info.source)!.add(fnName);
276
+ }
277
+ }
278
+ }
279
+
280
+ // Build output
281
+ let out = `// Auto-generated by @decocms/start/scripts/generate-invoke.ts
282
+ // Do not edit manually. Re-run the generator to update.
283
+ //
284
+ // Each server function is a top-level const so TanStack Start's compiler
285
+ // can transform createServerFn().handler() into RPC stubs on the client.
286
+ import { createServerFn } from "@tanstack/react-start";
287
+ `;
288
+
289
+ // Add function imports
290
+ for (const [source, fns] of fnImports) {
291
+ out += `import { ${[...fns].join(", ")} } from "${source}";\n`;
292
+ }
293
+
294
+ // Add type imports
295
+ for (const [source, types] of typeImports) {
296
+ // Don't duplicate if already imported as value
297
+ const valueImports = fnImports.get(source);
298
+ const onlyTypes = [...types].filter((t) => !valueImports?.has(t));
299
+ if (onlyTypes.length > 0) {
300
+ out += `import type { ${onlyTypes.join(", ")} } from "${source}";\n`;
301
+ }
302
+ }
303
+
304
+ out += `
305
+ function unwrapResult<T>(result: unknown): T {
306
+ if (result && typeof result === "object" && "data" in result) {
307
+ return (result as { data: T }).data;
308
+ }
309
+ return result as T;
310
+ }
311
+
312
+ // ---------------------------------------------------------------------------
313
+ // Top-level server function declarations
314
+ // ---------------------------------------------------------------------------
315
+ `;
316
+
317
+ for (const action of actions) {
318
+ const varName = `$${action.name}`;
319
+
320
+ if (action.callBody && action.importedFn) {
321
+ // Replace "input" references with "ctx.data" in the call body
322
+ let body = action.callBody;
323
+ // The callBody looks like: functionName(input.foo, input.bar)
324
+ // We need it to be: functionName(ctx.data.foo, ctx.data.bar)
325
+ body = body.replace(/\binput\./g, "ctx.data.");
326
+ // Handle cases like functionName(input) without dot
327
+ body = body.replace(/\binput\b(?!\.)/g, "ctx.data");
328
+
329
+ if (action.unwrap) {
330
+ out += `\nconst ${varName} = createServerFn({ method: "POST" })
331
+ .handler(async (ctx: { data: ${action.inputType} }) => {
332
+ const result = await ${body};
333
+ return unwrapResult(result);
334
+ });\n`;
335
+ } else {
336
+ out += `\nconst ${varName} = createServerFn({ method: "POST" })
337
+ .handler(async (ctx: { data: ${action.inputType} }) => {
338
+ return await ${body};
339
+ });\n`;
340
+ }
341
+ } else {
342
+ // Fallback: couldn't parse — generate a stub
343
+ out += `\n// TODO: could not auto-generate ${action.name} — add manually\nconst ${varName} = createServerFn({ method: "POST" })
344
+ .handler(async (ctx: { data: any }) => {
345
+ throw new Error("${action.name}: not implemented — regenerate invoke");
346
+ });\n`;
347
+ }
348
+ }
349
+
350
+ // Generate the invoke object
351
+ out += `
352
+ // ---------------------------------------------------------------------------
353
+ // Public invoke object — same DX as @decocms/apps/vtex/invoke
354
+ // ---------------------------------------------------------------------------
355
+
356
+ export const invoke = {
357
+ vtex: {
358
+ actions: {
359
+ `;
360
+
361
+ for (const action of actions) {
362
+ const varName = `$${action.name}`;
363
+ if (action.returnType !== "any") {
364
+ out += ` ${action.name}: ${varName} as unknown as (ctx: { data: ${action.inputType} }) => Promise<${action.returnType}>,\n`;
365
+ } else {
366
+ out += ` ${action.name}: ${varName},\n`;
367
+ }
368
+ }
369
+
370
+ out += ` },
371
+ },
372
+ } as const;
373
+ `;
374
+
375
+ // Write output
376
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
377
+ fs.writeFileSync(outFile, out);
378
+ console.log(`Generated ${actions.length} server functions → ${path.relative(cwd, outFile)}`);