diff --git a/e2e/start/basic/app/routeTree.gen.ts b/e2e/start/basic/app/routeTree.gen.ts index 97f04f7681..3dc72a54ff 100644 --- a/e2e/start/basic/app/routeTree.gen.ts +++ b/e2e/start/basic/app/routeTree.gen.ts @@ -15,6 +15,7 @@ import { Route as UsersImport } from './routes/users' import { Route as StatusImport } from './routes/status' import { Route as ServerFnsImport } from './routes/server-fns' import { Route as SearchParamsImport } from './routes/search-params' +import { Route as ScriptsImport } from './routes/scripts' import { Route as PostsImport } from './routes/posts' import { Route as LinksImport } from './routes/links' import { Route as IsomorphicFnsImport } from './routes/isomorphic-fns' @@ -66,6 +67,12 @@ const SearchParamsRoute = SearchParamsImport.update({ getParentRoute: () => rootRoute, } as any) +const ScriptsRoute = ScriptsImport.update({ + id: '/scripts', + path: '/scripts', + getParentRoute: () => rootRoute, +} as any) + const PostsRoute = PostsImport.update({ id: '/posts', path: '/posts', @@ -266,6 +273,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PostsImport parentRoute: typeof rootRoute } + '/scripts': { + id: '/scripts' + path: '/scripts' + fullPath: '/scripts' + preLoaderRoute: typeof ScriptsImport + parentRoute: typeof rootRoute + } '/search-params': { id: '/search-params' path: '/search-params' @@ -501,6 +515,7 @@ export interface FileRoutesByFullPath { '/isomorphic-fns': typeof IsomorphicFnsRoute '/links': typeof LinksRoute '/posts': typeof PostsRouteWithChildren + '/scripts': typeof ScriptsRoute '/search-params': typeof SearchParamsRoute '/server-fns': typeof ServerFnsRoute '/status': typeof StatusRoute @@ -530,6 +545,7 @@ export interface FileRoutesByTo { '/env-only': typeof EnvOnlyRoute '/isomorphic-fns': typeof IsomorphicFnsRoute '/links': typeof LinksRoute + '/scripts': typeof ScriptsRoute '/search-params': typeof SearchParamsRoute '/server-fns': typeof ServerFnsRoute '/status': typeof StatusRoute @@ -559,6 +575,7 @@ export interface FileRoutesById { '/isomorphic-fns': typeof IsomorphicFnsRoute '/links': typeof LinksRoute '/posts': typeof PostsRouteWithChildren + '/scripts': typeof ScriptsRoute '/search-params': typeof SearchParamsRoute '/server-fns': typeof ServerFnsRoute '/status': typeof StatusRoute @@ -592,6 +609,7 @@ export interface FileRouteTypes { | '/isomorphic-fns' | '/links' | '/posts' + | '/scripts' | '/search-params' | '/server-fns' | '/status' @@ -620,6 +638,7 @@ export interface FileRouteTypes { | '/env-only' | '/isomorphic-fns' | '/links' + | '/scripts' | '/search-params' | '/server-fns' | '/status' @@ -647,6 +666,7 @@ export interface FileRouteTypes { | '/isomorphic-fns' | '/links' | '/posts' + | '/scripts' | '/search-params' | '/server-fns' | '/status' @@ -679,6 +699,7 @@ export interface RootRouteChildren { IsomorphicFnsRoute: typeof IsomorphicFnsRoute LinksRoute: typeof LinksRoute PostsRoute: typeof PostsRouteWithChildren + ScriptsRoute: typeof ScriptsRoute SearchParamsRoute: typeof SearchParamsRoute ServerFnsRoute: typeof ServerFnsRoute StatusRoute: typeof StatusRoute @@ -696,6 +717,7 @@ const rootRouteChildren: RootRouteChildren = { IsomorphicFnsRoute: IsomorphicFnsRoute, LinksRoute: LinksRoute, PostsRoute: PostsRouteWithChildren, + ScriptsRoute: ScriptsRoute, SearchParamsRoute: SearchParamsRoute, ServerFnsRoute: ServerFnsRoute, StatusRoute: StatusRoute, @@ -722,6 +744,7 @@ export const routeTree = rootRoute "/isomorphic-fns", "/links", "/posts", + "/scripts", "/search-params", "/server-fns", "/status", @@ -759,6 +782,9 @@ export const routeTree = rootRoute "/posts/" ] }, + "/scripts": { + "filePath": "scripts.tsx" + }, "/search-params": { "filePath": "search-params.tsx" }, diff --git a/e2e/start/basic/app/routes/__root.tsx b/e2e/start/basic/app/routes/__root.tsx index 876210e143..d3e1f7d330 100644 --- a/e2e/start/basic/app/routes/__root.tsx +++ b/e2e/start/basic/app/routes/__root.tsx @@ -112,6 +112,14 @@ function RootDocument({ children }: { children: React.ReactNode }) { > Layout {' '} + + Scripts + {' '} ({ + scripts: [ + { + src: 'script.js', + ['data-testid']: 'script', + }, + isProd + ? undefined + : { + src: 'script2.js', + ['data-testid']: 'script2', + }, + ], + }), + component: ScriptsComponent, +}) + +function ScriptsComponent() { + return ( +
+

Scripts Test

+

+ Both `script.js` and `script2.js` are included in development, but only + `script.js` is included in production. +

+
+ ) +} diff --git a/e2e/start/basic/public/script.js b/e2e/start/basic/public/script.js new file mode 100644 index 0000000000..d24edb364a --- /dev/null +++ b/e2e/start/basic/public/script.js @@ -0,0 +1,2 @@ +// eslint-disable no-empty-file +// This empty script file is for testing purposes only diff --git a/e2e/start/basic/public/script2.js b/e2e/start/basic/public/script2.js new file mode 100644 index 0000000000..d24edb364a --- /dev/null +++ b/e2e/start/basic/public/script2.js @@ -0,0 +1,2 @@ +// eslint-disable no-empty-file +// This empty script file is for testing purposes only diff --git a/e2e/start/basic/tests/base.spec.ts b/e2e/start/basic/tests/base.spec.ts index 80c30f9d07..3b6158ed09 100644 --- a/e2e/start/basic/tests/base.spec.ts +++ b/e2e/start/basic/tests/base.spec.ts @@ -32,6 +32,15 @@ test('Navigating nested layouts', async ({ page }) => { await expect(page.locator('body')).toContainText("I'm layout B!") }) +test('Navigating to route with scripts', async ({ page }) => { + await page.goto('/') + + await page.getByRole('link', { name: 'Scripts' }).click() + + await expect(page.getByTestId('script')).toHaveCount(1) + await expect(page.getByTestId('script2')).toHaveCount(0) +}) + test('Navigating to a not-found route', async ({ page }) => { await page.goto('/') @@ -79,10 +88,12 @@ test('invoking a server function with custom response status code', async ({ expect(response.status()).toBe(225) expect(response.statusText()).toBe('hello') expect(response.headers()['content-type']).toBe('application/json') - expect(await response.json()).toEqual({ - result: { hello: 'world' }, - context: {}, - }) + expect(await response.json()).toEqual( + expect.objectContaining({ + result: { hello: 'world' }, + context: {}, + }), + ) resolve() }) }) @@ -193,7 +204,7 @@ test('env-only functions can only be called on the server or client respectively ) }) -test.only('Server function can return null for GET and POST calls', async ({ +test('Server function can return null for GET and POST calls', async ({ page, }) => { await page.goto('/server-fns') diff --git a/e2e/start/basic/tsconfig.json b/e2e/start/basic/tsconfig.json index d1b5b77660..a6747faec5 100644 --- a/e2e/start/basic/tsconfig.json +++ b/e2e/start/basic/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["**/*.ts", "**/*.tsx"], + "include": ["**/*.ts", "**/*.tsx", "public/script*.js"], "compilerOptions": { "strict": true, "esModuleInterop": true, diff --git a/packages/start/src/client/Scripts.tsx b/packages/start/src/client/Scripts.tsx index 3070caac46..19d6b305d3 100644 --- a/packages/start/src/client/Scripts.tsx +++ b/packages/start/src/client/Scripts.tsx @@ -33,8 +33,8 @@ export const Scripts = () => { scripts: ( state.matches .map((match) => match.scripts!) - .filter(Boolean) - .flat(1) as Array + .flat(1) + .filter(Boolean) as Array ).map(({ children, ...script }) => ({ tag: 'script', attrs: { diff --git a/packages/start/src/client/tests/index.test.tsx b/packages/start/src/client/tests/index.test.tsx index e1757cc884..321c1ab6ae 100644 --- a/packages/start/src/client/tests/index.test.tsx +++ b/packages/start/src/client/tests/index.test.tsx @@ -72,6 +72,55 @@ describe('ssr scripts', () => { ``, ) }) + + test('excludes `undefined` script values', async () => { + const rootRoute = createRootRoute({ + head: () => { + return { + scripts: [ + { src: 'script.js' }, + undefined, // 'script2.js' opted out by certain conditions, such as `NODE_ENV=production`. + ], + } + }, + component: () => { + return + }, + }) + + const indexRoute = createRoute({ + path: '/', + getParentRoute: () => rootRoute, + head: () => { + return { + scripts: [{ src: 'script3.js' }], + } + }, + }) + + const router = createRouter({ + history: createMemoryHistory({ + initialEntries: ['/'], + }), + routeTree: rootRoute.addChildren([indexRoute]), + }) + + router.isServer = true + + await router.load() + + expect(router.state.matches.map((d) => d.scripts).flat(1)).toEqual([ + { src: 'script.js' }, + undefined, + { src: 'script3.js' }, + ]) + + const { container } = render() + + expect(container.innerHTML).toEqual( + ``, + ) + }) }) describe('ssr meta', () => {