diff --git a/packages/structure/NEXT_STEPS.md b/packages/structure/NEXT_STEPS.md
index 0119e31a0b..af76dfae6a 100644
--- a/packages/structure/NEXT_STEPS.md
+++ b/packages/structure/NEXT_STEPS.md
@@ -1,3 +1,6 @@
+> If you look at the Structure package code, is there anything you would recommend doing to improve it? It could be anything. Something to make it more
+> performant, something to make it easier to work with, something to make it more modern. What's the one thing you'd recommend that'd have to biggest impact?
+
✦ If I had to recommend one change with the biggest impact on the Structure package, it would be replacing `ts-morph` with a high-performance AST parser like
[`oxc-parser`](https://github.com/oxc-project/oxc) or [`swc`](https://swc.rs/).
@@ -47,3 +50,48 @@ Why this is better than inheritance:
This approach is essentially how most modern compilers and high-performance tools (like ESLint or rust-analyzer) are built today. It trades "magical
getters" for a transparent, predictable data flow.
+
+✦ To take full advantage of a move to a high-performance parser like oxc, you should eventually update the interface, though you could keep the current one
+as a temporary compatibility layer.
+
+Here is the breakdown of why keeping the current interface would actually limit the benefits of the rewrite:
+
+1. The "Compatibility Layer" Tax
+ The current interface is built on ts-morph, which provides a very high-level, human-friendly API (e.g., sourceFile.getVariableDeclaration('QUERY')).
+
+- The Problem: oxc (and most high-performance parsers) returns a raw, flat AST.
+- The Consequence: If you keep the current interface, you have to write a massive "compatibility layer" that mimics ts-morph's API using the raw AST. This
+ layer would likely be written in JavaScript, adding back some of the overhead you were trying to remove.
+
+2. From "Pull" to "Push" (Performance)
+
+- Current (Pull): The inheritance/lazy-getter model is a "Pull" system. You ask for project.cells, and it triggers a chain of getters that eventually call
+ ts-morph.
+- Optimal (Push): High-performance tools work better as "Push" systems. You run a fast Rust-based scanner over the whole project in one pass, which
+ "pushes" a plain data object representing the whole structure.
+- If you keep the current interface, you are forcing a high-speed stream of data into a slow, stateful object container.
+
+3. Serializability and Concurrency
+ One of the biggest hidden benefits of oxc is that because it’s so fast and the data it produces is simple, you can easily move the work to Worker Threads.
+
+- The Limitation: You cannot send class instances (like RWProject) across worker boundaries easily.
+- The Win: If you update the interface to use Plain Old JavaScript Objects (POJOs), the Structure package could parse 50 files in parallel on 50 threads
+ and return a single JSON-like object. Other packages would then "query" this data rather than interacting with live class instances.
+
+4. What a "Modern" Interface would look like
+ Instead of other packages doing this:
+
+1 const project = getProject(path)
+2 const routes = project.router.routes // Deep getter chain
+
+They would do this:
+1 // The structure package returns a frozen snapshot of the project
+2 const projectData = await scanProject(path)
+3 const routes = findRoutes(projectData) // Fast lookup in a data structure
+
+Recommendation
+If you want to move to oxc, I would recommend a Two-Phase Approach:
+
+1. Phase 1 (Internal): Rewrite the internals to use oxc but keep the RWProject classes as "wrappers" so you don't break the CLI or Telemetry immediately.
+2. Phase 2 (Public API): Introduce a new functional API (scanProject) and slowly migrate the CLI to use it. Once the CLI is migrated, you can delete the
+ inheritance-based classes entirely.
diff --git a/packages/structure/benchmark.mts b/packages/structure/benchmark.mts
new file mode 100644
index 0000000000..76b52e066d
--- /dev/null
+++ b/packages/structure/benchmark.mts
@@ -0,0 +1,209 @@
+import path from 'node:path'
+import { performance } from 'node:perf_hooks'
+import { fileURLToPath } from 'node:url'
+
+import * as RWProjectJS from './src/model/RWProject.js'
+import type { RWProject as RWProjectJST } from './src/model/RWProject.js'
+
+// @ts-expect-error tsx makes me do this
+const RWProject: typeof RWProjectJST = RWProjectJS.default.RWProject
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const fixturePath = path.resolve(__dirname, '../../__fixtures__/test-project')
+
+const WARMUP_ITERATIONS = 2
+const ITERATIONS = 10
+
+// Enable manual GC if running with --expose-gc
+const gc = global.gc
+
+async function forceGC() {
+ if (gc) {
+ // Run GC multiple times to ensure everything is cleaned up
+ gc()
+ gc()
+ // Small delay to let GC settle
+ await new Promise((resolve) => setTimeout(resolve, 10))
+ }
+}
+
+function getMemoryMB() {
+ return process.memoryUsage().heapUsed / 1024 / 1024
+}
+
+function calculateStats(values: number[]) {
+ const sorted = [...values].sort((a, b) => a - b)
+ const mean = values.reduce((a, b) => a + b, 0) / values.length
+ const variance =
+ values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) /
+ values.length
+ const stdDev = Math.sqrt(variance)
+
+ return {
+ mean,
+ median: sorted[Math.floor(sorted.length / 2)],
+ min: sorted[0],
+ max: sorted[sorted.length - 1],
+ stdDev,
+ p95: sorted[Math.floor(sorted.length * 0.95)],
+ }
+}
+
+async function benchmark() {
+ console.log('CedarJS Structure Performance Benchmark')
+ console.log('=======================================\n')
+ console.log(`Implementation: ts-morph`)
+ console.log(`Target Project: ${fixturePath}`)
+ console.log(`Warmup Iterations: ${WARMUP_ITERATIONS}`)
+ console.log(`Measurement Iterations: ${ITERATIONS}`)
+ console.log(
+ `GC Control: ${gc ? 'enabled' : 'disabled (run with --expose-gc for accurate memory stats)'}`,
+ )
+ console.log('')
+
+ // Track individual measurements
+ const initTimes: number[] = []
+ const coldDiagnosticsTimes: number[] = []
+ const warmDiagnosticsTimes: number[] = []
+ const memoryDeltas: number[] = []
+ let diagnosticCount = 0
+
+ // Warmup phase (don't measure these)
+ console.log('Warming up...')
+ for (let i = 0; i < WARMUP_ITERATIONS; i++) {
+ const project = new RWProject({ projectRoot: fixturePath })
+ await project.collectDiagnostics()
+ await project.collectDiagnostics() // Warm cache
+ }
+ forceGC()
+ console.log('Warmup complete.\n')
+
+ // Measurement phase
+ console.log('Running measurements...')
+
+ // Establish a stable baseline
+ await forceGC()
+ const baselineMemory = getMemoryMB()
+
+ for (let i = 0; i < ITERATIONS; i++) {
+ // Stabilize memory before measurement
+ await forceGC()
+ const memBefore = getMemoryMB()
+
+ // 1. Project Initialization (Shallow)
+ const t0 = performance.now()
+ const project = new RWProject({ projectRoot: fixturePath })
+ const t1 = performance.now()
+ initTimes.push(t1 - t0)
+
+ // 2. Full Project Build & Diagnostics (Cold)
+ const t2 = performance.now()
+ const diagnosticsCold = await project.collectDiagnostics()
+ const t3 = performance.now()
+ coldDiagnosticsTimes.push(t3 - t2)
+ diagnosticCount = diagnosticsCold.length
+
+ // 3. Cached Diagnostics (Warm)
+ const t4 = performance.now()
+ await project.collectDiagnostics()
+ const t5 = performance.now()
+ warmDiagnosticsTimes.push(t5 - t4)
+
+ // 4. Memory Usage - measure peak before GC
+ const memPeak = getMemoryMB()
+ const memDelta = memPeak - memBefore
+
+ // Only record positive deltas (actual allocations)
+ // Negative values indicate GC interference
+ if (memDelta > 0) {
+ memoryDeltas.push(memDelta)
+ }
+
+ process.stdout.write('.')
+ }
+ console.log(' done!\n')
+
+ // Calculate statistics
+ const initStats = calculateStats(initTimes)
+ const coldStats = calculateStats(coldDiagnosticsTimes)
+ const warmStats = calculateStats(warmDiagnosticsTimes)
+ const memStats = calculateStats(memoryDeltas)
+
+ // Print Results
+ console.log('=== Initialization ===')
+ console.table({
+ Mean: `${initStats.mean.toFixed(2)} ms`,
+ Median: `${initStats.median.toFixed(2)} ms`,
+ Min: `${initStats.min.toFixed(2)} ms`,
+ Max: `${initStats.max.toFixed(2)} ms`,
+ 'Std Dev': `${initStats.stdDev.toFixed(2)} ms`,
+ P95: `${initStats.p95.toFixed(2)} ms`,
+ })
+
+ console.log('\n=== Cold Diagnostics (First Run) ===')
+ console.table({
+ Mean: `${coldStats.mean.toFixed(2)} ms`,
+ Median: `${coldStats.median.toFixed(2)} ms`,
+ Min: `${coldStats.min.toFixed(2)} ms`,
+ Max: `${coldStats.max.toFixed(2)} ms`,
+ 'Std Dev': `${coldStats.stdDev.toFixed(2)} ms`,
+ P95: `${coldStats.p95.toFixed(2)} ms`,
+ })
+
+ console.log('\n=== Warm Diagnostics (Cached) ===')
+ console.table({
+ Mean: `${warmStats.mean.toFixed(2)} ms`,
+ Median: `${warmStats.median.toFixed(2)} ms`,
+ Min: `${warmStats.min.toFixed(2)} ms`,
+ Max: `${warmStats.max.toFixed(2)} ms`,
+ 'Std Dev': `${warmStats.stdDev.toFixed(2)} ms`,
+ P95: `${warmStats.p95.toFixed(2)} ms`,
+ })
+
+ console.log('\n=== Memory Usage (Peak per Iteration) ===')
+ if (memoryDeltas.length > 0) {
+ console.table({
+ Mean: `${memStats.mean.toFixed(2)} MB`,
+ Median: `${memStats.median.toFixed(2)} MB`,
+ Min: `${memStats.min.toFixed(2)} MB`,
+ Max: `${memStats.max.toFixed(2)} MB`,
+ 'Std Dev': `${memStats.stdDev.toFixed(2)} MB`,
+ P95: `${memStats.p95.toFixed(2)} MB`,
+ Samples: memoryDeltas.length.toString(),
+ })
+ } else {
+ console.log('No valid memory measurements (run with --expose-gc)')
+ }
+
+ console.log('\n=== Summary ===')
+ console.log(`Total Diagnostics Found: ${diagnosticCount}`)
+ console.log(
+ `Total Time per Cold Run: ${(initStats.mean + coldStats.mean).toFixed(2)} ms`,
+ )
+ console.log(
+ `Speedup (Cold → Warm): ${(coldStats.mean / warmStats.mean).toFixed(1)}x`,
+ )
+
+ console.log('\n=== Notes ===')
+ console.log('- Init: Time to create RWProject instance (lightweight)')
+ console.log(
+ '- Cold: Full AST parsing + diagnostics (most relevant for comparison)',
+ )
+ console.log('- Warm: Re-running diagnostics with ts-morph internal caching')
+ console.log(
+ '- Memory Peak: Heap allocated per iteration (negative deltas filtered out)',
+ )
+ console.log(
+ '\n⚠️ For accurate memory stats, run with: node --expose-gc benchmark.mts',
+ )
+ if (!gc) {
+ console.log(
+ ' Memory measurements are unreliable without GC control enabled.',
+ )
+ }
+}
+
+benchmark().catch((err) => {
+ console.error('Benchmark failed:', err)
+ process.exit(1)
+})
diff --git a/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/api/db/schema.prisma b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/api/db/schema.prisma
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/api/prisma.config.cjs b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/api/prisma.config.cjs
new file mode 100644
index 0000000000..25003102a3
--- /dev/null
+++ b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/api/prisma.config.cjs
@@ -0,0 +1 @@
+module.exports = { schemaPath: 'db/schema.prisma' }
diff --git a/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/api/src/services/posts/posts.ts b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/api/src/services/posts/posts.ts
new file mode 100644
index 0000000000..78271de86f
--- /dev/null
+++ b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/api/src/services/posts/posts.ts
@@ -0,0 +1,2 @@
+export const posts = () => []
+export const post = ({ id }: { id: number }) => ({ id, title: 'Post ' + id })
diff --git a/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/package.json b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/package.json
new file mode 100644
index 0000000000..26edf72110
--- /dev/null
+++ b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/package.json
@@ -0,0 +1,4 @@
+{
+ "name": "structure-test-project",
+ "private": true
+}
diff --git a/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/redwood.toml b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/redwood.toml
new file mode 100644
index 0000000000..7234869d4f
--- /dev/null
+++ b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/redwood.toml
@@ -0,0 +1,7 @@
+[web]
+ port = 8910
+ apiUrl = "/.redwood/functions"
+[api]
+ port = 8911
+[browser]
+ open = true
diff --git a/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/web/src/Routes.tsx b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/web/src/Routes.tsx
new file mode 100644
index 0000000000..6d943a5527
--- /dev/null
+++ b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/web/src/Routes.tsx
@@ -0,0 +1,19 @@
+import { Router, Route, Set } from '@cedarjs/router'
+
+import MainLayout from 'src/layouts/MainLayout'
+
+const Routes = () => {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Routes
diff --git a/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/web/src/components/UnusualCell/UnusualCell.tsx b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/web/src/components/UnusualCell/UnusualCell.tsx
new file mode 100644
index 0000000000..30e75f742f
--- /dev/null
+++ b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/web/src/components/UnusualCell/UnusualCell.tsx
@@ -0,0 +1,16 @@
+export const QUERY = gql`
+ query UnusualCell($id: Int!) {
+ post(id: $id) {
+ id
+ title
+ }
+ }
+`
+
+export const Loading = () =>
Loading...
+
+// Unusual Success export
+const MySuccess = ({ post }) => {post.title}
+export { MySuccess as Success }
+
+export const Failure = ({ error }) => {error.message}
diff --git a/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/web/src/layouts/MainLayout/MainLayout.tsx b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/web/src/layouts/MainLayout/MainLayout.tsx
new file mode 100644
index 0000000000..704120713a
--- /dev/null
+++ b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/web/src/layouts/MainLayout/MainLayout.tsx
@@ -0,0 +1 @@
+export default ({ children }) => {children}
diff --git a/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/web/src/pages/HomePage/HomePage.tsx b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/web/src/pages/HomePage/HomePage.tsx
new file mode 100644
index 0000000000..e356b3f129
--- /dev/null
+++ b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/web/src/pages/HomePage/HomePage.tsx
@@ -0,0 +1 @@
+export default () => Home
diff --git a/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/web/src/pages/NestedPage/NestedPage.tsx b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/web/src/pages/NestedPage/NestedPage.tsx
new file mode 100644
index 0000000000..a726c6ddca
--- /dev/null
+++ b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/web/src/pages/NestedPage/NestedPage.tsx
@@ -0,0 +1 @@
+export default () => Nested
diff --git a/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/web/src/pages/NotFoundPage/NotFoundPage.tsx b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/web/src/pages/NotFoundPage/NotFoundPage.tsx
new file mode 100644
index 0000000000..3d6f782a8e
--- /dev/null
+++ b/packages/structure/src/__tests__/parity/__fixtures__/structure-test-project/web/src/pages/NotFoundPage/NotFoundPage.tsx
@@ -0,0 +1 @@
+export default () => Not Found
diff --git a/packages/structure/src/__tests__/parity/__snapshots__/diagnostics.test.ts.snap b/packages/structure/src/__tests__/parity/__snapshots__/diagnostics.test.ts.snap
new file mode 100644
index 0000000000..02ee364009
--- /dev/null
+++ b/packages/structure/src/__tests__/parity/__snapshots__/diagnostics.test.ts.snap
@@ -0,0 +1,461 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Diagnostic Parity > captures correct diagnostics for example-todo-main-with-errors 1`] = `
+[
+ {
+ "end": {
+ "character": 62,
+ "line": 2,
+ },
+ "message": "Service Not Implemented",
+ "severity": 1,
+ "start": {
+ "character": 4,
+ "line": 2,
+ },
+ "uri": "file:///api/src/graphql/todosMutations.sdl.js",
+ },
+ {
+ "end": {
+ "character": 68,
+ "line": 2,
+ },
+ "message": "Service Not Implemented",
+ "severity": 1,
+ "start": {
+ "character": 4,
+ "line": 2,
+ },
+ "uri": "file:///api/src/graphql/todosWithAuthInvalidRolesErrors.sdl.js",
+ },
+ {
+ "end": {
+ "character": 65,
+ "line": 2,
+ },
+ "message": "Service Not Implemented",
+ "severity": 1,
+ "start": {
+ "character": 4,
+ "line": 2,
+ },
+ "uri": "file:///api/src/graphql/todosWithAuthMissingRoleError.sdl.js",
+ },
+ {
+ "end": {
+ "character": 60,
+ "line": 2,
+ },
+ "message": "Service Not Implemented",
+ "severity": 1,
+ "start": {
+ "character": 4,
+ "line": 2,
+ },
+ "uri": "file:///api/src/graphql/todosWithAuthRoles.sdl.js",
+ },
+ {
+ "end": {
+ "character": 75,
+ "line": 3,
+ },
+ "message": "Service Not Implemented",
+ "severity": 1,
+ "start": {
+ "character": 4,
+ "line": 3,
+ },
+ "uri": "file:///api/src/graphql/todosWithAuthRoles.sdl.js",
+ },
+ {
+ "end": {
+ "character": 108,
+ "line": 2,
+ },
+ "message": "Service Not Implemented",
+ "severity": 1,
+ "start": {
+ "character": 4,
+ "line": 2,
+ },
+ "uri": "file:///api/src/graphql/todosWithBuiltInDirectives.sdl.js",
+ },
+ {
+ "end": {
+ "character": 0,
+ "line": 0,
+ },
+ "message": "Syntax Error: Expected Name, found String "ADMIN".
+
+GraphQL request:3:57
+2 | type Query {
+3 | todosWithMissingRolesAttribute: [Todo] @requireAuth("ADMIN")
+ | ^
+4 | }",
+ "severity": undefined,
+ "start": {
+ "character": 0,
+ "line": 0,
+ },
+ "uri": "file:///api/src/graphql/todosWithMissingAuthRolesAttributeError.sdl.js",
+ },
+ {
+ "end": {
+ "character": 0,
+ "line": 0,
+ },
+ "message": "Syntax Error: Expected Name, found Int "42".
+
+GraphQL request:3:64
+2 | type Query {
+3 | todosWithMissingRolesAttributeNumeric: [Todo] @requireAuth(42)
+ | ^
+4 | }",
+ "severity": undefined,
+ "start": {
+ "character": 0,
+ "line": 0,
+ },
+ "uri": "file:///api/src/graphql/todosWithMissingAuthRolesAttributeNumericError.sdl.js",
+ },
+ {
+ "end": {
+ "character": 56,
+ "line": 2,
+ },
+ "message": "Service Not Implemented",
+ "severity": 1,
+ "start": {
+ "character": 4,
+ "line": 2,
+ },
+ "uri": "file:///api/src/graphql/todosWithNumericRoleAuthError.sdl.js",
+ },
+ {
+ "end": {
+ "character": 1,
+ "line": 12,
+ },
+ "message": "We recommend that you name your query operation",
+ "severity": 2,
+ "start": {
+ "character": 24,
+ "line": 4,
+ },
+ "uri": "file:///web/src/components/TodoListCell/TodoListCell.js",
+ },
+ {
+ "end": {
+ "character": 21,
+ "line": 14,
+ },
+ "message": "Duplicate Path",
+ "severity": 1,
+ "start": {
+ "character": 18,
+ "line": 14,
+ },
+ "uri": "file:///web/src/Routes.js",
+ },
+ {
+ "end": {
+ "character": 21,
+ "line": 15,
+ },
+ "message": "Duplicate Path",
+ "severity": 1,
+ "start": {
+ "character": 18,
+ "line": 15,
+ },
+ "uri": "file:///web/src/Routes.js",
+ },
+ {
+ "end": {
+ "character": 32,
+ "line": 16,
+ },
+ "message": "Error: Route path contains duplicate parameter: "/{foo}/{foo}"",
+ "severity": 1,
+ "start": {
+ "character": 18,
+ "line": 16,
+ },
+ "uri": "file:///web/src/Routes.js",
+ },
+ {
+ "end": {
+ "character": 51,
+ "line": 16,
+ },
+ "message": "Page component not found",
+ "severity": 1,
+ "start": {
+ "character": 39,
+ "line": 16,
+ },
+ "uri": "file:///web/src/Routes.js",
+ },
+ {
+ "end": {
+ "character": 12,
+ "line": 13,
+ },
+ "message": "You must specify a 'notfound' page",
+ "severity": 1,
+ "start": {
+ "character": 4,
+ "line": 13,
+ },
+ "uri": "file:///web/src/Routes.js",
+ },
+]
+`;
+
+exports[`Diagnostic Parity > captures correct diagnostics for local:structure-test-project 1`] = `
+[
+ {
+ "end": {
+ "character": 0,
+ "line": 0,
+ },
+ "message": "Every Cell MUST export a Success variable (React Component)",
+ "severity": 1,
+ "start": {
+ "character": 0,
+ "line": 0,
+ },
+ "uri": "file:///web/src/components/UnusualCell/UnusualCell.tsx",
+ },
+]
+`;
+
+exports[`Diagnostic Parity > captures correct diagnostics for test-project 1`] = `
+[
+ {
+ "end": {
+ "character": 36,
+ "line": 185,
+ },
+ "message": "env value NODE_ENV is not available. add it to your .env file",
+ "severity": 2,
+ "start": {
+ "character": 16,
+ "line": 185,
+ },
+ "uri": "file:///api/src/functions/auth.ts",
+ },
+]
+`;
+
+exports[`Diagnostic Parity > captures the exact same diagnostics for a project with errors 1`] = `
+[
+ {
+ "end": {
+ "character": 21,
+ "line": 14,
+ },
+ "message": "Duplicate Path",
+ "severity": 1,
+ "start": {
+ "character": 18,
+ "line": 14,
+ },
+ "uri": "file:///web/src/Routes.js",
+ },
+ {
+ "end": {
+ "character": 21,
+ "line": 15,
+ },
+ "message": "Duplicate Path",
+ "severity": 1,
+ "start": {
+ "character": 18,
+ "line": 15,
+ },
+ "uri": "file:///web/src/Routes.js",
+ },
+ {
+ "end": {
+ "character": 32,
+ "line": 16,
+ },
+ "message": "Error: Route path contains duplicate parameter: "/{foo}/{foo}"",
+ "severity": 1,
+ "start": {
+ "character": 18,
+ "line": 16,
+ },
+ "uri": "file:///web/src/Routes.js",
+ },
+ {
+ "end": {
+ "character": 51,
+ "line": 16,
+ },
+ "message": "Page component not found",
+ "severity": 1,
+ "start": {
+ "character": 39,
+ "line": 16,
+ },
+ "uri": "file:///web/src/Routes.js",
+ },
+ {
+ "end": {
+ "character": 62,
+ "line": 2,
+ },
+ "message": "Service Not Implemented",
+ "severity": 1,
+ "start": {
+ "character": 4,
+ "line": 2,
+ },
+ "uri": "file:///api/src/graphql/todosMutations.sdl.js",
+ },
+ {
+ "end": {
+ "character": 68,
+ "line": 2,
+ },
+ "message": "Service Not Implemented",
+ "severity": 1,
+ "start": {
+ "character": 4,
+ "line": 2,
+ },
+ "uri": "file:///api/src/graphql/todosWithAuthInvalidRolesErrors.sdl.js",
+ },
+ {
+ "end": {
+ "character": 65,
+ "line": 2,
+ },
+ "message": "Service Not Implemented",
+ "severity": 1,
+ "start": {
+ "character": 4,
+ "line": 2,
+ },
+ "uri": "file:///api/src/graphql/todosWithAuthMissingRoleError.sdl.js",
+ },
+ {
+ "end": {
+ "character": 60,
+ "line": 2,
+ },
+ "message": "Service Not Implemented",
+ "severity": 1,
+ "start": {
+ "character": 4,
+ "line": 2,
+ },
+ "uri": "file:///api/src/graphql/todosWithAuthRoles.sdl.js",
+ },
+ {
+ "end": {
+ "character": 75,
+ "line": 3,
+ },
+ "message": "Service Not Implemented",
+ "severity": 1,
+ "start": {
+ "character": 4,
+ "line": 3,
+ },
+ "uri": "file:///api/src/graphql/todosWithAuthRoles.sdl.js",
+ },
+ {
+ "end": {
+ "character": 108,
+ "line": 2,
+ },
+ "message": "Service Not Implemented",
+ "severity": 1,
+ "start": {
+ "character": 4,
+ "line": 2,
+ },
+ "uri": "file:///api/src/graphql/todosWithBuiltInDirectives.sdl.js",
+ },
+ {
+ "end": {
+ "character": 56,
+ "line": 2,
+ },
+ "message": "Service Not Implemented",
+ "severity": 1,
+ "start": {
+ "character": 4,
+ "line": 2,
+ },
+ "uri": "file:///api/src/graphql/todosWithNumericRoleAuthError.sdl.js",
+ },
+ {
+ "end": {
+ "character": 0,
+ "line": 0,
+ },
+ "message": "Syntax Error: Expected Name, found Int "42".
+
+GraphQL request:3:64
+2 | type Query {
+3 | todosWithMissingRolesAttributeNumeric: [Todo] @requireAuth(42)
+ | ^
+4 | }",
+ "severity": undefined,
+ "start": {
+ "character": 0,
+ "line": 0,
+ },
+ "uri": "file:///api/src/graphql/todosWithMissingAuthRolesAttributeNumericError.sdl.js",
+ },
+ {
+ "end": {
+ "character": 0,
+ "line": 0,
+ },
+ "message": "Syntax Error: Expected Name, found String "ADMIN".
+
+GraphQL request:3:57
+2 | type Query {
+3 | todosWithMissingRolesAttribute: [Todo] @requireAuth("ADMIN")
+ | ^
+4 | }",
+ "severity": undefined,
+ "start": {
+ "character": 0,
+ "line": 0,
+ },
+ "uri": "file:///api/src/graphql/todosWithMissingAuthRolesAttributeError.sdl.js",
+ },
+ {
+ "end": {
+ "character": 1,
+ "line": 12,
+ },
+ "message": "We recommend that you name your query operation",
+ "severity": 2,
+ "start": {
+ "character": 24,
+ "line": 4,
+ },
+ "uri": "file:///web/src/components/TodoListCell/TodoListCell.js",
+ },
+ {
+ "end": {
+ "character": 12,
+ "line": 13,
+ },
+ "message": "You must specify a 'notfound' page",
+ "severity": 1,
+ "start": {
+ "character": 4,
+ "line": 13,
+ },
+ "uri": "file:///web/src/Routes.js",
+ },
+]
+`;
diff --git a/packages/structure/src/__tests__/parity/__snapshots__/snapshot.test.ts.snap b/packages/structure/src/__tests__/parity/__snapshots__/snapshot.test.ts.snap
new file mode 100644
index 0000000000..c7354e1c3e
--- /dev/null
+++ b/packages/structure/src/__tests__/parity/__snapshots__/snapshot.test.ts.snap
@@ -0,0 +1,854 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Project Serialization Parity > serializes example-todo-main correctly 1`] = `
+{
+ "cells": [
+ {
+ "name": "TodoListCell",
+ "queryOperationName": "TodoListCell_GetTodos",
+ },
+ {
+ "name": "NumTodosTwoCell",
+ "queryOperationName": "NumTodosCell_GetCount",
+ },
+ {
+ "name": "NumTodosCell",
+ "queryOperationName": "NumTodosCell_GetCount",
+ },
+ ],
+ "layouts": [
+ {
+ "name": "SetLayout",
+ },
+ ],
+ "pages": [
+ {
+ "constName": "BarPage",
+ "path": "/web/src/pages/BarPage/BarPage.tsx",
+ },
+ {
+ "constName": "FatalErrorPage",
+ "path": "/web/src/pages/FatalErrorPage/FatalErrorPage.js",
+ },
+ {
+ "constName": "FooPage",
+ "path": "/web/src/pages/FooPage/FooPage.tsx",
+ },
+ {
+ "constName": "HomePage",
+ "path": "/web/src/pages/HomePage/HomePage.tsx",
+ },
+ {
+ "constName": "NotFoundPage",
+ "path": "/web/src/pages/NotFoundPage/NotFoundPage.js",
+ },
+ {
+ "constName": "PrivatePage",
+ "path": "/web/src/pages/PrivatePage/PrivatePage.tsx",
+ },
+ {
+ "constName": "TypeScriptPage",
+ "path": "/web/src/pages/TypeScriptPage/TypeScriptPage.tsx",
+ },
+ {
+ "constName": "adminEditUserPage",
+ "path": "/web/src/pages/admin/EditUserPage/EditUserPage.jsx",
+ },
+ ],
+ "router": {
+ "routes": [
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "home",
+ "pageIdentifier": "HomePage",
+ "path": "/",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "typescriptPage",
+ "pageIdentifier": "TypeScriptPage",
+ "path": "/typescript",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "someOtherPage",
+ "pageIdentifier": "EditUserPage",
+ "path": "/somewhereElse",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "fooPage",
+ "pageIdentifier": "FooPage",
+ "path": "/foo",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "barPage",
+ "pageIdentifier": "BarPage",
+ "path": "/bar",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": true,
+ "name": "privatePage",
+ "pageIdentifier": "PrivatePage",
+ "path": "/private-page",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": true,
+ "name": "privatePageAdmin",
+ "pageIdentifier": "PrivatePage",
+ "path": "/private-page-admin",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": true,
+ "name": "privatePageAdminSuper",
+ "pageIdentifier": "PrivatePage",
+ "path": "/private-page-admin-super",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": true,
+ "isPrivate": false,
+ "name": undefined,
+ "pageIdentifier": "NotFoundPage",
+ "path": undefined,
+ "prerender": false,
+ "redirect": undefined,
+ },
+ ],
+ },
+ "services": [
+ {
+ "functions": [
+ {
+ "name": "todos",
+ "parameters": [],
+ },
+ {
+ "name": "createTodo",
+ "parameters": [
+ "body",
+ ],
+ },
+ {
+ "name": "numTodos",
+ "parameters": [],
+ },
+ {
+ "name": "updateTodoStatus",
+ "parameters": [
+ "id",
+ "status",
+ ],
+ },
+ {
+ "name": "renameTodo",
+ "parameters": [
+ "id",
+ "body",
+ ],
+ },
+ ],
+ "name": "todos",
+ },
+ ],
+}
+`;
+
+exports[`Project Serialization Parity > serializes local:structure-test-project correctly 1`] = `
+{
+ "cells": [
+ {
+ "name": "UnusualCell",
+ "queryOperationName": "UnusualCell",
+ },
+ ],
+ "layouts": [
+ {
+ "name": "MainLayout",
+ },
+ ],
+ "pages": [
+ {
+ "constName": "HomePage",
+ "path": "/web/src/pages/HomePage/HomePage.tsx",
+ },
+ {
+ "constName": "NestedPage",
+ "path": "/web/src/pages/NestedPage/NestedPage.tsx",
+ },
+ {
+ "constName": "NotFoundPage",
+ "path": "/web/src/pages/NotFoundPage/NotFoundPage.tsx",
+ },
+ {
+ "constName": "TemplatePage",
+ "path": "/web/src/pages/TemplatePage/TemplatePage.tsx",
+ },
+ ],
+ "router": {
+ "routes": [
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "home",
+ "pageIdentifier": "HomePage",
+ "path": "/",
+ "prerender": false,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "nested",
+ "pageIdentifier": "NestedPage",
+ "path": "/nested",
+ "prerender": false,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "template",
+ "pageIdentifier": "TemplatePage",
+ "path": "/template-path-\${id}",
+ "prerender": false,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": true,
+ "isPrivate": false,
+ "name": undefined,
+ "pageIdentifier": "NotFoundPage",
+ "path": undefined,
+ "prerender": false,
+ "redirect": undefined,
+ },
+ ],
+ },
+ "services": [
+ {
+ "functions": [
+ {
+ "name": "posts",
+ "parameters": [],
+ },
+ {
+ "name": "post",
+ "parameters": [
+ "id",
+ ],
+ },
+ ],
+ "name": "posts",
+ },
+ ],
+}
+`;
+
+exports[`Project Serialization Parity > serializes test-project correctly 1`] = `
+{
+ "cells": [
+ {
+ "name": "WaterfallBlogPostCell",
+ "queryOperationName": "FindWaterfallBlogPostQuery",
+ },
+ {
+ "name": "PostsCell",
+ "queryOperationName": "FindPosts",
+ },
+ {
+ "name": "PostCell",
+ "queryOperationName": "FindPostById",
+ },
+ {
+ "name": "EditPostCell",
+ "queryOperationName": "EditPostById",
+ },
+ {
+ "name": "EditContactCell",
+ "queryOperationName": "EditContactById",
+ },
+ {
+ "name": "ContactsCell",
+ "queryOperationName": "FindContacts",
+ },
+ {
+ "name": "ContactCell",
+ "queryOperationName": "FindContactById",
+ },
+ {
+ "name": "BlogPostsCell",
+ "queryOperationName": "BlogPostsQuery",
+ },
+ {
+ "name": "BlogPostCell",
+ "queryOperationName": "FindBlogPostQuery",
+ },
+ {
+ "name": "AuthorCell",
+ "queryOperationName": "FindAuthorQuery",
+ },
+ ],
+ "layouts": [
+ {
+ "name": "ScaffoldLayout",
+ },
+ {
+ "name": "BlogLayout",
+ },
+ ],
+ "pages": [
+ {
+ "constName": "AboutPage",
+ "path": "/web/src/pages/AboutPage/AboutPage.tsx",
+ },
+ {
+ "constName": "BlogPostPage",
+ "path": "/web/src/pages/BlogPostPage/BlogPostPage.tsx",
+ },
+ {
+ "constName": "ContactUsPage",
+ "path": "/web/src/pages/ContactUsPage/ContactUsPage.tsx",
+ },
+ {
+ "constName": "DoublePage",
+ "path": "/web/src/pages/DoublePage/DoublePage.tsx",
+ },
+ {
+ "constName": "FatalErrorPage",
+ "path": "/web/src/pages/FatalErrorPage/FatalErrorPage.tsx",
+ },
+ {
+ "constName": "ForgotPasswordPage",
+ "path": "/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx",
+ },
+ {
+ "constName": "HomePage",
+ "path": "/web/src/pages/HomePage/HomePage.tsx",
+ },
+ {
+ "constName": "LoginPage",
+ "path": "/web/src/pages/LoginPage/LoginPage.tsx",
+ },
+ {
+ "constName": "NotFoundPage",
+ "path": "/web/src/pages/NotFoundPage/NotFoundPage.tsx",
+ },
+ {
+ "constName": "ProfilePage",
+ "path": "/web/src/pages/ProfilePage/ProfilePage.tsx",
+ },
+ {
+ "constName": "ResetPasswordPage",
+ "path": "/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx",
+ },
+ {
+ "constName": "SignupPage",
+ "path": "/web/src/pages/SignupPage/SignupPage.tsx",
+ },
+ {
+ "constName": "WaterfallPage",
+ "path": "/web/src/pages/WaterfallPage/WaterfallPage.tsx",
+ },
+ {
+ "constName": "ContactContactPage",
+ "path": "/web/src/pages/Contact/ContactPage/ContactPage.tsx",
+ },
+ {
+ "constName": "ContactContactsPage",
+ "path": "/web/src/pages/Contact/ContactsPage/ContactsPage.tsx",
+ },
+ {
+ "constName": "ContactEditContactPage",
+ "path": "/web/src/pages/Contact/EditContactPage/EditContactPage.tsx",
+ },
+ {
+ "constName": "ContactNewContactPage",
+ "path": "/web/src/pages/Contact/NewContactPage/NewContactPage.tsx",
+ },
+ {
+ "constName": "PostEditPostPage",
+ "path": "/web/src/pages/Post/EditPostPage/EditPostPage.tsx",
+ },
+ {
+ "constName": "PostNewPostPage",
+ "path": "/web/src/pages/Post/NewPostPage/NewPostPage.tsx",
+ },
+ {
+ "constName": "PostPostPage",
+ "path": "/web/src/pages/Post/PostPage/PostPage.tsx",
+ },
+ {
+ "constName": "PostPostsPage",
+ "path": "/web/src/pages/Post/PostsPage/PostsPage.tsx",
+ },
+ ],
+ "router": {
+ "routes": [
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "double",
+ "pageIdentifier": "DoublePage",
+ "path": "/double",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "login",
+ "pageIdentifier": "LoginPage",
+ "path": "/login",
+ "prerender": false,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "signup",
+ "pageIdentifier": "SignupPage",
+ "path": "/signup",
+ "prerender": false,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "forgotPassword",
+ "pageIdentifier": "ForgotPasswordPage",
+ "path": "/forgot-password",
+ "prerender": false,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "resetPassword",
+ "pageIdentifier": "ResetPasswordPage",
+ "path": "/reset-password",
+ "prerender": false,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "newContact",
+ "pageIdentifier": "ContactNewContactPage",
+ "path": "/contacts/new",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "editContact",
+ "pageIdentifier": "ContactEditContactPage",
+ "path": "/contacts/{id:Int}/edit",
+ "prerender": false,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "contact",
+ "pageIdentifier": "ContactContactPage",
+ "path": "/contacts/{id:Int}",
+ "prerender": false,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "contacts",
+ "pageIdentifier": "ContactContactsPage",
+ "path": "/contacts",
+ "prerender": false,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "newPost",
+ "pageIdentifier": "PostNewPostPage",
+ "path": "/posts/new",
+ "prerender": false,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "editPost",
+ "pageIdentifier": "PostEditPostPage",
+ "path": "/posts/{id:Int}/edit",
+ "prerender": false,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "post",
+ "pageIdentifier": "PostPostPage",
+ "path": "/posts/{id:Int}",
+ "prerender": false,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "posts",
+ "pageIdentifier": "PostPostsPage",
+ "path": "/posts",
+ "prerender": false,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "waterfall",
+ "pageIdentifier": "WaterfallPage",
+ "path": "/waterfall/{id:Int}",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": true,
+ "name": "profile",
+ "pageIdentifier": "ProfilePage",
+ "path": "/profile",
+ "prerender": false,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "blogPost",
+ "pageIdentifier": "BlogPostPage",
+ "path": "/blog-post/{id:Int}",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "contactUs",
+ "pageIdentifier": "ContactUsPage",
+ "path": "/contact",
+ "prerender": false,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "about",
+ "pageIdentifier": "AboutPage",
+ "path": "/about",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "home",
+ "pageIdentifier": "HomePage",
+ "path": "/",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": true,
+ "isPrivate": false,
+ "name": undefined,
+ "pageIdentifier": "NotFoundPage",
+ "path": undefined,
+ "prerender": true,
+ "redirect": undefined,
+ },
+ ],
+ },
+ "services": [
+ {
+ "functions": [
+ {
+ "name": "user",
+ "parameters": [
+ "id",
+ ],
+ },
+ ],
+ "name": "users",
+ },
+ {
+ "functions": [
+ {
+ "name": "posts",
+ "parameters": [],
+ },
+ {
+ "name": "post",
+ "parameters": [
+ "id",
+ ],
+ },
+ {
+ "name": "createPost",
+ "parameters": [
+ "input",
+ ],
+ },
+ {
+ "name": "updatePost",
+ "parameters": [
+ "id",
+ "input",
+ ],
+ },
+ {
+ "name": "deletePost",
+ "parameters": [
+ "id",
+ ],
+ },
+ ],
+ "name": "posts",
+ },
+ {
+ "functions": [
+ {
+ "name": "contacts",
+ "parameters": [],
+ },
+ {
+ "name": "contact",
+ "parameters": [
+ "id",
+ ],
+ },
+ {
+ "name": "createContact",
+ "parameters": [
+ "input",
+ ],
+ },
+ {
+ "name": "updateContact",
+ "parameters": [
+ "id",
+ "input",
+ ],
+ },
+ {
+ "name": "deleteContact",
+ "parameters": [
+ "id",
+ ],
+ },
+ ],
+ "name": "contacts",
+ },
+ ],
+}
+`;
+
+exports[`Project Serialization Parity > serializes the entire project structure correctly 1`] = `
+{
+ "cells": [
+ {
+ "name": "TodoListCell",
+ "queryOperationName": "TodoListCell_GetTodos",
+ },
+ {
+ "name": "NumTodosTwoCell",
+ "queryOperationName": "NumTodosCell_GetCount",
+ },
+ {
+ "name": "NumTodosCell",
+ "queryOperationName": "NumTodosCell_GetCount",
+ },
+ ],
+ "layouts": [
+ {
+ "name": "SetLayout",
+ },
+ ],
+ "pages": [
+ {
+ "constName": "BarPage",
+ "path": "/web/src/pages/BarPage/BarPage.tsx",
+ },
+ {
+ "constName": "FatalErrorPage",
+ "path": "/web/src/pages/FatalErrorPage/FatalErrorPage.js",
+ },
+ {
+ "constName": "FooPage",
+ "path": "/web/src/pages/FooPage/FooPage.tsx",
+ },
+ {
+ "constName": "HomePage",
+ "path": "/web/src/pages/HomePage/HomePage.tsx",
+ },
+ {
+ "constName": "NotFoundPage",
+ "path": "/web/src/pages/NotFoundPage/NotFoundPage.js",
+ },
+ {
+ "constName": "PrivatePage",
+ "path": "/web/src/pages/PrivatePage/PrivatePage.tsx",
+ },
+ {
+ "constName": "TypeScriptPage",
+ "path": "/web/src/pages/TypeScriptPage/TypeScriptPage.tsx",
+ },
+ {
+ "constName": "adminEditUserPage",
+ "path": "/web/src/pages/admin/EditUserPage/EditUserPage.jsx",
+ },
+ ],
+ "router": {
+ "routes": [
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "home",
+ "pageIdentifier": "HomePage",
+ "path": "/",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "typescriptPage",
+ "pageIdentifier": "TypeScriptPage",
+ "path": "/typescript",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "someOtherPage",
+ "pageIdentifier": "EditUserPage",
+ "path": "/somewhereElse",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "fooPage",
+ "pageIdentifier": "FooPage",
+ "path": "/foo",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": false,
+ "name": "barPage",
+ "pageIdentifier": "BarPage",
+ "path": "/bar",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": true,
+ "name": "privatePage",
+ "pageIdentifier": "PrivatePage",
+ "path": "/private-page",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": true,
+ "name": "privatePageAdmin",
+ "pageIdentifier": "PrivatePage",
+ "path": "/private-page-admin",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": false,
+ "isPrivate": true,
+ "name": "privatePageAdminSuper",
+ "pageIdentifier": "PrivatePage",
+ "path": "/private-page-admin-super",
+ "prerender": true,
+ "redirect": undefined,
+ },
+ {
+ "isNotFound": true,
+ "isPrivate": false,
+ "name": undefined,
+ "pageIdentifier": "NotFoundPage",
+ "path": undefined,
+ "prerender": false,
+ "redirect": undefined,
+ },
+ ],
+ },
+ "services": [
+ {
+ "functions": [
+ {
+ "name": "todos",
+ "parameters": [],
+ },
+ {
+ "name": "createTodo",
+ "parameters": [
+ "body",
+ ],
+ },
+ {
+ "name": "numTodos",
+ "parameters": [],
+ },
+ {
+ "name": "updateTodoStatus",
+ "parameters": [
+ "id",
+ "status",
+ ],
+ },
+ {
+ "name": "renameTodo",
+ "parameters": [
+ "id",
+ "body",
+ ],
+ },
+ ],
+ "name": "todos",
+ },
+ ],
+}
+`;
diff --git a/packages/structure/src/__tests__/parity/api_contract.test.ts b/packages/structure/src/__tests__/parity/api_contract.test.ts
new file mode 100644
index 0000000000..54ec745ec0
--- /dev/null
+++ b/packages/structure/src/__tests__/parity/api_contract.test.ts
@@ -0,0 +1,64 @@
+import { resolve } from 'node:path'
+
+import { describe, it, expect } from 'vitest'
+
+import { getProject } from '../../index'
+
+describe('API Contract (Public API stability)', () => {
+ const projectRoot = resolve(
+ __dirname,
+ '../../../../../__fixtures__/example-todo-main',
+ )
+ const project = getProject(projectRoot)
+
+ it('RWProject has expected top-level accessors', () => {
+ expect(project).toHaveProperty('pages')
+ expect(project).toHaveProperty('router')
+ expect(project).toHaveProperty('services')
+ expect(project).toHaveProperty('cells')
+ expect(project).toHaveProperty('layouts')
+ expect(Array.isArray(project.pages)).toBe(true)
+ })
+
+ it('RWRouter and RWRoute have expected properties', () => {
+ const router = project.router
+ expect(router).toHaveProperty('routes')
+ expect(Array.isArray(router.routes)).toBe(true)
+
+ const route = router.routes[0]
+ expect(route).toHaveProperty('name')
+ expect(route).toHaveProperty('path')
+ expect(route).toHaveProperty('page')
+ expect(route).toHaveProperty('isPrivate')
+ expect(route).toHaveProperty('isNotFound')
+ expect(route).toHaveProperty('page_identifier_str')
+ // Important for Internal/Vite
+ expect(typeof route.isPrivate).toBe('boolean')
+ })
+
+ it('RWPage has expected properties', () => {
+ const page = project.pages[0]
+ expect(page).toHaveProperty('constName')
+ expect(page).toHaveProperty('path') // This is the file path
+ expect(typeof page.constName).toBe('string')
+ expect(typeof page.path).toBe('string')
+ })
+
+ it('RWCell has expected properties', () => {
+ const cell = project.cells[0]
+ expect(cell).toHaveProperty('queryOperationName')
+ expect(cell).toHaveProperty('isCell')
+ expect(typeof cell.isCell).toBe('boolean')
+ })
+
+ it('RWService has expected properties', () => {
+ const service = project.services[0]
+ expect(service).toHaveProperty('name')
+ expect(service).toHaveProperty('funcs')
+ expect(Array.isArray(service.funcs)).toBe(true)
+
+ const func = service.funcs[0]
+ expect(func).toHaveProperty('name')
+ expect(func).toHaveProperty('parameterNames')
+ })
+})
diff --git a/packages/structure/src/__tests__/parity/diagnostics.test.ts b/packages/structure/src/__tests__/parity/diagnostics.test.ts
new file mode 100644
index 0000000000..eeb47b7ddd
--- /dev/null
+++ b/packages/structure/src/__tests__/parity/diagnostics.test.ts
@@ -0,0 +1,59 @@
+import { resolve } from 'node:path'
+
+import { describe, it, expect } from 'vitest'
+
+import { getProject } from '../../index'
+
+describe('Diagnostic Parity', () => {
+ const fixtures = [
+ 'example-todo-main-with-errors',
+ 'test-project',
+ 'local:structure-test-project',
+ ]
+
+ fixtures.forEach((fixtureName) => {
+ it(`captures correct diagnostics for ${fixtureName}`, async () => {
+ let projectRoot: string
+ if (fixtureName.startsWith('local:')) {
+ projectRoot = resolve(
+ __dirname,
+ '__fixtures__',
+ fixtureName.replace('local:', ''),
+ )
+ } else {
+ projectRoot = resolve(
+ __dirname,
+ '../../../../../__fixtures__',
+ fixtureName,
+ )
+ }
+
+ const project = getProject(projectRoot)
+ const diagnostics = await project.collectDiagnostics()
+
+ const cleanDiagnostics = diagnostics
+ .map((d) => ({
+ message: d.diagnostic.message,
+ severity: d.diagnostic.severity,
+ start: {
+ line: d.diagnostic.range.start.line,
+ character: d.diagnostic.range.start.character,
+ },
+ end: {
+ line: d.diagnostic.range.end.line,
+ character: d.diagnostic.range.end.character,
+ },
+ uri: d.uri.replace(projectRoot, ''),
+ }))
+ .sort((a, b) => {
+ const uriComp = a.uri.localeCompare(b.uri)
+ if (uriComp !== 0) {
+ return uriComp
+ }
+ return a.message.localeCompare(b.message)
+ })
+
+ expect(cleanDiagnostics).toMatchSnapshot()
+ })
+ })
+})
diff --git a/packages/structure/src/__tests__/parity/edge_cases.test.ts b/packages/structure/src/__tests__/parity/edge_cases.test.ts
new file mode 100644
index 0000000000..98aad6fbe5
--- /dev/null
+++ b/packages/structure/src/__tests__/parity/edge_cases.test.ts
@@ -0,0 +1,50 @@
+import { resolve } from 'node:path'
+
+import { describe, it, expect } from 'vitest'
+
+import { getProject } from '../../index'
+
+describe('Error Handling and Edge Cases', () => {
+ const projectRootWithErrors = resolve(
+ __dirname,
+ '../../../../../__fixtures__/example-todo-main-with-errors',
+ )
+ const projectWithErrors = getProject(projectRootWithErrors)
+
+ it('handles malformed route syntax gracefully', async () => {
+ const routes = projectWithErrors.router.routes
+ const diagnostics = await projectWithErrors.router.collectDiagnostics()
+
+ // Should still be able to parse other routes
+ expect(routes.length).toBeGreaterThan(0)
+
+ // Should capture the syntax error in diagnostics
+ expect(
+ diagnostics.some((d) =>
+ d.diagnostic.message.includes("specify a 'notfound' page"),
+ ),
+ ).toBe(true)
+ })
+
+ it('identifies missing mandatory exports in Cells via exportedSymbols', async () => {
+ const cell = projectWithErrors.cells.find(
+ (c) => c.basenameNoExt === 'TodoListCell',
+ )
+ expect(cell).toBeDefined()
+
+ // @ts-expect-error accessing internal exportedSymbols for verification
+ const symbols = cell.exportedSymbols
+
+ expect(symbols.has('QUERY')).toBe(true)
+ expect(symbols.has('Success')).toBe(true)
+ expect(symbols.has('Failure')).toBe(false) // This is missing in the fixture
+ })
+
+ it('gracefully handles missing files', () => {
+ const project = getProject('/non/existent/path')
+ // Should not throw on init
+ expect(project.projectRoot).toBe('/non/existent/path')
+ // Should return empty arrays for children
+ expect(project.pages).toEqual([])
+ })
+})
diff --git a/packages/structure/src/__tests__/parity/extractors.test.ts b/packages/structure/src/__tests__/parity/extractors.test.ts
new file mode 100644
index 0000000000..f426ef3a85
--- /dev/null
+++ b/packages/structure/src/__tests__/parity/extractors.test.ts
@@ -0,0 +1,69 @@
+import { resolve } from 'node:path'
+
+import { describe, it, expect } from 'vitest'
+
+import { getProject } from '../../index'
+
+describe('Atomic Logic Parity', () => {
+ const fixtures = [
+ 'example-todo-main',
+ 'test-project',
+ 'local:structure-test-project',
+ ]
+
+ fixtures.forEach((fixtureName) => {
+ describe(`Fixture: ${fixtureName}`, () => {
+ let projectRoot: string
+ if (fixtureName.startsWith('local:')) {
+ projectRoot = resolve(
+ __dirname,
+ '__fixtures__',
+ fixtureName.replace('local:', ''),
+ )
+ } else {
+ projectRoot = resolve(
+ __dirname,
+ '../../../../../__fixtures__',
+ fixtureName,
+ )
+ }
+
+ const project = getProject(projectRoot)
+
+ it('correctly identifies a Cell vs a Component', () => {
+ const cells = project.cells
+ if (cells.length > 0) {
+ expect(cells[0].isCell).toBe(true)
+ }
+
+ const component = project.components.find(
+ (c) => !c.basenameNoExt.endsWith('Cell'),
+ )
+ if (component) {
+ // @ts-expect-error accessing internals for verification
+ expect(component.isCell).toBeUndefined()
+ }
+ })
+
+ it('extracts GraphQL operation names from Cells', () => {
+ for (const cell of project.cells) {
+ expect(cell.queryOperationName).toBeDefined()
+ }
+ })
+
+ it('finds all exported functions in services', () => {
+ for (const service of project.services) {
+ const funcNames = service.funcs.map((f) => f.name)
+ expect(funcNames.length).toBeGreaterThan(0)
+ }
+ })
+
+ it('correctly detects route attributes', () => {
+ for (const route of project.router.routes) {
+ expect(typeof route.isPrivate).toBe('boolean')
+ expect(typeof route.isNotFound).toBe('boolean')
+ }
+ })
+ })
+ })
+})
diff --git a/packages/structure/src/__tests__/parity/integration.test.ts b/packages/structure/src/__tests__/parity/integration.test.ts
new file mode 100644
index 0000000000..9e48645cc3
--- /dev/null
+++ b/packages/structure/src/__tests__/parity/integration.test.ts
@@ -0,0 +1,45 @@
+import { resolve } from 'node:path'
+
+import { describe, it, expect, beforeAll, afterAll } from 'vitest'
+
+import { getProjectRoutes } from '../../../../internal/src/routes'
+
+describe('Internal Package Integration', () => {
+ const projectRoot = resolve(
+ __dirname,
+ '../../../../../__fixtures__/test-project',
+ )
+ let originalCwd: string
+
+ beforeAll(() => {
+ originalCwd = process.cwd()
+ process.chdir(projectRoot)
+ })
+
+ afterAll(() => {
+ process.chdir(originalCwd)
+ })
+
+ it('getProjectRoutes (from @cedarjs/internal) returns correctly mapped routes', () => {
+ const routes = getProjectRoutes()
+ expect(routes.length).toBeGreaterThan(15)
+
+ const homeRoute = routes.find((r) => r.name === 'home')
+ expect(homeRoute).toBeDefined()
+ expect(homeRoute?.pathDefinition).toBe('/')
+ expect(homeRoute?.filePath).toContain('HomePage')
+ expect(homeRoute?.isPrivate).toBe(false)
+
+ const privateRoute = routes.find((r) => r.isPrivate === true)
+ expect(privateRoute).toBeDefined()
+ expect(privateRoute?.unauthenticated).toBeDefined()
+ expect(privateRoute?.name).toEqual('profile')
+
+ const paramRoute = routes.find((r) => r.name === 'editContact')
+ expect(paramRoute).toBeDefined()
+ expect(paramRoute?.pathDefinition).toBe('/posts/{id:Int}/edit')
+ expect(paramRoute?.filePath).toContain('EditPostPage')
+ expect(paramRoute?.isPrivate).toBe(false)
+ expect(paramRoute?.hasParams).toBe(true)
+ })
+})
diff --git a/packages/structure/src/__tests__/parity/snapshot.test.ts b/packages/structure/src/__tests__/parity/snapshot.test.ts
new file mode 100644
index 0000000000..7bc45caef8
--- /dev/null
+++ b/packages/structure/src/__tests__/parity/snapshot.test.ts
@@ -0,0 +1,71 @@
+import { resolve } from 'node:path'
+
+import { describe, it, expect } from 'vitest'
+
+import { getProject } from '../../index'
+
+describe('Project Serialization Parity', () => {
+ const fixtures = [
+ 'example-todo-main',
+ 'test-project',
+ 'local:structure-test-project',
+ ]
+
+ fixtures.forEach((fixtureName) => {
+ it(`serializes ${fixtureName} correctly`, async () => {
+ let projectRoot: string
+ if (fixtureName.startsWith('local:')) {
+ projectRoot = resolve(
+ __dirname,
+ '__fixtures__',
+ fixtureName.replace('local:', ''),
+ )
+ } else {
+ projectRoot = resolve(
+ __dirname,
+ '../../../../../__fixtures__',
+ fixtureName,
+ )
+ }
+
+ const project = getProject(projectRoot)
+
+ // Helper to strip absolute paths from snapshots to make them portable
+ const cleanPath = (p: string | undefined) => p?.replace(projectRoot, '')
+
+ const snapshot = {
+ pages: project.pages.map((p) => ({
+ constName: p.constName,
+ path: cleanPath(p.path),
+ })),
+ router: {
+ routes: project.router.routes.map((r) => ({
+ name: r.name,
+ path: r.path,
+ pageIdentifier: r.page_identifier_str,
+ isPrivate: r.isPrivate,
+ isNotFound: r.isNotFound,
+ prerender: r.prerender,
+ redirect: r.redirect,
+ })),
+ },
+ services: project.services.map((s) => ({
+ name: s.name,
+ functions: s.funcs.map((f) => ({
+ name: f.name,
+ parameters: f.parameterNames,
+ })),
+ })),
+ cells: project.cells.map((c) => ({
+ name: c.basenameNoExt,
+ queryOperationName: c.queryOperationName,
+ })),
+ layouts: project.layouts.map((l) => ({
+ name: l.basenameNoExt,
+ })),
+ }
+
+ expect(snapshot).toMatchSnapshot()
+ })
+ })
+})