Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/router-generator/tests/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ function makeNode(
}

describe('transform', () => {
it('updates parenthesized route calls without changing their surrounding source', () => {
const result = transform({
source:
"import { createFileRoute } from '@tanstack/react-router'; export const Route = ((createFileRoute('/old'))({}));",
ctx: { target: 'react', routeId: '/new', lazy: false },
node: makeNode(),
})
expect(result.result).toBe('modified')
if (result.result === 'modified') {
expect(result.output).toContain("((createFileRoute('/new'))({}))")
}
})

it('does not treat root route exports as missing Route exports', async () => {
const result = await transform({
source: [
Expand Down
66 changes: 66 additions & 0 deletions packages/router-plugin/tests/code-splitter-jsx.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { createServer } from 'vite'
import { expect, it } from 'vitest'
import { tanstackRouter } from '../src/vite'

it.each(['Widget', '_Widget', '$Widget'])(
'preserves the shared JSX component %s across separate loader and component chunks',
async (name) => {
// Keep the temporary app inside the package so runtime imports resolve.
const root = await mkdtemp(path.join(__dirname, '.jsx-reference-'))
let server: Awaited<ReturnType<typeof createServer>> | undefined
try {
await mkdir(path.join(root, 'routes'))
await writeFile(
path.join(root, 'routes/__root.tsx'),
`import { createRootRoute } from '@tanstack/react-router'
export const Route = createRootRoute({})`,
)
await writeFile(
path.join(root, 'routes/widget.tsx'),
`import React from 'react'
import { createFileRoute } from '@tanstack/react-router'
const ${name} = () => 'hello'
export const Route = createFileRoute('/widget')({
loader: () => ${name},
component: () => <${name} />,
})`,
)
server = await createServer({
root,
configFile: false,
logLevel: 'silent',
server: { watch: null },
plugins: [
tanstackRouter({
target: 'react',
routesDirectory: './routes',
generatedRouteTree: './routeTree.gen.ts',
autoCodeSplitting: true,
codeSplittingOptions: {
addHmr: false,
defaultBehavior: [['loader'], ['component']],
},
}),
],
})
// The physical module establishes shared ownership before chunks load.
await server.ssrLoadModule('/routes/widget.tsx')
const componentModule = await server.ssrLoadModule(
'/routes/widget.tsx?tsr-split=component',
)
const loaderModule = await server.ssrLoadModule(
'/routes/widget.tsx?tsr-split=loader',
)
const element = componentModule.component()
expect(typeof element.type).toBe('function')
expect(element.type()).toBe('hello')
expect(loaderModule.loader()()).toBe('hello')
} finally {
await server?.close()
await rm(root, { recursive: true, force: true })
}
},
30_000,
)
161 changes: 161 additions & 0 deletions packages/router-plugin/tests/code-splitter-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { execFile } from 'node:child_process'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { promisify } from 'node:util'
import { pathToFileURL } from 'node:url'
import { build } from 'vite'
import { expect, it } from 'vitest'
import { tanstackRouter } from '../src/vite'
import type { CodeSplitGroupings } from '../src/core/constants'

const runNode = promisify(execFile)
const fixture = path.resolve(
__dirname,
'code-splitter/test-files/react/shared-runtime.tsx',
)

const groupings: Array<{ name: string; groupings: CodeSplitGroupings }> = [
{ name: 'eager loader', groupings: [['component']] },
{ name: 'separate lazy loader', groupings: [['component'], ['loader']] },
{ name: 'combined lazy loader', groupings: [['component', 'loader']] },
]

it.each(groupings)(
'preserves executable route contracts with $name',
assertRuntimeContracts,
30_000,
)

// Shared extraction currently duplicates named default function declarations and
// drops TypeScript enum/namespace dependencies. Keep their intended contracts.
it.fails.each(['named-default', 'enum', 'namespace'] as const)(
'preserves executable route contracts with shared %s declarations',
async (syntax) => {
await assertRuntimeContracts({
groupings: [['component'], ['loader']],
syntax,
})
},
30_000,
)

async function assertRuntimeContracts({
groupings,
syntax,
}: {
groupings: CodeSplitGroupings
syntax?: 'named-default' | 'enum' | 'namespace'
}) {
// Keep the temporary app inside the package so real runtime imports resolve.
const root = await mkdtemp(path.join(__dirname, '.runtime-contract-'))
try {
await mkdir(path.join(root, 'routes'))
let source = await readFile(fixture, 'utf8')
if (syntax === 'named-default') {
source = source
.replace(
'function createSeed()',
'export default function createSeed()',
)
.replace('export default createSeed', '')
} else if (syntax === 'enum') {
source = source.replace(
'const Count = { Initial: 0 }',
'enum Count { Initial = 0 }',
)
} else if (syntax === 'namespace') {
source = source.replace(
"const Labels = { component: 'count' }",
"namespace Labels { export const component = 'count' }",
)
}
await writeFile(path.join(root, 'routes/shared-runtime.tsx'), source)
await writeFile(
path.join(root, 'routes/__root.tsx'),
`import { createRootRoute } from '@tanstack/react-router'
export const Route = createRootRoute({})`,
)
await writeFile(
path.join(root, 'entry.ts'),
`import { createElement } from 'react'
import { renderToString } from 'react-dom/server'
import { Route } from './routes/shared-runtime'
export { Route, firstState, secondState, 'odd-name' } from './routes/shared-runtime'
export function render() {
return renderToString(createElement(Route.options.component))
}`,
)

await build({
root,
configFile: false,
logLevel: 'silent',
plugins: [
tanstackRouter({
target: 'react',
routesDirectory: './routes',
generatedRouteTree: './routeTree.gen.ts',
autoCodeSplitting: true,
codeSplittingOptions: {
addHmr: false,
defaultBehavior: groupings,
},
}),
],
build: {
ssr: path.join(root, 'entry.ts'),
outDir: 'dist',
minify: false,
sourcemap: true,
rollupOptions: {
output: {
entryFileNames: 'entry.mjs',
chunkFileNames: '[name].mjs',
},
},
},
})

const throwLine =
source.split('\n').findIndex((line) => line.includes('throw new Error')) +
1
const entryUrl = pathToFileURL(path.join(root, 'dist/entry.mjs')).href
const combined = groupings.some(
(group) => group.includes('component') && group.includes('loader'),
)
const { stdout } = await runNode(process.execPath, [
'--enable-source-maps',
'--input-type=module',
'--eval',
`import assert from 'node:assert/strict'
const messages = []
console.log = (message) => messages.push(message)
const { Route, render, firstState, secondState, 'odd-name': oddState } = await import(${JSON.stringify(entryUrl)})
assert.deepEqual(messages, ['shared-runtime:seed', 'shared-runtime:state', 'shared-runtime:reference'])
const state = Route.options.beforeLoad({})
assert.strictEqual(firstState, state)
assert.strictEqual(secondState, state)
assert.strictEqual(oddState, state)
assert.equal(state.count, 0)
assert.strictEqual(await Route.options.loader({}), state)
assert.equal(state.count, 1)
assert.deepEqual(messages, ${JSON.stringify(combined ? ['shared-runtime:seed', 'shared-runtime:state', 'shared-runtime:reference', 'shared-runtime:component'] : ['shared-runtime:seed', 'shared-runtime:state', 'shared-runtime:reference'])})
await Route.options.component.preload()
assert.equal(render(), 'count:1')
assert.strictEqual(await Route.options.loader({}), state)
assert.equal(render(), 'count:2')
await Route.options.component.preload()
assert.deepEqual(messages, ['shared-runtime:seed', 'shared-runtime:state', 'shared-runtime:reference', 'shared-runtime:component'])
await Route.options.loader({})
assert.throws(render, (error) => {
assert.equal(error.message, 'route source-map contract')
assert.match(error.stack, /shared-runtime\\.tsx(?:\\?[^\\n:]+)?:${throwLine}:/)
return true
})
process.stdout.write('route contracts passed')`,
])
expect(stdout).toBe('route contracts passed')
} finally {
await rm(root, { recursive: true, force: true })
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { state, createState, createSeed, increment } from "shared-runtime.tsx?tsr-shared=1";
const $$splitComponentImporter = () => import('shared-runtime.tsx?tsr-split=component');
import { lazyRouteComponent } from '@tanstack/react-router';
import { createFileRoute } from '@tanstack/react-router';
console.log('shared-runtime:reference');
export const Route = createFileRoute('/shared-runtime')({
beforeLoad: () => state,
loader: () => {
increment();
return state;
},
component: lazyRouteComponent($$splitComponentImporter, 'component')
});
function createState(initialState: {
count: number;
}): {
state: {
count: number;
};
increment: () => number;
};
export { state as firstState, state as secondState, state as 'odd-name' };
export default createSeed;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { state, createState, createSeed, Labels } from "shared-runtime.tsx?tsr-shared=1";
const label = createComponentLabel();
function createState(initialState: {
count: number;
}): {
state: {
count: number;
};
increment: () => number;
};
function createComponentLabel() {
console.log('shared-runtime:component');
return Labels.component;
}
export { state as firstState, state as secondState, state as 'odd-name' };
export default createSeed;
const SplitComponent = () => {
if (state.count === 3) {
throw new Error('route source-map contract');
}
return `${label}:${state.count}`;
};
export { SplitComponent as component };
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { state, createState, createSeed } from "shared-runtime.tsx?tsr-shared=1";
function createState(initialState: {
count: number;
}): {
state: {
count: number;
};
increment: () => number;
};
export { state as firstState, state as secondState, state as 'odd-name' };
export default createSeed;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { state, createState, createSeed } from "shared-runtime.tsx?tsr-shared=1";
function createState(initialState: {
count: number;
}): {
state: {
count: number;
};
increment: () => number;
};
export { state as firstState, state as secondState, state as 'odd-name' };
export default createSeed;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const Count = {
Initial: 0
};
const Labels = {
component: 'count'
};
const seed = createSeed();
const {
state,
increment
} = createState(seed);
function createSeed() {
console.log('shared-runtime:seed');
return {
count: Count.Initial,
label: Labels.component
};
}
function createState(initialState: {
count: number;
}) {
console.log('shared-runtime:state');
return {
state: initialState,
increment: () => initialState.count++
};
}
export { Count, createSeed, createState, increment, Labels, seed, state };
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { state, createState, createSeed } from "shared-runtime.tsx?tsr-shared=1";
const $$splitComponentImporter = () => import('shared-runtime.tsx?tsr-split=component---errorComponent---notFoundComponent---pendingComponent');
import { lazyRouteComponent } from '@tanstack/react-router';
const $$splitLoaderImporter = () => import('shared-runtime.tsx?tsr-split=loader');
import { lazyFn } from '@tanstack/react-router';
import { createFileRoute } from '@tanstack/react-router';
console.log('shared-runtime:reference');
export const Route = createFileRoute('/shared-runtime')({
beforeLoad: () => state,
loader: lazyFn($$splitLoaderImporter, 'loader'),
component: lazyRouteComponent($$splitComponentImporter, 'component')
});
function createState(initialState: {
count: number;
}): {
state: {
count: number;
};
increment: () => number;
};
export { state as firstState, state as secondState, state as 'odd-name' };
export default createSeed;
Loading
Loading