diff --git a/docs/02-app/01-building-your-application/06-optimizing/10-third-party-libraries.mdx b/docs/02-app/01-building-your-application/06-optimizing/10-third-party-libraries.mdx index e4fe3ed67dab2..b82cce869d257 100644 --- a/docs/02-app/01-building-your-application/06-optimizing/10-third-party-libraries.mdx +++ b/docs/02-app/01-building-your-application/06-optimizing/10-third-party-libraries.mdx @@ -30,7 +30,7 @@ All supported third-party libraries from Google can be imported from `@next/thir ### Google Tag Manager The `GoogleTagManager` component can be used to instantiate a [Google Tag -Manager](https://developers.google.com/maps/documentation/embed/embedding-map) container to your +Manager](https://developers.google.com/tag-platform/tag-manager) container to your page. By default, it fetches the original inline script after hydration occurs on the page. diff --git a/lerna.json b/lerna.json index bba2f8622adda..53733075a77f7 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "14.0.1-canary.3" + "version": "14.0.1-canary.4" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 4b9327c88e92c..54a24f07cf343 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "14.0.1-canary.3", + "version": "14.0.1-canary.4", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 06157f0301728..4573f581c9962 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "14.0.1-canary.3", + "version": "14.0.1-canary.4", "description": "ESLint configuration used by Next.js.", "main": "index.js", "license": "MIT", @@ -10,7 +10,7 @@ }, "homepage": "https://nextjs.org/docs/app/building-your-application/configuring/eslint#eslint-config", "dependencies": { - "@next/eslint-plugin-next": "14.0.1-canary.3", + "@next/eslint-plugin-next": "14.0.1-canary.4", "@rushstack/eslint-patch": "^1.3.3", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0", "eslint-import-resolver-node": "^0.3.6", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 8065635ddb117..4cf70ffe104da 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "14.0.1-canary.3", + "version": "14.0.1-canary.4", "description": "ESLint plugin for Next.js.", "main": "dist/index.js", "license": "MIT", diff --git a/packages/font/package.json b/packages/font/package.json index e99f9617060c5..2fb238621ca93 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,6 +1,6 @@ { "name": "@next/font", - "version": "14.0.1-canary.3", + "version": "14.0.1-canary.4", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 02d772ff94c18..c1bc66b9aab3b 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "14.0.1-canary.3", + "version": "14.0.1-canary.4", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 9c4f1d542e339..4d955fdfc28dd 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "14.0.1-canary.3", + "version": "14.0.1-canary.4", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index c61759ca17588..5333dac24528a 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "14.0.1-canary.3", + "version": "14.0.1-canary.4", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 78eff4297d338..e40e9975976c2 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "14.0.1-canary.3", + "version": "14.0.1-canary.4", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 9e07195a002c7..d55ee77a49254 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "14.0.1-canary.3", + "version": "14.0.1-canary.4", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index 893e9bbb78eaa..d2bfb047704f9 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "14.0.1-canary.3", + "version": "14.0.1-canary.4", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 7e4de4b2cf7d8..5478499cced6f 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "14.0.1-canary.3", + "version": "14.0.1-canary.4", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-swc/crates/next-core/src/app_structure.rs b/packages/next-swc/crates/next-core/src/app_structure.rs index 27b6f09c355c7..3e028e64d39bc 100644 --- a/packages/next-swc/crates/next-core/src/app_structure.rs +++ b/packages/next-swc/crates/next-core/src/app_structure.rs @@ -400,6 +400,25 @@ pub struct LoaderTree { pub global_metadata: Vc, } +#[turbo_tasks::value_impl] +impl LoaderTree { + /// Returns true if there's a page match in this loader tree. + #[turbo_tasks::function] + pub async fn has_page(&self) -> Result> { + if self.segment == "__PAGE__" { + return Ok(Vc::cell(true)); + } + + for (_, tree) in &self.parallel_routes { + if *tree.has_page().await? { + return Ok(Vc::cell(true)); + } + } + + Ok(Vc::cell(false)) + } +} + #[derive( Clone, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, ValueDebugFormat, Debug, TaskInput, )] @@ -425,6 +444,10 @@ fn is_parallel_route(name: &str) -> bool { name.starts_with('@') } +fn is_group_route(name: &str) -> bool { + name.starts_with('(') && name.ends_with(')') +} + fn match_parallel_route(name: &str) -> Option<&str> { name.strip_prefix('@') } @@ -677,14 +700,10 @@ async fn directory_tree_to_loader_tree( tree.segment = "children".to_string(); } - let mut has_page = false; - if let Some(page) = (app_path == for_app_path) .then_some(components.page) .flatten() { - has_page = true; - // When resolving metadata with corresponding module // (https://github.com/vercel/next.js/blob/aa1ee5995cdd92cc9a2236ce4b6aa2b67c9d32b2/packages/next/src/lib/metadata/resolve-metadata.ts#L340) // layout takes precedence over page (https://github.com/vercel/next.js/blob/aa1ee5995cdd92cc9a2236ce4b6aa2b67c9d32b2/packages/next/src/server/lib/app-dir-module.ts#L22) @@ -751,9 +770,26 @@ async fn directory_tree_to_loader_tree( continue; } - // TODO: detect duplicate page in group segment - if !has_page { + // skip groups which don't have a page match. + if is_group_route(subdir_name) && !*subtree.has_page().await? { + continue; + } + + if !tree.parallel_routes.contains_key("children") { tree.parallel_routes.insert("children".to_string(), subtree); + } else { + // TODO: improve error message to have the full paths + DirectoryTreeIssue { + app_dir, + message: Vc::cell(format!( + "You cannot have two parallel pages that resolve to the same path. Route \ + {} has multiple matches in {}", + for_app_path, app_page + )), + severity: IssueSeverity::Error.cell(), + } + .cell() + .emit(); } } else if let Some(key) = parallel_route_key { bail!( @@ -772,7 +808,7 @@ async fn directory_tree_to_loader_tree( ..Default::default() } .cell(); - } else if components.layout.is_some() || current_level_is_parallel_route { + } else if current_level_is_parallel_route { // default fallback component tree.components = Components { default: Some( diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 321ca4627458c..7879d8f99bc95 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "14.0.1-canary.3", + "version": "14.0.1-canary.4", "private": true, "scripts": { "clean": "node ../../scripts/rm.mjs native", diff --git a/packages/next/package.json b/packages/next/package.json index e1ca719bc4add..97b82f6927250 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "14.0.1-canary.3", + "version": "14.0.1-canary.4", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -92,7 +92,7 @@ ] }, "dependencies": { - "@next/env": "14.0.1-canary.3", + "@next/env": "14.0.1-canary.4", "@swc/helpers": "0.5.2", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", @@ -146,11 +146,11 @@ "@mswjs/interceptors": "0.23.0", "@napi-rs/cli": "2.16.2", "@napi-rs/triples": "1.1.0", - "@next/polyfill-module": "14.0.1-canary.3", - "@next/polyfill-nomodule": "14.0.1-canary.3", - "@next/react-dev-overlay": "14.0.1-canary.3", - "@next/react-refresh-utils": "14.0.1-canary.3", - "@next/swc": "14.0.1-canary.3", + "@next/polyfill-module": "14.0.1-canary.4", + "@next/polyfill-nomodule": "14.0.1-canary.4", + "@next/react-dev-overlay": "14.0.1-canary.4", + "@next/react-refresh-utils": "14.0.1-canary.4", + "@next/swc": "14.0.1-canary.4", "@opentelemetry/api": "1.4.1", "@playwright/test": "^1.35.1", "@taskr/clear": "1.1.0", diff --git a/packages/next/src/build/collect-build-traces.ts b/packages/next/src/build/collect-build-traces.ts index 4a38017e52f98..9dde7a2952035 100644 --- a/packages/next/src/build/collect-build-traces.ts +++ b/packages/next/src/build/collect-build-traces.ts @@ -266,7 +266,6 @@ export async function collectBuildTraces({ const sharedIgnores = [ '**/next/dist/compiled/next-server/**/*.dev.js', - '**/node_modules/react{,-dom,-dom-server-turbopack}/**/*.development.js', isStandalone ? null : '**/next/dist/compiled/jest-worker/**/*', '**/next/dist/compiled/webpack/(bundle4|bundle5).js', '**/node_modules/webpack5/**/*', @@ -294,6 +293,7 @@ export async function collectBuildTraces({ const serverIgnores = [ ...sharedIgnores, + '**/node_modules/react{,-dom,-dom-server-turbopack}/**/*.development.js', '**/*.d.ts', '**/*.map', '**/next/dist/pages/**/*', diff --git a/packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts b/packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts index a566eb3d2e7bc..f66bd2d3912c7 100644 --- a/packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts @@ -3,6 +3,16 @@ import fs from 'fs' import path from 'path' import { imageExtMimeTypeMap } from '../../../lib/mime-type' +function errorOnBadHandler(resourcePath: string) { + return ` + if (typeof handler !== 'function') { + throw new Error('Default export is missing in ${JSON.stringify( + resourcePath + )}') + } + ` +} + const cacheHeader = { none: 'no-cache, no-store', longCache: 'public, immutable, no-transform, max-age=31536000', @@ -78,6 +88,8 @@ import { resolveRouteData } from 'next/dist/build/webpack/loaders/metadata/resol const contentType = ${JSON.stringify(getContentType(resourcePath))} const fileType = ${JSON.stringify(getFilenameAndExtension(resourcePath).name)} +${errorOnBadHandler(resourcePath)} + export async function GET() { const data = await handler() const content = resolveRouteData(data, fileType) @@ -103,6 +115,8 @@ const imageModule = { ...userland } const handler = imageModule.default const generateImageMetadata = imageModule.generateImageMetadata +${errorOnBadHandler(resourcePath)} + export async function GET(_, ctx) { const { __metadata_id__ = [], ...params } = ctx.params || {} const targetId = __metadata_id__[0] @@ -160,6 +174,8 @@ const generateSitemaps = sitemapModule.generateSitemaps const contentType = ${JSON.stringify(getContentType(resourcePath))} const fileType = ${JSON.stringify(getFilenameAndExtension(resourcePath).name)} +${errorOnBadHandler(resourcePath)} + ${'' /* re-export the userland route configs */} export * from ${JSON.stringify(resourcePath)} diff --git a/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts b/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts index 42f61cd32fa75..ee62a55b7bcbc 100644 --- a/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts +++ b/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts @@ -197,7 +197,7 @@ export class FlightClientEntryPlugin { const modPath = mod.matchResource || mod.resourceResolveData?.path const modQuery = mod.resourceResolveData?.query || '' // query is already part of mod.resource - // so it's only neccessary to add it for matchResource or mod.resourceResolveData + // so it's only necessary to add it for matchResource or mod.resourceResolveData const modResource = modPath ? modPath + modQuery : mod.resource if (mod.layer !== WEBPACK_LAYERS.serverSideRendering) { diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index 537c67c2af522..bd2c1432dceb6 100644 --- a/packages/react-dev-overlay/package.json +++ b/packages/react-dev-overlay/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-dev-overlay", - "version": "14.0.1-canary.3", + "version": "14.0.1-canary.4", "description": "A development-only overlay for developing React applications.", "repository": { "url": "vercel/next.js", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index ac617d432b577..b5efeec74562f 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "14.0.1-canary.3", + "version": "14.0.1-canary.4", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index 557993106692f..7d4df7d51f871 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "14.0.1-canary.3", + "version": "14.0.1-canary.4", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -22,7 +22,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "14.0.1-canary.3", + "next": "14.0.1-canary.4", "outdent": "0.8.0", "prettier": "2.5.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea39243e210d8..3344813f7ddab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -735,7 +735,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 14.0.1-canary.3 + specifier: 14.0.1-canary.4 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.3.3 @@ -796,7 +796,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 14.0.1-canary.3 + specifier: 14.0.1-canary.4 version: link:../next-env '@swc/helpers': specifier: 0.5.2 @@ -920,19 +920,19 @@ importers: specifier: 1.1.0 version: 1.1.0 '@next/polyfill-module': - specifier: 14.0.1-canary.3 + specifier: 14.0.1-canary.4 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 14.0.1-canary.3 + specifier: 14.0.1-canary.4 version: link:../next-polyfill-nomodule '@next/react-dev-overlay': - specifier: 14.0.1-canary.3 + specifier: 14.0.1-canary.4 version: link:../react-dev-overlay '@next/react-refresh-utils': - specifier: 14.0.1-canary.3 + specifier: 14.0.1-canary.4 version: link:../react-refresh-utils '@next/swc': - specifier: 14.0.1-canary.3 + specifier: 14.0.1-canary.4 version: link:../next-swc '@opentelemetry/api': specifier: 1.4.1 @@ -1583,7 +1583,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 14.0.1-canary.3 + specifier: 14.0.1-canary.4 version: link:../next outdent: specifier: 0.8.0 diff --git a/test/e2e/app-dir/app/standalone-gsp.test.ts b/test/e2e/app-dir/app/standalone-gsp.test.ts new file mode 100644 index 0000000000000..99a281078ac58 --- /dev/null +++ b/test/e2e/app-dir/app/standalone-gsp.test.ts @@ -0,0 +1,88 @@ +import { createNextDescribe } from 'e2e-utils' +import fs from 'fs-extra' +import os from 'os' +import path from 'path' +import { + findPort, + initNextServerScript, + killApp, + fetchViaHTTP, +} from 'next-test-utils' + +if (!(globalThis as any).isNextStart) { + it('should skip for non-next start', () => {}) +} else { + createNextDescribe( + 'output: standalone with getStaticProps', + { + files: __dirname, + skipStart: true, + dependencies: { + swr: 'latest', + }, + }, + ({ next }) => { + beforeAll(async () => { + await next.patchFile( + 'next.config.js', + (await next.readFile('next.config.js')).replace('// output', 'output') + ) + + await next.patchFile( + 'pages/gsp.js', + ` + import useSWR from 'swr' + + console.log(useSWR) + + export default function Home() { + return

Hello

+ } + + export async function getStaticProps() { + return { + props: { + foo: "bar", + }, + }; + } + ` + ) + + await next.start() + }) + + it('should work correctly with output standalone', async () => { + const tmpFolder = path.join( + os.tmpdir(), + 'next-standalone-' + Date.now() + ) + await fs.move(path.join(next.testDir, '.next/standalone'), tmpFolder) + let server: any + + try { + const testServer = path.join(tmpFolder, 'server.js') + const appPort = await findPort() + server = await initNextServerScript( + testServer, + /- Local:/, + { + ...process.env, + PORT: appPort.toString(), + }, + undefined, + { + cwd: tmpFolder, + } + ) + + const res = await fetchViaHTTP(appPort, '/gsp') + expect(res.status).toBe(200) + } finally { + if (server) await killApp(server) + await fs.remove(tmpFolder) + } + }) + } + ) +} diff --git a/test/e2e/app-dir/metadata-dynamic-routes/index.test.ts b/test/e2e/app-dir/metadata-dynamic-routes/index.test.ts index 60c77d18241b3..188575f9ae6d0 100644 --- a/test/e2e/app-dir/metadata-dynamic-routes/index.test.ts +++ b/test/e2e/app-dir/metadata-dynamic-routes/index.test.ts @@ -497,6 +497,33 @@ createNextDescribe( await next.fetch('/metadata-base/unset/sitemap.xml/0') } }) + + it('should error if the default export of dynamic image is missing', async () => { + const ogImageFilePath = 'app/opengraph-image.tsx' + const ogImageFileContent = await next.readFile(ogImageFilePath) + const ogImageFileContentWithoutDefaultExport = + ogImageFileContent.replace( + 'export default function', + 'export function' + ) + + try { + await next.patchFile( + ogImageFilePath, + ogImageFileContentWithoutDefaultExport + ) + const currentNextCliOutputLength = next.cliOutput.length + + await check(async () => { + await next.fetch('/opengraph-image') + const output = next.cliOutput.slice(currentNextCliOutputLength) + expect(output).toContain(`Default export is missing in`) + return 'success' + }, /success/) + } finally { + await next.patchFile(ogImageFilePath, ogImageFileContent) + } + }) } if (isNextStart) {