From 3db5c16546211a564bd2adb29788ae651667486a Mon Sep 17 00:00:00 2001 From: Thomas Knickman Date: Mon, 7 Aug 2023 11:15:26 -0400 Subject: [PATCH 001/387] fix(node): pnpm 8.6 needs node 16.14 (#53677) The `pnpm` version was recently updated to `8.6`, however the node version was left at `16.8`. `pnpm@8.6` requires at least `node@16.14` ([source](https://github.com/pnpm/pnpm/blob/main/pnpm/package.json#L145)). This fixes the engines spec to avoid conflicting versions. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eadc032adcdec..4c529dd614d22 100644 --- a/package.json +++ b/package.json @@ -243,7 +243,7 @@ "@types/react-dom": "18.2.4" }, "engines": { - "node": ">=16.8.0", + "node": ">=16.14.0", "pnpm": "8.6.11" }, "packageManager": "pnpm@8.6.11" From de0ff6fd59b269de08e2bdb926e5af1a163c6be7 Mon Sep 17 00:00:00 2001 From: Jude Gao Date: Mon, 7 Aug 2023 12:24:42 -0400 Subject: [PATCH 002/387] Fix doc grammatical errors (#53672) ### Improving Documentation - Ran `pnpm prettier-fix` --- .../03-rendering/01-static-and-dynamic.mdx | 4 ++-- docs/02-app/01-building-your-application/04-caching/index.mdx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/02-app/01-building-your-application/03-rendering/01-static-and-dynamic.mdx b/docs/02-app/01-building-your-application/03-rendering/01-static-and-dynamic.mdx index e83353fb2e115..3505a4bf7cb2e 100644 --- a/docs/02-app/01-building-your-application/03-rendering/01-static-and-dynamic.mdx +++ b/docs/02-app/01-building-your-application/03-rendering/01-static-and-dynamic.mdx @@ -26,8 +26,8 @@ During rendering, if a [dynamic function](#dynamic-functions) or uncached data r | -------------------- | ----------------- | ---------- | | Statically Rendered | No | Cached | | Dynamically Rendered | Yes | Cached | -| Dynamic Rendered | No | Not Cached | -| Dynamic Rendered | Yes | Not Cached | +| Dynamically Rendered | No | Not Cached | +| Dynamically Rendered | Yes | Not Cached | From the table above, for a route to be fully static, all data must be cached. However, you can have a dynamically rendered route that uses both cached and uncached data fetches. This is useful when you have a page that mostly re-uses cached data, but has some uncached data. It allows you to opt into dynamic rendering without worrying about the performance impact of fetching all the data at request time. diff --git a/docs/02-app/01-building-your-application/04-caching/index.mdx b/docs/02-app/01-building-your-application/04-caching/index.mdx index 3389414660c61..abc9453b73b8d 100644 --- a/docs/02-app/01-building-your-application/04-caching/index.mdx +++ b/docs/02-app/01-building-your-application/04-caching/index.mdx @@ -85,7 +85,7 @@ const item = await getItem() // cache HIT height="742" /> -- While rendering a route, the first time a particular request is called, it's result will not be in memory and it'll be a cache `MISS`. +- While rendering a route, the first time a particular request is called, its result will not be in memory and it'll be a cache `MISS`. - Therefore, the function will be executed, and the data will be fetched from the external source, and the result will be stored in memory. - Subsequent function calls of the request in the same render pass will be a cache `HIT`, and the data will be returned from memory without executing the function. - Once the route has been rendered and the rendering pass is complete, memory is "reset" and all request memoization entries are cleared. @@ -341,7 +341,7 @@ Next.js has an in-memory client-side cache that stores the React Server Componen height="1375" /> -As users navigates between routes, Next.js caches visited route segments and [prefetches](/docs/app/building-your-application/routing/linking-and-navigating#1-prefetching) the routes the user is likely to navigate to (based on `` components in their viewport). +As users navigate between routes, Next.js caches visited route segments and [prefetches](/docs/app/building-your-application/routing/linking-and-navigating#1-prefetching) the routes the user is likely to navigate to (based on `` components in their viewport). This results in an improved navigation experience for the user: From 7ec76eb7e402ee601fcc23d11a76b1b2339321e1 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 7 Aug 2023 13:24:45 -0400 Subject: [PATCH 003/387] fix(create-next-app): fix CI defaults (default to typescript) (#53686) Since the default for `create-next-app` has been TypeScript for some time, we should make sure this is also the case for CI. This PR also makes sure that CI will use the same value as if the question was asked. Therefore changing defaults in the future will automatically change the behavior of CI defaults. - Related https://github.com/vercel/next.js/issues/42592 --- packages/create-next-app/index.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/create-next-app/index.ts b/packages/create-next-app/index.ts index 3c12a7469b0af..dacdbe869151b 100644 --- a/packages/create-next-app/index.ts +++ b/packages/create-next-app/index.ts @@ -255,10 +255,9 @@ async function run(): Promise { if (!program.typescript && !program.javascript) { if (ciInfo.isCI) { - // default to JavaScript in CI as we can't prompt to + // default to TypeScript in CI as we can't prompt to // prevent breaking setup flows - program.typescript = false - program.javascript = true + program.typescript = getPrefOrDefault('typescript') } else { const styledTypeScript = blue('TypeScript') const { typescript } = await prompts( @@ -295,7 +294,7 @@ async function run(): Promise { !process.argv.includes('--no-eslint') ) { if (ciInfo.isCI) { - program.eslint = true + program.eslint = getPrefOrDefault('eslint') } else { const styledEslint = blue('ESLint') const { eslint } = await prompts({ @@ -317,7 +316,7 @@ async function run(): Promise { !process.argv.includes('--no-tailwind') ) { if (ciInfo.isCI) { - program.tailwind = false + program.tailwind = getPrefOrDefault('tailwind') } else { const tw = blue('Tailwind CSS') const { tailwind } = await prompts({ @@ -339,7 +338,7 @@ async function run(): Promise { !process.argv.includes('--no-src-dir') ) { if (ciInfo.isCI) { - program.srcDir = false + program.srcDir = getPrefOrDefault('srcDir') } else { const styledSrcDir = blue('`src/` directory') const { srcDir } = await prompts({ From 654bd8c80339646beda5405511c62702f54c08df Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Mon, 7 Aug 2023 19:26:44 +0200 Subject: [PATCH 004/387] add unit test case for next.rs api (#53679) ### What? unit test for next.rs api ### Why? ensure that it's working ### How? --- packages/next/src/build/swc/index.ts | 10 +- test/unit/__snapshots__/next-api.test.ts.snap | 817 ++++++++++++++++++ test/unit/next-api.test.ts | 256 ++++++ 3 files changed, 1078 insertions(+), 5 deletions(-) create mode 100644 test/unit/__snapshots__/next-api.test.ts.snap create mode 100644 test/unit/next-api.test.ts diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index d7682f3367d1f..e43e915ec271d 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -430,7 +430,7 @@ interface TurboEngineOptions { memoryLimit?: number } -interface Issue { +export interface Issue { severity: string category: string filePath: string @@ -449,7 +449,7 @@ interface Issue { subIssues: Issue[] } -interface Diagnostics {} +export interface Diagnostics {} export type TurbopackResult = T & { issues: Issue[] @@ -462,7 +462,7 @@ interface Middleware { matcher?: string[] } -interface Entrypoints { +export interface Entrypoints { routes: Map middleware?: Middleware pagesDocumentEndpoint: Endpoint @@ -470,11 +470,11 @@ interface Entrypoints { pagesErrorEndpoint: Endpoint } -interface Update { +export interface Update { update: unknown } -interface Project { +export interface Project { update(options: ProjectOptions): Promise entrypointsSubscribe(): AsyncIterableIterator> hmrEvents(identifier: string): AsyncIterableIterator> diff --git a/test/unit/__snapshots__/next-api.test.ts.snap b/test/unit/__snapshots__/next-api.test.ts.snap new file mode 100644 index 0000000000000..799c2f925bcee --- /dev/null +++ b/test/unit/__snapshots__/next-api.test.ts.snap @@ -0,0 +1,817 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`next.rs api should allow to write app Node.js page to disk: diagnostics 1`] = `Array []`; + +exports[`next.rs api should allow to write app Node.js page to disk: issues 1`] = `Array []`; + +exports[`next.rs api should allow to write app Node.js page to disk: rsc diagnostics 1`] = `Array []`; + +exports[`next.rs api should allow to write app Node.js page to disk: rsc issues 1`] = `Array []`; + +exports[`next.rs api should allow to write app Node.js route to disk: diagnostics 1`] = `Array []`; + +exports[`next.rs api should allow to write app Node.js route to disk: issues 1`] = `Array []`; + +exports[`next.rs api should allow to write app edge route to disk: diagnostics 1`] = `Array []`; + +exports[`next.rs api should allow to write app edge route to disk: issues 1`] = `Array []`; + +exports[`next.rs api should allow to write pages Node.js api to disk: diagnostics 1`] = `Array []`; + +exports[`next.rs api should allow to write pages Node.js api to disk: issues 1`] = `Array []`; + +exports[`next.rs api should allow to write pages Node.js page to disk: diagnostics 1`] = `Array []`; + +exports[`next.rs api should allow to write pages Node.js page to disk: issues 1`] = `Array []`; + +exports[`next.rs api should allow to write pages edge api to disk: diagnostics 1`] = `Array []`; + +exports[`next.rs api should allow to write pages edge api to disk: issues 1`] = ` +Array [ + Object { + "category": "resolve", + "description": "unable to resolve module \\"buffer\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"buffer\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 945, + "line": 10, + }, + "source": "[project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js", + "start": Object { + "column": 928, + "line": 10, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"buffer\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"buffer\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 945, + "line": 10, + }, + "source": "[project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js", + "start": Object { + "column": 928, + "line": 10, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"crypto\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"crypto\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 995, + "line": 10, + }, + "source": "[project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js", + "start": Object { + "column": 978, + "line": 10, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"crypto\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"crypto\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 995, + "line": 10, + }, + "source": "[project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js", + "start": Object { + "column": 978, + "line": 10, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"stream\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"stream\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 1114, + "line": 10, + }, + "source": "[project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js", + "start": Object { + "column": 1097, + "line": 10, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"stream\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"stream\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 1114, + "line": 10, + }, + "source": "[project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js", + "start": Object { + "column": 1097, + "line": 10, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"util\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"util\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 1162, + "line": 10, + }, + "source": "[project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js", + "start": Object { + "column": 1147, + "line": 10, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"util\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"util\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 1162, + "line": 10, + }, + "source": "[project]/.../node_modules/next/dist/compiled/jsonwebtoken/index.js", + "start": Object { + "column": 1147, + "line": 10, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"buffer\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"buffer\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/raw-body/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 504, + "line": 50, + }, + "source": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "start": Object { + "column": 487, + "line": 50, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"buffer\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"buffer\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/raw-body/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 504, + "line": 50, + }, + "source": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "start": Object { + "column": 487, + "line": 50, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"events\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"events\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/raw-body/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 554, + "line": 50, + }, + "source": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "start": Object { + "column": 537, + "line": 50, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"events\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"events\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/raw-body/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 554, + "line": 50, + }, + "source": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "start": Object { + "column": 537, + "line": 50, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"path\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"path\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/raw-body/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 669, + "line": 50, + }, + "source": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "start": Object { + "column": 654, + "line": 50, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"path\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"path\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/raw-body/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 669, + "line": 50, + }, + "source": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "start": Object { + "column": 654, + "line": 50, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"stream\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"stream\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/raw-body/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 719, + "line": 50, + }, + "source": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "start": Object { + "column": 702, + "line": 50, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"stream\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"stream\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/raw-body/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 719, + "line": 50, + }, + "source": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "start": Object { + "column": 702, + "line": 50, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"string_decoder\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"string_decoder\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/raw-body/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 777, + "line": 50, + }, + "source": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "start": Object { + "column": 752, + "line": 50, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"string_decoder\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"string_decoder\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/raw-body/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 777, + "line": 50, + }, + "source": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "start": Object { + "column": 752, + "line": 50, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"util\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"util\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/raw-body/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 825, + "line": 50, + }, + "source": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "start": Object { + "column": 810, + "line": 50, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"util\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"util\\" +Path where resolving has started: [project]/.../node_modules/next/dist/compiled/raw-body/index.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 825, + "line": 50, + }, + "source": "[project]/.../node_modules/next/dist/compiled/raw-body/index.js", + "start": Object { + "column": 810, + "line": 50, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"querystring\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"querystring\\" +Path where resolving has started: [project]/.../node_modules/next/dist/server/api-utils/node.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/server/api-utils/node.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 42, + "line": 154, + }, + "source": "[project]/.../node_modules/next/dist/server/api-utils/node.js", + "start": Object { + "column": 20, + "line": 154, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"querystring\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"querystring\\" +Path where resolving has started: [project]/.../node_modules/next/dist/server/api-utils/node.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/server/api-utils/node.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 42, + "line": 154, + }, + "source": "[project]/.../node_modules/next/dist/server/api-utils/node.js", + "start": Object { + "column": 20, + "line": 154, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"stream\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"stream\\" +Path where resolving has started: [project]/.../node_modules/next/dist/server/api-utils/node.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/server/api-utils/node.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 34, + "line": 30, + }, + "source": "[project]/.../node_modules/next/dist/server/api-utils/node.js", + "start": Object { + "column": 17, + "line": 30, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"stream\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"stream\\" +Path where resolving has started: [project]/.../node_modules/next/dist/server/api-utils/node.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/server/api-utils/node.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 34, + "line": 30, + }, + "source": "[project]/.../node_modules/next/dist/server/api-utils/node.js", + "start": Object { + "column": 17, + "line": 30, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"crypto\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"crypto\\" +Path where resolving has started: [project]/.../node_modules/next/dist/server/crypto-utils.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/server/crypto-utils.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 73, + "line": 22, + }, + "source": "[project]/.../node_modules/next/dist/server/crypto-utils.js", + "start": Object { + "column": 56, + "line": 22, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, + Object { + "category": "resolve", + "description": "unable to resolve module \\"crypto\\"", + "detail": "It was not possible to find the requested file. +Parsed request as written in source code: module \\"crypto\\" +Path where resolving has started: [project]/.../node_modules/next/dist/server/crypto-utils.js +Type of request: commonjs request +Import map: No import map entry +", + "documentationLink": "", + "filePath": "[project]/.../node_modules/next/dist/server/crypto-utils.js", + "severity": "error", + "source": Object { + "end": Object { + "column": 73, + "line": 22, + }, + "source": "[project]/.../node_modules/next/dist/server/crypto-utils.js", + "start": Object { + "column": 56, + "line": 22, + }, + }, + "subIssues": Array [], + "title": "Error resolving commonjs request", + }, +] +`; + +exports[`next.rs api should allow to write pages edge page to disk: diagnostics 1`] = `Array []`; + +exports[`next.rs api should allow to write pages edge page to disk: issues 1`] = `Array []`; + +exports[`next.rs api should detect the correct routes: diagnostics 1`] = ` +Array [ + Object { + "category": "NextFeatureTelemetry_category_tbd", + "name": "EVENT_BUILD_FEATURE_USAGE", + "payload": Object { + "modularizeImports": "true", + }, + }, + Object { + "category": "NextFeatureTelemetry_category_tbd", + "name": "EVENT_BUILD_FEATURE_USAGE", + "payload": Object { + "skipMiddlewareUrlNormalize": "false", + }, + }, + Object { + "category": "NextFeatureTelemetry_category_tbd", + "name": "EVENT_BUILD_FEATURE_USAGE", + "payload": Object { + "skipTrailingSlashRedirect": "false", + }, + }, + Object { + "category": "NextFeatureTelemetry_category_tbd", + "name": "EVENT_BUILD_FEATURE_USAGE", + "payload": Object { + "swcEmotion": "false", + }, + }, + Object { + "category": "NextFeatureTelemetry_category_tbd", + "name": "EVENT_BUILD_FEATURE_USAGE", + "payload": Object { + "swcExperimentalDecorators": "false", + }, + }, + Object { + "category": "NextFeatureTelemetry_category_tbd", + "name": "EVENT_BUILD_FEATURE_USAGE", + "payload": Object { + "swcImportSource": "false", + }, + }, + Object { + "category": "NextFeatureTelemetry_category_tbd", + "name": "EVENT_BUILD_FEATURE_USAGE", + "payload": Object { + "swcMinify": "true", + }, + }, + Object { + "category": "NextFeatureTelemetry_category_tbd", + "name": "EVENT_BUILD_FEATURE_USAGE", + "payload": Object { + "swcReactRemoveProperties": "false", + }, + }, + Object { + "category": "NextFeatureTelemetry_category_tbd", + "name": "EVENT_BUILD_FEATURE_USAGE", + "payload": Object { + "swcRelay": "false", + }, + }, + Object { + "category": "NextFeatureTelemetry_category_tbd", + "name": "EVENT_BUILD_FEATURE_USAGE", + "payload": Object { + "swcRemoveConsole": "false", + }, + }, + Object { + "category": "NextFeatureTelemetry_category_tbd", + "name": "EVENT_BUILD_FEATURE_USAGE", + "payload": Object { + "swcStyledComponents": "false", + }, + }, + Object { + "category": "NextFeatureTelemetry_category_tbd", + "name": "EVENT_BUILD_FEATURE_USAGE", + "payload": Object { + "transpilePackages": "false", + }, + }, + Object { + "category": "NextFeatureTelemetry_category_tbd", + "name": "EVENT_BUILD_FEATURE_USAGE", + "payload": Object { + "turbotrace": "false", + }, + }, + Object { + "category": "NextFeatureTelemetry_category_tbd", + "name": "EVENT_BUILD_FEATURE_USAGE", + "payload": Object { + "x86_64-unknown-linux-gnu": "true", + }, + }, +] +`; + +exports[`next.rs api should detect the correct routes: issues 1`] = `Array []`; diff --git a/test/unit/next-api.test.ts b/test/unit/next-api.test.ts new file mode 100644 index 0000000000000..c12b45412e6fb --- /dev/null +++ b/test/unit/next-api.test.ts @@ -0,0 +1,256 @@ +import { NextInstance, createNext } from 'e2e-utils' +import { trace } from 'next/src/trace' +import { PHASE_DEVELOPMENT_SERVER } from 'next/constants' +import { + Diagnostics, + Entrypoints, + Issue, + loadBindings, + Project, + TurbopackResult, +} from 'next/src/build/swc' +import loadConfig from 'next/src/server/config' +import path from 'path' + +function normalizePath(path: string) { + return path + .replace(/\[project\].+\/node_modules\//g, '[project]/.../node_modules/') + .replace( + /\[project\]\/packages\/next\//g, + '[project]/.../node_modules/next/' + ) +} + +function normalizeIssues(issues: Issue[]) { + return issues + .map((issue) => ({ + ...issue, + detail: issue.detail && normalizePath(issue.detail), + filePath: issue.filePath && normalizePath(issue.filePath), + source: issue.source && { + ...issue.source, + source: normalizePath(issue.source.source.ident), + }, + })) + .sort((a, b) => { + const a_ = JSON.stringify(a) + const b_ = JSON.stringify(b) + if (a_ < b_) return -1 + if (a_ > b_) return 1 + return 0 + }) +} + +function normalizeDiagnostics(diagnostics: Diagnostics[]) { + return diagnostics.sort((a, b) => { + const a_ = JSON.stringify(a) + const b_ = JSON.stringify(b) + if (a_ < b_) return -1 + if (a_ > b_) return 1 + return 0 + }) +} + +describe('next.rs api', () => { + let next: NextInstance + beforeAll(async () => { + await trace('setup next instance').traceAsyncFn(async (rootSpan) => { + next = await createNext({ + skipStart: true, + files: { + 'pages/index.js': 'export default () =>
hello world
', + 'pages/page-nodejs.js': 'export default () =>
hello world
', + 'pages/page-edge.js': + 'export default () =>
hello world
\nexport const config = { runtime: "experimental-edge" }', + 'pages/api/nodejs.js': + 'export default () => Response.json({ hello: "world" })', + 'pages/api/edge.js': + 'export default () => Response.json({ hello: "world" })\nexport const config = { runtime: "edge" }', + 'app/layout.ts': + 'export default function RootLayout({ children }: { children: any }) { return ({children})}', + 'app/loading.ts': + 'export default function Loading() { return <>Loading }', + // 'app/app-edge/page.ts': 'export default () =>
hello world
\nexport const runtime = "edge"', + 'app/app-nodejs/page.ts': + 'export default () =>
hello world
', + 'app/route-nodejs/route.ts': + 'export function GET() { return Response.json({ hello: "world" }) }', + 'app/route-edge/route.ts': + 'export function GET() { return Response.json({ hello: "world" }) }\nexport const runtime = "edge"', + }, + }) + }) + }) + afterAll(() => next.destroy()) + + let project: Project + beforeAll(async () => { + console.log(next.testDir) + const nextConfig = await loadConfig(PHASE_DEVELOPMENT_SERVER, next.testDir) + const bindings = await loadBindings() + project = await bindings.turbo.createProject({ + env: {}, + jsConfig: { + compilerOptions: {}, + }, + nextConfig: nextConfig, + projectPath: next.testDir, + rootPath: process.env.NEXT_SKIP_ISOLATE + ? path.resolve(__dirname, '../..') + : next.testDir, + watch: true, + }) + }) + + it('should detect the correct routes', async () => { + const entrypointsSubscribtion = project.entrypointsSubscribe() + const entrypoints = await entrypointsSubscribtion.next() + expect(entrypoints.done).toBe(false) + expect(Array.from(entrypoints.value.routes.keys()).sort()).toEqual([ + '/', + '/api/edge', + '/api/nodejs', + // TODO app edge pages are not supported yet + // '/app-edge', + '/app-nodejs', + '/page-edge', + '/page-nodejs', + '/route-edge', + '/route-nodejs', + ]) + expect(normalizeIssues(entrypoints.value.issues)).toMatchSnapshot('issues') + expect(normalizeDiagnostics(entrypoints.value.diagnostics)).toMatchSnapshot( + 'diagnostics' + ) + entrypointsSubscribtion.return() + }) + + const routes = [ + { + name: 'pages edge api', + path: '/api/edge', + type: 'page-api', + runtime: 'edge', + config: {}, + }, + { + name: 'pages Node.js api', + path: '/api/nodejs', + type: 'page-api', + runtime: 'nodejs', + config: {}, + }, + // TODO app edge pages are not supported yet + // { + // name: 'app edge page', + // path: '/app-edge', + // type: 'app-page', + // runtime: 'edge', + // config: {}, + // }, + { + name: 'app Node.js page', + path: '/app-nodejs', + type: 'app-page', + runtime: 'nodejs', + config: {}, + }, + { + name: 'pages edge page', + path: '/page-edge', + type: 'page', + runtime: 'edge', + config: {}, + }, + { + name: 'pages Node.js page', + path: '/page-nodejs', + type: 'page', + runtime: 'nodejs', + config: {}, + }, + { + name: 'app edge route', + path: '/route-edge', + type: 'app-route', + runtime: 'edge', + config: {}, + }, + { + name: 'app Node.js route', + path: '/route-nodejs', + type: 'app-route', + runtime: 'nodejs', + config: {}, + }, + ] + for (const { name, path, type, runtime, config } of routes) { + // eslint-disable-next-line no-loop-func + it(`should allow to write ${name} to disk`, async () => { + const entrypointsSubscribtion = project.entrypointsSubscribe() + const entrypoints: TurbopackResult = ( + await entrypointsSubscribtion.next() + ).value + const route = entrypoints.routes.get(path) + entrypointsSubscribtion.return() + + expect(route.type).toBe(type) + + switch (route.type) { + case 'page-api': + case 'app-route': { + const result = await route.endpoint.writeToDisk() + expect(result.type).toBe(runtime) + expect(result.config).toEqual(config) + expect(normalizeIssues(result.issues)).toMatchSnapshot('issues') + expect(normalizeDiagnostics(result.diagnostics)).toMatchSnapshot( + 'diagnostics' + ) + break + } + case 'page': { + const result = await route.htmlEndpoint.writeToDisk() + expect(result.type).toBe(runtime) + expect(result.config).toEqual(config) + expect(normalizeIssues(result.issues)).toMatchSnapshot('issues') + expect(normalizeDiagnostics(result.diagnostics)).toMatchSnapshot( + 'diagnostics' + ) + + // TODO This crashes + // const result2 = await route.dataEndpoint.writeToDisk() + // expect(result2.type).toBe(runtime) + // expect(result2.config).toEqual(config) + // expect(normalizeIssues(result2.issues)).toMatchSnapshot('data issues') + // expect(normalizeDiagnostics(result2.diagnostics)).toMatchSnapshot( + // 'data diagnostics' + // ) + break + } + case 'app-page': { + const result = await route.htmlEndpoint.writeToDisk() + expect(result.type).toBe(runtime) + expect(result.config).toEqual(config) + expect(normalizeIssues(result.issues)).toMatchSnapshot('issues') + expect(normalizeDiagnostics(result.diagnostics)).toMatchSnapshot( + 'diagnostics' + ) + + const result2 = await route.rscEndpoint.writeToDisk() + expect(result2.type).toBe(runtime) + expect(result2.config).toEqual(config) + expect(normalizeIssues(result2.issues)).toMatchSnapshot('rsc issues') + expect(normalizeDiagnostics(result2.diagnostics)).toMatchSnapshot( + 'rsc diagnostics' + ) + + break + } + default: { + throw new Error('unknown route type') + break + } + } + }) + } +}) From 4b783490489e4f617b1c72c6c96cbcfe506ab1ce Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Mon, 7 Aug 2023 21:15:18 +0200 Subject: [PATCH 005/387] fix azure test cases (#53692) --- azure-pipelines.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ddb9b975fb8cf..f6669b8f94d05 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -108,6 +108,8 @@ stages: - script: node run-tests.js --type unit condition: eq(variables['isDocsOnly'], 'No') displayName: 'Run tests' + env: + NEXT_TEST_MODE: 'dev' # - job: test_e2e_dev # pool: From 033732a3e50a4c6eb25b8d7bc4e268812df6d6e9 Mon Sep 17 00:00:00 2001 From: Janicklas Ralph Date: Mon, 7 Aug 2023 12:38:13 -0700 Subject: [PATCH 006/387] Adding GoogleMaps and Youtube embed components (#52909) Adding GoogleMapsEmbed and YoutubeEmber components into `@next/third-parties` Adding tests and README Each component is tagged with `data-ntpc` attribute cc: @kara @housseindjirdeh @huozhi @gnoff Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com> --- packages/third-parties/README.md | 43 +++++++++++++++++++ packages/third-parties/package.json | 8 +++- .../scripts/update-third-parties.js | 20 ++++++--- .../{base.tsx => ThirdPartyScriptEmbed.tsx} | 19 +++++--- packages/third-parties/src/google/index.tsx | 40 +++++++++++++++++ packages/third-parties/src/types/google.ts | 17 ++++++++ packages/third-parties/tsconfig.json | 11 ++--- .../app/google-maps-embed/page.js | 18 ++++++++ test/e2e/app-dir/third-parties/app/layout.tsx | 7 +++ .../third-parties/app/youtube-embed/page.js | 12 ++++++ test/e2e/app-dir/third-parties/basic.test.ts | 32 ++++++++++++++ test/e2e/third-parties/index.test.ts | 32 ++++++++++++++ .../third-parties/pages/google-maps-embed.js | 18 ++++++++ test/e2e/third-parties/pages/youtube-embed.js | 12 ++++++ 14 files changed, 267 insertions(+), 22 deletions(-) create mode 100644 packages/third-parties/README.md rename packages/third-parties/src/{base.tsx => ThirdPartyScriptEmbed.tsx} (77%) create mode 100644 packages/third-parties/src/google/index.tsx create mode 100644 packages/third-parties/src/types/google.ts create mode 100644 test/e2e/app-dir/third-parties/app/google-maps-embed/page.js create mode 100644 test/e2e/app-dir/third-parties/app/layout.tsx create mode 100644 test/e2e/app-dir/third-parties/app/youtube-embed/page.js create mode 100644 test/e2e/app-dir/third-parties/basic.test.ts create mode 100644 test/e2e/third-parties/index.test.ts create mode 100644 test/e2e/third-parties/pages/google-maps-embed.js create mode 100644 test/e2e/third-parties/pages/youtube-embed.js diff --git a/packages/third-parties/README.md b/packages/third-parties/README.md new file mode 100644 index 0000000000000..77cffc8b0c26b --- /dev/null +++ b/packages/third-parties/README.md @@ -0,0 +1,43 @@ +# Experimental `@next/third-parties` + +`@next/third-parties` is a collection of components and utilities that can be used to efficiently load third-party libraries into your Next.js application. + +> Note: `@next/third-parties` is still experimental and under active development. + +# Usage + +## Google Third-Parties + +### YouTube Embed + +The `YoutubeEmbed` component is used to load and display a YouTube embed. This component loads faster by using [lite-youtube-embed](https://github.com/paulirish/lite-youtube-embed) under the hood. + +```js +import { YoutubeEmbed } from '@next/third-parties/google' + +export default function Page() { + return +} +``` + +### Google Maps Embed + +The `GoogleMapsEmbed` component can be used to add a [Google Maps Embed](https://developers.google.com/maps/documentation/embed/get-started) to your page. By default, it uses the `loading` attribute to lazy-load below the fold. + +```js +import { GoogleMapsEmbed } from '@next/third-parties/google' + +export default function Page() { + return ( + + ) +} +``` + +To get a better idea of how these components work, take a look at this [demo](https://test-next-script-housseindjirdeh.vercel.app/). diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index 9c1d601556c41..0fd756a5c0035 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -6,13 +6,17 @@ "url": "vercel/next.js", "directory": "packages/third-parties" }, - "main": "dist/index.js", + "exports": { + "./google": "./dist/google/index.js" + }, "files": [ "dist" ], "license": "MIT", "scripts": { - "manual-build": "rm -rf dist && tsc -d -p tsconfig.json && node scripts/update-third-parties", + "build": "rm -rf dist && tsc -d -p tsconfig.json", + "prepublishOnly": "cd ../../ && turbo run build", + "updateThirdParties": "rm -rf src/**/index.tsx && node scripts/update-third-parties", "dev": "tsc -d -w -p tsconfig.json", "typescript": "tsec --noEmit -p tsconfig.json" }, diff --git a/packages/third-parties/scripts/update-third-parties.js b/packages/third-parties/scripts/update-third-parties.js index 154c61e4d323a..5f7ed1b30e571 100644 --- a/packages/third-parties/scripts/update-third-parties.js +++ b/packages/third-parties/scripts/update-third-parties.js @@ -23,7 +23,11 @@ function generateComponent(thirdParty) { let props = '' if (stylesheets?.length > 0) { - props += ` stylesheets={${JSON.stringify(stylesheets)}}` + // TODO: Remove ts-ignore after new - - ` - ) - res.end() -} diff --git a/examples/cms-contentful/pages/api/revalidate.js b/examples/cms-contentful/pages/api/revalidate.js deleted file mode 100644 index fd5a7078d37f8..0000000000000 --- a/examples/cms-contentful/pages/api/revalidate.js +++ /dev/null @@ -1,31 +0,0 @@ -// pages/api/revalidate.js - -export default async function handler(req, res) { - // should be secret, custom header coming in from Contentful - let inboundRevalToken = req.headers['x-vercel-reval-key'] - - // Check for secret to confirm this is a valid request - if (!inboundRevalToken) { - return res - .status(401) - .json({ message: 'x-vercel-reval-key header not defined' }) - } else if (inboundRevalToken !== process.env.CONTENTFUL_REVALIDATE_SECRET) { - return res.status(401).json({ message: 'Invalid token' }) - } - - try { - // Note: if this fails to parse you may have forget to set the - // "content-type" header correctly as mentioned here https://github.com/vercel/next.js/blob/canary/examples/cms-contentful/README.md#step-9-try-using-on-demand-revalidation - let postSlug = req.body.fields.slug['en-US'] - - // revalidate the individual post and the home page - await res.revalidate(`/posts/${postSlug}`) - await res.revalidate('/') - - return res.json({ revalidated: true }) - } catch (err) { - // If there was an error, Next.js will continue - // to show the last successfully generated page - return res.status(500).send('Error revalidating') - } -} diff --git a/examples/cms-contentful/pages/index.js b/examples/cms-contentful/pages/index.js deleted file mode 100644 index 5e93ffefb73ad..0000000000000 --- a/examples/cms-contentful/pages/index.js +++ /dev/null @@ -1,43 +0,0 @@ -import Container from '../components/container' -import MoreStories from '../components/more-stories' -import HeroPost from '../components/hero-post' -import Intro from '../components/intro' -import Layout from '../components/layout' -import { getAllPostsForHome } from '../lib/api' -import Head from 'next/head' -import { CMS_NAME } from '../lib/constants' - -export default function Index({ preview, allPosts }) { - const heroPost = allPosts[0] - const morePosts = allPosts.slice(1) - return ( - <> - - - {`Next.js Blog Example with ${CMS_NAME}`} - - - - {heroPost && ( - - )} - {morePosts.length > 0 && } - - - - ) -} - -export async function getStaticProps({ preview = false }) { - const allPosts = (await getAllPostsForHome(preview)) ?? [] - return { - props: { preview, allPosts }, - } -} diff --git a/examples/cms-contentful/pages/posts/[slug].js b/examples/cms-contentful/pages/posts/[slug].js deleted file mode 100644 index 344deee1b4217..0000000000000 --- a/examples/cms-contentful/pages/posts/[slug].js +++ /dev/null @@ -1,74 +0,0 @@ -import { useRouter } from 'next/router' -import Head from 'next/head' -import ErrorPage from 'next/error' -import Container from '../../components/container' -import PostBody from '../../components/post-body' -import MoreStories from '../../components/more-stories' -import Header from '../../components/header' -import PostHeader from '../../components/post-header' -import SectionSeparator from '../../components/section-separator' -import Layout from '../../components/layout' -import { getAllPostsWithSlug, getPostAndMorePosts } from '../../lib/api' -import PostTitle from '../../components/post-title' -import { CMS_NAME } from '../../lib/constants' - -export default function Post({ post, morePosts, preview }) { - const router = useRouter() - - if (!router.isFallback && !post) { - return - } - - return ( - - -
- {router.isFallback ? ( - Loading… - ) : ( - <> -
- - - {`${post.title} | Next.js Blog Example with ${CMS_NAME}`} - - - - - -
- - {morePosts && morePosts.length > 0 && ( - - )} - - )} - - - ) -} - -export async function getStaticProps({ params, preview = false }) { - const data = await getPostAndMorePosts(params.slug, preview) - - return { - props: { - preview, - post: data?.post ?? null, - morePosts: data?.morePosts ?? null, - }, - } -} - -export async function getStaticPaths() { - const allPosts = await getAllPostsWithSlug() - return { - paths: allPosts?.map(({ slug }) => `/posts/${slug}`) ?? [], - fallback: true, - } -} diff --git a/examples/cms-contentful/postcss.config.js b/examples/cms-contentful/postcss.config.js index 3fa0a9514dc9d..33ad091d26d8a 100644 --- a/examples/cms-contentful/postcss.config.js +++ b/examples/cms-contentful/postcss.config.js @@ -1,5 +1,3 @@ -// If you want to use other PostCSS plugins, see the following: -// https://tailwindcss.com/docs/using-with-preprocessors module.exports = { plugins: { tailwindcss: {}, diff --git a/examples/cms-contentful/public/favicon/android-chrome-192x192.png b/examples/cms-contentful/public/favicon/android-chrome-192x192.png deleted file mode 100644 index 2f07282a59cdadaf579b129d650f588d89bef63e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4795 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 diff --git a/examples/cms-contentful/public/favicon/android-chrome-512x512.png b/examples/cms-contentful/public/favicon/android-chrome-512x512.png deleted file mode 100644 index dbb0faea84049b991417262cb8f14ae08201c443..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14640 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 MboFyt=akR{06}d?wEzGB diff --git a/examples/cms-contentful/public/favicon/browserconfig.xml b/examples/cms-contentful/public/favicon/browserconfig.xml deleted file mode 100644 index 9824d87b11517..0000000000000 --- a/examples/cms-contentful/public/favicon/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - #000000 - - - diff --git a/examples/cms-contentful/public/favicon/favicon-16x16.png b/examples/cms-contentful/public/favicon/favicon-16x16.png deleted file mode 100644 index 29deaf6716e7744b24b8d2f94443b50d3acffe68..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 595 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6SkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YDR+ueoXe|!I#{Xiaj ziKnkC`y&=<4t4b@G6n~MLO(oR978nD=T6+{bvQtxb^jfYOS`%RMA#j5UMSyS|6pbD zi;d6k9bZ#^z+(Rks>-)2dl_&J^V`G?Q{pEo#nr8;@~ z`krL*_G;*v!^IjrakA;BEvmm}T6sI&)8w8wxgoVPV2$ltj)nWqZSPSP<9cp)#pk-y z3!nU3->tb0Uyz+-@SssyAx-@8dugfqY?qVb-(9v$nAvu{N21_p{^`Vs3aO80xy!%& z$oZzjQ>@>dc#PfLX7k6kor!0-63$vnKbR`q7inF2)PmzwPWtVW3*#8c~vxSdwa$T$Bo=7>o=IEp!b` zbdAhI3@ogSErCeez{twLptqKz2}MJ0eoAIqC2kFW6gJoaHH24%M3e+2mMat#<)>xl zq$-qD7Nja<7L+72FjUNW{E3I7Fib<^l>g~7o=<}qn3cKplDUPIg}o<>FbgZVG?*Mt wVOHK8qHy}gl@mwK9FaM~e!9V9ftMb`D{;Y+Pfn&&fmSehy85}Sb4q9e02)x*r~m)} diff --git a/examples/cms-contentful/public/favicon/favicon-32x32.png b/examples/cms-contentful/public/favicon/favicon-32x32.png deleted file mode 100644 index e3b4277bf093d204f4088ba46590bc9e6f37bf67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 880 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f+@+{-G$+Qd;gjJKpuOE zr>`sfBNk~6b@eGS1_y!iqMj~}As)x)PCc6)94K(Key6_H)~zdg8;|I@2rLO(;bG?z zv&88`{sbpi5kZkx_Z~U92;G^MrtDPSr1Z{Z$_>33u1k7BO}vhVOI&wNh%niD+%@X$ zZ@)Q_cPr<7PJ1`kQuttQpkKj6ndP1vjjEhOWtlh|D8oV>EHcW z2`<~itr`hdERnPRyIxS?X6yERwEJ~uLB~SbPr1@5O?#Bx=6LEzEKE!9dp*_eMU(Ed zOXYj7Gp6&Iy74^bx!uWbaa8i1;AYbmu2VWbzipE=f6`I4{V1>b0b5hc!z--i9*Rrf zc~&FA*drwr$HMEyV|ezVK~u}Z``UW*K1{N?ewgD|Z@JAOtM4m~kM0y;b3Sd&eAu1q z$c$$R0{c=ke$>x>Y&m@gTTSxW-@a09s_U6r8pQY`9`a?bI&&PJ`$1|x1z#mL#e9}@HJ8bK zzir;N-}0T6{L6#yK6nF6GpZ%75hW>!C8<`)MX5lF!N|bSLf61V*T_7?z{1Md5{R@7 zjI0a{dTTkFP&DM`r(~v8;@0p-VS^n|LwHq4L`hI$xk5ovep+TuszOO+L8?M#K}j+L zL&coOpLjS5!!$Hb`JX=H`80@uS(#fenOj&{*n6@Fv#^3ogUR6(X64Nx3a4*eIdSC7 j5t$?GryD#LcNSTKFL46_E}iL5mO)AEZS| zhz??kp>#q_Pa+X9S9!)q#7v>f>h;_1zJ2P{J?EY~6zP?$I_K=Y*ZTkWKIiUX?Ol$O z;bc0!dO3Q{aq`JLhPRsE;xZ>o<~Kd63*DEFk1M;XdugYEZ_Oa-Nh{-%0D^{MKAR3)TR78`8wfiHZP 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 diff --git a/examples/cms-contentful/public/favicon/mstile-150x150.png b/examples/cms-contentful/public/favicon/mstile-150x150.png deleted file mode 100644 index f2dfd904bf1be62be8351fcbc60a1028269923f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3567 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 AfdBvi diff --git a/examples/cms-contentful/public/favicon/safari-pinned-tab.svg b/examples/cms-contentful/public/favicon/safari-pinned-tab.svg deleted file mode 100644 index 72ab6e050cb11..0000000000000 --- a/examples/cms-contentful/public/favicon/safari-pinned-tab.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - -Created by potrace 1.11, written by Peter Selinger 2001-2013 - - - - - - - - - - diff --git a/examples/cms-contentful/public/favicon/site.webmanifest b/examples/cms-contentful/public/favicon/site.webmanifest deleted file mode 100644 index a672d9a233c59..0000000000000 --- a/examples/cms-contentful/public/favicon/site.webmanifest +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "Next.js", - "short_name": "Next.js", - "icons": [ - { - "src": "/favicons/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/favicons/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#000000", - "background_color": "#000000", - "display": "standalone" -} diff --git a/examples/cms-contentful/tailwind.config.js b/examples/cms-contentful/tailwind.config.js deleted file mode 100644 index ea0efae6d75f5..0000000000000 --- a/examples/cms-contentful/tailwind.config.js +++ /dev/null @@ -1,38 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: [ - './pages/**/*.{js,ts,jsx,tsx}', - './components/**/*.{js,ts,jsx,tsx}', - ], - theme: { - extend: { - colors: { - 'accent-1': '#FAFAFA', - 'accent-2': '#EAEAEA', - 'accent-7': '#333', - success: '#0070f3', - cyan: '#79FFE1', - }, - spacing: { - 28: '7rem', - }, - letterSpacing: { - tighter: '-.04em', - }, - lineHeight: { - tight: 1.2, - }, - fontSize: { - '5xl': '2.5rem', - '6xl': '2.75rem', - '7xl': '4.5rem', - '8xl': '6.25rem', - }, - boxShadow: { - small: '0 5px 10px rgba(0, 0, 0, 0.12)', - medium: '0 8px 30px rgba(0, 0, 0, 0.12)', - }, - }, - }, - plugins: [], -} diff --git a/examples/cms-contentful/tailwind.config.ts b/examples/cms-contentful/tailwind.config.ts new file mode 100644 index 0000000000000..163165c9be978 --- /dev/null +++ b/examples/cms-contentful/tailwind.config.ts @@ -0,0 +1,21 @@ +import type { Config } from 'tailwindcss' +import typography from '@tailwindcss/typography' + +export default { + content: [ + './app/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './lib/**/*.{ts,tsx}', + ], + theme: { + extend: { + fontFamily: { + sans: ['var(--font-inter)'], + }, + }, + }, + future: { + hoverOnlyWhenSupported: true, + }, + plugins: [typography], +} satisfies Config diff --git a/examples/cms-contentful/tsconfig.json b/examples/cms-contentful/tsconfig.json new file mode 100644 index 0000000000000..e06a4454ab062 --- /dev/null +++ b/examples/cms-contentful/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/cms-contentful/types/author.json b/examples/cms-contentful/types/author.json deleted file mode 100644 index c9659d4b3aa33..0000000000000 --- a/examples/cms-contentful/types/author.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "Main": { - "name": { - "type": "Text", - "config": { - "label": "name" - } - }, - "picture": { - "type": "Image", - "config": { - "constraint": {}, - "thumbnails": [], - "label": "picture" - } - } - } -} diff --git a/examples/cms-contentful/types/post.json b/examples/cms-contentful/types/post.json deleted file mode 100644 index 303fe0eeafc22..0000000000000 --- a/examples/cms-contentful/types/post.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "Main": { - "title": { - "type": "StructuredText", - "config": { - "single": "heading1, heading2, heading3, heading4, heading5, heading6", - "label": "title" - } - }, - "content": { - "type": "StructuredText", - "config": { - "multi": "paragraph, preformatted, heading1, heading2, heading3, heading4, heading5, heading6, strong, em, hyperlink, image, embed, list-item, o-list-item, o-list-item", - "label": "content" - } - }, - "excerpt": { - "type": "Text", - "config": { - "label": "excerpt" - } - }, - "coverimage": { - "type": "Image", - "config": { - "constraint": {}, - "thumbnails": [], - "label": "coverimage" - } - }, - "date": { - "type": "Date", - "config": { - "label": "date" - } - }, - "author": { - "type": "Link", - "config": { - "select": "document", - "customtypes": ["author"], - "label": "author" - } - }, - "uid": { - "type": "UID", - "config": { - "label": "slug" - } - } - } -} From 6773ea86362b4dd0588ecab27beda0e2723fbbf7 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 21 Aug 2023 10:45:00 -0400 Subject: [PATCH 178/387] fix: improve error message when `output: export` in app router (#54202) There are a few cases that are not handled by App Router when using `output: export` config. A few of them are expected, but some are yet to be implemented. Regardless of the intent of the future, this PR ensures the error messages match what was documented to be [unsupported](https://nextjs.org/docs/app/building-your-application/deploying/static-exports#unsupported-features). - Documentation: https://github.com/vercel/next.js/pull/53592 - Issue: #48022 --- .../build/analysis/get-page-static-info.ts | 47 +++++++++++++++++-- packages/next/src/build/index.ts | 10 ++++ packages/next/src/build/utils.ts | 26 ++++++---- .../webpack/loaders/get-module-build-info.ts | 1 + packages/next/src/server/base-server.ts | 18 +++++++ .../app-dir-export/app/client/[slug]/page.js | 15 ------ .../app-dir-export/app/client/page.js | 9 ++++ .../test/dynamic-missing-gsp-dev.test.ts | 21 +++++++++ .../test/dynamic-missing-gsp-prod.test.ts | 21 +++++++++ test/integration/app-dir-export/test/utils.ts | 11 +++++ test/lib/next-test-utils.ts | 5 ++ 11 files changed, 154 insertions(+), 30 deletions(-) delete mode 100644 test/integration/app-dir-export/app/client/[slug]/page.js create mode 100644 test/integration/app-dir-export/app/client/page.js create mode 100644 test/integration/app-dir-export/test/dynamic-missing-gsp-dev.test.ts create mode 100644 test/integration/app-dir-export/test/dynamic-missing-gsp-prod.test.ts diff --git a/packages/next/src/build/analysis/get-page-static-info.ts b/packages/next/src/build/analysis/get-page-static-info.ts index d67b6d60bd466..63699cd8f8b4b 100644 --- a/packages/next/src/build/analysis/get-page-static-info.ts +++ b/packages/next/src/build/analysis/get-page-static-info.ts @@ -43,6 +43,7 @@ export interface PageStaticInfo { ssg?: boolean ssr?: boolean rsc?: RSCModuleType + generateStaticParams?: boolean middleware?: MiddlewareConfig amp?: boolean | 'hybrid' extraConfig?: Record @@ -74,7 +75,15 @@ export function getRSCModuleInformation( const clientEntryType = clientInfoMatch?.[2] as 'cjs' | 'auto' const type = clientRefs ? RSC_MODULE_TYPES.client : RSC_MODULE_TYPES.server - return { type, actions, clientRefs, clientEntryType, isClientRef } + const hasUseClientDirective = /^\s*['"]use client['"]/.test(source) + return { + type, + actions, + clientRefs, + clientEntryType, + isClientRef, + hasUseClientDirective, + } } const warnedInvalidValueMap = { @@ -113,6 +122,7 @@ function checkExports( preferredRegion?: string | string[] generateImageMetadata?: boolean generateSitemaps?: boolean + generateStaticParams: boolean extraProperties?: Set } { const exportsSet = new Set([ @@ -120,6 +130,7 @@ function checkExports( 'getServerSideProps', 'generateImageMetadata', 'generateSitemaps', + 'generateStaticParams', ]) if (Array.isArray(swcAST?.body)) { try { @@ -129,6 +140,7 @@ function checkExports( let ssg: boolean = false let generateImageMetadata: boolean = false let generateSitemaps: boolean = false + let generateStaticParams = false let extraProperties = new Set() for (const node of swcAST.body) { @@ -169,6 +181,7 @@ function checkExports( ssr = id === 'getServerSideProps' generateImageMetadata = id === 'generateImageMetadata' generateSitemaps = id === 'generateSitemaps' + generateStaticParams = id === 'generateStaticParams' } if ( @@ -181,6 +194,7 @@ function checkExports( ssr = id === 'getServerSideProps' generateImageMetadata = id === 'generateImageMetadata' generateSitemaps = id === 'generateSitemaps' + generateStaticParams = id === 'generateStaticParams' } } @@ -199,6 +213,8 @@ function checkExports( generateImageMetadata = true if (!generateSitemaps && value === 'generateSitemaps') generateSitemaps = true + if (!generateStaticParams && value === 'generateStaticParams') + generateStaticParams = true if (!runtime && value === 'runtime') warnInvalidValue( pageFilePath, @@ -222,6 +238,7 @@ function checkExports( preferredRegion, generateImageMetadata, generateSitemaps, + generateStaticParams, extraProperties, } } catch (err) {} @@ -234,6 +251,7 @@ function checkExports( preferredRegion: undefined, generateImageMetadata: false, generateSitemaps: false, + generateStaticParams: false, extraProperties: undefined, } } @@ -437,14 +455,21 @@ export async function getPageStaticInfo(params: { const fileContent = (await tryToReadFile(pageFilePath, !isDev)) || '' if ( - /runtime|preferredRegion|getStaticProps|getServerSideProps|export const/.test( + /runtime|preferredRegion|getStaticProps|getServerSideProps|generateStaticParams|export const/.test( fileContent ) ) { const swcAST = await parseModule(pageFilePath, fileContent) - const { ssg, ssr, runtime, preferredRegion, extraProperties } = - checkExports(swcAST, pageFilePath) - const rsc = getRSCModuleInformation(fileContent).type + const { + ssg, + ssr, + runtime, + preferredRegion, + generateStaticParams, + extraProperties, + } = checkExports(swcAST, pageFilePath) + const rscInfo = getRSCModuleInformation(fileContent) + const rsc = rscInfo.type // default / failsafe value for config let config: any @@ -548,10 +573,21 @@ export async function getPageStaticInfo(params: { nextConfig ) + if ( + pageType === 'app' && + rscInfo.hasUseClientDirective && + generateStaticParams + ) { + throw new Error( + `Page "${page}" cannot use both "use client" and export function "generateStaticParams()".` + ) + } + return { ssr, ssg, rsc, + generateStaticParams, amp: config.amp || false, ...(middlewareConfig && { middleware: middlewareConfig }), ...(resolvedRuntime && { runtime: resolvedRuntime }), @@ -564,6 +600,7 @@ export async function getPageStaticInfo(params: { ssr: false, ssg: false, rsc: RSC_MODULE_TYPES.server, + generateStaticParams: false, amp: false, runtime: undefined, } diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index b7f62f93d4f91..a5d666ab6538f 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -1612,6 +1612,16 @@ export default async function build( const hasGenerateStaticParams = !!workerResult.prerenderRoutes?.length + if ( + config.output === 'export' && + isDynamic && + !hasGenerateStaticParams + ) { + throw new Error( + `Page "${page}" is missing "generateStaticParams()" so it cannot be used with "output: export" config.` + ) + } + if ( // Mark the app as static if: // - It has no dynamic param diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 9cde31aec24f1..a263c11a070b1 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -1138,25 +1138,31 @@ export const collectGenerateParams = async ( ? segment[2]?.layout?.[0]?.() : segment[2]?.page?.[0]?.()) const config = collectAppConfig(mod) - + const page: string | undefined = segment[0] const isClientComponent = isClientReference(mod) - const isDynamicSegment = segment[0] && /^\[.+\]$/.test(segment[0]) + const isDynamicSegment = /^\[.+\]$/.test(page || '') + const { generateStaticParams, getStaticPaths } = mod || {} + + //console.log({parentSegments, page, isDynamicSegment, isClientComponent, generateStaticParams}) + if (isDynamicSegment && isClientComponent && generateStaticParams) { + throw new Error( + `Page "${page}" cannot export "generateStaticParams()" because it is a client component` + ) + } const result = { isLayout, isDynamicSegment, segmentPath: `/${parentSegments.join('/')}${ - segment[0] && parentSegments.length > 0 ? '/' : '' - }${segment[0]}`, + page && parentSegments.length > 0 ? '/' : '' + }${page}`, config, - getStaticPaths: isClientComponent ? undefined : mod?.getStaticPaths, - generateStaticParams: isClientComponent - ? undefined - : mod?.generateStaticParams, + getStaticPaths: isClientComponent ? undefined : getStaticPaths, + generateStaticParams: isClientComponent ? undefined : generateStaticParams, } - if (segment[0]) { - parentSegments.push(segment[0]) + if (page) { + parentSegments.push(page) } if (result.config || result.generateStaticParams || result.getStaticPaths) { diff --git a/packages/next/src/build/webpack/loaders/get-module-build-info.ts b/packages/next/src/build/webpack/loaders/get-module-build-info.ts index f0ca52bf2f864..e67d061db736f 100644 --- a/packages/next/src/build/webpack/loaders/get-module-build-info.ts +++ b/packages/next/src/build/webpack/loaders/get-module-build-info.ts @@ -32,6 +32,7 @@ export interface RSCMeta { clientRefs?: string[] clientEntryType?: 'cjs' | 'auto' isClientRef?: boolean + hasUseClientDirective?: boolean requests?: string[] // client requests in flight client entry } diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 7a3d9caaedefa..6a6febecd39f0 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -1456,6 +1456,24 @@ export default abstract class Server { const hasFallback = typeof fallbackMode !== 'undefined' + if (this.nextConfig.output === 'export') { + const page = components.pathname + const isDynamic = isDynamicRoute(page) + if (isDynamic) { + if (fallbackMode !== 'static') { + throw new Error( + `Page "${page}" is missing exported function "generateStaticParams()", which is required with "output: export" config.` + ) + } + const resolvedWithoutSlash = removeTrailingSlash(resolvedUrlPathname) + if (!staticPaths?.includes(resolvedWithoutSlash)) { + throw new Error( + `Page "${page}" is missing param "${resolvedWithoutSlash}" in "generateStaticParams()", which is required with "output: export" config.` + ) + } + } + } + if (hasFallback) { hasStaticPaths = true } diff --git a/test/integration/app-dir-export/app/client/[slug]/page.js b/test/integration/app-dir-export/app/client/[slug]/page.js deleted file mode 100644 index 8e8ff5f8b141d..0000000000000 --- a/test/integration/app-dir-export/app/client/[slug]/page.js +++ /dev/null @@ -1,15 +0,0 @@ -'use client' -export const dynamic = 'force-static' - -export function generateStaticParams() { - return [{ slug: 'use client should not fail build' }] -} - -export default function Page({ params }) { - return ( -
-

Use Client Example

-

{params.slug}

-
- ) -} diff --git a/test/integration/app-dir-export/app/client/page.js b/test/integration/app-dir-export/app/client/page.js new file mode 100644 index 0000000000000..0800094685df8 --- /dev/null +++ b/test/integration/app-dir-export/app/client/page.js @@ -0,0 +1,9 @@ +'use client' + +export default function Page() { + return ( +
+

Use Client Example

+
+ ) +} diff --git a/test/integration/app-dir-export/test/dynamic-missing-gsp-dev.test.ts b/test/integration/app-dir-export/test/dynamic-missing-gsp-dev.test.ts new file mode 100644 index 0000000000000..bec23e1858285 --- /dev/null +++ b/test/integration/app-dir-export/test/dynamic-missing-gsp-dev.test.ts @@ -0,0 +1,21 @@ +import { runTests } from './utils' + +it('should error when dynamic route is missing generateStaticParams', async () => { + await runTests({ + isDev: true, + dynamicPage: 'undefined', + generateStaticParamsOpt: 'set noop', + expectedErrMsg: + 'Page "/another/[slug]/page" is missing exported function "generateStaticParams()", which is required with "output: export" config.', + }) +}) + +it('should error when client component has generateStaticParams', async () => { + await runTests({ + isDev: true, + dynamicPage: 'undefined', + generateStaticParamsOpt: 'set client', + expectedErrMsg: + 'Page "/another/[slug]/page" cannot use both "use client" and export function "generateStaticParams()".', + }) +}) diff --git a/test/integration/app-dir-export/test/dynamic-missing-gsp-prod.test.ts b/test/integration/app-dir-export/test/dynamic-missing-gsp-prod.test.ts new file mode 100644 index 0000000000000..2d1f2089ec369 --- /dev/null +++ b/test/integration/app-dir-export/test/dynamic-missing-gsp-prod.test.ts @@ -0,0 +1,21 @@ +import { runTests } from './utils' + +it('should error when dynamic route is missing generateStaticParams', async () => { + await runTests({ + isDev: false, + dynamicPage: 'undefined', + generateStaticParamsOpt: 'set noop', + expectedErrMsg: + 'Page "/another/[slug]" is missing "generateStaticParams()" so it cannot be used with "output: export" config.', + }) +}) + +it('should error when client component has generateStaticParams', async () => { + await runTests({ + isDev: false, + dynamicPage: 'undefined', + generateStaticParamsOpt: 'set client', + expectedErrMsg: + 'Page "/another/[slug]/page" cannot use both "use client" and export function "generateStaticParams()".', + }) +}) diff --git a/test/integration/app-dir-export/test/utils.ts b/test/integration/app-dir-export/test/utils.ts index ea90991feb1ac..cf16e9625f7d0 100644 --- a/test/integration/app-dir-export/test/utils.ts +++ b/test/integration/app-dir-export/test/utils.ts @@ -41,6 +41,8 @@ export const expectedWhenTrailingSlashTrue = [ 'another/second/index.txt', 'api/json', 'api/txt', + 'client/index.html', + 'client/index.txt', 'favicon.ico', 'image-import/index.html', 'image-import/index.txt', @@ -62,6 +64,8 @@ const expectedWhenTrailingSlashFalse = [ 'another/second.txt', 'api/json', 'api/txt', + 'client.html', + 'client.txt', 'favicon.ico', 'image-import.html', 'image-import.txt', @@ -87,12 +91,14 @@ export async function runTests({ trailingSlash = true, dynamicPage, dynamicApiRoute, + generateStaticParamsOpt, expectedErrMsg, }: { isDev?: boolean trailingSlash?: boolean dynamicPage?: string dynamicApiRoute?: string + generateStaticParamsOpt?: 'set noop' | 'set client' expectedErrMsg?: string }) { if (trailingSlash !== undefined) { @@ -113,6 +119,11 @@ export async function runTests({ `const dynamic = ${dynamicApiRoute}` ) } + if (generateStaticParamsOpt === 'set noop') { + slugPage.replace('export function generateStaticParams', 'function noop') + } else if (generateStaticParamsOpt === 'set client') { + slugPage.prepend('"use client"\n') + } await fs.remove(distDir) await fs.remove(exportDir) const port = await findPort() diff --git a/test/lib/next-test-utils.ts b/test/lib/next-test-utils.ts index 7da56c88138d6..4c723d6bde913 100644 --- a/test/lib/next-test-utils.ts +++ b/test/lib/next-test-utils.ts @@ -688,6 +688,11 @@ export class File { this.write(newContent) } + prepend(str: string) { + const content = readFileSync(this.path, 'utf8') + this.write(str + content) + } + delete() { unlinkSync(this.path) } From 5d758c7ec0a3b2ff20ecdda708722c7369b5eeae Mon Sep 17 00:00:00 2001 From: Tooooooooon Date: Mon, 21 Aug 2023 23:32:13 +0800 Subject: [PATCH 179/387] update readme in nextjs testing (#54322) Co-authored-by: Michael Novotny <446260+manovotny@users.noreply.github.com> --- .../01-building-your-application/05-optimizing/10-testing.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/03-pages/01-building-your-application/05-optimizing/10-testing.mdx b/docs/03-pages/01-building-your-application/05-optimizing/10-testing.mdx index d861a091ecff6..75c5937582624 100644 --- a/docs/03-pages/01-building-your-application/05-optimizing/10-testing.mdx +++ b/docs/03-pages/01-building-your-application/05-optimizing/10-testing.mdx @@ -432,9 +432,11 @@ setupFilesAfterEnv: ['/jest.setup.js'] Then, inside `jest.setup.js`, add the following import: ```js filename="jest.setup.js" -import '@testing-library/jest-dom/extend-expect' +import '@testing-library/jest-dom' ``` +> [`extend-expect` was removed in `v6.0`](https://github.com/testing-library/jest-dom/releases/tag/v6.0.0), so if you are using `@testing-library/jest-dom` before version 6, you will need to import `@testing-library/jest-dom/extend-expect` instead. + If you need to add more setup options before each test, it's common to add them to the `jest.setup.js` file above. **Optional: Absolute Imports and Module Path Aliases** From 3bce82e80758dfb9b0e3e28e43e978dc428fb833 Mon Sep 17 00:00:00 2001 From: Delba de Oliveira <32464864+delbaoliveira@users.noreply.github.com> Date: Mon, 21 Aug 2023 17:33:38 +0100 Subject: [PATCH 180/387] Docs: Fix broken links (#54340) Fix broken links picked up by crawl-analysis: https://vercel.slack.com/archives/C03S9JCH2Q5/p1692621216597749 --- .../08-deploying/01-static-exports.mdx | 2 +- docs/05-community/01-contribution-guide.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/02-app/01-building-your-application/08-deploying/01-static-exports.mdx b/docs/02-app/01-building-your-application/08-deploying/01-static-exports.mdx index 6f53c4d2e5880..541b562a77185 100644 --- a/docs/02-app/01-building-your-application/08-deploying/01-static-exports.mdx +++ b/docs/02-app/01-building-your-application/08-deploying/01-static-exports.mdx @@ -288,7 +288,7 @@ Features that require a Node.js server, or dynamic logic that cannot be computed - [Dynamic Routes](/docs/app/building-your-application/routing/dynamic-routes) with `dynamicParams: true` - [Dynamic Routes](/docs/app/building-your-application/routing/dynamic-routes) without `generateStaticParams()` -- [Route Handlers](/app/building-your-application/routing/route-handlers) that rely on Request +- [Route Handlers](/docs/app/building-your-application/routing/route-handlers) that rely on Request - [Cookies](/docs/app/api-reference/functions/cookies) - [Rewrites](/docs/app/api-reference/next-config-js/rewrites) - [Redirects](/docs/app/api-reference/next-config-js/redirects) diff --git a/docs/05-community/01-contribution-guide.mdx b/docs/05-community/01-contribution-guide.mdx index a90a8f6bbec98..9e51bb61be50b 100644 --- a/docs/05-community/01-contribution-guide.mdx +++ b/docs/05-community/01-contribution-guide.mdx @@ -66,7 +66,7 @@ Please let us know if you have any questions or need further assistance in your ## File Structure -The docs use **file-system routing**. Each folder and files inside [`/docs`](/vercel/next.js/tree/canary/docs) represent a route segment. These segments are used to generate the URL paths, navigation, and breadcrumbs. +The docs use **file-system routing**. Each folder and files inside [`/docs`](https://github.com/vercel/next.js/tree/canary/docs) represent a route segment. These segments are used to generate the URL paths, navigation, and breadcrumbs. The file structure reflects the navigation that you see on the site, and by default, navigation items are sorted alphabetically. However, we can change the order of the items by prepending a two-digit number (`00-`) to the folder or file name. From b7eb6d43b969ec8adf58b84c325ad5136c7e1a33 Mon Sep 17 00:00:00 2001 From: "Mohamed A. Salah" Date: Mon, 21 Aug 2023 20:05:57 +0300 Subject: [PATCH 181/387] set hostname to 0.0.0.0 (#54342) When I try to deploy to Google Cloud Run it fails after some investigation I saw this line in .next/standalone/server.js ```javascript const hostname = process.env.HOSTNAME || 'localhost' ``` This some how make this log when i run docker ```shell - ready started server on 172.17.0.2:3000, url: http://172.17.0.2:3000 ``` I don't know why it's logging this address even if the server running on localhost. So this my fix Set hostname to 0.0.0.0 to avoid deployment failing on Google cloud run. --- examples/with-docker/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/with-docker/Dockerfile b/examples/with-docker/Dockerfile index f68a80ab1ee6f..791eff1edbecd 100644 --- a/examples/with-docker/Dockerfile +++ b/examples/with-docker/Dockerfile @@ -55,5 +55,7 @@ USER nextjs EXPOSE 3000 ENV PORT 3000 +# set hostname to localhost +ENV HOSTNAME "0.0.0.0" CMD ["node", "server.js"] From 3370022ac2a4dee3437b9fe294ecb3d12ca963ba Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 21 Aug 2023 11:49:21 -0700 Subject: [PATCH 182/387] add test case for CSP with bootstrap scripts and preinit modules (#54348) in #54059 the nonce attribute was added to preinitialized scripts to when using this CSP directive. The test added asserts there is at least one script that has the nonce attribute. I've changed this to 2 because currently our builds produce at least two "main" scripts, the main chunk and the webpack runtime. The way we bootstrap there is always exactly one bootstrap script which means if we only assert that there is one script with a nonce we might not be asserting anything about the preinit script path. If we ever update our webpack config to produce a single main script this test will fail but we should never do that (it's bad for caching) and so it shouldn't happen and if it does it will hopefully force us to consider if we're making a mistake Additionally I've added another test that is more e2e. it asserts that the page bootstraps even when using CSP (in prod). In Dev it asserts the CSP attributes but it expects the bootstrap to fail because our dev mode violates the CSP directive with eval. --- .../app/bootstrap/[[...*]]/ClientComponent.js | 11 ++++++ .../app/app/bootstrap/[[...*]]/page.js | 15 ++++++++ test/e2e/app-dir/app/app/bootstrap/page.js | 8 ---- test/e2e/app-dir/app/index.test.ts | 38 ++++++++++++++++++- test/e2e/app-dir/app/middleware.js | 15 +++++++- 5 files changed, 75 insertions(+), 12 deletions(-) create mode 100644 test/e2e/app-dir/app/app/bootstrap/[[...*]]/ClientComponent.js create mode 100644 test/e2e/app-dir/app/app/bootstrap/[[...*]]/page.js delete mode 100644 test/e2e/app-dir/app/app/bootstrap/page.js diff --git a/test/e2e/app-dir/app/app/bootstrap/[[...*]]/ClientComponent.js b/test/e2e/app-dir/app/app/bootstrap/[[...*]]/ClientComponent.js new file mode 100644 index 0000000000000..6cbcf95b770f1 --- /dev/null +++ b/test/e2e/app-dir/app/app/bootstrap/[[...*]]/ClientComponent.js @@ -0,0 +1,11 @@ +'use client' + +import { useState, useEffect } from 'react' + +export function ClientComponent() { + const [val, setVal] = useState('initial') + useEffect(() => { + setVal('[[updated]]') + }, []) + return {val} +} diff --git a/test/e2e/app-dir/app/app/bootstrap/[[...*]]/page.js b/test/e2e/app-dir/app/app/bootstrap/[[...*]]/page.js new file mode 100644 index 0000000000000..4e53effb26f73 --- /dev/null +++ b/test/e2e/app-dir/app/app/bootstrap/[[...*]]/page.js @@ -0,0 +1,15 @@ +import { ClientComponent } from './ClientComponent' + +export default async function Page() { + return ( + <> +
+ This fixture is to assert where the bootstrap scripts and other required + scripts emit during SSR +
+
+ +
+ + ) +} diff --git a/test/e2e/app-dir/app/app/bootstrap/page.js b/test/e2e/app-dir/app/app/bootstrap/page.js deleted file mode 100644 index d83b335ed8698..0000000000000 --- a/test/e2e/app-dir/app/app/bootstrap/page.js +++ /dev/null @@ -1,8 +0,0 @@ -export default async function Page() { - return ( -
- This fixture is to assert where the bootstrap scripts and other required - scripts emit during SSR -
- ) -} diff --git a/test/e2e/app-dir/app/index.test.ts b/test/e2e/app-dir/app/index.test.ts index c0b21cbe03ab9..0c5bae1ab70bb 100644 --- a/test/e2e/app-dir/app/index.test.ts +++ b/test/e2e/app-dir/app/index.test.ts @@ -1373,8 +1373,9 @@ createNextDescribe( // Find all the script tags without src attributes. const elements = $('script[src]') - // Expect there to be at least 1 script tag without a src attribute. - expect(elements.length).toBeGreaterThan(0) + // Expect there to be at least 2 script tag with a src attribute. + // The main chunk and the webpack runtime. + expect(elements.length).toBeGreaterThan(1) // Expect all inline scripts to have the nonce value. elements.each((i, el) => { @@ -1899,6 +1900,39 @@ createNextDescribe( expect($('script[async]').length).toBeGreaterThan(1) expect($('body').find('script[async]').length).toBe(1) }) + + if (!isDev) { + it('should successfully bootstrap even when using CSP', async () => { + // This path has a nonce applied in middleware + const browser = await next.browser('/bootstrap/with-nonce') + const response = await next.fetch('/bootstrap/with-nonce') + // We expect this page to response with CSP headers requiring a nonce for scripts + expect(response.headers.get('content-security-policy')).toContain( + "script-src 'nonce" + ) + // We expect to find the updated text which demonstrates our app + // was able to bootstrap successfully (scripts run) + expect( + await browser.eval('document.getElementById("val").textContent') + ).toBe('[[updated]]') + }) + } else { + it('should fail to bootstrap when using CSP in Dev due to eval', async () => { + // This test is here to ensure that we don't accidentally turn CSP off + // for the prod version. + const browser = await next.browser('/bootstrap/with-nonce') + const response = await next.fetch('/bootstrap/with-nonce') + // We expect this page to response with CSP headers requiring a nonce for scripts + expect(response.headers.get('content-security-policy')).toContain( + "script-src 'nonce" + ) + // We expect our app to fail to bootstrap due to invalid eval use in Dev. + // We assert the html is in it's SSR'd state. + expect( + await browser.eval('document.getElementById("val").textContent') + ).toBe('initial') + }) + } }) } ) diff --git a/test/e2e/app-dir/app/middleware.js b/test/e2e/app-dir/app/middleware.js index efd1bf33146e5..2f7830a92a708 100644 --- a/test/e2e/app-dir/app/middleware.js +++ b/test/e2e/app-dir/app/middleware.js @@ -3,9 +3,9 @@ import { NextResponse } from 'next/server' /** * @param {import('next/server').NextRequest} request - * @returns {NextResponse | undefined} + * @returns {Promise} */ -export function middleware(request) { +export async function middleware(request) { if (request.nextUrl.pathname === '/searchparams-normalization-bug') { const headers = new Headers(request.headers) headers.set('test', request.nextUrl.searchParams.get('val') || '') @@ -25,6 +25,17 @@ export function middleware(request) { return NextResponse.rewrite(new URL('/dashboard', request.url)) } + // In dev this route will fail to bootstrap because webpack uses eval which is dissallowed by + // this policy. In production this route will work + if (request.nextUrl.pathname === '/bootstrap/with-nonce') { + const nonce = crypto.randomUUID() + return NextResponse.next({ + headers: { + 'Content-Security-Policy': `script-src 'nonce-${nonce}' 'strict-dynamic';`, + }, + }) + } + if (request.nextUrl.pathname.startsWith('/internal/test')) { const method = request.nextUrl.pathname.endsWith('rewrite') ? 'rewrite' From 36c14da3d58540009b4c08c56d33d5accff34912 Mon Sep 17 00:00:00 2001 From: OJ Kwon <1210596+kwonoj@users.noreply.github.com> Date: Mon, 21 Aug 2023 12:25:16 -0700 Subject: [PATCH 183/387] ci(trace): allow to opt in to upload full trace (#54347) ### What? Came to realize it might be useful to upload full trace. This is avoided by default as trace might grow excessively, however might be useful to audit individual trace if it can be uploaded. Closes WEB-1416 --- packages/next/src/trace/trace-uploader.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/next/src/trace/trace-uploader.ts b/packages/next/src/trace/trace-uploader.ts index 8634ca4196539..fac5a5c9a48a0 100644 --- a/packages/next/src/trace/trace-uploader.ts +++ b/packages/next/src/trace/trace-uploader.ts @@ -9,6 +9,9 @@ import { createInterface } from 'readline' import { createReadStream } from 'fs' import path from 'path' +// Predefined set of the event names to be included in the trace. +// If the trace span's name matches to one of the event names in the set, +// it'll up uploaded to the trace server. const EVENT_FILTER = new Set([ 'client-hmr-latency', 'hot-reloader', @@ -16,7 +19,16 @@ const EVENT_FILTER = new Set([ 'webpack-invalidated-server', ]) -const { NEXT_TRACE_UPLOAD_DEBUG } = process.env +const { + NEXT_TRACE_UPLOAD_DEBUG, + // An external env to allow to upload full trace without picking up the relavant spans. + // This is mainly for the debugging purpose, to allwo manual audit for full trace for the given build. + // [NOTE] This may fail if build is large and generated trace is excessively large. + NEXT_TRACE_UPLOAD_FULL, +} = process.env + +const isDebugEnabled = !!NEXT_TRACE_UPLOAD_DEBUG || !!NEXT_TRACE_UPLOAD_FULL +const shouldUploadFullTrace = !!NEXT_TRACE_UPLOAD_FULL const [, , traceUploadUrl, mode, _isTurboSession, projectDir, distDir] = process.argv @@ -78,6 +90,7 @@ interface TraceMetadata { if ( // Always include root spans event.parentId === undefined || + shouldUploadFullTrace || EVENT_FILTER.has(event.name) ) { if ( @@ -114,7 +127,7 @@ interface TraceMetadata { traces: [...traces.values()], } - if (NEXT_TRACE_UPLOAD_DEBUG) { + if (isDebugEnabled) { console.log('Sending request with body', JSON.stringify(body, null, 2)) } @@ -122,11 +135,12 @@ interface TraceMetadata { method: 'POST', headers: { 'Content-Type': 'application/json', + 'x-trace-transfer-mode': shouldUploadFullTrace ? 'full' : 'default', }, body: JSON.stringify(body), }) - if (NEXT_TRACE_UPLOAD_DEBUG) { + if (isDebugEnabled) { console.log('Received response', res.status, await res.json()) } })() From 892839ff83f25b368f71a1e79195699d7e4536fa Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Mon, 21 Aug 2023 13:34:42 -0700 Subject: [PATCH 184/387] fix: server actions blocking navigation events (#54307) A long-running server action shouldn't block page navigation. This makes use of a global mutable to detect when a navigation event occurs -- this change will unblock other reducers (such as navigation). If we bailed on the action, we trigger a `router.refresh()` to ensure any side effects from the action are refetched. Closes NEXT-1131 Fixes #49425 --- .../next/src/client/components/app-router.tsx | 13 +++++++-- .../reducers/server-action-reducer.ts | 28 ++++++++++++++++--- .../router-reducer/router-reducer-types.ts | 1 + test/e2e/app-dir/actions/app-action.test.ts | 20 +++++++++++++ .../e2e/app-dir/actions/app/client/actions.js | 5 ++++ test/e2e/app-dir/actions/app/client/page.js | 11 +++++++- .../e2e/app-dir/actions/app/server/actions.js | 5 ++++ .../e2e/app-dir/actions/app/server/counter.js | 11 +++++++- test/e2e/app-dir/actions/app/server/page.js | 3 +- 9 files changed, 88 insertions(+), 9 deletions(-) diff --git a/packages/next/src/client/components/app-router.tsx b/packages/next/src/client/components/app-router.tsx index d15454d159913..2382d6a83925e 100644 --- a/packages/next/src/client/components/app-router.tsx +++ b/packages/next/src/client/components/app-router.tsx @@ -38,6 +38,7 @@ import { RouterChangeByServerResponse, RouterNavigate, ServerActionDispatcher, + ServerActionMutable, } from './router-reducer/router-reducer-types' import { createHrefFromUrl } from './router-reducer/create-href-from-url' import { @@ -59,7 +60,6 @@ import { createInfinitePromise } from './infinite-promise' import { NEXT_RSC_UNION_QUERY } from './app-router-headers' import { removeBasePath } from '../remove-base-path' import { hasBasePath } from '../has-base-path' - const isServer = typeof window === 'undefined' // Ensure the initialParallelRoutes are not combined because of double-rendering in the browser with Strict Mode. @@ -73,6 +73,10 @@ export function getServerActionDispatcher() { return globalServerActionDispatcher } +let globalServerActionMutable: ServerActionMutable['globalMutable'] = { + refresh: () => {}, // noop until the router is initialized +} + export function urlToUrlWithoutFlightMarker(url: string): URL { const urlWithoutFlightParameters = new URL(url, location.origin) urlWithoutFlightParameters.searchParams.delete(NEXT_RSC_UNION_QUERY) @@ -141,7 +145,7 @@ function useServerActionDispatcher(dispatch: React.Dispatch) { dispatch({ ...actionPayload, type: ACTION_SERVER_ACTION, - mutable: {}, + mutable: { globalMutable: globalServerActionMutable }, cache: createEmptyCacheNode(), }) }) @@ -182,6 +186,7 @@ function useNavigate(dispatch: React.Dispatch): RouterNavigate { return useCallback( (href, navigateType, forceOptimisticNavigation, shouldScroll) => { const url = new URL(addBasePath(href), location.href) + globalServerActionMutable.pendingNavigatePath = href return dispatch({ type: ACTION_NAVIGATE, @@ -351,6 +356,10 @@ function Router({ } }, [appRouter]) + useEffect(() => { + globalServerActionMutable.refresh = appRouter.refresh + }, [appRouter.refresh]) + if (process.env.NODE_ENV !== 'production') { // This hook is in a conditional but that is ok because `process.env.NODE_ENV` never changes // eslint-disable-next-line react-hooks/rules-of-hooks diff --git a/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts index 670f5336812bc..9429708b213b0 100644 --- a/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts @@ -5,6 +5,7 @@ import { } from '../../../../server/app-render/types' import { callServer } from '../../../app-call-server' import { + ACTION, NEXT_ROUTER_STATE_TREE, NEXT_URL, RSC_CONTENT_TYPE_HEADER, @@ -51,7 +52,7 @@ async function fetchServerAction( method: 'POST', headers: { Accept: RSC_CONTENT_TYPE_HEADER, - 'Next-Action': actionId, + [ACTION]: actionId, [NEXT_ROUTER_STATE_TREE]: encodeURIComponent(JSON.stringify(state.tree)), ...(process.env.__NEXT_ACTIONS_DEPLOYMENT_ID && process.env.NEXT_DEPLOYMENT_ID @@ -147,9 +148,28 @@ export function serverActionReducer( return handleMutable(state, mutable) } - if (!action.mutable.inFlightServerAction) { - action.mutable.inFlightServerAction = createRecordFromThenable( - fetchServerAction(state, action) + if (mutable.inFlightServerAction) { + // unblock if a navigation event comes through + // while we've suspended on an action + if ( + mutable.globalMutable.pendingNavigatePath && + mutable.globalMutable.pendingNavigatePath !== href + ) { + return state + } + } else { + mutable.inFlightServerAction = createRecordFromThenable( + fetchServerAction(state, action).then((result) => { + // if the server action resolves after a navigation took place, + // reset ServerActionMutable values & trigger a refresh so that any stale data gets updated + if (mutable.globalMutable.pendingNavigatePath) { + mutable.inFlightServerAction = null + mutable.globalMutable.pendingNavigatePath = undefined + mutable.globalMutable.refresh() + } else { + return result + } + }) ) } diff --git a/packages/next/src/client/components/router-reducer/router-reducer-types.ts b/packages/next/src/client/components/router-reducer/router-reducer-types.ts index 7e7ac6f70a76e..48840a29db6db 100644 --- a/packages/next/src/client/components/router-reducer/router-reducer-types.ts +++ b/packages/next/src/client/components/router-reducer/router-reducer-types.ts @@ -42,6 +42,7 @@ export interface Mutable { export interface ServerActionMutable extends Mutable { inFlightServerAction?: Promise | null + globalMutable: { pendingNavigatePath?: string; refresh: () => void } actionResultResolved?: boolean } diff --git a/test/e2e/app-dir/actions/app-action.test.ts b/test/e2e/app-dir/actions/app-action.test.ts index 691f41b7cf567..5ed1ef6e1b0f1 100644 --- a/test/e2e/app-dir/actions/app-action.test.ts +++ b/test/e2e/app-dir/actions/app-action.test.ts @@ -258,6 +258,26 @@ createNextDescribe( await check(() => browser.elementByCss('#value').text(), 'Value = 2') }) + it('should not block navigation events while a server action is in flight', async () => { + let browser = await next.browser('/client') + + await browser.elementByCss('#slow-inc').click() + + // navigate to server + await browser.elementByCss('#navigate-server').click() + // intentionally bailing after 2 retries so we don't retry to the point where the async function resolves + await check(() => browser.url(), `${next.url}/server`, true, 2) + + browser = await next.browser('/server') + + await browser.elementByCss('#slow-inc').click() + + // navigate to client + await browser.elementByCss('#navigate-client').click() + // intentionally bailing after 2 retries so we don't retry to the point where the async function resolves + await check(() => browser.url(), `${next.url}/client`, true, 2) + }) + if (isNextStart) { it('should not expose action content in sourcemaps', async () => { const sourcemap = ( diff --git a/test/e2e/app-dir/actions/app/client/actions.js b/test/e2e/app-dir/actions/app/client/actions.js index 9cb7445eaa8ee..d9d37f41b5ad3 100644 --- a/test/e2e/app-dir/actions/app/client/actions.js +++ b/test/e2e/app-dir/actions/app/client/actions.js @@ -14,6 +14,11 @@ export async function inc(value) { return value + 1 } +export async function slowInc(value) { + await new Promise((resolve) => setTimeout(resolve, 10000)) + return value + 1 +} + export async function dec(value) { return value - 1 } diff --git a/test/e2e/app-dir/actions/app/client/page.js b/test/e2e/app-dir/actions/app/client/page.js index e841265bb1013..5d40906ee6516 100644 --- a/test/e2e/app-dir/actions/app/client/page.js +++ b/test/e2e/app-dir/actions/app/client/page.js @@ -8,6 +8,7 @@ import double, { redirectAction, getHeaders, renamed, + slowInc, } from './actions' import { test } from './actions-lib' @@ -19,7 +20,6 @@ export default function Counter() { + + - -``` - -The password form field must only contain digits (0 to 9), lowercase alphabets (a to z) and it must be no more than 15 characters in length. No other characters (#,$,&, etc.) are allowed. The rule in RegEx is written as `[a-z0-9]{1,15}`. - -![form-validate-regex](https://assets.vercel.com/image/upload/dpr_auto,q_auto,f_auto/nextjs/guides/building-forms/form-validate-regex.jpg) - -> To learn more about HTML forms, check out the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Learn/Forms). - -## Part 2: Project Setup - -In the following section you will be creating forms in React using Next.js. - -Create a new Next.js app. You can use the [create-next-app](/docs/pages/api-reference/create-next-app) for a quick start. In your command line terminal, run the following: - -``` -npx create-next-app -``` - -Answer the questions to create your project, and give it a name, this example uses [`next-forms`](https://github.com/vercel/next.js/tree/canary/examples/next-forms). Next `cd` into this directory, and run `npm run dev` or `yarn dev` command to start the development server. - -Open the URL printed in the terminal to ensure that your app is running successfully. - -## Part 3: Setting up a Next.js Form API Route - -Both the client and the server will be built using Next.js. For the server part, create an API endpoint where you will send the form data. - -Next.js offers a file-based system for routing that's built on the [concept of pages](/docs/pages/building-your-application/routing/pages-and-layouts). Any file inside the folder `pages/api` is mapped to `/api/*` and will be treated as an API endpoint instead of a page. This [API endpoint](/docs/pages/building-your-application/routing/api-routes) is going to be server-side only. - -Go to `pages/api`, create a file called `form.js` and paste this code written in Node.js: - -```js -export default function handler(req, res) { - // Get data submitted in request's body. - const body = req.body - - // Optional logging to see the responses - // in the command line where next.js app is running. - console.log('body: ', body) - - // Guard clause checks for first and last name, - // and returns early if they are not found - if (!body.first || !body.last) { - // Sends a HTTP bad request error code - return res.status(400).json({ data: 'First or last name not found' }) - } - - // Found the name. - // Sends a HTTP success code - res.status(200).json({ data: `${body.first} ${body.last}` }) -} -``` - -This form `handler` function will receive the request `req` from the client (i.e. submitted form data). And in return, it'll send a response `res` as JSON that will have both the first and the last name. You can access this API endpoint at `http://localhost:3000/api/form` or replace the localhost URL with an actual Vercel deployment when you deploy. - -> Moreover, you can also attach this API to a database like MongoDB or Google Sheets. This way, your submitted form data will be securely stored for later use. For this guide, no database is used. Instead, the same data is returned to the user to demo how it's done. - -### Form Submission without JavaScript - -You can now use `/api/form` relative endpoint inside the `action` attribute of the form. You are sending form data to the server when the form is submitted via `POST` HTTP method (which is used to send data). - -```html -
- - - - - -
-``` - -If you submit this form, it will submit the data to the forms API endpoint `/api/form`. The server then responds, generally handling the data and loading the URL defined by the action attribute, causing a new page load. So in this case you'll be redirected to `http://localhost:3000/api/form` with the following response from the server. - -![form-no-js](https://assets.vercel.com/image/upload/dpr_auto,q_auto,f_auto/nextjs/guides/building-forms/form-no-js.jpg) - -## Part 4: Configuring Forms in Next.js - -You have created a Next.js API Route for form submission. Now it's time to configure the client (the form itself) inside Next.js using React. The first step will be extending your knowledge of HTML forms and converting it into React (using [JSX](https://react.dev/learn/writing-markup-with-jsx)). - -Here's the same form in a [React function component](https://react.dev/reference/react/Component) written using [JSX](https://react.dev/learn/writing-markup-with-jsx). - -```js -export default function Form() { - return ( -
- - - - - - - -
- ) -} -``` - -Here's what changed: - -- The `for` attribute is changed to `htmlFor`. (Since `for` is a keyword associated with the "for" loop in JavaScript, React elements use `htmlFor` instead.) -- The `action` attribute now has a relative URL which is the form API endpoint. - -This completes the basic structure of your Next.js-based form. - -> You can view the entire source code of [next-forms](https://github.com/vercel/next.js/tree/canary/examples/next-forms) example repo that we're creating here as a working example. Feel free to clone it and start right away. This demo is built with create-next-app, and you can preview the basic form CSS styles inside `/styles/global.css` file. - -![forms with nextjs](https://assets.vercel.com/image/upload/dpr_auto,q_auto,f_auto/nextjs/guides/building-forms/forms-with-nextjs.png) - -## Part 5: Form Submission without JavaScript - -JavaScript brings interactivity to our web applications, but sometimes you need to control the JavaScript bundle from being too large, or your sites visitors might have JavaScript disabled. - -There are several reasons why users disable JavaScript: - -- Addressing bandwidth constraints -- Increasing device (phone or laptop) battery life -- For privacy so they won’t be tracked with analytical scripts - -Regardless of the reason, disabling JavaScript will impact site functionality partially, if not completely. - -Next open the `next-forms` directory. Inside the `/pages` directory, create a file `no-js-form.js`. - -> **Quick Tip**: In Next.js, a page is a React Component exported from a `.js`, `.jsx`, `.ts`, or `.tsx` file in the Pages Router. Each page is associated with a route based on its file name. -> -> Example: If you create `pages/no-js-form.js`, it will be accessible at `your-domain.tld/no-js-form`. - -Let's use the same code from above: - -```js -export default function PageWithoutJSbasedForm() { - return ( -
- - - - - - - -
- ) -} -``` - -With JavaScript disabled, when you hit the Submit button, an event is triggered, which collects the form data and sends it to our forms API endpoint as defined in the `action` attribute and using `POST` HTTP `method`. You'll be redirected to the `/api/form` endpoint since that's how form `action` works. - -The form data will be submitted on the server as a request `req` to the form handler function written above. It will process the data and return a JSON string as a response `res` with your submitted name included. - -> To improve the experience here, as a response you can redirect the user to a page and thank them for submitting the form. - -## Part 6: Form Submission with JavaScript Enabled - -Inside `/pages`, you'll create another file called `js-form.js`. This will create a `/js-form` page on your Next.js app. - -Now, as soon as the form is submitted, we prevent the form's default behavior of reloading the page. We'll take the form data, convert it to JSON string, and send it to our server, the API endpoint. Finally, our server will respond with the name submitted. All of this with a basic JavaScript `handleSubmit()` function. - -Here's what this function looks like. It's well documented for you to understand each step: - -```js -export default function PageWithJSbasedForm() { - // Handles the submit event on form submit. - const handleSubmit = async (event) => { - // Stop the form from submitting and refreshing the page. - event.preventDefault() - - // Get data from the form. - const data = { - first: event.target.first.value, - last: event.target.last.value, - } - - // Send the data to the server in JSON format. - const JSONdata = JSON.stringify(data) - - // API endpoint where we send form data. - const endpoint = '/api/form' - - // Form the request for sending data to the server. - const options = { - // The method is POST because we are sending data. - method: 'POST', - // Tell the server we're sending JSON. - headers: { - 'Content-Type': 'application/json', - }, - // Body of the request is the JSON data we created above. - body: JSONdata, - } - - // Send the form data to our forms API on Vercel and get a response. - const response = await fetch(endpoint, options) - - // Get the response data from server as JSON. - // If server returns the name submitted, that means the form works. - const result = await response.json() - alert(`Is this your full name: ${result.data}`) - } - return ( - // We pass the event to the handleSubmit() function on submit. -
- - - - - - - -
- ) -} -``` - -It's a Next.js page with a React function component called `PageWithJSbasedForm` with a `
` element written in JSX. There's no action on the `` element. Instead, we use the `onSubmit` event handler to send data to our `{handleSubmit}` function. - -The `handleSubmit()` function processes your form data through a series of steps: - -- The `event.preventDefault()` stops the `` element from refreshing the entire page. -- We created a JavaScript object called `data` with the `first` and `last` values from the form. -- JSON is a language-agnostic data transfer format. So we use `JSON.stringify(data)` to convert the data to JSON. -- We then use `fetch()` to send the data to our `/api/form` endpoint using JSON and HTTP `POST` method. -- Server sends back a response with the name submitted. Woohoo! 🥳 - -## Conclusion - -This guide has covered the following: - -- The basic HTML `form` element -- Understanding forms with React.js -- Validating forms data with and without JavaScript -- Using Next.js API Routes to handle `req` and `res` from the client and server - -For more details go through [Next.js Learn Course](https://nextjs.org/learn/basics/create-nextjs-app). diff --git a/examples/next-forms/.env.example b/examples/next-forms/.env.example new file mode 100644 index 0000000000000..f5febb2996400 --- /dev/null +++ b/examples/next-forms/.env.example @@ -0,0 +1,6 @@ +POSTGRES_URL= +POSTGRES_URL_NON_POOLING= +POSTGRES_USER= +POSTGRES_HOST= +POSTGRES_PASSWORD= +POSTGRES_DATABASE= \ No newline at end of file diff --git a/examples/next-forms/.eslintrc.json b/examples/next-forms/.eslintrc.json deleted file mode 100644 index a2569c2c7ca0a..0000000000000 --- a/examples/next-forms/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "root": true, - "extends": "next/core-web-vitals" -} diff --git a/examples/next-forms/.gitignore b/examples/next-forms/.gitignore index 8f322f0d8f495..210939dd745af 100644 --- a/examples/next-forms/.gitignore +++ b/examples/next-forms/.gitignore @@ -11,6 +11,7 @@ # next.js /.next/ /out/ +next-env.d.ts # production /build @@ -25,11 +26,14 @@ yarn-debug.log* yarn-error.log* # local env files +.env .env*.local # vercel .vercel +# Turborepo +.turbo + # typescript *.tsbuildinfo -next-env.d.ts diff --git a/examples/next-forms/README.md b/examples/next-forms/README.md index 0c63725841bff..284ae387978a4 100644 --- a/examples/next-forms/README.md +++ b/examples/next-forms/README.md @@ -1,11 +1,9 @@ -# Building Web Forms with Next.js Example +# Forms with Next.js and Server Actions -This example shows how you can build forms with Next.js. +This example shows how you can build forms with Next.js and Server Actions. ## Deploy your own -Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) or preview live with [StackBlitz](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/next-forms) - [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/next.js/tree/canary/examples/next-forms&project-name=next-forms&repository-name=next-forms) ## How to use diff --git a/examples/next-forms/app/actions.ts b/examples/next-forms/app/actions.ts new file mode 100644 index 0000000000000..bfb5a11b29d7d --- /dev/null +++ b/examples/next-forms/app/actions.ts @@ -0,0 +1,48 @@ +'use server' + +import { revalidatePath } from 'next/cache' +import { sql } from '@vercel/postgres' +import { z } from 'zod' + +// CREATE TABLE todos ( +// id SERIAL PRIMARY KEY, +// text TEXT NOT NULL +// ); + +export async function createTodo(formData: FormData) { + const schema = z.object({ + todo: z.string().nonempty(), + }) + const data = schema.parse({ + todo: formData.get('todo'), + }) + + try { + await sql` + INSERT INTO todos (text) + VALUES (${data.todo}) + ` + + revalidatePath('/') + + return { message: 'Saved successfully' } + } catch (e) { + return { message: 'Failed to create todo' } + } +} + +export async function deleteTodo(formData: FormData) { + const schema = z.object({ + id: z.string().nonempty(), + }) + const data = schema.parse({ + id: formData.get('id'), + }) + + await sql` + DELETE FROM todos + WHERE id = ${data.id}; + ` + + revalidatePath('/') +} diff --git a/examples/next-forms/app/client/page.tsx b/examples/next-forms/app/client/page.tsx new file mode 100644 index 0000000000000..301391bf91add --- /dev/null +++ b/examples/next-forms/app/client/page.tsx @@ -0,0 +1,21 @@ +'use client' + +import { createTodo } from '@/app/actions' +import { useState } from 'react' + +export default function Form() { + const [message, setMessage] = useState('') + + async function onCreateTodo(formData: FormData) { + const res = await createTodo(formData) + setMessage(res.message) + } + + return ( + + + +

{message}

+
+ ) +} diff --git a/examples/next-forms/public/favicon.ico b/examples/next-forms/app/favicon.ico similarity index 100% rename from examples/next-forms/public/favicon.ico rename to examples/next-forms/app/favicon.ico diff --git a/examples/next-forms/app/layout.tsx b/examples/next-forms/app/layout.tsx new file mode 100644 index 0000000000000..4eb2fac51a870 --- /dev/null +++ b/examples/next-forms/app/layout.tsx @@ -0,0 +1,16 @@ +export const metadata = { + title: 'Next.js Forms Example', + description: 'Example application with forms and Postgres.', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/examples/next-forms/app/page.tsx b/examples/next-forms/app/page.tsx new file mode 100644 index 0000000000000..4459833b9686a --- /dev/null +++ b/examples/next-forms/app/page.tsx @@ -0,0 +1,30 @@ +import { sql } from '@vercel/postgres' +import { createTodo, deleteTodo } from '@/app/actions' + +export const runtime = 'edge' +export const preferredRegion = 'home' + +export default async function Home() { + let data = await sql`SELECT * FROM todos` + const { rows: todos } = data + + return ( +
+
+ + +
+
    + {todos.map((todo) => ( +
  • + {todo.text} +
    + + +
    +
  • + ))} +
+
+ ) +} diff --git a/examples/next-forms/next.config.js b/examples/next-forms/next.config.js index 8b61df4e50f8a..950e2f42ec32e 100644 --- a/examples/next-forms/next.config.js +++ b/examples/next-forms/next.config.js @@ -1,4 +1,8 @@ /** @type {import('next').NextConfig} */ -module.exports = { - reactStrictMode: true, +const nextConfig = { + experimental: { + serverActions: true, + }, } + +module.exports = nextConfig diff --git a/examples/next-forms/package.json b/examples/next-forms/package.json index f438585da981e..c558339a8a0d6 100644 --- a/examples/next-forms/package.json +++ b/examples/next-forms/package.json @@ -3,19 +3,17 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start", - "lint": "next lint" + "start": "next start" }, "dependencies": { + "@types/node": "20.5.1", + "@types/react": "18.2.20", + "@types/react-dom": "18.2.7", + "@vercel/postgres": "0.4.1", "next": "latest", "react": "18.2.0", - "react-dom": "18.2.0" - }, - "devDependencies": { - "@types/node": "18.7.15", - "@types/react": "18.0.18", - "eslint": "8.23.0", - "eslint-config-next": "latest", - "typescript": "4.8.2" + "react-dom": "18.2.0", + "typescript": "5.1.6", + "zod": "^3.22.2" } } diff --git a/examples/next-forms/pages/_app.tsx b/examples/next-forms/pages/_app.tsx deleted file mode 100644 index edfaad68a7807..0000000000000 --- a/examples/next-forms/pages/_app.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import type { AppProps } from 'next/app' -import '../styles/globals.css' - -export default function MyApp({ Component, pageProps }: AppProps) { - return -} diff --git a/examples/next-forms/pages/api/form.ts b/examples/next-forms/pages/api/form.ts deleted file mode 100644 index 23aa4fdc140b8..0000000000000 --- a/examples/next-forms/pages/api/form.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from 'next' - -type ResponseData = { - data: string -} - -export default function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const body = req.body - console.log('body: ', body) - - // Both of these are required. - if (!body.first || !body.last) { - return res.json({ data: 'First or last name not found' }) - } - - // Found the name. - res.json({ data: `${body.first} ${body.last}` }) -} diff --git a/examples/next-forms/pages/index.tsx b/examples/next-forms/pages/index.tsx deleted file mode 100644 index 048db70b3640b..0000000000000 --- a/examples/next-forms/pages/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import Head from 'next/head' -import Image from 'next/image' -import Link from 'next/link' -import styles from '../styles/Home.module.css' - -export default function IndexPage() { - return ( -
- - Next.js forms - - - - -
-

- Forms with Next.js! -

- -

- Get started by looking at{' '} - pages/js-form.js and{' '} - pages/no-js-form.js -

- -
- -

Form with JavaScript →

-

Learn to handle forms with JavaScript in Next.js.

- - - -

Form without JavaScript →

-

Learn to handle forms without JavaScript in Next.js.

- -
-
- - -
- ) -} diff --git a/examples/next-forms/pages/js-form.tsx b/examples/next-forms/pages/js-form.tsx deleted file mode 100644 index ee2734ba386df..0000000000000 --- a/examples/next-forms/pages/js-form.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import Link from 'next/link' -import { FormEvent } from 'react' -import styles from '../styles/Home.module.css' - -export default function PageWithJSbasedForm() { - // Handle the submit event on form submit. - const handleSubmit = async (event: FormEvent) => { - // Stop the form from submitting and refreshing the page. - event.preventDefault() - - // Cast the event target to an html form - const form = event.target as HTMLFormElement - - // Get data from the form. - const data = { - first: form.first.value as string, - last: form.last.value as string, - } - - // Send the form data to our API and get a response. - const response = await fetch('/api/form', { - // Body of the request is the JSON data we created above. - body: JSON.stringify(data), - // Tell the server we're sending JSON. - headers: { - 'Content-Type': 'application/json', - }, - // The method is POST because we are sending data. - method: 'POST', - }) - - // Get the response data from server as JSON. - // If server returns the name submitted, that means the form works. - const result = await response.json() - alert(`Is this your full name: ${result.data}`) - } - return ( -
-

- Form with JavaScript. -

- -

- Get started by looking at{' '} - pages/js-form.js -

- -
- - - - - -
-
- ) -} diff --git a/examples/next-forms/pages/no-js-form.tsx b/examples/next-forms/pages/no-js-form.tsx deleted file mode 100644 index b32ab759f8c01..0000000000000 --- a/examples/next-forms/pages/no-js-form.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import Link from 'next/link' -import styles from '../styles/Home.module.css' - -export default function Form() { - return ( -
-

- Form without JavaScript. -

-

- Get started by looking at{' '} - pages/no-js-form.js -

- - {/* - * action: The action attribute defines where the data gets sent. - * Its value must be a valid relative or absolute URL. - * If this attribute isn't provided, the data will be sent to the URL - * of the page containing the form — the current page. - * method: The HTTP method to submit the form with. (case insensitive) - */} -
- - - - - -
-
- ) -} diff --git a/examples/next-forms/public/vercel.svg b/examples/next-forms/public/vercel.svg deleted file mode 100644 index fbf0e25a651c2..0000000000000 --- a/examples/next-forms/public/vercel.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/examples/next-forms/styles/Home.module.css b/examples/next-forms/styles/Home.module.css deleted file mode 100644 index 35454bb748190..0000000000000 --- a/examples/next-forms/styles/Home.module.css +++ /dev/null @@ -1,121 +0,0 @@ -.container { - min-height: 100vh; - padding: 0 0.5rem; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - height: 100vh; -} - -.main { - padding: 5rem 0; - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.footer { - width: 100%; - height: 100px; - border-top: 1px solid #eaeaea; - display: flex; - justify-content: center; - align-items: center; -} - -.footer a { - display: flex; - justify-content: center; - align-items: center; - flex-grow: 1; -} - -.title a { - color: #0070f3; - text-decoration: none; -} - -.title a:hover, -.title a:focus, -.title a:active { - text-decoration: underline; -} - -.title { - margin: 0; - line-height: 1.15; - font-size: 4rem; -} - -.title, -.description { - text-align: center; -} - -.description { - line-height: 1.5; - font-size: 1.5rem; -} - -.code { - background: #fafafa; - border-radius: 5px; - padding: 0.75rem; - font-size: 1.1rem; - font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, - Bitstream Vera Sans Mono, Courier New, monospace; -} - -.grid { - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - max-width: 800px; - margin-top: 3rem; -} - -.card { - margin: 1rem; - padding: 1.5rem; - text-align: left; - color: inherit; - text-decoration: none; - border: 1px solid #eaeaea; - border-radius: 10px; - transition: color 0.15s ease, border-color 0.15s ease; - width: 45%; -} - -.card:hover, -.card:focus, -.card:active { - color: #0070f3; - border-color: #0070f3; -} - -.card h2 { - margin: 0 0 1rem 0; - font-size: 1.5rem; -} - -.card p { - margin: 0; - font-size: 1.25rem; - line-height: 1.5; -} - -.logo { - height: 1em; - margin-left: 0.5rem; -} - -@media (max-width: 600px) { - .grid { - width: 100%; - flex-direction: column; - } -} diff --git a/examples/next-forms/styles/globals.css b/examples/next-forms/styles/globals.css deleted file mode 100644 index 1114e3bad5dc9..0000000000000 --- a/examples/next-forms/styles/globals.css +++ /dev/null @@ -1,72 +0,0 @@ -html, -body { - padding: 0; - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, - Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; -} - -a { - color: inherit; - text-decoration: none; -} - -* { - box-sizing: border-box; -} - -form { - display: table; - min-width: 350px; - margin: 0 auto; - padding: 1rem; -} - -label { - display: table; - width: 100%; - margin-bottom: 0.5rem; - color: #3b3b3b; -} - -input { - display: table; - width: 100%; - padding: 0.5rem; - border: 1px solid #ccc; - border-radius: 0.25rem; - box-sizing: border-box; - margin-bottom: 2.5rem; -} - -input:focus { - outline: none; - border-color: #0070f3; -} - -button { - display: table; - width: 100%; - padding: 0.5rem; - border: none; - border-radius: 0.25rem; - box-sizing: border-box; - margin-bottom: 2.5rem; - background-color: #0070f3; - color: #fff; - cursor: pointer; -} - -button:hover { - background-color: #0060e9; -} - -.container { - min-height: 100vh; - padding: 0 0.5rem; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - height: 100vh; -} diff --git a/examples/next-forms/tsconfig.json b/examples/next-forms/tsconfig.json index 99710e857874f..e06a4454ab062 100644 --- a/examples/next-forms/tsconfig.json +++ b/examples/next-forms/tsconfig.json @@ -13,8 +13,16 @@ "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", - "incremental": true + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } diff --git a/examples/with-stripe-typescript/README.md b/examples/with-stripe-typescript/README.md index f193e94edb1a2..77da54231b2aa 100644 --- a/examples/with-stripe-typescript/README.md +++ b/examples/with-stripe-typescript/README.md @@ -6,7 +6,7 @@ This is a full-stack TypeScript example using: - Next.js - [react-stripe-js](https://github.com/stripe/react-stripe-js) for [Checkout](https://stripe.com/checkout) and [Elements](https://stripe.com/elements) - Backend - - Next.js [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) and [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions) + - Next.js [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) and [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/forms-and-mutations) - [stripe-node with TypeScript](https://github.com/stripe/stripe-node#usage-with-typescript) ## Demo diff --git a/packages/next-swc/crates/core/src/server_actions.rs b/packages/next-swc/crates/core/src/server_actions.rs index 56280a85d6990..2e6009812c542 100644 --- a/packages/next-swc/crates/core/src/server_actions.rs +++ b/packages/next-swc/crates/core/src/server_actions.rs @@ -110,7 +110,7 @@ impl ServerActions { handler .struct_span_err( body.span, - "It is not allowed to define inline \"use server\" annotated Server Actions in Client Components.\nTo use Server Actions in a Client Component, you can either export them from a separate file with \"use server\" at the top, or pass them down through props from a Server Component.\n\nRead more: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#with-client-components\n", + "It is not allowed to define inline \"use server\" annotated Server Actions in Client Components.\nTo use Server Actions in a Client Component, you can either export them from a separate file with \"use server\" at the top, or pass them down through props from a Server Component.\n\nRead more: https://nextjs.org/docs/app/api-reference/server-actions#with-client-components\n", ) .emit() }); @@ -1261,7 +1261,7 @@ fn remove_server_directive_index_in_module( handler .struct_span_err( *span, - "To use Server Actions, please enable the feature flag in your Next.js config. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#convention", + "To use Server Actions, please enable the feature flag in your Next.js config. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching/forms-and-mutations#convention", ) .emit() }); @@ -1360,7 +1360,7 @@ fn remove_server_directive_index_in_fn( handler .struct_span_err( *span, - "To use Server Actions, please enable the feature flag in your Next.js config. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#convention", + "To use Server Actions, please enable the feature flag in your Next.js config. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching/forms-and-mutations#convention", ) .emit() }); diff --git a/packages/next-swc/crates/core/tests/errors/server-actions/client-graph/1/output.stderr b/packages/next-swc/crates/core/tests/errors/server-actions/client-graph/1/output.stderr index 7d6701bc06260..8deb33bca9a6d 100644 --- a/packages/next-swc/crates/core/tests/errors/server-actions/client-graph/1/output.stderr +++ b/packages/next-swc/crates/core/tests/errors/server-actions/client-graph/1/output.stderr @@ -2,7 +2,7 @@ x It is not allowed to define inline "use server" annotated Server Actions in Client Components. | To use Server Actions in a Client Component, you can either export them from a separate file with "use server" at the top, or pass them down through props from a Server Component. | - | Read more: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#with-client-components + | Read more: https://nextjs.org/docs/app/api-reference/server-actions#with-client-components | ,-[input.js:3:1] 3 | export default function App() { diff --git a/packages/next/src/server/app-render/action-handler.ts b/packages/next/src/server/app-render/action-handler.ts index 9d8a4d22e8f0b..eb0822e571fb2 100644 --- a/packages/next/src/server/app-render/action-handler.ts +++ b/packages/next/src/server/app-render/action-handler.ts @@ -385,7 +385,7 @@ export async function handleAction({ // Exceeded the size limit e.message = e.message + - '\nTo configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#size-limitation' + '\nTo configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/api-reference/server-actions#size-limitation' } throw e } diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index 45dbc0b227d6e..31ef671af8332 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -398,7 +398,7 @@ function assignDefaults( ) if (isNaN(value) || value < 1) { throw new Error( - 'Server Actions Size Limit must be a valid number or filesize format lager than 1MB: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions' + 'Server Actions Size Limit must be a valid number or filesize format lager than 1MB: https://nextjs.org/docs/app/api-reference/server-actions#size-limitation' ) } } From eee137615e330af5fe2a50de636edbab554fdb74 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 25 Aug 2023 12:45:58 -0700 Subject: [PATCH 255/387] Update release stats install/build executing (#54576) Aims to make debugging the release stats failure easier as currently we don't see the logs are the command is executing x-ref: https://github.com/vercel/next.js/actions/runs/5976635577/job/16221230094 x-ref: https://github.com/vercel/next.js/pull/54536 --- .../actions/next-stats-action/src/index.js | 20 ++++++------------- .../next-stats-action/src/util/exec.js | 14 +++++++++++++ .github/workflows/build_and_deploy.yml | 2 ++ .github/workflows/pull_request_stats.yml | 1 + 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/.github/actions/next-stats-action/src/index.js b/.github/actions/next-stats-action/src/index.js index 3e66f1ca0c880..402c7cfadfcbc 100644 --- a/.github/actions/next-stats-action/src/index.js +++ b/.github/actions/next-stats-action/src/index.js @@ -7,14 +7,8 @@ const addComment = require('./add-comment') const actionInfo = require('./prepare/action-info')() const { mainRepoDir, diffRepoDir } = require('./constants') const loadStatsConfig = require('./prepare/load-stats-config') -const { - cloneRepo, - checkoutRef, - mergeBranch, - getCommitId, - linkPackages, - getLastStable, -} = require('./prepare/repo-setup')(actionInfo) +const { cloneRepo, mergeBranch, getCommitId, linkPackages, getLastStable } = + require('./prepare/repo-setup')(actionInfo) const allowedActions = new Set(['synchronize', 'opened']) @@ -109,21 +103,19 @@ if (!allowedActions.has(actionInfo.actionName) && !actionInfo.isRelease) { const usePnpm = await fs.pathExists(path.join(dir, 'pnpm-lock.yaml')) if (!statsConfig.skipInitialInstall) { - await exec( + await exec.spawnPromise( `cd ${dir}${ usePnpm ? // --no-frozen-lockfile is used here to tolerate lockfile // changes from merging latest changes ` && pnpm install --no-frozen-lockfile` : ' && yarn install --network-timeout 1000000' - }`, - false + }` ) - await exec( + await exec.spawnPromise( statsConfig.initialBuildCommand || - `cd ${dir} && ${usePnpm ? 'pnpm build' : 'echo built'}`, - false + `cd ${dir} && ${usePnpm ? 'pnpm build' : 'echo built'}` ) } } diff --git a/.github/actions/next-stats-action/src/util/exec.js b/.github/actions/next-stats-action/src/util/exec.js index a944a4e7ae0ce..8daacf12a4098 100644 --- a/.github/actions/next-stats-action/src/util/exec.js +++ b/.github/actions/next-stats-action/src/util/exec.js @@ -34,4 +34,18 @@ exec.spawn = function spawn(command = '', opts = {}) { return child } +exec.spawnPromise = function spawnPromise(command = '', opts = {}) { + return new Promise((resolve, reject) => { + const child = exec.spawn(command) + child.on('exit', (code, signal) => { + if (code || signal) { + return reject( + new Error(`bad exit code/signal code: ${code} signal: ${signal}`) + ) + } + resolve() + }) + }) +} + module.exports = exec diff --git a/.github/workflows/build_and_deploy.yml b/.github/workflows/build_and_deploy.yml index f5a0022c428c1..9e50e929140d5 100644 --- a/.github/workflows/build_and_deploy.yml +++ b/.github/workflows/build_and_deploy.yml @@ -229,6 +229,7 @@ jobs: name: stable - ${{ matrix.settings.target }} - node@16 runs-on: ${{ matrix.settings.host }} + timeout-minutes: 30 steps: # https://github.com/actions/virtual-environments/issues/1187 - name: tune linux network @@ -509,6 +510,7 @@ jobs: - 'linux' - 'x64' - 'metal' + timeout-minutes: 25 needs: [publishRelease] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/pull_request_stats.yml b/.github/workflows/pull_request_stats.yml index 42bff979d0dea..49489b5df8221 100644 --- a/.github/workflows/pull_request_stats.yml +++ b/.github/workflows/pull_request_stats.yml @@ -31,6 +31,7 @@ jobs: stats: name: PR Stats needs: build + timeout-minutes: 25 runs-on: - 'self-hosted' - 'linux' From b4a5663c5f0890f9799e8d0d8c0ce4e7f71bb410 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Fri, 25 Aug 2023 22:29:39 +0200 Subject: [PATCH 256/387] `optimize_barrel` SWC transform and new `optimizePackageImports` config (#54572) ## Implementation Base on #54530, we're implementing a `optimize_barrel` transform to optimize barrel files to only export the names we need. If the transformed file isn't a "barrel file", we just re-export the names from it without any transformation. Take `lucide-react` as an example, with #54530 we are able to transform ```js import { IceCream } from 'lucide-react' ``` to ```js import { IceCream } from '__barrel_optimize__?names=IceCream!=!lucide-react?__barrel_optimize_noop__=IceCream' ``` And then, we apply that new request with a new Webpack module rule to use the SWC loader with option `optimizeBarrelExports: ['IceCream']`, which eventually got passed to this new `optimize_barrel` transform and does the optimization. ## Notes We'll have to add a new `getModularizeImportAliases` alias list to map `lucide-react` to the ESM version, as we have the `['main', 'module']` resolve order for the server compiler. Otherwise this optimization doesn't work in that compiler. There's no e2e test added because it's already covered by the `modularize-imports` test as we removed the default `lucide-react` transform rules and it still works. We'll need to test other libs before migrating them to the new `optimizePackageImports` option. --- Closes #54571, closes #53605, closes #53789, closes #53894, closes #54063. --- packages/next-swc/crates/core/src/lib.rs | 9 + .../crates/core/src/named_import_transform.rs | 6 +- .../crates/core/src/optimize_barrel.rs | 268 ++++++++++++++++++ .../next-swc/crates/core/tests/fixture.rs | 30 ++ .../named-import-transform/1/output.js | 2 +- .../named-import-transform/2/output.js | 4 +- .../tests/fixture/optimize-barrel/1/input.js | 3 + .../tests/fixture/optimize-barrel/1/output.js | 3 + .../tests/fixture/optimize-barrel/2/input.js | 6 + .../tests/fixture/optimize-barrel/2/output.js | 2 + .../tests/fixture/optimize-barrel/3/input.js | 6 + .../tests/fixture/optimize-barrel/3/output.js | 2 + .../tests/fixture/optimize-barrel/4/input.js | 9 + .../tests/fixture/optimize-barrel/4/output.js | 7 + .../tests/fixture/optimize-barrel/5/input.js | 2 + .../tests/fixture/optimize-barrel/5/output.js | 2 + packages/next-swc/crates/core/tests/full.rs | 1 + packages/next/src/build/swc/options.ts | 21 +- packages/next/src/build/webpack-config.ts | 63 +++- .../webpack/loaders/barrel-optimize-loader.ts | 5 - .../build/webpack/loaders/next-swc-loader.ts | 3 + packages/next/src/server/config-schema.ts | 3 + packages/next/src/server/config-shared.ts | 5 + packages/next/src/server/config.ts | 51 +--- .../auto-modularize-imports/app/layout.js | 12 - .../basic/auto-modularize-imports/app/page.js | 11 - 26 files changed, 449 insertions(+), 87 deletions(-) create mode 100644 packages/next-swc/crates/core/src/optimize_barrel.rs create mode 100644 packages/next-swc/crates/core/tests/fixture/optimize-barrel/1/input.js create mode 100644 packages/next-swc/crates/core/tests/fixture/optimize-barrel/1/output.js create mode 100644 packages/next-swc/crates/core/tests/fixture/optimize-barrel/2/input.js create mode 100644 packages/next-swc/crates/core/tests/fixture/optimize-barrel/2/output.js create mode 100644 packages/next-swc/crates/core/tests/fixture/optimize-barrel/3/input.js create mode 100644 packages/next-swc/crates/core/tests/fixture/optimize-barrel/3/output.js create mode 100644 packages/next-swc/crates/core/tests/fixture/optimize-barrel/4/input.js create mode 100644 packages/next-swc/crates/core/tests/fixture/optimize-barrel/4/output.js create mode 100644 packages/next-swc/crates/core/tests/fixture/optimize-barrel/5/input.js create mode 100644 packages/next-swc/crates/core/tests/fixture/optimize-barrel/5/output.js delete mode 100644 packages/next/src/build/webpack/loaders/barrel-optimize-loader.ts delete mode 100644 test/development/basic/auto-modularize-imports/app/layout.js delete mode 100644 test/development/basic/auto-modularize-imports/app/page.js diff --git a/packages/next-swc/crates/core/src/lib.rs b/packages/next-swc/crates/core/src/lib.rs index 019ca39f214cc..11aea351c7b38 100644 --- a/packages/next-swc/crates/core/src/lib.rs +++ b/packages/next-swc/crates/core/src/lib.rs @@ -59,6 +59,7 @@ pub mod disallow_re_export_all_in_page; pub mod named_import_transform; pub mod next_dynamic; pub mod next_ssg; +pub mod optimize_barrel; pub mod page_config; pub mod react_remove_properties; pub mod react_server_components; @@ -132,6 +133,9 @@ pub struct TransformOptions { #[serde(default)] pub auto_modularize_imports: Option, + #[serde(default)] + pub optimize_barrel_exports: Option, + #[serde(default)] pub font_loaders: Option, @@ -253,6 +257,11 @@ where Some(config) => Either::Left(named_import_transform::named_import_transform(config.clone())), None => Either::Right(noop()), }, + match &opts.optimize_barrel_exports { + Some(config) => Either::Left(optimize_barrel::optimize_barrel( + file.name.clone(),config.clone())), + None => Either::Right(noop()), + }, opts.emotion .as_ref() .and_then(|config| { diff --git a/packages/next-swc/crates/core/src/named_import_transform.rs b/packages/next-swc/crates/core/src/named_import_transform.rs index 1799d2d024e7c..4e95899cb4ee3 100644 --- a/packages/next-swc/crates/core/src/named_import_transform.rs +++ b/packages/next-swc/crates/core/src/named_import_transform.rs @@ -60,10 +60,10 @@ impl Fold for NamedImportTransform { } if !skip_transform { + let names = specifier_names.join(","); let new_src = format!( - "barrel-optimize-loader?names={}!{}", - specifier_names.join(","), - src_value + "__barrel_optimize__?names={}!=!{}?__barrel_optimize_noop__={}", + names, src_value, names, ); // Create a new import declaration, keep everything the same except the source diff --git a/packages/next-swc/crates/core/src/optimize_barrel.rs b/packages/next-swc/crates/core/src/optimize_barrel.rs new file mode 100644 index 0000000000000..0fee0a2e3e955 --- /dev/null +++ b/packages/next-swc/crates/core/src/optimize_barrel.rs @@ -0,0 +1,268 @@ +use serde::Deserialize; +use turbopack_binding::swc::core::{ + common::{FileName, DUMMY_SP}, + ecma::{ + ast::*, + utils::{private_ident, quote_str}, + visit::Fold, + }, +}; + +#[derive(Clone, Debug, Deserialize)] +pub struct Config { + pub names: Vec, +} + +pub fn optimize_barrel(filename: FileName, config: Config) -> impl Fold { + OptimizeBarrel { + filepath: filename.to_string(), + names: config.names, + } +} + +#[derive(Debug, Default)] +struct OptimizeBarrel { + filepath: String, + names: Vec, +} + +impl Fold for OptimizeBarrel { + fn fold_module_items(&mut self, items: Vec) -> Vec { + // One pre-pass to find all the local idents that we are referencing. + let mut local_idents = vec![]; + for item in &items { + if let ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export_named)) = item { + if export_named.src.is_none() { + for spec in &export_named.specifiers { + if let ExportSpecifier::Named(s) = spec { + let str_name; + if let Some(name) = &s.exported { + str_name = match &name { + ModuleExportName::Ident(n) => n.sym.to_string(), + ModuleExportName::Str(n) => n.value.to_string(), + }; + } else { + str_name = match &s.orig { + ModuleExportName::Ident(n) => n.sym.to_string(), + ModuleExportName::Str(n) => n.value.to_string(), + }; + } + + // If the exported name needs to be kept, track the local ident. + if self.names.contains(&str_name) { + if let ModuleExportName::Ident(i) = &s.orig { + local_idents.push(i.sym.clone()); + } + } + } + } + } + } + } + + // The second pass to rebuild the module items. + let mut new_items = vec![]; + + // We only apply this optimization to barrel files. Here we consider + // a barrel file to be a file that only exports from other modules. + // Besides that, lit expressions are allowed as well ("use client", etc.). + let mut is_barrel = true; + for item in &items { + match item { + ModuleItem::ModuleDecl(decl) => { + match decl { + // export { foo } from './foo'; + ModuleDecl::ExportNamed(export_named) => { + for spec in &export_named.specifiers { + match spec { + ExportSpecifier::Namespace(s) => { + let name_str = match &s.name { + ModuleExportName::Ident(n) => n.sym.to_string(), + ModuleExportName::Str(n) => n.value.to_string(), + }; + if self.names.contains(&name_str) { + new_items.push(item.clone()); + } + } + ExportSpecifier::Named(s) => { + if let Some(name) = &s.exported { + let name_str = match &name { + ModuleExportName::Ident(n) => n.sym.to_string(), + ModuleExportName::Str(n) => n.value.to_string(), + }; + + if self.names.contains(&name_str) { + new_items.push(ModuleItem::ModuleDecl( + ModuleDecl::ExportNamed(NamedExport { + span: DUMMY_SP, + specifiers: vec![ExportSpecifier::Named( + ExportNamedSpecifier { + span: DUMMY_SP, + orig: s.orig.clone(), + exported: Some( + ModuleExportName::Ident( + Ident::new( + name_str.into(), + DUMMY_SP, + ), + ), + ), + is_type_only: false, + }, + )], + src: export_named.src.clone(), + type_only: false, + asserts: None, + }), + )); + } + } else { + let name_str = match &s.orig { + ModuleExportName::Ident(n) => n.sym.to_string(), + ModuleExportName::Str(n) => n.value.to_string(), + }; + + if self.names.contains(&name_str) { + new_items.push(ModuleItem::ModuleDecl( + ModuleDecl::ExportNamed(NamedExport { + span: DUMMY_SP, + specifiers: vec![ExportSpecifier::Named( + ExportNamedSpecifier { + span: DUMMY_SP, + orig: s.orig.clone(), + exported: None, + is_type_only: false, + }, + )], + src: export_named.src.clone(), + type_only: false, + asserts: None, + }), + )); + } + } + } + _ => { + is_barrel = false; + break; + } + } + } + } + // Keep import statements that create the local idents we need. + ModuleDecl::Import(import_decl) => { + for spec in &import_decl.specifiers { + match spec { + ImportSpecifier::Named(s) => { + if local_idents.contains(&s.local.sym) { + new_items.push(ModuleItem::ModuleDecl( + ModuleDecl::Import(ImportDecl { + span: DUMMY_SP, + specifiers: vec![ImportSpecifier::Named( + ImportNamedSpecifier { + span: DUMMY_SP, + local: s.local.clone(), + imported: s.imported.clone(), + is_type_only: false, + }, + )], + src: import_decl.src.clone(), + type_only: false, + asserts: None, + }), + )); + } + } + ImportSpecifier::Default(s) => { + if local_idents.contains(&s.local.sym) { + new_items.push(ModuleItem::ModuleDecl( + ModuleDecl::Import(ImportDecl { + span: DUMMY_SP, + specifiers: vec![ImportSpecifier::Default( + ImportDefaultSpecifier { + span: DUMMY_SP, + local: s.local.clone(), + }, + )], + src: import_decl.src.clone(), + type_only: false, + asserts: None, + }), + )); + } + } + ImportSpecifier::Namespace(s) => { + if local_idents.contains(&s.local.sym) { + new_items.push(ModuleItem::ModuleDecl( + ModuleDecl::Import(ImportDecl { + span: DUMMY_SP, + specifiers: vec![ImportSpecifier::Namespace( + ImportStarAsSpecifier { + span: DUMMY_SP, + local: s.local.clone(), + }, + )], + src: import_decl.src.clone(), + type_only: false, + asserts: None, + }), + )); + } + } + } + } + } + _ => { + // Export expressions are not allowed in barrel files. + is_barrel = false; + break; + } + } + } + ModuleItem::Stmt(stmt) => match stmt { + Stmt::Expr(expr) => match &*expr.expr { + Expr::Lit(_) => { + new_items.push(item.clone()); + } + _ => { + is_barrel = false; + break; + } + }, + _ => { + is_barrel = false; + break; + } + }, + } + } + + // If the file is not a barrel file, we need to create a new module that + // re-exports from the original file. + // This is to avoid creating multiple instances of the original module. + if !is_barrel { + new_items = vec![ModuleItem::ModuleDecl(ModuleDecl::ExportNamed( + NamedExport { + span: DUMMY_SP, + specifiers: self + .names + .iter() + .map(|name| { + ExportSpecifier::Named(ExportNamedSpecifier { + span: DUMMY_SP, + orig: ModuleExportName::Ident(private_ident!(name.clone())), + exported: None, + is_type_only: false, + }) + }) + .collect(), + src: Some(Box::new(quote_str!(self.filepath.to_string()))), + type_only: false, + asserts: None, + }, + ))]; + } + + new_items + } +} diff --git a/packages/next-swc/crates/core/tests/fixture.rs b/packages/next-swc/crates/core/tests/fixture.rs index 9a38810175ff2..8046417205f1f 100644 --- a/packages/next-swc/crates/core/tests/fixture.rs +++ b/packages/next-swc/crates/core/tests/fixture.rs @@ -6,6 +6,7 @@ use next_swc::{ named_import_transform::named_import_transform, next_dynamic::next_dynamic, next_ssg::next_ssg, + optimize_barrel::optimize_barrel, page_config::page_config_test, react_remove_properties::remove_properties, react_server_components::server_components, @@ -481,6 +482,35 @@ fn named_import_transform_fixture(input: PathBuf) { ); } +#[fixture("tests/fixture/optimize-barrel/**/input.js")] +fn optimize_barrel_fixture(input: PathBuf) { + let output = input.parent().unwrap().join("output.js"); + test_fixture( + syntax(), + &|_tr| { + let unresolved_mark = Mark::new(); + let top_level_mark = Mark::new(); + + chain!( + resolver(unresolved_mark, top_level_mark, false), + optimize_barrel( + FileName::Real(PathBuf::from("/some-project/node_modules/foo/file.js")), + json( + r#" + { + "names": ["x", "y", "z"] + } + "# + ) + ) + ) + }, + &input, + &output, + Default::default(), + ); +} + fn json(s: &str) -> T where T: DeserializeOwned, diff --git a/packages/next-swc/crates/core/tests/fixture/named-import-transform/1/output.js b/packages/next-swc/crates/core/tests/fixture/named-import-transform/1/output.js index f0d084c179e09..53fc2884247cf 100644 --- a/packages/next-swc/crates/core/tests/fixture/named-import-transform/1/output.js +++ b/packages/next-swc/crates/core/tests/fixture/named-import-transform/1/output.js @@ -1,3 +1,3 @@ -import { A, B, C as F } from "barrel-optimize-loader?names=A,B,C!foo"; +import { A, B, C as F } from "__barrel_optimize__?names=A,B,C!=!foo?__barrel_optimize_noop__=A,B,C"; import D from 'bar'; import E from 'baz'; diff --git a/packages/next-swc/crates/core/tests/fixture/named-import-transform/2/output.js b/packages/next-swc/crates/core/tests/fixture/named-import-transform/2/output.js index f2874acce5044..b4d1a9b11dece 100644 --- a/packages/next-swc/crates/core/tests/fixture/named-import-transform/2/output.js +++ b/packages/next-swc/crates/core/tests/fixture/named-import-transform/2/output.js @@ -1,3 +1,3 @@ -import { A, B, C as F } from "barrel-optimize-loader?names=A,B,C!foo"; -import { D } from "barrel-optimize-loader?names=D!bar"; +import { A, B, C as F } from "__barrel_optimize__?names=A,B,C!=!foo?__barrel_optimize_noop__=A,B,C"; +import { D } from "__barrel_optimize__?names=D!=!bar?__barrel_optimize_noop__=D"; import E from 'baz'; diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/1/input.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/1/input.js new file mode 100644 index 0000000000000..747c0573c9167 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/1/input.js @@ -0,0 +1,3 @@ +export { foo, b as y } from './1' +export { x, a } from './2' +export { z } diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/1/output.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/1/output.js new file mode 100644 index 0000000000000..0edcd356f6fc2 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/1/output.js @@ -0,0 +1,3 @@ +export { b as y } from './1'; +export { x } from './2'; +export { z }; diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/2/input.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/2/input.js new file mode 100644 index 0000000000000..6f255555ee995 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/2/input.js @@ -0,0 +1,6 @@ +// De-optimize this file +const foo = 1 + +export { foo, b as y } from './1' +export { x, a } from './2' +export { z } diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/2/output.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/2/output.js new file mode 100644 index 0000000000000..3f5d1a74b87bd --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/2/output.js @@ -0,0 +1,2 @@ +// De-optimize this file +export { x, y, z } from "/some-project/node_modules/foo/file.js"; diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/3/input.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/3/input.js new file mode 100644 index 0000000000000..630019266de2e --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/3/input.js @@ -0,0 +1,6 @@ +// De-optimize this file +export * from 'x' + +export { foo, b as y } from './1' +export { x, a } from './2' +export { z } diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/3/output.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/3/output.js new file mode 100644 index 0000000000000..3f5d1a74b87bd --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/3/output.js @@ -0,0 +1,2 @@ +// De-optimize this file +export { x, y, z } from "/some-project/node_modules/foo/file.js"; diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/4/input.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/4/input.js new file mode 100644 index 0000000000000..bd460bc9a27bd --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/4/input.js @@ -0,0 +1,9 @@ +'use client' + +import foo, { a, b } from 'foo' +import z from 'bar' + +export { a as x } +export { y } from '1' +export { b } +export { foo as default, z } diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/4/output.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/4/output.js new file mode 100644 index 0000000000000..2a8069f942f66 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/4/output.js @@ -0,0 +1,7 @@ +'use client'; +import { a } from 'foo' +import z from 'bar' + +export { a as x }; +export { y } from '1'; +export { z }; diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/5/input.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/5/input.js new file mode 100644 index 0000000000000..605b446a453c6 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/5/input.js @@ -0,0 +1,2 @@ +import * as index from './icons/index.js' +export { index as x } diff --git a/packages/next-swc/crates/core/tests/fixture/optimize-barrel/5/output.js b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/5/output.js new file mode 100644 index 0000000000000..605b446a453c6 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/optimize-barrel/5/output.js @@ -0,0 +1,2 @@ +import * as index from './icons/index.js' +export { index as x } diff --git a/packages/next-swc/crates/core/tests/full.rs b/packages/next-swc/crates/core/tests/full.rs index b7cb7f4a5d87f..92af9032f1e90 100644 --- a/packages/next-swc/crates/core/tests/full.rs +++ b/packages/next-swc/crates/core/tests/full.rs @@ -79,6 +79,7 @@ fn test(input: &Path, minify: bool) { server_actions: None, cjs_require_optimizer: None, auto_modularize_imports: None, + optimize_barrel_exports: None, }; let unresolved_mark = Mark::new(); diff --git a/packages/next/src/build/swc/options.ts b/packages/next/src/build/swc/options.ts index 62a5a86f9be78..6af12afce99e6 100644 --- a/packages/next/src/build/swc/options.ts +++ b/packages/next/src/build/swc/options.ts @@ -301,6 +301,7 @@ export function getLoaderSWCOptions({ isPageFile, hasReactRefresh, modularizeImports, + optimizePackageImports, swcPlugins, compilerOptions, jsConfig, @@ -310,6 +311,7 @@ export function getLoaderSWCOptions({ hasServerComponents, isServerLayer, isServerActionsEnabled, + optimizeBarrelExports, }: // This is not passed yet as "paths" resolving is handled by webpack currently. // resolvedBaseUrl, { @@ -321,6 +323,9 @@ export function getLoaderSWCOptions({ isPageFile: boolean hasReactRefresh: boolean modularizeImports: NextConfig['modularizeImports'] + optimizePackageImports?: NonNullable< + NextConfig['experimental'] + >['optimizePackageImports'] swcPlugins: ExperimentalConfig['swcPlugins'] compilerOptions: NextConfig['compiler'] jsConfig: any @@ -330,6 +335,7 @@ export function getLoaderSWCOptions({ hasServerComponents?: boolean isServerLayer: boolean isServerActionsEnabled?: boolean + optimizeBarrelExports?: string[] }) { let baseOptions: any = getBaseSWCOptions({ filename, @@ -370,10 +376,17 @@ export function getLoaderSWCOptions({ }, }, } - baseOptions.autoModularizeImports = { - packages: [ - // TODO: Add a list of packages that should be optimized by default - ], + + // Modularize import optimization for barrel files + if (optimizePackageImports) { + baseOptions.autoModularizeImports = { + packages: optimizePackageImports, + } + } + if (optimizeBarrelExports) { + baseOptions.optimizeBarrelExports = { + names: optimizeBarrelExports, + } } const isNextDist = nextDistPath.test(filename) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 47cc4377ccbf0..2ca96b4d71d94 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -532,6 +532,27 @@ function getOptimizedAliases(): { [pkg: string]: string } { ) } +// Alias these modules to be resolved with "module" if possible. +function getModularizeImportAliases(packages: string[]) { + const aliases: { [pkg: string]: string } = {} + const mainFields = ['module', 'main'] + + for (const pkg of packages) { + try { + const descriptionFileData = require(`${pkg}/package.json`) + + for (const field of mainFields) { + if (descriptionFileData.hasOwnProperty(field)) { + aliases[pkg] = `${pkg}/${descriptionFileData[field]}` + break + } + } + } catch {} + } + + return aliases +} + export function attachReactRefresh( webpackConfig: webpack.Configuration, targetLoader: webpack.RuleSetUseItem @@ -1165,6 +1186,14 @@ export default async function getBaseWebpackConfig( ...(isClient || isEdgeServer ? getOptimizedAliases() : {}), ...(reactProductionProfiling ? getReactProfilingInProduction() : {}), + // For Node server, we need to re-alias the package imports to prefer to + // resolve to the module export. + ...(isNodeServer + ? getModularizeImportAliases( + config.experimental.optimizePackageImports || [] + ) + : {}), + [RSC_ACTION_VALIDATE_ALIAS]: 'next/dist/build/webpack/loaders/next-flight-loader/action-validate', @@ -1397,6 +1426,12 @@ export default async function getBaseWebpackConfig( return } + // __barrel_optimize__ is a special marker that tells Next.js to + // optimize the import by removing unused exports. This has to be compiled. + if (request.startsWith('__barrel_optimize__')) { + return + } + // When in esm externals mode, and using import, we resolve with // ESM resolving options. // Also disable esm request when appDir is enabled @@ -1936,7 +1971,6 @@ export default async function getBaseWebpackConfig( 'next-invalid-import-error-loader', 'next-metadata-route-loader', 'modularize-import-loader', - 'barrel-optimize-loader', ].reduce((alias, loader) => { // using multiple aliases to replace `resolveLoader.modules` alias[loader] = path.join(__dirname, 'webpack', 'loaders', loader) @@ -1951,6 +1985,25 @@ export default async function getBaseWebpackConfig( }, module: { rules: [ + { + test: /__barrel_optimize__/, + use: ({ + resourceQuery, + issuerLayer, + }: { + resourceQuery: string + issuerLayer: string + }) => { + const names = resourceQuery.slice('?names='.length).split(',') + return [ + getSwcLoader({ + isServerLayer: + issuerLayer === WEBPACK_LAYERS.reactServerComponents, + optimizeBarrelExports: names, + }), + ] + }, + }, ...(hasAppDir ? [ { @@ -2006,9 +2059,7 @@ export default async function getBaseWebpackConfig( ...(hasAppDir && !isClient ? [ { - issuerLayer: { - or: [isWebpackServerLayer], - }, + issuerLayer: isWebpackServerLayer, test: { // Resolve it if it is a source code file, and it has NOT been // opted out of bundling. @@ -2144,9 +2195,7 @@ export default async function getBaseWebpackConfig( ? [ { test: codeCondition.test, - issuerLayer: { - or: [isWebpackServerLayer], - }, + issuerLayer: isWebpackServerLayer, exclude: [asyncStoragesRegex], use: swcLoaderForServerLayer, }, diff --git a/packages/next/src/build/webpack/loaders/barrel-optimize-loader.ts b/packages/next/src/build/webpack/loaders/barrel-optimize-loader.ts deleted file mode 100644 index e294d7f7b9675..0000000000000 --- a/packages/next/src/build/webpack/loaders/barrel-optimize-loader.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default function transformSource(this: any, source: string) { - // const { names }: any = this.getOptions() - // const { resourcePath } = this - return source -} diff --git a/packages/next/src/build/webpack/loaders/next-swc-loader.ts b/packages/next/src/build/webpack/loaders/next-swc-loader.ts index 0c054724a8e46..9d26ab993f28b 100644 --- a/packages/next/src/build/webpack/loaders/next-swc-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-swc-loader.ts @@ -53,6 +53,7 @@ async function loaderTransform( swcCacheDir, hasServerComponents, isServerLayer, + optimizeBarrelExports, } = loaderOptions const isPageFile = filename.startsWith(pagesDir) const relativeFilePathFromRoot = path.relative(rootDir, filename) @@ -66,6 +67,7 @@ async function loaderTransform( development: this.mode === 'development', hasReactRefresh, modularizeImports: nextConfig?.modularizeImports, + optimizePackageImports: nextConfig?.experimental?.optimizePackageImports, swcPlugins: nextConfig?.experimental?.swcPlugins, compilerOptions: nextConfig?.compiler, jsConfig, @@ -75,6 +77,7 @@ async function loaderTransform( hasServerComponents, isServerActionsEnabled: nextConfig?.experimental?.serverActions, isServerLayer, + optimizeBarrelExports, }) const programmaticOptions = { diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index d51ee7c759595..b2f974bc69c2d 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -473,6 +473,9 @@ const configSchema = { }, }, }, + optimizePackageImports: { + type: 'array', + }, instrumentationHook: { type: 'boolean', }, diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 870528c8c2040..a9e34c7c0b6c6 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -236,6 +236,11 @@ export interface ExperimentalConfig { webVitalsAttribution?: Array<(typeof WEB_VITALS)[number]> + /** + * Automatically apply the "modularizeImports" optimization to imports of the specified packages. + */ + optimizePackageImports?: string[] + turbo?: ExperimentalTurboOptions turbotrace?: { logLevel?: diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index 31ef671af8332..b5b5e787b5aa4 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -678,48 +678,6 @@ function assignDefaults( 'lodash-es': { transform: 'lodash-es/{{member}}', }, - 'lucide-react': { - // Note that we need to first resolve to the base path (`lucide-react`) and join the subpath, - // instead of just resolving `lucide-react/esm/icons/{{kebabCase member}}` because this package - // doesn't have proper `exports` fields for individual icons in its package.json. - transform: { - // Special aliases - '(SortAsc|LucideSortAsc|SortAscIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/arrow-up-narrow-wide!lucide-react', - '(SortDesc|LucideSortDesc|SortDescIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/arrow-down-wide-narrow!lucide-react', - '(Verified|LucideVerified|VerifiedIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/badge-check!lucide-react', - '(Slash|LucideSlash|SlashIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/ban!lucide-react', - '(CurlyBraces|LucideCurlyBraces|CurlyBracesIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/braces!lucide-react', - '(CircleSlashed|LucideCircleSlashed|CircleSlashedIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/circle-slash-2!lucide-react', - '(SquareGantt|LucideSquareGantt|SquareGanttIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/gantt-chart-square!lucide-react', - '(SquareKanbanDashed|LucideSquareKanbanDashed|SquareKanbanDashedIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/kanban-square-dashed!lucide-react', - '(SquareKanban|LucideSquareKanban|SquareKanbanIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/kanban-square!lucide-react', - '(Edit3|LucideEdit3|Edit3Icon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/pen-line!lucide-react', - '(Edit|LucideEdit|EditIcon|PenBox|LucidePenBox|PenBoxIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/pen-square!lucide-react', - '(Edit2|LucideEdit2|Edit2Icon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/pen!lucide-react', - '(Stars|LucideStars|StarsIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/sparkles!lucide-react', - '(TextSelection|LucideTextSelection|TextSelectionIcon)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/text-select!lucide-react', - // General rules - 'Lucide(.*)': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/{{ kebabCase memberMatches.[1] }}!lucide-react', - '(.*)Icon': - 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/{{ kebabCase memberMatches.[1] }}!lucide-react', - '*': 'modularize-import-loader?name={{ member }}&from=default&as=default&join=../esm/icons/{{ kebabCase member }}!lucide-react', - }, - }, '@headlessui/react': { transform: { Transition: @@ -775,6 +733,15 @@ function assignDefaults( }, } + const userProvidedOptimizePackageImports = + result.experimental?.optimizePackageImports || [] + if (!result.experimental) { + result.experimental = {} + } + result.experimental.optimizePackageImports = [ + ...new Set([...userProvidedOptimizePackageImports, 'lucide-react']), + ] + return result } diff --git a/test/development/basic/auto-modularize-imports/app/layout.js b/test/development/basic/auto-modularize-imports/app/layout.js deleted file mode 100644 index 8525f5f8c0b2a..0000000000000 --- a/test/development/basic/auto-modularize-imports/app/layout.js +++ /dev/null @@ -1,12 +0,0 @@ -export const metadata = { - title: 'Next.js', - description: 'Generated by Next.js', -} - -export default function RootLayout({ children }) { - return ( - - {children} - - ) -} diff --git a/test/development/basic/auto-modularize-imports/app/page.js b/test/development/basic/auto-modularize-imports/app/page.js deleted file mode 100644 index 501ce5d145923..0000000000000 --- a/test/development/basic/auto-modularize-imports/app/page.js +++ /dev/null @@ -1,11 +0,0 @@ -'use client' - -import { IceCream } from 'lucide-react' - -export default function Page() { - return ( -
- -
- ) -} From 529a1be6c1309261d26d4a5a843439b97d7a234f Mon Sep 17 00:00:00 2001 From: vercel-release-bot Date: Fri, 25 Aug 2023 21:26:50 +0000 Subject: [PATCH 257/387] v13.4.20-canary.9 --- lerna.json | 2 +- packages/create-next-app/package.json | 2 +- packages/eslint-config-next/package.json | 4 ++-- packages/eslint-plugin-next/package.json | 2 +- packages/font/package.json | 2 +- packages/next-bundle-analyzer/package.json | 2 +- packages/next-codemod/package.json | 2 +- packages/next-env/package.json | 2 +- packages/next-mdx/package.json | 2 +- packages/next-plugin-storybook/package.json | 2 +- packages/next-polyfill-module/package.json | 2 +- packages/next-polyfill-nomodule/package.json | 2 +- packages/next-swc/package.json | 2 +- packages/next/package.json | 14 +++++++------- packages/react-dev-overlay/package.json | 2 +- packages/react-refresh-utils/package.json | 2 +- packages/third-parties/package.json | 4 ++-- pnpm-lock.yaml | 16 ++++++++-------- 18 files changed, 33 insertions(+), 33 deletions(-) diff --git a/lerna.json b/lerna.json index d9652b89f9cb5..7a7ee33fe2f00 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "13.4.20-canary.8" + "version": "13.4.20-canary.9" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 998d8b0f41717..9877b9b02c0ab 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "13.4.20-canary.8", + "version": "13.4.20-canary.9", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 70666bc2b68a2..2c0c51ac0d469 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "13.4.20-canary.8", + "version": "13.4.20-canary.9", "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": "13.4.20-canary.8", + "@next/eslint-plugin-next": "13.4.20-canary.9", "@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 f459e6734ff05..b208211eec6f4 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": "13.4.20-canary.8", + "version": "13.4.20-canary.9", "description": "ESLint plugin for NextJS.", "main": "dist/index.js", "license": "MIT", diff --git a/packages/font/package.json b/packages/font/package.json index ee456c481f39a..4514f8b01b56a 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,6 +1,6 @@ { "name": "@next/font", - "version": "13.4.20-canary.8", + "version": "13.4.20-canary.9", "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 8e7c91657e27b..56ffb33d2c75f 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "13.4.20-canary.8", + "version": "13.4.20-canary.9", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 924b22226028f..e71ada192e683 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "13.4.20-canary.8", + "version": "13.4.20-canary.9", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index abd60409c745d..148a28a942b08 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "13.4.20-canary.8", + "version": "13.4.20-canary.9", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index bfcd43f40b289..0501fecd8c141 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "13.4.20-canary.8", + "version": "13.4.20-canary.9", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 5eb4ae97e9c04..6292ace658b0b 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "13.4.20-canary.8", + "version": "13.4.20-canary.9", "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 c95d6943a3ee9..311631270df8e 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "13.4.20-canary.8", + "version": "13.4.20-canary.9", "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 b1881a9f9bec1..834034d1fe9b5 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "13.4.20-canary.8", + "version": "13.4.20-canary.9", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 9e6ffdcdb35e3..707d4dcee5399 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "13.4.20-canary.8", + "version": "13.4.20-canary.9", "private": true, "scripts": { "clean": "node ../../scripts/rm.mjs native", diff --git a/packages/next/package.json b/packages/next/package.json index ee09b0a3f2a2f..db2996031d4ad 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "13.4.20-canary.8", + "version": "13.4.20-canary.9", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -89,7 +89,7 @@ ] }, "dependencies": { - "@next/env": "13.4.20-canary.8", + "@next/env": "13.4.20-canary.9", "@swc/helpers": "0.5.1", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", @@ -144,11 +144,11 @@ "@mswjs/interceptors": "0.23.0", "@napi-rs/cli": "2.16.2", "@napi-rs/triples": "1.1.0", - "@next/polyfill-module": "13.4.20-canary.8", - "@next/polyfill-nomodule": "13.4.20-canary.8", - "@next/react-dev-overlay": "13.4.20-canary.8", - "@next/react-refresh-utils": "13.4.20-canary.8", - "@next/swc": "13.4.20-canary.8", + "@next/polyfill-module": "13.4.20-canary.9", + "@next/polyfill-nomodule": "13.4.20-canary.9", + "@next/react-dev-overlay": "13.4.20-canary.9", + "@next/react-refresh-utils": "13.4.20-canary.9", + "@next/swc": "13.4.20-canary.9", "@opentelemetry/api": "1.4.1", "@playwright/test": "^1.35.1", "@segment/ajv-human-errors": "2.1.2", diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index 039d7f8cc3916..af1f7ae304f84 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": "13.4.20-canary.8", + "version": "13.4.20-canary.9", "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 48995684dab40..3ece66dc1d2e8 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": "13.4.20-canary.8", + "version": "13.4.20-canary.9", "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 61fef045344a2..36fbe4788294e 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "13.4.20-canary.8", + "version": "13.4.20-canary.9", "private": true, "repository": { "url": "vercel/next.js", @@ -24,7 +24,7 @@ "third-party-capital": "1.0.17" }, "devDependencies": { - "next": "13.4.20-canary.8", + "next": "13.4.20-canary.9", "outdent": "0.8.0", "prettier": "2.5.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78ec09ba1c22f..3041fac5a35ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -726,7 +726,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 13.4.20-canary.8 + specifier: 13.4.20-canary.9 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.3.3 @@ -787,7 +787,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 13.4.20-canary.8 + specifier: 13.4.20-canary.9 version: link:../next-env '@swc/helpers': specifier: 0.5.1 @@ -914,19 +914,19 @@ importers: specifier: 1.1.0 version: 1.1.0 '@next/polyfill-module': - specifier: 13.4.20-canary.8 + specifier: 13.4.20-canary.9 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 13.4.20-canary.8 + specifier: 13.4.20-canary.9 version: link:../next-polyfill-nomodule '@next/react-dev-overlay': - specifier: 13.4.20-canary.8 + specifier: 13.4.20-canary.9 version: link:../react-dev-overlay '@next/react-refresh-utils': - specifier: 13.4.20-canary.8 + specifier: 13.4.20-canary.9 version: link:../react-refresh-utils '@next/swc': - specifier: 13.4.20-canary.8 + specifier: 13.4.20-canary.9 version: link:../next-swc '@opentelemetry/api': specifier: 1.4.1 @@ -1682,7 +1682,7 @@ importers: version: 1.0.17 devDependencies: next: - specifier: 13.4.20-canary.8 + specifier: 13.4.20-canary.9 version: link:../next outdent: specifier: 0.8.0 From 8afd85d47fbb4e2759258683eedcf94a40f316ba Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 25 Aug 2023 15:44:15 -0700 Subject: [PATCH 258/387] Add missing install env for release stats (#54581) This should fix the stalled action as it seems we aren't skipping this postinstall step when we should be which explains why it was passing for PR stats but not release stats x-ref: https://github.com/vercel/next.js/actions/runs/5980582756/job/16227150469 --- .github/workflows/build_and_deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build_and_deploy.yml b/.github/workflows/build_and_deploy.yml index 9e50e929140d5..45a57ae29e4ca 100644 --- a/.github/workflows/build_and_deploy.yml +++ b/.github/workflows/build_and_deploy.yml @@ -528,6 +528,7 @@ jobs: - uses: ./.github/actions/next-stats-action env: PR_STATS_COMMENT_TOKEN: ${{ secrets.PR_STATS_COMMENT_TOKEN }} + NEXT_SKIP_NATIVE_POSTINSTALL: 1 upload_turbopack_bytesize: name: Upload Turbopack Bytesize trace to Datadog From 1fe5f9511d708ffb2c261f4e3a79dd15888546a5 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Sat, 26 Aug 2023 13:45:46 +0200 Subject: [PATCH 259/387] Reuse edgeConditionNames variable (#54594) Small change to reuse the same array defined right above. Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/next/src/build/webpack-config.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 2ca96b4d71d94..e684b766fd995 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -115,12 +115,7 @@ const edgeConditionNames = [ const mainFieldsPerCompiler: Record = { [COMPILER_NAMES.server]: ['main', 'module'], [COMPILER_NAMES.client]: ['browser', 'module', 'main'], - [COMPILER_NAMES.edgeServer]: [ - 'edge-light', - 'worker', - // inherits the default conditions - '...', - ], + [COMPILER_NAMES.edgeServer]: edgeConditionNames, } const BABEL_CONFIG_FILES = [ From c8ff6591333cd906a53f6300b7298ad3648c6257 Mon Sep 17 00:00:00 2001 From: Vercel Release Bot <88769842+vercel-release-bot@users.noreply.github.com> Date: Sat, 26 Aug 2023 12:46:25 -0400 Subject: [PATCH 260/387] Update font data (#54585) This auto-generated PR updates font data with latest available --- packages/font/src/google/font-data.json | 5 +++++ packages/font/src/google/index.ts | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/packages/font/src/google/font-data.json b/packages/font/src/google/font-data.json index bfa950f0f7046..8f03ff077770e 100644 --- a/packages/font/src/google/font-data.json +++ b/packages/font/src/google/font-data.json @@ -10578,6 +10578,11 @@ "styles": ["normal"], "subsets": ["latin", "latin-ext"] }, + "Pushster": { + "weights": ["400"], + "styles": ["normal"], + "subsets": ["latin", "latin-ext"] + }, "Qahiri": { "weights": ["400"], "styles": ["normal"], diff --git a/packages/font/src/google/index.ts b/packages/font/src/google/index.ts index 2bd917b6e734d..e5f34e794268f 100644 --- a/packages/font/src/google/index.ts +++ b/packages/font/src/google/index.ts @@ -18231,6 +18231,18 @@ export declare function Purple_Purse< adjustFontFallback?: boolean subsets?: Array<'latin' | 'latin-ext'> }): T extends undefined ? NextFont : NextFontWithVariable +export declare function Pushster< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'latin-ext'> +}): T extends undefined ? NextFont : NextFontWithVariable export declare function Qahiri< T extends CssVariable | undefined = undefined >(options: { From f2766b979baa1dd7f03719e9529a3ff28c7ca5cd Mon Sep 17 00:00:00 2001 From: Vector73 <119798962+Vector73@users.noreply.github.com> Date: Sun, 27 Aug 2023 01:54:16 +0530 Subject: [PATCH 261/387] docs: Fixes typo in route handlers (#54605) --- .../01-routing/10-route-handlers.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-app/01-building-your-application/01-routing/10-route-handlers.mdx b/docs/02-app/01-building-your-application/01-routing/10-route-handlers.mdx index 5e53df7e3eb10..4a444cb9f72b5 100644 --- a/docs/02-app/01-building-your-application/01-routing/10-route-handlers.mdx +++ b/docs/02-app/01-building-your-application/01-routing/10-route-handlers.mdx @@ -388,7 +388,7 @@ export async function GET(request, { params }) { ### Streaming -Streaming is commonly used in combination with Large Language Models (LLMs), such an OpenAI, for AI-generated content. Learn more about the [AI SDK](https://sdk.vercel.ai/docs). +Streaming is commonly used in combination with Large Language Models (LLMs), such as OpenAI, for AI-generated content. Learn more about the [AI SDK](https://sdk.vercel.ai/docs). ```ts filename="app/api/chat/route.ts" switcher import { Configuration, OpenAIApi } from 'openai-edge' From eca06a56b25ee151d9bbf7c1b0c6c4e74aefa604 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Sat, 26 Aug 2023 23:28:46 +0200 Subject: [PATCH 262/387] Add cleanup logic to worker.ts (#54500) This implements the same cleanup logic used for start-server and render-workers for the workers used during build. It's more of a contingency as we do call `.end()` on the worker too. Fixes #45508 Co-authored-by: Zack Tanner <1939140+ztanner@users.noreply.github.com> --- packages/next/src/lib/worker.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/next/src/lib/worker.ts b/packages/next/src/lib/worker.ts index edba72894e76f..5beecff8c70ff 100644 --- a/packages/next/src/lib/worker.ts +++ b/packages/next/src/lib/worker.ts @@ -5,6 +5,14 @@ type FarmOptions = ConstructorParameters[1] const RESTARTED = Symbol('restarted') +const cleanupWorkers = (worker: JestWorker) => { + for (const curWorker of ((worker as any)._workerPool?._workers || []) as { + _child?: ChildProcess + }[]) { + curWorker._child?.kill('SIGINT') + } +} + export class Worker { private _worker: JestWorker | undefined @@ -123,6 +131,7 @@ export class Worker { if (!worker) { throw new Error('Farm is ended, no more calls can be done to it') } + cleanupWorkers(worker) this._worker = undefined return worker.end() } @@ -132,6 +141,7 @@ export class Worker { */ close(): void { if (this._worker) { + cleanupWorkers(this._worker) this._worker.end() } } From 6d401c1a4735ddaf0c7e479d64c4ae38d50105c0 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sun, 27 Aug 2023 00:55:21 +0200 Subject: [PATCH 263/387] chore: add extra error info for rsc info helper (#54609) When I was working on #54199 , if the file path is wrong the next app build error only shows "can't read file from ...", it took me hours to figure out where it actually fails in next build command. Adding an prefix for the error message so that could identify it easier. Also clean up other unused comments and incorrect named variables --- packages/next/src/build/analysis/get-page-static-info.ts | 3 ++- packages/next/src/build/index.ts | 1 - packages/next/src/server/lib/find-page-file.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/next/src/build/analysis/get-page-static-info.ts b/packages/next/src/build/analysis/get-page-static-info.ts index 6d6cc421f895f..ec08c72f4a6c6 100644 --- a/packages/next/src/build/analysis/get-page-static-info.ts +++ b/packages/next/src/build/analysis/get-page-static-info.ts @@ -286,8 +286,9 @@ async function tryToReadFile(filePath: string, shouldThrow: boolean) { return await fs.readFile(filePath, { encoding: 'utf8', }) - } catch (error) { + } catch (error: any) { if (shouldThrow) { + error.message = `Next.js ERROR: Failed to read file ${filePath}:\n${error.message}` throw error } } diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index c0fde0830c4c1..a86398b9d5658 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -2865,7 +2865,6 @@ export default async function build( // If there's /not-found inside app, we prefer it over the pages 404 if (hasStaticApp404) { - // await moveExportedPage('/_error', '/404', '/404', false, 'html') await moveExportedAppNotFoundTo404() } else { // Only move /404 to /404 when there is no custom 404 as in that case we don't know about the 404 page diff --git a/packages/next/src/server/lib/find-page-file.ts b/packages/next/src/server/lib/find-page-file.ts index b3d01383b68d6..155462775ba9b 100644 --- a/packages/next/src/server/lib/find-page-file.ts +++ b/packages/next/src/server/lib/find-page-file.ts @@ -91,7 +91,7 @@ export function createValidFileMatcher( pageExtensions )}$` ) - const leafOnlyNotFoundFileRegex = new RegExp( + const rootNotFoundFileRegex = new RegExp( `^not-found\\.${getExtensionRegexString(pageExtensions)}$` ) /** TODO-METADATA: support other metadata routes @@ -136,7 +136,7 @@ export function createValidFileMatcher( return false } const rest = filePath.slice(appDirPath.length + 1) - return leafOnlyNotFoundFileRegex.test(rest) + return rootNotFoundFileRegex.test(rest) } return { From 40b322635295275b8864c38bb47383e4b74d194f Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Sat, 26 Aug 2023 15:58:00 -0700 Subject: [PATCH 264/387] modify bench scripts to not conflict with dev task (#54600) I don't believe these benches were intended to run as part of the monorepo `dev` task. This renames the `dev` task to `dev-application` similar to `build-application` (which I assume was aliased for a similar reason) Fixes #54592 --- bench/nested-deps-app-router/package.json | 4 ++-- bench/nested-deps/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bench/nested-deps-app-router/package.json b/bench/nested-deps-app-router/package.json index ead6a2d7b7fd9..b6663ecedd36f 100644 --- a/bench/nested-deps-app-router/package.json +++ b/bench/nested-deps-app-router/package.json @@ -2,10 +2,10 @@ "name": "bench-nested-deps-app-router", "scripts": { "prepare-bench": "rimraf components && fuzzponent -d 2 -s 206 -o components", - "dev": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK=1 next dev", + "dev-application": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK=1 next dev", "build-application": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK=1 next build", "start": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK=1 next start", - "dev-nocache": "rimraf .next && pnpm dev", + "dev-nocache": "rimraf .next && pnpm dev-application", "dev-cpuprofile-nocache": "rimraf .next && cross-env NEXT_PRIVATE_LOCAL_WEBPACK=1 node --cpu-prof ../../node_modules/next/dist/bin/next", "build-nocache": "rimraf .next && pnpm build-application" }, diff --git a/bench/nested-deps/package.json b/bench/nested-deps/package.json index dd4484b996184..2decc5ba1c34f 100644 --- a/bench/nested-deps/package.json +++ b/bench/nested-deps/package.json @@ -2,10 +2,10 @@ "name": "bench-nested-deps", "scripts": { "prepare-bench": "rimraf components && fuzzponent -d 2 -s 206 -o components", - "dev": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK=1 next dev", + "dev-application": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK=1 next dev", "build-application": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK=1 next build", "start": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK=1 next start", - "dev-nocache": "rimraf .next && pnpm dev", + "dev-nocache": "rimraf .next && pnpm dev-application", "dev-cpuprofile-nocache": "rimraf .next && cross-env NEXT_PRIVATE_LOCAL_WEBPACK=1 node --cpu-prof ../../node_modules/next/dist/bin/next", "build-nocache": "rimraf .next && pnpm build-application" }, From 52e0b1d1ef1e82ff9971d44e0f8324804df9d52c Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sat, 26 Aug 2023 21:22:30 -0400 Subject: [PATCH 265/387] Fix router CPU profiling (#54497) Since we've merged the app and router processes, the `__NEXT_PRIVATE_CPU_PROFILE` env was missing. --- packages/next/src/server/lib/start-server.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/next/src/server/lib/start-server.ts b/packages/next/src/server/lib/start-server.ts index 0f78b15d6ef68..661bcd8701842 100644 --- a/packages/next/src/server/lib/start-server.ts +++ b/packages/next/src/server/lib/start-server.ts @@ -18,6 +18,11 @@ import { import { checkIsNodeDebugging } from './is-node-debugging' const debug = setupDebug('next:start-server') +if (process.env.NEXT_CPU_PROF) { + process.env.__NEXT_PRIVATE_CPU_PROFILE = `CPU.router` + require('./cpu-profile') +} + export interface StartServerOptions { dir: string port: number From c1b753c795d37b8e5d5147b77ce3384bc98c07ad Mon Sep 17 00:00:00 2001 From: Alex Hawley Date: Sat, 26 Aug 2023 20:27:38 -0500 Subject: [PATCH 266/387] (Example) Add CMS Sitecore XM Cloud Example (#54535) ### What? This PR adds the `cms-sitecore-xmcloud` example. Sitecore XM Cloud is a headless CMS platform that empowers content editors to manage content, layout, and structural aspects of web pages. The Sitecore JSS framework connects frontend JavaScript applications to the XM Cloud API, allowing retrieval of content and presentation layout information. Sitecore JSS for Next.js integrates Next.js with Sitecore JSS, offering a structured approach to connect a Next.js application to XM Cloud. The `cms-sitecore-xmcloud` example, derived from the Sitecore JSS initializer, showcases the frontend application exclusively, omitting backend implementation details. ### Why? While Sitecore offers detailed documentation for creating XM Cloud projects and JSS applications, existing starter templates often combine frontend and backend configurations. The `cms-sitecore-xmcloud` example focuses solely on the frontend application and includes environment variable examples to establish a connection with XM Cloud, eliminating the need for a .NET-compatible machine. ### How? For comprehensive guidance on setting up an XM Cloud site and JSS application, refer to Sitecore and Vercel documentation. The `cms-sitecore-xmcloud` example is created using the Sitecore JSS initializer, integrating Next.js and SXA (Sitecore Experience Accelerator) add-ons. Further documentation can be found at: - [Deploying to Vercel](https://doc.sitecore.com/xmc/en/developers/xm-cloud/walkthrough--deploying-your-front-end-application-to-vercel.html) - [Documentation (Experience Platform)](https://doc.sitecore.com/xp/en/developers/hd/210/sitecore-headless-development/sitecore-javascript-rendering-sdk--jss--for-next-js.html) - [Documentation (XM Cloud)](https://doc.sitecore.com/xmc/en/developers/xm-cloud/sitecore-javascript-rendering-sdk--jss--for-next-js.html) - [Documentation (Create an XM Cloud project from a starter template)](https://doc.sitecore.com/xmc/en/developers/xm-cloud/create-an-xm-cloud-project-from-a-starter-template-in-the-xm-cloud-deploy-app.html) Co-authored-by: Steven Tey <28986134+steven-tey@users.noreply.github.com> Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com> --- examples/cms-sitecore-xmcloud/.env.example | 60 + examples/cms-sitecore-xmcloud/.gitignore | 33 + .../cms-sitecore-xmcloud/.graphql-let.yml | 8 + examples/cms-sitecore-xmcloud/LICENSE.txt | 202 + examples/cms-sitecore-xmcloud/README.md | 64 + examples/cms-sitecore-xmcloud/next-env.d.ts | 5 + examples/cms-sitecore-xmcloud/next.config.js | 69 + examples/cms-sitecore-xmcloud/package.json | 89 + .../cms-sitecore-xmcloud/public/favicon.ico | Bin 0 -> 15086 bytes .../cms-sitecore-xmcloud/public/sc_logo.svg | 20 + .../cms-sitecore-xmcloud/scripts/bootstrap.ts | 21 + .../scripts/config/index.ts | 39 + .../scripts/config/plugins/computed.ts | 21 + .../scripts/config/plugins/fallback.ts | 19 + .../scripts/config/plugins/package-json.ts | 22 + .../scripts/config/plugins/scjssconfig.ts | 29 + .../fetch-graphql-introspection-data.ts | 48 + .../scripts/generate-component-factory.ts | 97 + .../scripts/generate-config.ts | 63 + .../scripts/generate-plugins.ts | 138 + .../scripts/scaffold-component.ts | 95 + .../scripts/temp/.npmignore | 2 + .../scripts/temp/config-plugins.ts | 4 + .../scripts/templates/component-factory.ts | 142 + .../scripts/templates/component-src.ts | 27 + .../cms-sitecore-xmcloud/scripts/utils.ts | 64 + .../config/xmcloud-nextjs-starter.config | 165 + examples/cms-sitecore-xmcloud/src/Layout.tsx | 63 + .../cms-sitecore-xmcloud/src/Navigation.tsx | 27 + .../cms-sitecore-xmcloud/src/NotFound.tsx | 19 + examples/cms-sitecore-xmcloud/src/Scripts.tsx | 5 + .../cms-sitecore-xmcloud/src/assets/app.css | 39 + .../src/assets/basic/_component.scss | 18 + .../src/assets/basic/_container.scss | 79 + .../src/assets/basic/_fonts.scss | 1 + .../src/assets/basic/_footer.scss | 32 + .../src/assets/basic/_header.scss | 49 + .../src/assets/basic/_navigation.scss | 152 + .../src/assets/basic/_promo.scss | 58 + .../src/assets/basic/_rich-text.scss | 11 + .../src/assets/basic/_variables.scss | 10 + .../src/assets/basic/main.scss | 8 + .../cms-sitecore-xmcloud/src/assets/main.scss | 5 + .../src/assets/sass/_app.scss | 105 + .../src/assets/sass/abstracts/_functions.scss | 7 + .../src/assets/sass/abstracts/_mixins.scss | 121 + .../src/assets/sass/abstracts/_vars.scss | 3 + .../assets/sass/abstracts/vars/_colors.scss | 283 + .../sass/abstracts/vars/_fontSizes.scss | 16 + .../assets/sass/abstracts/vars/_margins.scss | 11 + .../src/assets/sass/base/fonts/_fonts.scss | 1 + .../src/assets/sass/base/fonts/index.scss | 1 + .../src/assets/sass/base/index.scss | 3 + .../assets/sass/base/links/_link-button.scss | 29 + .../src/assets/sass/base/links/index.scss | 1 + .../src/assets/sass/base/reset/_inputs.scss | 67 + .../src/assets/sass/base/reset/_links.scss | 14 + .../sass/base/reset/_ui-datepicker.scss | 7 + .../base/richtext/_richtext-files-icons.scss | 203 + .../assets/sass/base/richtext/_richtext.scss | 101 + .../src/assets/sass/base/richtext/index.scss | 2 + .../assets/sass/base/typehead/_typehead.scss | 95 + .../src/assets/sass/base/typehead/index.scss | 1 + .../_component-column-splitter.scss | 14 + .../sass/components/_component-container.scss | 21 + .../sass/components/_component-image.scss | 18 + .../components/_component-navigation.scss | 54 + .../sass/components/_component-promo.scss | 42 + .../_component-richtext-content.scss | 20 + .../sass/components/common/_alignment.scss | 26 + .../assets/sass/components/common/_boxed.scss | 16 + .../sass/components/common/_clearfix.scss | 11 + .../sass/components/common/_highlighted.scss | 63 + .../sass/components/common/_link-button.scss | 18 + .../sass/components/common/_promoted-box.scss | 3 + .../assets/sass/components/common/index.scss | 6 + .../sass/components/container/_bordered.scss | 24 + .../components/container/_title-row-box.scss | 69 + .../sass/components/container/index.scss | 1 + .../image-alignment/_image-left.scss | 3 + .../image-alignment/_image-right.scss | 3 + .../sass/components/image/_image-banner.scss | 15 + .../components/image/_image-default-size.scss | 6 + .../assets/sass/components/image/index.scss | 2 + .../src/assets/sass/components/index.scss | 17 + .../sass/components/layout/_acaindent.scss | 5 + .../sass/components/layout/_background.scss | 27 + .../assets/sass/components/layout/index.scss | 1 + .../link-list/_component-link-list.scss | 50 + .../components/link-list/_list-vertical.scss | 19 + .../sass/components/link-list/index.scss | 2 + .../navigation/_navigation-fat.scss | 57 + .../_navigation-main-horizontal-vertical.scss | 158 + .../navigation/_navigation-mobile.scss | 89 + .../navigation/_navigation-sidebar.scss | 29 + .../navigation/_sitemap-navigation.scss | 20 + .../sass/components/navigation/index.scss | 5 + .../promo/_absolute-bottom-link.scss | 8 + .../sass/components/promo/_promo-hero.scss | 42 + .../sass/components/promo/_promo-shadow.scss | 43 + .../assets/sass/components/promo/index.scss | 3 + .../rich-text/_rich-text-lists.scss | 63 + .../sass/components/rich-text/index.scss | 1 + .../spacing/_background-colors.scss | 14 + .../sass/components/spacing/_indent.scss | 10 + .../assets/sass/components/spacing/index.scss | 2 + .../components/title/_component-title.scss | 25 + .../assets/sass/components/title/index.scss | 1 + .../src/assets/sass/main.scss | 4 + .../src/assets/sass/variants/index.scss | 6 + .../assets/sass/variants/link-list/index.scss | 0 .../sass/variants/navigation/index.scss | 0 .../sass/variants/page-content/index.scss | 0 .../src/assets/sass/variants/promo/index.scss | 0 .../assets/sass/variants/rich-text/index.scss | 0 .../src/assets/sass/variants/title/index.scss | 0 .../src/components/ColumnSplitter.tsx | 65 + .../src/components/Container.tsx | 67 + .../src/components/ContentBlock.tsx | 29 + .../src/components/FEaaSWrapper.tsx | 18 + .../src/components/Image.tsx | 97 + .../src/components/LinkList.tsx | 92 + .../src/components/Navigation.tsx | 170 + .../src/components/PageContent.tsx | 68 + .../PartialDesignDynamicPlaceholder.tsx | 20 + .../src/components/Promo.tsx | 90 + .../src/components/RichText.tsx | 32 + .../src/components/RowSplitter.tsx | 55 + .../src/components/Title.tsx | 101 + .../src/lib/component-props/index.ts | 23 + .../src/lib/data-fetcher.ts | 18 + .../src/lib/dictionary-service-factory.ts | 39 + .../src/lib/extract-path/index.ts | 38 + .../src/lib/layout-service-factory.ts | 34 + .../src/lib/middleware/index.ts | 32 + .../src/lib/middleware/plugins/redirects.ts | 40 + .../src/lib/next-config/plugins/graphql.js | 35 + .../src/lib/next-config/plugins/robots.js | 19 + .../src/lib/next-config/plugins/sass.js | 22 + .../src/lib/next-config/plugins/sitemap.js | 19 + .../src/lib/page-props-factory/index.ts | 50 + .../plugins/component-props.ts | 43 + .../page-props-factory/plugins/normal-mode.ts | 88 + .../plugins/preview-mode.ts | 34 + .../lib/page-props-factory/plugins/site.ts | 23 + .../src/lib/page-props.ts | 18 + .../src/lib/site-resolver/index.ts | 28 + .../src/lib/site-resolver/plugins/default.ts | 18 + .../src/lib/sitemap-fetcher/index.ts | 27 + .../plugins/graphql-sitemap-service.ts | 31 + .../cms-sitecore-xmcloud/src/middleware.ts | 19 + .../cms-sitecore-xmcloud/src/pages/404.tsx | 55 + .../cms-sitecore-xmcloud/src/pages/500.tsx | 74 + .../src/pages/[[...path]].tsx | 114 + .../cms-sitecore-xmcloud/src/pages/_app.tsx | 23 + .../cms-sitecore-xmcloud/src/pages/_error.tsx | 35 + .../src/pages/api/editing/data/[key].ts | 25 + .../src/pages/api/editing/render.ts | 31 + .../src/pages/api/healthz.ts | 12 + .../src/pages/api/robots.ts | 29 + .../src/pages/api/sitemap.ts | 79 + .../cms-sitecore-xmcloud/src/temp/.gitignore | 3 + .../src/temp/GraphQLIntrospectionResult.json | 22205 ++++++++++++++++ examples/cms-sitecore-xmcloud/tsconfig.json | 38 + .../tsconfig.scripts.json | 9 + 165 files changed, 28663 insertions(+) create mode 100644 examples/cms-sitecore-xmcloud/.env.example create mode 100644 examples/cms-sitecore-xmcloud/.gitignore create mode 100644 examples/cms-sitecore-xmcloud/.graphql-let.yml create mode 100644 examples/cms-sitecore-xmcloud/LICENSE.txt create mode 100644 examples/cms-sitecore-xmcloud/README.md create mode 100644 examples/cms-sitecore-xmcloud/next-env.d.ts create mode 100644 examples/cms-sitecore-xmcloud/next.config.js create mode 100644 examples/cms-sitecore-xmcloud/package.json create mode 100644 examples/cms-sitecore-xmcloud/public/favicon.ico create mode 100644 examples/cms-sitecore-xmcloud/public/sc_logo.svg create mode 100644 examples/cms-sitecore-xmcloud/scripts/bootstrap.ts create mode 100644 examples/cms-sitecore-xmcloud/scripts/config/index.ts create mode 100644 examples/cms-sitecore-xmcloud/scripts/config/plugins/computed.ts create mode 100644 examples/cms-sitecore-xmcloud/scripts/config/plugins/fallback.ts create mode 100644 examples/cms-sitecore-xmcloud/scripts/config/plugins/package-json.ts create mode 100644 examples/cms-sitecore-xmcloud/scripts/config/plugins/scjssconfig.ts create mode 100644 examples/cms-sitecore-xmcloud/scripts/fetch-graphql-introspection-data.ts create mode 100644 examples/cms-sitecore-xmcloud/scripts/generate-component-factory.ts create mode 100644 examples/cms-sitecore-xmcloud/scripts/generate-config.ts create mode 100644 examples/cms-sitecore-xmcloud/scripts/generate-plugins.ts create mode 100644 examples/cms-sitecore-xmcloud/scripts/scaffold-component.ts create mode 100644 examples/cms-sitecore-xmcloud/scripts/temp/.npmignore create mode 100644 examples/cms-sitecore-xmcloud/scripts/temp/config-plugins.ts create mode 100644 examples/cms-sitecore-xmcloud/scripts/templates/component-factory.ts create mode 100644 examples/cms-sitecore-xmcloud/scripts/templates/component-src.ts create mode 100644 examples/cms-sitecore-xmcloud/scripts/utils.ts create mode 100644 examples/cms-sitecore-xmcloud/sitecore/config/xmcloud-nextjs-starter.config create mode 100644 examples/cms-sitecore-xmcloud/src/Layout.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/Navigation.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/NotFound.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/Scripts.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/assets/app.css create mode 100644 examples/cms-sitecore-xmcloud/src/assets/basic/_component.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/basic/_container.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/basic/_fonts.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/basic/_footer.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/basic/_header.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/basic/_navigation.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/basic/_promo.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/basic/_rich-text.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/basic/_variables.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/basic/main.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/main.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/_app.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/_functions.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/_mixins.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/_vars.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/vars/_colors.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/vars/_fontSizes.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/vars/_margins.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/base/fonts/_fonts.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/base/fonts/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/base/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/base/links/_link-button.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/base/links/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/base/reset/_inputs.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/base/reset/_links.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/base/reset/_ui-datepicker.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/base/richtext/_richtext-files-icons.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/base/richtext/_richtext.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/base/richtext/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/base/typehead/_typehead.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/base/typehead/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-column-splitter.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-container.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-image.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-navigation.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-promo.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-richtext-content.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_alignment.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_boxed.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_clearfix.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_highlighted.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_link-button.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_promoted-box.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/common/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/container/_bordered.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/container/_title-row-box.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/container/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/image-alignment/_image-left.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/image-alignment/_image-right.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/image/_image-banner.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/image/_image-default-size.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/image/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/layout/_acaindent.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/layout/_background.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/layout/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/link-list/_component-link-list.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/link-list/_list-vertical.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/link-list/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_navigation-fat.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_navigation-main-horizontal-vertical.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_navigation-mobile.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_navigation-sidebar.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_sitemap-navigation.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/promo/_absolute-bottom-link.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/promo/_promo-hero.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/promo/_promo-shadow.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/promo/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/rich-text/_rich-text-lists.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/rich-text/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/spacing/_background-colors.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/spacing/_indent.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/spacing/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/title/_component-title.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/components/title/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/main.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/variants/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/variants/link-list/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/variants/navigation/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/variants/page-content/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/variants/promo/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/variants/rich-text/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/assets/sass/variants/title/index.scss create mode 100644 examples/cms-sitecore-xmcloud/src/components/ColumnSplitter.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/components/Container.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/components/ContentBlock.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/components/FEaaSWrapper.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/components/Image.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/components/LinkList.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/components/Navigation.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/components/PageContent.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/components/PartialDesignDynamicPlaceholder.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/components/Promo.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/components/RichText.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/components/RowSplitter.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/components/Title.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/lib/component-props/index.ts create mode 100644 examples/cms-sitecore-xmcloud/src/lib/data-fetcher.ts create mode 100644 examples/cms-sitecore-xmcloud/src/lib/dictionary-service-factory.ts create mode 100644 examples/cms-sitecore-xmcloud/src/lib/extract-path/index.ts create mode 100644 examples/cms-sitecore-xmcloud/src/lib/layout-service-factory.ts create mode 100644 examples/cms-sitecore-xmcloud/src/lib/middleware/index.ts create mode 100644 examples/cms-sitecore-xmcloud/src/lib/middleware/plugins/redirects.ts create mode 100644 examples/cms-sitecore-xmcloud/src/lib/next-config/plugins/graphql.js create mode 100644 examples/cms-sitecore-xmcloud/src/lib/next-config/plugins/robots.js create mode 100644 examples/cms-sitecore-xmcloud/src/lib/next-config/plugins/sass.js create mode 100644 examples/cms-sitecore-xmcloud/src/lib/next-config/plugins/sitemap.js create mode 100644 examples/cms-sitecore-xmcloud/src/lib/page-props-factory/index.ts create mode 100644 examples/cms-sitecore-xmcloud/src/lib/page-props-factory/plugins/component-props.ts create mode 100644 examples/cms-sitecore-xmcloud/src/lib/page-props-factory/plugins/normal-mode.ts create mode 100644 examples/cms-sitecore-xmcloud/src/lib/page-props-factory/plugins/preview-mode.ts create mode 100644 examples/cms-sitecore-xmcloud/src/lib/page-props-factory/plugins/site.ts create mode 100644 examples/cms-sitecore-xmcloud/src/lib/page-props.ts create mode 100644 examples/cms-sitecore-xmcloud/src/lib/site-resolver/index.ts create mode 100644 examples/cms-sitecore-xmcloud/src/lib/site-resolver/plugins/default.ts create mode 100644 examples/cms-sitecore-xmcloud/src/lib/sitemap-fetcher/index.ts create mode 100644 examples/cms-sitecore-xmcloud/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts create mode 100644 examples/cms-sitecore-xmcloud/src/middleware.ts create mode 100644 examples/cms-sitecore-xmcloud/src/pages/404.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/pages/500.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/pages/[[...path]].tsx create mode 100644 examples/cms-sitecore-xmcloud/src/pages/_app.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/pages/_error.tsx create mode 100644 examples/cms-sitecore-xmcloud/src/pages/api/editing/data/[key].ts create mode 100644 examples/cms-sitecore-xmcloud/src/pages/api/editing/render.ts create mode 100644 examples/cms-sitecore-xmcloud/src/pages/api/healthz.ts create mode 100644 examples/cms-sitecore-xmcloud/src/pages/api/robots.ts create mode 100644 examples/cms-sitecore-xmcloud/src/pages/api/sitemap.ts create mode 100644 examples/cms-sitecore-xmcloud/src/temp/.gitignore create mode 100644 examples/cms-sitecore-xmcloud/src/temp/GraphQLIntrospectionResult.json create mode 100644 examples/cms-sitecore-xmcloud/tsconfig.json create mode 100644 examples/cms-sitecore-xmcloud/tsconfig.scripts.json diff --git a/examples/cms-sitecore-xmcloud/.env.example b/examples/cms-sitecore-xmcloud/.env.example new file mode 100644 index 0000000000000..12b0dbb5b0a63 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/.env.example @@ -0,0 +1,60 @@ +# Read the Vercel + Sitecore docs for the full setup instructions: https://vercel.com/docs/integrations/sitecore + +# For development purposes, note Next.js supports a .env.local +# file, which is already configured to be git ignored. +# Read more about Next.js support of environment variables here: +# https://nextjs.org/docs/basic-features/environment-variables +JSS_APP_NAME= + +# The public URL to use for absolute URLs, which are required when +# the Next.js app is run within Sitecore editors. +# This should match the `serverSideRenderingEngineApplicationUrl` +# in your Sitecore configuration (see \sitecore\config\xmcloud-nextjs-starter.config). +# Be sure to update these values accordingly as your public endpoint changes. +# See https://jss.sitecore.com/docs/fundamentals/services/view-engine +PUBLIC_URL=http://localhost:3000 + +# To secure the Sitecore editor endpoint exposed by your Next.js app +# (`/api/editing/render` by default), a secret token is used. This (client-side) +# value must match your server-side value (see \sitecore\config\xmcloud-nextjs-starter.config). +# We recommend an alphanumeric value of at least 16 characters. +JSS_EDITING_SECRET= + +# Your Sitecore API key is needed to build the app. Typically, the API key is +# defined in `scjssconfig.json` (as `sitecore.apiKey`). This file may not exist +# when building locally (if you've never run `jss setup`), or when building in a +# higher environment (since `scjssconfig.json` is ignored from source control). +# In this case, use this environment variable to provide the value at build time. +SITECORE_API_KEY= + +# Your Sitecore API hostname is needed to build the app. Typically, the API host is +# defined in `scjssconfig.json` (as `sitecore.layoutServiceHost`). This file may +# not exist when building locally (if you've never run `jss setup`), or when building +# in a higher environment (since `scjssconfig.json` is ignored from source control). +# In this case, use this environment variable to provide the value at build time. +SITECORE_API_HOST= + +# Your GraphQL Edge endpoint. This is required for Sitecore Experience Edge. +# For Sitecore XM, this is typically optional. By default, the endpoint is calculated using +# the resolved Sitecore API hostname + the `graphQLEndpointPath` defined in your `package.json`. +GRAPH_QL_ENDPOINT= + +# Your default app language. +DEFAULT_LANGUAGE= + +# The way in which layout and dictionary data is fetched from Sitecore +FETCH_WITH=GraphQL + +# Indicates whether SSG `getStaticPaths` pre-render any pages +# Set the environment variable DISABLE_SSG_FETCH=true +# to enable full ISR (Incremental Static Regeneration) flow +DISABLE_SSG_FETCH= + +# Sitecore JSS npm packages utilize the debug module for debug logging. +# https://www.npmjs.com/package/debug +# Set the DEBUG environment variable to 'sitecore-jss:*' to see all logs: +#DEBUG=sitecore-jss:* +# Or be selective and show for example only layout service logs: +#DEBUG=sitecore-jss:layout +# Or everything BUT layout service logs: +#DEBUG=sitecore-jss:*,-sitecore-jss:layout diff --git a/examples/cms-sitecore-xmcloud/.gitignore b/examples/cms-sitecore-xmcloud/.gitignore new file mode 100644 index 0000000000000..b9d296990d879 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/.gitignore @@ -0,0 +1,33 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +/node_modules + +# testing +/coverage + +# next.js +/.next*/ +/out/ + +# graphql code generation +/.generated +*.graphql.d.ts +*.graphqls.d.ts + +# misc +.DS_Store + +# local env files +.env.local +.env.*.local + +# Log files +*.log* + +# sitecore +scjssconfig.json +*.deploysecret.config + +# vercel +.vercel \ No newline at end of file diff --git a/examples/cms-sitecore-xmcloud/.graphql-let.yml b/examples/cms-sitecore-xmcloud/.graphql-let.yml new file mode 100644 index 0000000000000..85b6e0d8a2f26 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/.graphql-let.yml @@ -0,0 +1,8 @@ +schema: + - './src/temp/GraphQLIntrospectionResult.json' +documents: 'src/**/*.graphql' +plugins: + - typescript-operations + - typed-document-node +config: + useIndexSignature: true diff --git a/examples/cms-sitecore-xmcloud/LICENSE.txt b/examples/cms-sitecore-xmcloud/LICENSE.txt new file mode 100644 index 0000000000000..d645695673349 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/cms-sitecore-xmcloud/README.md b/examples/cms-sitecore-xmcloud/README.md new file mode 100644 index 0000000000000..a83926b59e022 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/README.md @@ -0,0 +1,64 @@ +# A Next.js Example Using Sitecore JSS and Sitecore XM Cloud + +This example connects to a Sitecore XM Cloud site using the Sitecore JavaScript Rendering SDK (JSS) for Next.js and includes example components and configuration for headless SXA (Sitecore Experience Accelerator). For more information on creating and deploying a headless Sitecore solution to XM Cloud please refer to [Vercel's Sitecore XM Cloud Integration Guide](https://vercel.com/docs/integrations/sitecore) or official [Sitecore documentation](https://doc.sitecore.com/xmc/en/developers/xm-cloud/create-an-xm-cloud-project-from-a-starter-template-in-the-xm-cloud-deploy-app.html). + +## Demo + +### [https://vercel-sitecore-xmcloud-demo.vercel.app](https://vercel-sitecore-xmcloud-demo.vercel.app) + +## Deploy your own + +Using the Deploy Button below, you'll deploy the Next.js project as well as connect it to your XM Cloud project with the required environment variables. + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?demo-title=Sitecore+XM+Cloud+Next.js+Starter&demo-description=Simple+Next.js+blog+site+that+connects+to+a+Sitecore+XM+Cloud+site+using+the+Sitecore+JavaScript+Rendering+SDK+%28JSS%29.&demo-url=https%3A%2F%2Fvercel-sitecore-xmcloud-demo.vercel.app%2F&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2FJAWlcS27EakxvDFRjmLwD%2F412631142afd83d7b3a926cb7c3e44bd%2FCleanShot_2023-08-25_at_20.09.25_2x.png&project-name=Sitecore+XM+Cloud+Next.js+Starter&repository-name=sitecore-starter&repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fcms-sitecore-xmcloud&from=templates&skippable-integrations=1&env=JSS_APP_NAME%2CPUBLIC_URL%2CJSS_EDITING_SECRET%2CSITECORE_API_KEY%2CSITECORE_API_HOST%2CGRAPH_QL_ENDPOINT%2CDEFAULT_LANGUAGE%2CFETCH_WITH%2CDISABLE_SSG_FETCH&envDescription=Instructions+on+how+to+get+these+env+vars&envLink=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fcms-sitecore-xmcloud%2F.env.example) + +- `JSS_APP_NAME`: The name of the JSS app that is configured in XM Cloud. +- `GRAPH_QL_ENDPOINT`: The GraphQL Edge endpoint. This is required for Sitecore Experience Edge. +- `SITECORE_API_KEY`: The Sitecore API key is required to build the app. +- `SITECORE_API_HOST`: The host of the Sitecore API. +- `FETCH_WITH`: The fetch method to the Sitecore API. This can be either `GraphQL` or `REST`. + +### Related examples + +- [AgilityCMS](/examples/cms-agilitycms) +- [Builder.io](/examples/cms-builder-io) +- [ButterCMS](/examples/cms-buttercms) +- [Contentful](/examples/cms-contentful) +- [Cosmic](/examples/cms-cosmic) +- [DatoCMS](/examples/cms-datocms) +- [DotCMS](/examples/cms-dotcms) +- [Drupal](/examples/cms-drupal) +- [Enterspeed](/examples/cms-enterspeed) +- [Ghost](/examples/cms-ghost) +- [GraphCMS](/examples/cms-graphcms) +- [Kontent](/examples/cms-kontent-ai) +- [Prepr](/examples/cms-prepr) +- [Prismic](/examples/cms-prismic) +- [Sanity](/examples/cms-sanity) +- [Sitefinity](/examples/cms-sitefinity) +- [Storyblok](/examples/cms-storyblok) +- [TakeShape](/examples/cms-takeshape) +- [Umbraco heartcore](/examples/cms-umbraco-heartcore) +- [Webiny](/examples/cms-webiny) +- [Blog Starter](/examples/blog-starter) +- [WordPress](/examples/cms-wordpress) + +## How to use + +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example: + +```bash +npx create-next-app --example cms-sitecore-xmcloud cms-sitecore-xmcloud-app +``` + +```bash +yarn create-next-app --example cms-sitecore-xmcloud cms-sitecore-xmcloud-app +``` + +```bash +pnpm create-next-app --example cms-sitecore-xmcloud cms-sitecore-xmcloud-app +``` + +## Configuration + +To configure and run this example you can follow our [Sitecore XM Cloud Integration Guide](https://vercel.com/docs/integrations/sitecore) diff --git a/examples/cms-sitecore-xmcloud/next-env.d.ts b/examples/cms-sitecore-xmcloud/next-env.d.ts new file mode 100644 index 0000000000000..4f11a03dc6cc3 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/cms-sitecore-xmcloud/next.config.js b/examples/cms-sitecore-xmcloud/next.config.js new file mode 100644 index 0000000000000..ca92b3473e276 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/next.config.js @@ -0,0 +1,69 @@ +const jssConfig = require('./src/temp/config') +const { getPublicUrl } = require('@sitecore-jss/sitecore-jss-nextjs') +const plugins = require('./src/temp/next-config-plugins') || {} + +const publicUrl = getPublicUrl() + +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + // Set assetPrefix to our public URL + assetPrefix: publicUrl, + + // Allow specifying a distinct distDir when concurrently running app in a container + distDir: process.env.NEXTJS_DIST_DIR || '.next', + + // Make the same PUBLIC_URL available as an environment variable on the client bundle + env: { + PUBLIC_URL: publicUrl, + }, + + i18n: { + // These are all the locales you want to support in your application. + // These should generally match (or at least be a subset of) those in Sitecore. + locales: ['en'], + // This is the locale that will be used when visiting a non-locale + // prefixed path e.g. `/styleguide`. + defaultLocale: jssConfig.defaultLanguage, + }, + + // Enable React Strict Mode + reactStrictMode: true, + + async rewrites() { + // When in connected mode we want to proxy Sitecore paths off to Sitecore + return [ + // API endpoints + { + source: '/sitecore/api/:path*', + destination: `${jssConfig.sitecoreApiHost}/sitecore/api/:path*`, + }, + // media items + { + source: '/-/:path*', + destination: `${jssConfig.sitecoreApiHost}/-/:path*`, + }, + // visitor identification + { + source: '/layouts/system/:path*', + destination: `${jssConfig.sitecoreApiHost}/layouts/system/:path*`, + }, + // healthz check + { + source: '/healthz', + destination: '/api/healthz', + }, + // rewrite for Sitecore service pages + { + source: '/sitecore/service/:path*', + destination: `${jssConfig.sitecoreApiHost}/sitecore/service/:path*`, + }, + ] + }, +} + +module.exports = () => { + // Run the base config through any configured plugins + return Object.values(plugins).reduce((acc, plugin) => plugin(acc), nextConfig) +} diff --git a/examples/cms-sitecore-xmcloud/package.json b/examples/cms-sitecore-xmcloud/package.json new file mode 100644 index 0000000000000..19be60eb7a576 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/package.json @@ -0,0 +1,89 @@ +{ + "private": true, + "config": { + "appName": "xmcloud-nextjs-starter", + "rootPlaceholders": [ + "jss-main" + ], + "sitecoreConfigPath": "/App_Config/Include/zzz", + "graphQLEndpointPath": "/sitecore/api/graph/edge", + "language": "en", + "templates": [ + "nextjs", + "nextjs-sxa" + ] + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/sitecore/jss.git" + }, + "bugs": { + "url": "https://github.com/sitecore/jss/issues" + }, + "dependencies": { + "@sitecore-jss/sitecore-jss-nextjs": "~21.1.6", + "bootstrap": "^5.1.3", + "font-awesome": "^4.7.0", + "graphql": "~15.8.0", + "graphql-tag": "^2.12.6", + "next": "^13.1.6", + "next-localization": "^0.12.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@graphql-codegen/cli": "^1.21.8", + "@graphql-codegen/import-types-preset": "^2.2.6", + "@graphql-codegen/plugin-helpers": "^3.1.2", + "@graphql-codegen/typed-document-node": "^2.3.12", + "@graphql-codegen/typescript": "^2.8.7", + "@graphql-codegen/typescript-operations": "^2.5.12", + "@graphql-codegen/typescript-resolvers": "^2.7.12", + "@graphql-typed-document-node/core": "^3.1.1", + "@sitecore-jss/sitecore-jss-cli": "~21.1.6", + "@types/node": "^18.11.18", + "@types/react": "^18.0.12", + "@types/react-dom": "^18.0.5", + "@typescript-eslint/eslint-plugin": "^5.49.0", + "@typescript-eslint/parser": "^5.49.0", + "chalk": "~4.1.2", + "chokidar": "~3.5.3", + "constant-case": "^3.0.4", + "cross-env": "~7.0.3", + "dotenv": "^16.0.3", + "eslint": "^8.32.0", + "eslint-config-next": "^13.1.5", + "eslint-config-prettier": "^8.6.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-yaml": "^0.5.0", + "graphql-let": "^0.18.6", + "npm-run-all": "~4.1.5", + "prettier": "^2.8.3", + "sass": "^1.52.3", + "sass-alias": "^1.0.5", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.1.2", + "typescript": "~4.9.4", + "yaml-loader": "^0.8.0" + }, + "scripts": { + "dev": "npm run next:dev", + "start": "npm run next:start", + "jss": "jss", + "lint": "eslint ./src/**/*.tsx ./src/**/*.ts ./scripts/**/*.ts", + "bootstrap": "ts-node --project tsconfig.scripts.json scripts/bootstrap.ts", + "build": "npm-run-all --serial bootstrap next:build", + "graphql:update": "ts-node --project tsconfig.scripts.json ./scripts/fetch-graphql-introspection-data.ts", + "next:build": "next build", + "next:dev": "cross-env NODE_OPTIONS='--inspect' next dev", + "next:start": "next start", + "scaffold": "ts-node --project tsconfig.scripts.json scripts/scaffold-component.ts", + "start:connected": "npm-run-all --serial bootstrap --parallel next:dev start:watch-components", + "start:production": "npm-run-all --serial bootstrap next:build next:start", + "start:watch-components": "ts-node --project tsconfig.scripts.json scripts/generate-component-factory.ts --watch" + } +} diff --git a/examples/cms-sitecore-xmcloud/public/favicon.ico b/examples/cms-sitecore-xmcloud/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..065a432e4186a62ed48411fa6ad2c72fafd73f58 GIT binary patch literal 15086 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 literal 0 HcmV?d00001 diff --git a/examples/cms-sitecore-xmcloud/public/sc_logo.svg b/examples/cms-sitecore-xmcloud/public/sc_logo.svg new file mode 100644 index 0000000000000..1be7e78814a1d --- /dev/null +++ b/examples/cms-sitecore-xmcloud/public/sc_logo.svg @@ -0,0 +1,20 @@ + + + + Logo + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/examples/cms-sitecore-xmcloud/scripts/bootstrap.ts b/examples/cms-sitecore-xmcloud/scripts/bootstrap.ts new file mode 100644 index 0000000000000..7ef1ffeceac22 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/scripts/bootstrap.ts @@ -0,0 +1,21 @@ +/* + BOOTSTRAPPING + The bootstrap process runs before build, and generates JS that needs to be + included into the build - specifically, plugins, the global config module, + and the component name to component mapping. +*/ + +/* + PLUGINS GENERATION +*/ +import './generate-plugins' + +/* + CONFIG GENERATION +*/ +import './generate-config' + +/* + COMPONENT FACTORY GENERATION +*/ +import './generate-component-factory' diff --git a/examples/cms-sitecore-xmcloud/scripts/config/index.ts b/examples/cms-sitecore-xmcloud/scripts/config/index.ts new file mode 100644 index 0000000000000..932cf3e42dbcd --- /dev/null +++ b/examples/cms-sitecore-xmcloud/scripts/config/index.ts @@ -0,0 +1,39 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const plugins = require('scripts/temp/config-plugins') + +/** + * JSS configuration object + */ +export interface JssConfig extends Record { + sitecoreApiKey?: string + sitecoreApiHost?: string + jssAppName?: string + graphQLEndpointPath?: string + defaultLanguage?: string + graphQLEndpoint?: string +} + +export interface ConfigPlugin { + /** + * Detect order when the plugin should be called, e.g. 0 - will be called first (can be a plugin which data is required for other plugins) + */ + order: number + /** + * A function which will be called during config generation + * @param {JssConfig} config Current (accumulated) config + */ + exec(config: JssConfig): Promise +} + +export class JssConfigFactory { + public async create(defaultConfig: JssConfig = {}): Promise { + return (Object.values(plugins) as ConfigPlugin[]) + .sort((p1, p2) => p1.order - p2.order) + .reduce( + (promise, plugin) => promise.then((config) => plugin.exec(config)), + Promise.resolve(defaultConfig) + ) + } +} + +export const jssConfigFactory = new JssConfigFactory() diff --git a/examples/cms-sitecore-xmcloud/scripts/config/plugins/computed.ts b/examples/cms-sitecore-xmcloud/scripts/config/plugins/computed.ts new file mode 100644 index 0000000000000..2a898db95f07e --- /dev/null +++ b/examples/cms-sitecore-xmcloud/scripts/config/plugins/computed.ts @@ -0,0 +1,21 @@ +import { ConfigPlugin, JssConfig } from '..' + +/** + * This plugin will set computed config props. + * The "graphQLEndpoint" is an example of making a _computed_ config setting + * based on other config settings. + */ +class ComputedPlugin implements ConfigPlugin { + // should come after other plugins (but before fallback) + order = 10 + + async exec(config: JssConfig) { + return Object.assign({}, config, { + graphQLEndpoint: + config.graphQLEndpoint || + `${config.sitecoreApiHost}${config.graphQLEndpointPath}`, + }) + } +} + +export const computedPlugin = new ComputedPlugin() diff --git a/examples/cms-sitecore-xmcloud/scripts/config/plugins/fallback.ts b/examples/cms-sitecore-xmcloud/scripts/config/plugins/fallback.ts new file mode 100644 index 0000000000000..d428dca3cc524 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/scripts/config/plugins/fallback.ts @@ -0,0 +1,19 @@ +import { ConfigPlugin, JssConfig } from '..' + +/** + * This config will set fallback values for properties that were left empty + * If neither env, nor other places had a proper value, this will ensure a fallback is set + */ +class FallbackPlugin implements ConfigPlugin { + // should always come last + order = 100 + + async exec(config: JssConfig) { + return Object.assign({}, config, { + defaultLanguage: config.defaultLanguage || 'en', + sitecoreApiKey: config.sitecoreApiKey || 'no-api-key-set', + }) + } +} + +export const fallbackPlugin = new FallbackPlugin() diff --git a/examples/cms-sitecore-xmcloud/scripts/config/plugins/package-json.ts b/examples/cms-sitecore-xmcloud/scripts/config/plugins/package-json.ts new file mode 100644 index 0000000000000..7b767c98a198c --- /dev/null +++ b/examples/cms-sitecore-xmcloud/scripts/config/plugins/package-json.ts @@ -0,0 +1,22 @@ +import { ConfigPlugin, JssConfig } from '..' +import packageConfig from 'package.json' + +/** + * This plugin will set config props based on package.json. + */ +class PackageJsonPlugin implements ConfigPlugin { + order = 1 + + async exec(config: JssConfig) { + if (!packageConfig.config) return config + + return Object.assign({}, config, { + jssAppName: config.jssAppName || packageConfig.config.appName, + graphQLEndpointPath: + config.graphQLEndpointPath || packageConfig.config.graphQLEndpointPath, + defaultLanguage: config.defaultLanguage || packageConfig.config.language, + }) + } +} + +export const packageJsonPlugin = new PackageJsonPlugin() diff --git a/examples/cms-sitecore-xmcloud/scripts/config/plugins/scjssconfig.ts b/examples/cms-sitecore-xmcloud/scripts/config/plugins/scjssconfig.ts new file mode 100644 index 0000000000000..47c5e8d157edd --- /dev/null +++ b/examples/cms-sitecore-xmcloud/scripts/config/plugins/scjssconfig.ts @@ -0,0 +1,29 @@ +import { ConfigPlugin, JssConfig } from '..' + +/** + * This plugin will set config props based on scjssconfig.json. + * scjssconfig.json may not exist if you've never run `jss setup` (development) + * or are depending on environment variables instead (production). + */ +class ScJssConfigPlugin implements ConfigPlugin { + order = 1 + + async exec(config: JssConfig) { + let scJssConfig + try { + scJssConfig = require('scjssconfig.json') + } catch (e) { + return config + } + + if (!scJssConfig) return config + + return Object.assign({}, config, { + sitecoreApiKey: config.sitecoreApiKey || scJssConfig.sitecore?.apiKey, + sitecoreApiHost: + config.sitecoreApiHost || scJssConfig.sitecore?.layoutServiceHost, + }) + } +} + +export const scjssconfigPlugin = new ScJssConfigPlugin() diff --git a/examples/cms-sitecore-xmcloud/scripts/fetch-graphql-introspection-data.ts b/examples/cms-sitecore-xmcloud/scripts/fetch-graphql-introspection-data.ts new file mode 100644 index 0000000000000..2a66c72352712 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/scripts/fetch-graphql-introspection-data.ts @@ -0,0 +1,48 @@ +import { GraphQLRequestClient } from '@sitecore-jss/sitecore-jss-nextjs' +import fs from 'fs' +import { getIntrospectionQuery } from 'graphql' + +// This script load graphql introspection data in order to use graphql code generator and generate typescript types +// The `jss graphql:update` command should be executed when Sitecore templates related to the site are altered. + +let jssConfig + +try { + // eslint-disable-next-line + jssConfig = require('../src/temp/config') +} catch (e) { + console.error( + 'Unable to require JSS config. Ensure `jss setup` has been run, and the app has been started at least once after setup.' + ) + console.error(e) + process.exit(1) +} + +console.log( + `Fetch graphql introspection data from ${jssConfig.graphQLEndpoint}...` +) + +const client = new GraphQLRequestClient(jssConfig.graphQLEndpoint, { + apiKey: jssConfig.sitecoreApiKey, +}) + +client + .request(getIntrospectionQuery()) + .then((result) => { + fs.writeFile( + './src/temp/GraphQLIntrospectionResult.json', + JSON.stringify(result, null, 2), + (err) => { + if (err) { + console.error('Error writing GraphQLIntrospectionResult file', err) + return + } + + console.log('GraphQL Introspection Data successfully fetched!') + } + ) + }) + .catch((e) => { + console.error(e) + process.exit(1) + }) diff --git a/examples/cms-sitecore-xmcloud/scripts/generate-component-factory.ts b/examples/cms-sitecore-xmcloud/scripts/generate-component-factory.ts new file mode 100644 index 0000000000000..72d0d411ce6dc --- /dev/null +++ b/examples/cms-sitecore-xmcloud/scripts/generate-component-factory.ts @@ -0,0 +1,97 @@ +import fs from 'fs' +import path from 'path' +import generateComponentFactory, { + ComponentFile, + PackageDefinition, +} from './templates/component-factory' +import { getItems, watchItems } from './utils' + +/* + COMPONENT FACTORY GENERATION + Generates the `/src/temp/componentFactory.ts` file, which maps JSS React components + to Sitecore renderings. + + The component factory is a mapping between a string name and a React component instance. + When the Sitecore Layout service returns a layout definition, it returns named components. + This mapping is used to construct the component hierarchy for the layout. + + Generating the componentFactory is optional, and it can be maintained manually if preferred. + + The default convention uses the component's filename (without the extension) as the component + name. For example, the file `/components/ComponentName.ts` would map to component `ComponentName`. + This can be customized in writeComponentFactory(). + + This script supports two modes. In default mode, the component factory file is written once. + In watch mode, the component factory source folder is watched, and componentFactory.ts is + regenerated whenever files are added or deleted. Run in watch mode by passing a `--watch` argument + when calling the script. +*/ + +const componentFactoryPath = path.resolve('src/temp/componentFactory.ts') +const componentRootPath = 'src/components' + +const isWatch = process.argv.some((arg) => arg === '--watch') + +function getComponentList(path: string): (PackageDefinition | ComponentFile)[] { + const components = getItems({ + path, + resolveItem: (path, name) => ({ + path: `${path}/${name}`, + componentName: name, + moduleName: name.replace(/[^\w]+/g, ''), + }), + cb: (name) => console.debug(`Registering JSS component ${name}`), + }) + + return components +} + +/** + * Generates the component factory file and saves it to the filesystem. + * By convention, we expect to find React components under src/components/** (subfolders are + * searched recursively). The filename, with the extension stripped, is used for the component's + * string name (for mapping to Sitecore). The filename, with extension and non-word characters + * stripped, is used to identify the component's JavaScript module definition (for initializing + * new component instances in code). + * Modify this function to use a different convention. + */ +function writeComponentFactory() { + /** + * You can specify components which you want to import from external/internal packages + * in format: + * { + * name: 'package name', + * components: [ + * { + * componentName: 'component name', // component rendering name, + * moduleName: 'module name' // component name to import from the package + * } + * ] + * } + */ + const packages: PackageDefinition[] = [] + const components = getComponentList(componentRootPath) + + components.unshift(...packages) + + const fileContent = generateComponentFactory(components) + console.log(`Writing component factory to ${componentFactoryPath}`) + fs.writeFileSync(componentFactoryPath, fileContent, { + encoding: 'utf8', + }) +} + +/** + * Watches component directory for changes. When files are added or deleted, the component factory + * file (componentFactory.ts) is regenerated. This is used during `jss start` to pick up new or + * removed components at runtime. + */ +function watchComponentFactory() { + console.log( + `Watching for changes to component factory sources in ${componentRootPath}...` + ) + + watchItems(componentRootPath, writeComponentFactory) +} + +;(isWatch ? watchComponentFactory : writeComponentFactory)() diff --git a/examples/cms-sitecore-xmcloud/scripts/generate-config.ts b/examples/cms-sitecore-xmcloud/scripts/generate-config.ts new file mode 100644 index 0000000000000..6ecf1f4b30134 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/scripts/generate-config.ts @@ -0,0 +1,63 @@ +import 'dotenv/config' +import fs from 'fs' +import path from 'path' +import { constantCase } from 'constant-case' +import { JssConfig, jssConfigFactory } from './config' + +/* + CONFIG GENERATION + Generates the /src/temp/config.js file which contains runtime configuration + that the app can import and use. +*/ + +const defaultConfig: JssConfig = { + sitecoreApiKey: process.env[`${constantCase('sitecoreApiKey')}`], + sitecoreApiHost: process.env[`${constantCase('sitecoreApiHost')}`], + jssAppName: process.env[`${constantCase('jssAppName')}`], + graphQLEndpointPath: process.env[`${constantCase('graphQLEndpointPath')}`], + defaultLanguage: process.env[`${constantCase('defaultLanguage')}`], + graphQLEndpoint: process.env[`${constantCase('graphQLEndpoint')}`], +} + +/** + * Writes the config object to disk with support for environment variables. + * @param {JssConfig} config JSS configuration to write. + */ +function writeConfig(config: JssConfig): void { + let configText = `/* eslint-disable */ +// Do not edit this file, it is auto-generated at build time! +// See scripts/bootstrap.ts to modify the generation of this file. +const config = {};\n` + + // Set configuration values, allowing override with environment variables + Object.keys(config).forEach((prop) => { + configText += `config.${prop} = process.env.${constantCase(prop)} || '${ + config[prop] + }',\n` + }) + configText += `module.exports = config;` + + const configPath = path.resolve('src/temp/config.js') + console.log(`Writing runtime config to ${configPath}`) + fs.writeFileSync(configPath, configText, { encoding: 'utf8' }) +} + +/** + * Generates the JSS config based on config plugins (under ./config/plugins) + * and then writes the config to disk. + * @param {JssConfig} defaultConfig Default configuration. + */ +function generateConfig(defaultConfig: JssConfig): void { + jssConfigFactory + .create(defaultConfig) + .then((config) => { + writeConfig(config) + }) + .catch((e) => { + console.error('Error generating config') + console.error(e) + process.exit(1) + }) +} + +generateConfig(defaultConfig) diff --git a/examples/cms-sitecore-xmcloud/scripts/generate-plugins.ts b/examples/cms-sitecore-xmcloud/scripts/generate-plugins.ts new file mode 100644 index 0000000000000..2ec8450569096 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/scripts/generate-plugins.ts @@ -0,0 +1,138 @@ +import fs from 'fs' +import path from 'path' +import { getItems } from './utils' + +/* + PLUGINS GENERATION + NOTE: pluginName: the name of the plugin in the src/lib folder + Generates the `/src/temp/{pluginName}-plugins.ts` file, which exports list of plugins + + Generating the plugins is optional, and it can be maintained manually if preferred. + + The default convention uses the plugin's filename (without the extension) as the first part of the component + name. For example, the file `/lib/page-props-factory/plugins/exampleName.ts` would map to plugin `exampleNamePlugin`. + This can be customized in writePlugins(). +*/ + +enum ModuleType { + CJS, + ESM, +} + +interface PluginDefinition { + listPath: string + rootPath: string + moduleType: ModuleType +} + +interface PluginFile { + path: string + name: string +} + +const pluginDefinitions = [ + { + listPath: 'scripts/temp/config-plugins.ts', + rootPath: 'scripts/config/plugins', + moduleType: ModuleType.ESM, + }, + { + listPath: 'src/temp/sitemap-fetcher-plugins.ts', + rootPath: 'src/lib/sitemap-fetcher/plugins', + moduleType: ModuleType.ESM, + }, + { + listPath: 'src/temp/middleware-plugins.ts', + rootPath: 'src/lib/middleware/plugins', + moduleType: ModuleType.ESM, + }, + { + listPath: 'src/temp/page-props-factory-plugins.ts', + rootPath: 'src/lib/page-props-factory/plugins', + moduleType: ModuleType.ESM, + }, + { + listPath: 'src/temp/next-config-plugins.js', + rootPath: 'src/lib/next-config/plugins', + moduleType: ModuleType.CJS, + }, + { + listPath: 'src/temp/extract-path-plugins.ts', + rootPath: 'src/lib/extract-path/plugins', + moduleType: ModuleType.ESM, + }, + { + listPath: 'src/temp/site-resolver-plugins.ts', + rootPath: 'src/lib/site-resolver/plugins', + moduleType: ModuleType.ESM, + }, +] + +function getPluginList(path: string, pluginName: string): PluginFile[] { + const plugins = getItems({ + path, + resolveItem: (path, name) => ({ + path: `${path}/${name}`, + name: `${name.replace(/-./g, (x) => x[1].toUpperCase())}Plugin`, + }), + cb: (name) => console.debug(`Registering ${pluginName} plugin ${name}`), + }) + + return plugins +} + +/** + * Generates the plugins file and saves it to the filesystem. + * By convention, we expect to find plugins under src/lib/{pluginName}/plugins/** (subfolders are + * searched recursively). The filename, with extension and non-word characters + * stripped, is used to identify the plugin's JavaScript module definition (for adding + * new plugin to the factory). + * Modify this function to use a different convention. + */ +function writePlugins( + listPath: string, + rootPath: string, + moduleType: ModuleType +) { + const segments = rootPath.split('/') + const pluginName = segments[segments.length - 2] + const plugins = getPluginList(rootPath, pluginName) + let fileContent = '' + + fileContent = plugins + .map((plugin) => { + return moduleType === ModuleType.CJS + ? `exports.${plugin.name} = require('${plugin.path.replace( + 'src/', + '../' + )}');` + : `export { ${plugin.name} } from '${plugin.path}';` + }) + .join('\r\n') + .concat('\r\n') + + if (!plugins.length) { + fileContent = + moduleType === ModuleType.CJS + ? 'module.exports = {};\r\n' + : 'export {};\r\n' + } + + const filePath = path.resolve(listPath) + console.log(`Writing ${pluginName} plugins to ${filePath}`) + fs.writeFileSync(filePath, fileContent, { + encoding: 'utf8', + }) +} + +function run(definitions: PluginDefinition[]) { + definitions.forEach((definition) => { + writePlugins( + definition.listPath, + definition.rootPath, + definition.moduleType + ) + }) +} + +run(pluginDefinitions) diff --git a/examples/cms-sitecore-xmcloud/scripts/scaffold-component.ts b/examples/cms-sitecore-xmcloud/scripts/scaffold-component.ts new file mode 100644 index 0000000000000..32d2f705facf9 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/scripts/scaffold-component.ts @@ -0,0 +1,95 @@ +/* + Component Scaffolding Script + This is a script that enables scaffolding a new JSS component using `jss scaffold `. + The default convention is that component names must start with a capital letter, and can contain + letters, number, underscores, or dashes. + + If the parameter includes a path, it must be relative to the src/components folder. + For example, `jss scaffold search/SearchBox` will create a component called `SearchBox` in + `src/components/search/SearchBox.tsx`. Specifying a relative path is optional, and just providing + the name is ok. + + Edit this script if you wish to use your own conventions for component storage in your JSS app. +*/ + +/* eslint-disable no-throw-literal,no-console */ + +import fs from 'fs' +import path from 'path' +import chalk from 'chalk' +import generateComponentSrc from './templates/component-src' + +const componentRootPath = 'src/components' + +// Matches component names that start with a capital letter, and contain only letters, number, +// underscores, or dashes. Optionally, the component name can be preceded by a relative path +const nameParamFormat = new RegExp(/^((?:[\w-]+\/)*)([A-Z][\w-]+)$/) +const componentArg = process.argv[2] + +if (!componentArg) { + throw 'Component name was not passed. Usage: jss scaffold ' +} + +const regExResult = nameParamFormat.exec(componentArg) + +if (regExResult === null) { + throw `Component name should start with an uppercase letter and contain only letters, numbers, +dashes, or underscores. If specifying a path, it must be relative to src/components` +} + +const componentPath = regExResult[1] +const componentName = regExResult[2] +const filename = `${componentName}.tsx` + +/** + * Force to use `crlf` line endings, we are using `crlf` across the project. + * Replace: `lf` (\n), `cr` (\r) + * @param {string} content + */ +function editLineEndings(content: string) { + return content.replace(/\r|\n/gm, '\r\n') +} + +/** + * Creates a file relative to the specified path if the file doesn't exist. Creates directories as needed. + * @param {string} rootPath - the root path + * @param {string} fileContent - the file content + * @param {string} filename - the filename + * @returns the new file's filepath + */ +function scaffoldFile( + rootPath: string, + fileContent: string, + filename: string +): string | null { + const outputDir = path.join(rootPath, componentPath) + const outputFile = path.join(outputDir, filename) + + if (fs.existsSync(outputFile)) { + console.log(chalk.red(`Skipping creating ${outputFile}; already exists.`)) + return null + } + + fs.mkdirSync(outputDir, { recursive: true }) + fs.writeFileSync(outputFile, editLineEndings(fileContent), 'utf8') + console.log(chalk.green(`File ${outputFile} has been scaffolded.`)) + return outputFile +} + +const componentOutputPath = scaffoldFile( + componentRootPath, + generateComponentSrc(componentName), + filename +) + +console.log( + chalk.green(` +Scaffolding of ${componentName} complete. +Next steps:`) +) + +if (componentOutputPath) { + console.log( + `* Implement the React component in ${chalk.green(componentOutputPath)}` + ) +} diff --git a/examples/cms-sitecore-xmcloud/scripts/temp/.npmignore b/examples/cms-sitecore-xmcloud/scripts/temp/.npmignore new file mode 100644 index 0000000000000..d6b7ef32c8478 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/scripts/temp/.npmignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/examples/cms-sitecore-xmcloud/scripts/temp/config-plugins.ts b/examples/cms-sitecore-xmcloud/scripts/temp/config-plugins.ts new file mode 100644 index 0000000000000..aacc0009fad84 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/scripts/temp/config-plugins.ts @@ -0,0 +1,4 @@ +export { computedPlugin } from 'scripts/config/plugins/computed' +export { fallbackPlugin } from 'scripts/config/plugins/fallback' +export { packageJsonPlugin } from 'scripts/config/plugins/package-json' +export { scjssconfigPlugin } from 'scripts/config/plugins/scjssconfig' diff --git a/examples/cms-sitecore-xmcloud/scripts/templates/component-factory.ts b/examples/cms-sitecore-xmcloud/scripts/templates/component-factory.ts new file mode 100644 index 0000000000000..63cc562fc9e0d --- /dev/null +++ b/examples/cms-sitecore-xmcloud/scripts/templates/component-factory.ts @@ -0,0 +1,142 @@ +/** + * Describes a file that represents a component definition + */ +export interface ComponentFile { + path: string + moduleName: string + componentName: string +} + +export interface PackageDefinition { + name: string + components: { + moduleName: string + componentName: string + }[] +} + +const isLazyLoadingModule = (componentPath: string) => + componentPath.includes('.dynamic') + +const removeDynamicModuleNameEnding = (moduleName: string) => + moduleName.replace(/\.?dynamic$/i, '') + +/** + * Generates the contents of the component factory file using a predefined string template. + * @param components - the list of component files to include + * @returns component factory file contents + */ +function generateComponentFactory( + components: (PackageDefinition | ComponentFile)[] +): string { + const componentFiles = components.filter( + (component) => (component as ComponentFile).path + ) as ComponentFile[] + const packages = components.filter( + (component) => (component as PackageDefinition).components + ) as PackageDefinition[] + + const hasLazyModules = componentFiles.find((component) => + isLazyLoadingModule(component.path) + ) + + return `/* eslint-disable */ +// Do not edit this file, it is auto-generated at build time! +// See scripts/generate-component-factory.ts to modify the generation of this file. + +${hasLazyModules ? "import dynamic from 'next/dynamic'" : ''} + +${packages.map((pkg) => { + const list = pkg.components.map((c) => c.moduleName).join(', ') + + return `import { ${list} } from '${pkg.name}'` +})} +${componentFiles + .map((component) => { + if (isLazyLoadingModule(component.path)) { + const moduleName = removeDynamicModuleNameEnding(component.moduleName) + return `const ${moduleName} = { + module: () => import('${component.path}'), + element: (isEditing?: boolean) => isEditing ? require('${component.path}')?.default : dynamic(${moduleName}.module) +}` + } + + return `import * as ${component.moduleName} from '${component.path}';` + }) + .join('\n')} + +const components = new Map(); +${packages.map((p) => + p.components.map( + (component) => + `components.set('${component.componentName}', ${component.moduleName})` + ) +)} +${componentFiles + .map( + (component) => + `components.set('${ + isLazyLoadingModule(component.path) + ? removeDynamicModuleNameEnding(component.componentName) + : component.componentName + }', ${ + isLazyLoadingModule(component.path) + ? removeDynamicModuleNameEnding(component.moduleName) + : component.moduleName + });` + ) + .join('\n')} + +// Next.js 'dynamic' import and JavaScript 'dynamic' import are different. +// Next.js 'dynamic(...)' returns common 'React.ComponentType' while +// 'import('...')' returns 'Promise' that will resolve module. +// componentModule uses 'import(...)' because primary usage of it to get not only 'React Component' (default export) but all named exports. +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports +// componentFactory uses 'dynamic(...)' because primary usage of it to render 'React Component' (default export). +// See https://nextjs.org/docs/advanced-features/dynamic-import +// At the end you will have single preloaded script for each lazy loading module. +// Editing mode doesn't work well with dynamic components in nextjs: dynamic components are not displayed without refresh after a rendering is added. +// This happens beacuse Sitecore editors simply insert updated HTML generated on server side. This conflicts with nextjs dynamic logic as no HTML gets rendered for dynamic component +// So we use require() to obtain dynamic components in editing mode while preserving dynamic logic for non-editing scenarios +// As we need to be able to seamlessly work with dynamic components in both editing and normal modes, different componentFactory functions will be passed to app + +export function componentModule(componentName: string) { + const component = components.get(componentName); + + // check that component is lazy loading module + if (!component?.default && component?.module) { + // return js dynamic import + return component.module(); + } + + return component; +} + +function baseComponentFactory(componentName: string, exportName?: string, isEditing?: boolean) { + const DEFAULT_EXPORT_NAME = 'Default'; + const component = components.get(componentName); + + // check that component should be dynamically imported + if (component?.element) { + // return next.js dynamic import + return component.element(isEditing); + } + + if (exportName && exportName !== DEFAULT_EXPORT_NAME) { + return component[exportName]; + } + + return component?.Default || component?.default || component; +} + +export function componentFactory(componentName: string, exportName?: string) { + return baseComponentFactory(componentName, exportName, false); +} + +export function editingComponentFactory(componentName: string, exportName?: string) { + return baseComponentFactory(componentName, exportName, true); +} +` +} + +export default generateComponentFactory diff --git a/examples/cms-sitecore-xmcloud/scripts/templates/component-src.ts b/examples/cms-sitecore-xmcloud/scripts/templates/component-src.ts new file mode 100644 index 0000000000000..9adae12a29c11 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/scripts/templates/component-src.ts @@ -0,0 +1,27 @@ +/** + * Generates React boilerplate for a component under `src/components` + * @param componentName - the component name + * @returns component src boilerplate as a string + */ +function generateComponentSrc(componentName: string): string { + return `import { Text, Field, withDatasourceCheck } from '@sitecore-jss/sitecore-jss-nextjs'; +import { ComponentProps } from 'lib/component-props'; + +type ${componentName}Props = ComponentProps & { + fields: { + heading: Field; + }; +}; + +const ${componentName} = (props: ${componentName}Props): JSX.Element => ( +
+

${componentName} Component

+ +
+); + +export default withDatasourceCheck()<${componentName}Props>(${componentName}); +` +} + +export default generateComponentSrc diff --git a/examples/cms-sitecore-xmcloud/scripts/utils.ts b/examples/cms-sitecore-xmcloud/scripts/utils.ts new file mode 100644 index 0000000000000..ab9e496f5b67d --- /dev/null +++ b/examples/cms-sitecore-xmcloud/scripts/utils.ts @@ -0,0 +1,64 @@ +import fs from 'fs' +import chokidar from 'chokidar' + +/** + * Run watch mode, watching on @var rootPath + */ +export function watchItems(rootPath: string, cb: () => void): void { + chokidar + .watch(rootPath, { ignoreInitial: true, awaitWriteFinish: true }) + .on('add', cb) + .on('unlink', cb) +} + +/** + * Using @var path find all files recursively and generate output using @var resolveItem by calling it for each file + * @param path plugins path + * @param resolveItem will resolve item in required data format + * @param cb will be called when new item is found + * @param fileFormat Matches specific files + * @returns {Item[]} items + */ +export function getItems(settings: { + path: string + resolveItem: (path: string, name: string) => Item + cb?: (name: string) => void + fileFormat?: RegExp +}): Item[] { + const { + path, + resolveItem, + cb, + fileFormat = new RegExp(/(.+)(? { + if (item.isDirectory()) { + folders.push(item) + } + + if (fileFormat.test(item.name)) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const name = item.name.match(fileFormat)![1] + items.push(resolveItem(path, name)) + cb && cb(name) + } + }) + + for (const folder of folders) { + items.push( + ...getItems({ + path: `${path}/${folder.name}`, + resolveItem, + cb, + fileFormat, + }) + ) + } + + return items +} diff --git a/examples/cms-sitecore-xmcloud/sitecore/config/xmcloud-nextjs-starter.config b/examples/cms-sitecore-xmcloud/sitecore/config/xmcloud-nextjs-starter.config new file mode 100644 index 0000000000000..e0a7e02d4a43f --- /dev/null +++ b/examples/cms-sitecore-xmcloud/sitecore/config/xmcloud-nextjs-starter.config @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + mw=100,mh=50 + + + mw=16 + mw=32 + mw=48 + mw=64 + mw=96 + mw=128 + mw=256 + mw=384 + mw=640 + mw=750 + mw=828 + mw=1080 + mw=1200 + mw=1920 + mw=2048 + mw=3840 + + + + + + + + + + false + + + + + + + false + + + + + + + diff --git a/examples/cms-sitecore-xmcloud/src/Layout.tsx b/examples/cms-sitecore-xmcloud/src/Layout.tsx new file mode 100644 index 0000000000000..42f6d7e765fc6 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/Layout.tsx @@ -0,0 +1,63 @@ +/** + * This Layout is needed for Starter Kit. + */ +import React from 'react' +import Head from 'next/head' +import { + Placeholder, + getPublicUrl, + LayoutServiceData, + Field, +} from '@sitecore-jss/sitecore-jss-nextjs' +import Scripts from 'src/Scripts' + +// Prefix public assets with a public URL to enable compatibility with Sitecore Experience Editor. +// If you're not supporting the Experience Editor, you can remove this. +const publicUrl = getPublicUrl() + +interface LayoutProps { + layoutData: LayoutServiceData +} + +interface RouteFields { + [key: string]: unknown + Title?: Field +} + +const Layout = ({ layoutData }: LayoutProps): JSX.Element => { + const { route } = layoutData.sitecore + const fields = route?.fields as RouteFields + const isPageEditing = layoutData.sitecore.context.pageEditing + const mainClassPageEditing = isPageEditing ? 'editing-mode' : 'prod-mode' + + return ( + <> + + + {fields?.Title?.value?.toString() || 'Page'} + + + + {/* root placeholder for the app, which we add components to using route data */} +
+
+ +
+
+
+ {route && } +
+
+
+ +
+
+ + ) +} + +export default Layout diff --git a/examples/cms-sitecore-xmcloud/src/Navigation.tsx b/examples/cms-sitecore-xmcloud/src/Navigation.tsx new file mode 100644 index 0000000000000..c1e7a1446f617 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/Navigation.tsx @@ -0,0 +1,27 @@ +import { getPublicUrl } from '@sitecore-jss/sitecore-jss-nextjs' + +// Prefix public assets with a public URL to enable compatibility with Sitecore editors. +// If you're not supporting Sitecore editors, you can remove this. +const publicUrl = getPublicUrl() + +const Navigation = (): JSX.Element => ( +
+ +
+) + +export default Navigation diff --git a/examples/cms-sitecore-xmcloud/src/NotFound.tsx b/examples/cms-sitecore-xmcloud/src/NotFound.tsx new file mode 100644 index 0000000000000..e4e59d16b6e3c --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/NotFound.tsx @@ -0,0 +1,19 @@ +import Head from 'next/head' + +/** + * Rendered in case if we have 404 error + */ +const NotFound = (): JSX.Element => ( + <> + + 404: NotFound + +
+

Page not found

+

This page does not exist.

+ Go to the Home page +
+ +) + +export default NotFound diff --git a/examples/cms-sitecore-xmcloud/src/Scripts.tsx b/examples/cms-sitecore-xmcloud/src/Scripts.tsx new file mode 100644 index 0000000000000..c57b322012fde --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/Scripts.tsx @@ -0,0 +1,5 @@ +const Scripts = (): JSX.Element | null => { + return null +} + +export default Scripts diff --git a/examples/cms-sitecore-xmcloud/src/assets/app.css b/examples/cms-sitecore-xmcloud/src/assets/app.css new file mode 100644 index 0000000000000..489cdc72b0b2d --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/app.css @@ -0,0 +1,39 @@ +a[target='_blank']:after { + content: '\1F5D7'; +} + +/* + Hides Sitecore editor markup, + if you run the app in connected mode while the Sitecore cookies + are set to edit mode. +*/ +.scChromeData, +.scpm { + display: none !important; +} + +/* + Styles for default JSS error components +*/ +.sc-jss-editing-error, +.sc-jss-placeholder-error { + padding: 1em; + background-color: lightyellow; +} + +/* + Style for default content block +*/ +.contentTitle { + font-size: 3.5rem; + font-weight: 300; + line-height: 1.2; +} + +a { + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/basic/_component.scss b/examples/cms-sitecore-xmcloud/src/assets/basic/_component.scss new file mode 100644 index 0000000000000..6081809a40dee --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/basic/_component.scss @@ -0,0 +1,18 @@ +.component-content { + @include respond-to(mobile-large) { + .row { + padding: 0; + margin: 0; + } + } +} +@include respond-to(mobile-large) { + .row { + margin: 0; + padding: 0; + > * { + padding: 0; + margin: 0; + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/basic/_container.scss b/examples/cms-sitecore-xmcloud/src/assets/basic/_container.scss new file mode 100644 index 0000000000000..8644503433da7 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/basic/_container.scss @@ -0,0 +1,79 @@ +@import '@sass/abstracts/mixins'; +@import 'variables'; + +body { + font-family: Roboto; + color: $main-color; +} + +@media (min-width: 1400px) { + .container { + max-width: 1250px; + } +} + +/**** START STYLE MAX HEIGHT ****/ +.prod-mode { + display: flex; + flex-direction: column; + height: 100vh; + + main { + flex: 1 0 auto; + } + + footer { + flex-shrink: 0; + } +} +/**** END STYLE MAX HEIGHT ****/ + +main { + .main-header { + margin-top: 55px; + margin-bottom: 30px; + + @include respond-to(mobile-large) { + margin-bottom: 0; + } + + h4 { + font-size: $text-size-50; + font-weight: 500; + line-height: 70px; + } + } + + .main-news-header { + margin-top: 60px; + + h2 { + font-size: $text-size-36; + line-height: 43px; + } + } + + @include respond-to(mobile-large) { + padding-top: 0; + margin-bottom: 0; + + .main-header { + margin-top: 10px; + + h4 { + font-size: $text-size-24; + line-height: 32px; + } + } + + .main-news-header { + padding-bottom: 35px; + + h2 { + margin-top: 0; + font-size: $text-size-18; + line-height: 22px; + } + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/basic/_fonts.scss b/examples/cms-sitecore-xmcloud/src/assets/basic/_fonts.scss new file mode 100644 index 0000000000000..d383a9572f687 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/basic/_fonts.scss @@ -0,0 +1 @@ +@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap'); diff --git a/examples/cms-sitecore-xmcloud/src/assets/basic/_footer.scss b/examples/cms-sitecore-xmcloud/src/assets/basic/_footer.scss new file mode 100644 index 0000000000000..bc97d5ce85074 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/basic/_footer.scss @@ -0,0 +1,32 @@ +@import '@sass/abstracts/mixins'; +@import 'variables'; + +footer { + @include respond-to(mobile-large) { + padding-top: 0; + } + .container-dark-background { + background-color: #262626; + } + + .contacts { + font-size: $text-size-14; + + a { + color: $text-white; + } + + p, + span { + color: $text-white; + } + } + + .indent-inner { + padding: 65px; + + @include respond-to(mobile-large) { + padding: 40px 0; + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/basic/_header.scss b/examples/cms-sitecore-xmcloud/src/assets/basic/_header.scss new file mode 100644 index 0000000000000..e5810fee4a454 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/basic/_header.scss @@ -0,0 +1,49 @@ +@import '@sass/abstracts/vars/colors'; +@import '@sass/abstracts/mixins'; +@import 'variables'; + +.prod-mode { + #header { + display: flex; + + @include respond-to(mobile-large) { + padding-bottom: 0; + flex-direction: column-reverse; + } + } +} + +header { + #header { + .bs-title { + padding-left: 50px; + + h1 { + font-size: $text-size-24; + font-weight: 600; + line-height: 18px; + padding-top: 40px; + margin: 0; + } + } + + @include respond-to(mobile-large) { + padding-bottom: 0; + padding-top: 0; + flex-direction: column-reverse; + + .bs-title { + padding-left: 0; + text-align: center; + margin-top: -5px; + + h1 { + font-weight: 500; + line-height: 29px; + padding-top: 0; + margin-bottom: 10px; + } + } + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/basic/_navigation.scss b/examples/cms-sitecore-xmcloud/src/assets/basic/_navigation.scss new file mode 100644 index 0000000000000..43e7ef89ff932 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/basic/_navigation.scss @@ -0,0 +1,152 @@ +@import '@sass/abstracts/vars'; +@import 'variables'; + +$hamburger-width: 28px; +$hamburger-height: 18px; +$border-size: 6px; +$hamburger-margin: 18px; + +.navigation { + .menu-mobile-navigate-wrapper { + width: 100%; + } +} + +.navigation.navigation-horizontal { + width: 100%; + padding-right: 70px; + margin-top: -15px; + + .menu-mobile-navigate { + display: none; + } + + @include respond-to(mobile-large) { + padding-right: 0; + } + + .component-content { + display: inline-block; + + @include respond-to(mobile-large) { + display: none; + } + + ul.clearfix { + list-style: none; + } + .level0 { + display: flex; + } + + .level0, + .level1 { + float: left; + margin-left: 30px; + + > .navigation-title { + > a { + border-width: 0; + font-size: $text-size-14; + font-weight: 400; + } + } + } + } + + @include respond-to(mobile-large) { + .component-content { + display: none; + position: fixed; + top: 0; + bottom: 0; + right: 0; + left: 0; + background-color: $bg-black-active; + z-index: 1; + + * { + text-align: center !important; + } + + nav { + padding-top: 110px; + + .level0 { + display: block; + } + } + + ul { + margin: 0; + padding: 0; + } + + .level0, + .level1 { + float: unset; + margin-left: 0; + > .navigation-title { + > a { + font-size: $text-size-30; + line-height: 90px; + > span { + color: $text-white; + } + } + } + } + } + + .menu-mobile-navigate-wrapper { + .menu-mobile-navigate { + display: inline-block; + z-index: 2; + position: absolute; + right: $hamburger-margin; + top: $hamburger-margin; + height: $hamburger-width; + width: $hamburger-width; + opacity: 0; + + &:checked ~ { + .menu-humburger { + &::before { + content: '\00d7'; + color: $text-basic-active; + font-size: $text-size-48; + position: fixed; + top: 0; + right: 0; + margin-top: calc($hamburger-margin + ($hamburger-margin / 2)); + margin-right: $hamburger-margin; + line-height: 0; + border: 0; + } + } + + .component-content { + position: fixed; + display: inline-block; + } + } + } + + .menu-humburger { + &::before { + content: ''; + width: $hamburger-width; + height: $hamburger-height; + margin-right: $hamburger-margin; + margin-top: calc($hamburger-margin + ($hamburger-margin / 2)) + 8px; + display: block; + border-top: 6px solid $bg-black; + border-bottom: 6px solid $bg-black; + float: right; + z-index: 99; + cursor: pointer; + } + } + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/basic/_promo.scss b/examples/cms-sitecore-xmcloud/src/assets/basic/_promo.scss new file mode 100644 index 0000000000000..255b26c9b3d95 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/basic/_promo.scss @@ -0,0 +1,58 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; +/**PROMO**/ + +.promo { + &.main-promo-no-border { + padding-left: 0; + padding-right: 0; + padding-bottom: 0; + margin-bottom: 80px; + + @include respond-to(mobile-large) { + margin-bottom: 0; + } + + > .component-content { + border: 0; + max-width: 583px; + margin-right: 50px; + + > div { + padding-bottom: 0; + } + + @include respond-to(mobile-large) { + max-width: 100%; + margin-right: 0; + } + + .promo-text { + .field-promotext { + font-size: 14px; + + h3 { + font-size: $text-size-18; + margin: 15px 0; + + @include respond-to(mobile-large) { + margin: 10px 0; + } + } + p { + margin: 10px 0; + } + } + } + @include respond-to(mobile-large) { + > div { + padding: 0; + } + + .promo-text { + padding: 15px 30px 5px 30px; + } + } + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/basic/_rich-text.scss b/examples/cms-sitecore-xmcloud/src/assets/basic/_rich-text.scss new file mode 100644 index 0000000000000..ae6878e5a0ce8 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/basic/_rich-text.scss @@ -0,0 +1,11 @@ +@import '@sass/abstracts/vars'; +@import 'variables'; + +.rich-text { + font-size: $text-size-16; + + @include respond-to(mobile-large) { + padding: 0 30px 10px 30px; + font-size: $text-size-14; + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/basic/_variables.scss b/examples/cms-sitecore-xmcloud/src/assets/basic/_variables.scss new file mode 100644 index 0000000000000..ad4c5f8bc79f6 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/basic/_variables.scss @@ -0,0 +1,10 @@ +$text-size-14: 14px; +$text-size-16: 16px; +$text-size-18: 18px; +$text-size-24: 24px; +$text-size-30: 30px; +$text-size-36: 36px; +$text-size-48: 48px; +$text-size-50: 50px; + +$main-color: #27272a; diff --git a/examples/cms-sitecore-xmcloud/src/assets/basic/main.scss b/examples/cms-sitecore-xmcloud/src/assets/basic/main.scss new file mode 100644 index 0000000000000..2ce2e49be80e5 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/basic/main.scss @@ -0,0 +1,8 @@ +@import 'fonts'; +@import 'container'; +@import 'header'; +@import 'promo'; +@import 'navigation'; +@import 'component'; +@import 'rich-text'; +@import 'footer'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/main.scss b/examples/cms-sitecore-xmcloud/src/assets/main.scss new file mode 100644 index 0000000000000..addfe5ca592e7 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/main.scss @@ -0,0 +1,5 @@ +@import 'basic/fonts'; +@import 'font-awesome/scss/font-awesome'; +@import 'bootstrap/scss/bootstrap'; +@import 'sass/main.scss'; +@import 'basic/main.scss'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/_app.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/_app.scss new file mode 100644 index 0000000000000..10ba4ad39b1c0 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/_app.scss @@ -0,0 +1,105 @@ +html { + font-size: 62.5%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + margin-bottom: 0px; + height: auto !important; +} + +body { + @include opensans-font-stack(); + position: relative; + overflow: auto; + color: $text-basic; + font-size: 16px; + line-height: 1.5; + background: $page-bg; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + height: auto !important; + &.on-page-editor { + background: $page-bg-editor !important; + } +} + +a { + color: $text-basic; + cursor: pointer; + font-size: 0.85em; + &:hover { + color: $text-basic-active; + } +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-weight: bold; +} + +h1 { + font-size: 2em; +} + +h2 { + font-size: 1.5em; +} + +h3 { + font-size: 1.2em; +} + +h4 { + font-size: 1em; +} + +h5 { + font-size: 0.83em; +} + +h6 { + font-size: 0.67em; +} + +ul li { + list-style-type: none; +} + +.xa-variable { + border: 0px; + padding: 1px; + margin: 0px; + background-color: #ebebe4; + color: #545454; + user-select: none; + pointer-events: none; +} + +//Navigation Bar fix +#breadcrumbMenuSubcontrol_context_menu * { + box-sizing: initial; +} + +//End navigation bar fix +.menu-mobile-navigate { + display: none; +} + +#header, +#content, +#footer, +.sc-jss-empty-placeholder { + width: 100%; + display: flex; + flex-wrap: wrap; +} + +.sc-jss-placeholder-error { + background: #ff0000; + outline: 5px solid #e36565; + padding: 10px; + color: #fff; + max-width: 500px; +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/_functions.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/_functions.scss new file mode 100644 index 0000000000000..fdef2da81ddb1 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/_functions.scss @@ -0,0 +1,7 @@ +@function headings($from: 1, $to: 6) { + @if $from==$to { + @return 'h#{$from}'; + } @else { + @return 'h#{$from},' + headings($from + 1, $to); + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/_mixins.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/_mixins.scss new file mode 100644 index 0000000000000..b6be54ed08466 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/_mixins.scss @@ -0,0 +1,121 @@ +@import 'vars'; + +/* breakpoints */ + +$break-desktop: 1100px; +$break-mobile: 380px; +$break-mobile-horizontal: 640px; +$break-mobile-large: 992px; +@mixin wrapper() { + max-width: 960px; + margin: 0 auto !important; +} +@mixin clearfix { + &:after { + content: ''; + display: table; + clear: both; + } +} +@mixin headings($from: 1, $to: 6) { + @for $i from $from through $to { + h#{$i} { + @content; + } + } +} +@mixin font-size($sizeValue: 1.6) { + font-size: ($sizeValue * 10) + px; + font-size: $sizeValue + rem; +} +@mixin proxima-font($weight: semibold) { + @if ($weight==semibold) { + font-family: 'ProximaNova-Semibold', arial, helvetica, sans-serif; + } @else if($weight==light) { + font-family: 'ProximaNova-Light', arial, helvetica, sans-serif; + } +} +@mixin opensans-font-stack() { + font-family: 'Open Sans', Helvetica, Verdana, Tahoma, sans-serif; +} +@mixin loading-gif() { + background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiBmaWxsPSJibGFjayI+DQogIDxwYXRoICBvcGFjaXR5PSIuMjUiIGQ9Ik0xNiAwIEExNiAxNiAwIDAgMCAxNiAzMiBBMTYgMTYgMCAwIDAgMTYgMCBNMTYgNCBBMTIgMTIgMCAwIDEgMTYgMjggQTEyIDEyIDAgMCAxIDE2IDQiLz4NCiAgPHBhdGggZmlsbD0nIzFhODBiNicgZD0iTTE2IDAgQTE2IDE2IDAgMCAxIDMyIDE2IEwyOCAxNiBBMTIgMTIgMCAwIDAgMTYgNHoiPg0KICAgIDxhbmltYXRlVHJhbnNmb3JtIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSIgdHlwZT0icm90YXRlIiBmcm9tPSIwIDE2IDE2IiB0bz0iMzYwIDE2IDE2IiBkdXI9IjAuOHMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPg0KICA8L3BhdGg+DQo8L3N2Zz4NCg==); + background-position: center center; + background-repeat: no-repeat; +} +@mixin respond-to($media) { + @if $media==mobile { + @media only screen and (max-width: $break-mobile) { + @content; + } + } @else if $media==mobile-horizontal { + @media only screen and (max-width: $break-mobile-horizontal - 1) { + @content; + } + } @else if $media==mobile-large { + @media only screen and (max-width: $break-mobile-large) { + @content; + } + } @else if $media==tablet { + @media only screen and (min-width: $break-mobile + 1) and (max-width: $break-desktop - 1) { + @content; + } + } @else if $media==all-mobile { + @media only screen and (max-width: $break-desktop - 1) { + @content; + } + } @else if $media==desktop { + @media only screen and (min-width: $break-desktop) { + @content; + } + } +} +@mixin border-basic( + $position: all, + $border-color: $border-gray, + $border-width: 1px +) { + @if ($position!=all) { + border-#{$position}-width: $border-width; + border-#{$position}-style: solid; + border-#{$position}-color: $border-color; + } @else { + border-width: $border-width; + border-style: solid; + border-color: $border-color; + } +} +@mixin fixed-bg($pos, $min-height: 240px) { + background-position: $pos; + background-attachment: fixed; + min-height: $min-height; +} +@mixin linear-gradient($direction, $color-stops...) { + // Direction has been omitted and happens to be a color-stop + @if is-direction($direction) ==false { + $color-stops: $direction, $color-stops; + $direction: 180deg; + } + background: nth(nth($color-stops, 1), 1); + background: -webkit-linear-gradient( + legacy-direction($direction), + $color-stops + ); + background: linear-gradient($direction, $color-stops); +} +@mixin default-link-button { + @include border-basic(); + @include font-size(1.2); + display: inline-block; + vertical-align: middle; + box-sizing: border-box; + margin: 5px 0; + padding: 10px 15px; + text-align: center; + text-decoration: none; + font-weight: bold; + &:hover { + color: $text-basic; + background: $bg-light-gray; + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/_vars.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/_vars.scss new file mode 100644 index 0000000000000..c3c1d94e358d0 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/_vars.scss @@ -0,0 +1,3 @@ +@import 'vars/colors'; +@import 'vars/margins'; +@import 'vars/fontSizes'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/vars/_colors.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/vars/_colors.scss new file mode 100644 index 0000000000000..c7b73f40c87e1 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/vars/_colors.scss @@ -0,0 +1,283 @@ +//Backgrounds colors +$bg-transparent: transparent !default; +$bg-basic-color: #ffffff !default; +$bg-basic-color-active: #cccccc !default; +$bg-light-gray: #f7f7f7 !default; +$bg-light-gray-active: #dadada !default; +$bg-blue: #89c6cc; +$bg-blue-active: #15909c !default; +$bg-submenu: #edebeb !default; +$bg-submenu-active: #f6f6f6 !default; +$bg-black: #000; +$bg-black-active: #3d3d3d; +$bg-dark-gray: #262626; +//Text colors +$text-white: #fff !default; +$text-heading-color: #222 !default; +$text-basic: #747474 !default; +$text-basic-active: #878787 !default; +$text-blue: #89c6cc !default; +$text-blue-active: #15909c !default; +$text-submenu-active: #222 !default; +$text-disabled: #aaa !default; +$text-black: #000 !default; +$text-red: #de232f; +$text-gray: #262626; +//Border colors +$border-gray: #d2d2d2 !default; +$border-white: #ffffff !default; +$border-basic-color: #89c6cc !default; +$border-basic-active: #15909c !default; +$border-nav-submenu: #e1e1e1; +//Styles for each component separately +//Accordion +$accordion-header-bg: transparent; +$accordion-header-bg-active: transparent; +$accordion-toggled-bg: transparent; +$accordion-header-border: $border-gray; +//Breadcrumb +$breadcrumb-dropdown-bg: $bg-basic-color; +$breadcrumb-dropdown-bg-active: $bg-blue; +$breadcrumb-dropdown-text-active: $text-white; +$breadcrumb-bg: transparent; +$breadcrumb-color: $text-basic; +$breadcrumb-color-active: $text-blue-active; +//Buttons colors +$btn-green-light: #a0ce4e; +$btn-green: #92be43; +$btn-red-active: #c34e30; +$btn-red: #bc4526; +//Carousel +$carousel-bg: $bg-basic-color; +$carousel-nav: $text-black; +$carousel-nav-active: $text-basic-active; +$carousel-nav-border: $border-basic-color; +//Container component +$container-title-row-bg: $bg-light-gray; +//Event List +$event-list-bg: transparent; +$event-list-item-bg: $bg-basic-color; +$event-list-item-color: $text-basic; +$event-list-title-border: $border-basic-color; +//Calendar +$calendar-bg: $bg-basic-color; +$calendar-header-bg: $bg-basic-color-active; +$calendar-day-color-active: $text-white; +$calendar-title-color: $text-white; +//Feed +$feed-bg: transparent; +$feed-item-bg: transparent; +//Field Editor +$field-editor-bg: transparent; +$field-editor-table-border: $bg-light-gray; +$field-editor-text-header: $text-black; +$field-editor-text-header-active: $text-red; +$field-editor-text: $text-basic; +//File List +$file-list-bg: transparent; +$file-list-item-bg: transparent; +$file-list-title-color: $text-basic; +$file-list-title-border: transparent; +$file-list-item-color: $text-basic; +$file-list-item-color-active: $text-basic-active; +$file-list-item-size: $text-basic; +$file-list-item-borer: $border-basic-color; +$file-list-link-btn-color-active: $text-white; +$file-list-link-btn-color: $text-basic; +$file-list-link-btn-bg: $bg-blue-active; +$file-list-link-btn-bg-active: $bg-blue; +//Flip +$flip-bg: transparent; +$flip-slides-bg: $bg-basic-color; +$flip-slides0-bg: #f6f6f6; +$flip-slides1-bg: $bg-blue-active; +$flip-slides1-color: $text-white; +$flip-border: $border-gray; +//Gallery +$galleria-container-bg: transparent; +$gallery-info-bg: $bg-basic-color; +$gallery-info-border: $border-gray; +$gallery-info-text: $text-basic; +$gallery-nav-active: $text-white; +$gallery-nav: $text-basic; +$gallery-counter-color: $text-white; +//Language selector +$lang-selector-bg: $bg-basic-color; +$lang-selector-border: $border-basic-color; +$lang-selector-item-bg-active: $bg-basic-color; +$lang-selector-item-border: $border-white; +$lang-selector-item-border-active: $border-basic-active; +//Site selector +$site-selector-color: $text-basic; +//Link List +$link-list-bg: transparent; +$link-list-header-border: $border-basic-color; +$link-list-items-bg: transparent; +$link-list-item-bg: transparent; +$link-list-item-color: $text-basic; +$link-list-item-color-active: $text-basic-active; +$link-list-item-border-active: $border-basic-color; +//Login +$login-bg: transparent; +//Logout +$logout-bg: transparent; +$logout-link-text-color: $text-basic; +$logout-link-text-color-active: $text-basic-active; +$logout-link-border: $border-basic-color; +//Map +$map-bg: transparent; +$map-border: none; +//Page List +$page-list-bg: $bg-transparent; +$page-list-item-bg: $bg-transparent; +$page-list-item-title-text: $text-black; +$page-list-item-border: $border-basic-color; +//Pagination +$list-pagination-bg: transparent; +$list-pagination-active-bg: $bg-blue; +$list-pagination-active-color: $text-white; +$list-pagination-active-color: $text-blue; +$list-pagination-active-bg: $bg-submenu-active; +$list-pagination-active-border: $border-basic-active; +//Play list +$play-list-bg: transparent; +$play-list-item-bg: transparent; +$play-list-item-color: $text-basic; +$play-list-item-color-active: $text-white; +$play-list-nav-active: $text-blue; +$play-list-item-bg: $bg-light-gray; +$play-list-item-active-bg: $bg-blue; +$play-list-border: $border-basic-color; +$play-list-title-border: $border-basic-color; +//Promo +$promo-bg: $bg-basic-color; +$promo-bg-hero: rgba(0, 0, 0, 0.5); +$promo-border: $border-gray; +$promo-hero-text-color: $text-white; +$promo-shadow-border: $border-basic-color; +//Rich Text Content +$rich-content-bg: transparent; +$rich-content-color: $text-basic; +$rich-content-border: transparent; +$rich-content-link-color: $text-red; +$rich-content-link-color-active: $text-basic-active; +//Search +$search-filter: $text-basic; +$search-filter-border: $border-basic-color; +//Menu colors +$menu-hover-color: #1b809e; +$menu-active-color: #176f89; +//Navigation +$nav-bg: transparent; +$nav-color-root: $text-basic; +$nav-color-root-active: $text-basic; +$nav-border-root: $border-basic-color; +$nav-border-root-active: $border-basic-color; +$nav-color-submenu: $text-submenu-active; +$nav-color-submenu-active: $text-submenu-active; +$nav-bg-root: $bg-submenu-active; +$nav-bg-submenu: $bg-submenu-active; +$nav-bg-submenu-active: $bg-submenu-active; +$nav-border-submenu: $border-basic-color; +$nav-submenu-item-border: $border-gray; +$nav-submenu-border-active: $border-basic-color; +//Social Media Share +$social-media-share-bg: transparent; +//Tabs +$tab-heading-bg: $bg-light-gray; +$tab-heading-active-bg: $bg-basic-color; +$tab-heading-color: $text-heading-color; +$tab-heading-active-color: $text-black; +$tab-container-bg: transparent; +$tab-container-border: $border-basic-color; +//Title +$title-bg: transparent; +$title-color: $text-basic; +$title-color-active: $text-basic-active; +//Toggle +$toggle-header-bg: $bg-basic-color; +$toggle-content-bg: $bg-basic-color; +$toggle-show-color: $text-basic-active; +//Search Components +$search-btn-bg: transparent; +$search-btn-active-bg: #e0e0e0; +$search-btn-active-border: #adadad; +//Image component +$image-caption-color: $text-basic; +//Media Link Component +$media-link-bg: transparent; +$media-link-border: $border-basic-color; +$media-link-color: $text-basic; +$media-link-color-active: $text-basic-active; +//Tag Component +$tag-color: $text-basic; +$tag-color-active: $text-basic-active; +$tag-border-active: $border-basic-active; +$tag-link-bg: $bg-blue; +$tag-link-bg-active: $bg-blue-active; +$tag-link-color: $text-white; +//Link Component +$link-bg: transparent; +$link-text-color: $text-basic; +$link-text-color-active: $text-basic-active; +$link-border: $border-basic-color; +//Overlay +$overlay-bg: $bg-light-gray; +//Search Components +$search-title-border: $border-basic-color; +$search-title-color: $text-basic; +$search-item-color: $text-basic; +$search-item-color-active: $text-basic; +$search-item-border: $border-basic-color; +$search-item-border-active: $border-basic-active; +// +//Search Facet Summary +$search-facet-summary-border: transparent; +$search-facet-summary-background: transparent; +$search-facet-summary-item-color: $text-basic; +$search-facet-summary-item-color-horizontal: $text-basic; +$search-facet-summary-item-border: $border-gray; +$search-facet-summary-item-border-horizontal: $border-basic-color; +$search-facet-summary-item-shadow: $border-gray; +$search-facet-summary-clear-border-horizontal: $btn-red; +$search-facet-summary-clear-color: $text-red; +$search-facet-summary-clear-color-horizontal: $text-blue; +// +$search-filter-radius-active: $text-blue; +$search-filter-radius-border: $border-gray; +$search-filter-radius-bg: $border-gray; +// +$search-filter-slider-border-active: $border-basic-color; +$search-filter-slider-bg-active: $bg-blue; +$search-filter-slider-btn-border: $border-gray; +$search-filter-slider-btn-bg: $bg-light-gray; +$search-filter-slider-btn-bg-active: $bg-light-gray-active; +//Serach Pagination +$search-pagination-bg: transparent; +$search-pagination-active-bg: $bg-blue; +$search-pagination-active-color: $text-white; +$search-pagination-hover-color: $text-blue; +$search-pagination-hover-bg: $bg-submenu-active; +$search-pagination-hover-border: $border-basic-active; +//Search selector +$serach-selector-variant-color-active: $text-blue-active; +//Typehead +$tt-color: $text-basic; +$tt-color-active: $text-blue; +$tt-price-color: $text-blue; +$tt-dropdown-bg: $bg-light-gray; +$tt-suggestion-bg-active: $bg-light-gray-active; +$tt-dropdown-border: $border-gray; +//Video +$video-control-bg: $bg-basic-color; +$video-time-color: $text-basic; +$video-time-total-bg: $bg-black; +$video-time-handle-border: $border-gray; +$video-time-handle-bg: $bg-black; +//Form component +$form-bg: transparent; +$form-border: transparent; +$form-color: $text-basic; +//Main +$page-bg: $bg-basic-color; +$page-bg-editor: none; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/vars/_fontSizes.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/vars/_fontSizes.scss new file mode 100644 index 0000000000000..ca825908578af --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/vars/_fontSizes.scss @@ -0,0 +1,16 @@ +$font-small: 11px; +$font-normal: 13px; +$font-medium: 16px; +$font-big: 20px; +$font-extrabig: 24px; +//Navigation +$navigation-font-basic: 18px; +$navigation-font-basic-submenu: 14px; +//Tabs +$tab-font-header: 16px; +//Accordion +$accordion-header: 16px; +//Breadcrumb +$breadcrumb-font: 15px; +// Link List +$link-list-title-font: 15px; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/vars/_margins.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/vars/_margins.scss new file mode 100644 index 0000000000000..e4e3b68770a14 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/abstracts/vars/_margins.scss @@ -0,0 +1,11 @@ +$default-padding: 5px; +$default-inputs-padding: 12px 10px; + +$extrasmall-margin-bottom: 2px; +$extrasmall-margin: 5px; +$small-margin: 10px; +$middle-margin: 20px; +$large-margin: 30px; +$extralarge-margin: 40px; + +$small-padding: 10px; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/base/fonts/_fonts.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/base/fonts/_fonts.scss new file mode 100644 index 0000000000000..49d54e9f19c92 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/base/fonts/_fonts.scss @@ -0,0 +1 @@ +@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i,800,800i&subset=cyrillic,cyrillic-ext,greek,greek-ext,latin-ext,vietnamese'); diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/base/fonts/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/base/fonts/index.scss new file mode 100644 index 0000000000000..2b1a3796bbc5a --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/base/fonts/index.scss @@ -0,0 +1 @@ +@import 'fonts'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/base/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/base/index.scss new file mode 100644 index 0000000000000..72c7ca6829ec8 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/base/index.scss @@ -0,0 +1,3 @@ +@import 'fonts'; +@import 'links'; +@import 'reset/ui-datepicker'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/base/links/_link-button.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/base/links/_link-button.scss new file mode 100644 index 0000000000000..cd6d696c5c09d --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/base/links/_link-button.scss @@ -0,0 +1,29 @@ +@import '@sass/abstracts/vars/colors'; +@import '@sass/abstracts/mixins'; + +.button-default, +.default { + @include default-link-button(); + background: #f6f6f6; + color: $text-basic; +} +.button-success, +.success { + @include default-link-button(); + background: $btn-green-light; + color: $text-white; + &:hover { + color: $text-white; + background: $btn-green; + } +} +.button-warning, +.warning { + @include default-link-button(); + background: $btn-red; + color: $text-white; + &:hover { + color: $text-white; + background: $btn-red-active; + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/base/links/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/base/links/index.scss new file mode 100644 index 0000000000000..b47c809b056d3 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/base/links/index.scss @@ -0,0 +1 @@ +@import 'link-button'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/base/reset/_inputs.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/base/reset/_inputs.scss new file mode 100644 index 0000000000000..2b76bcb99ef67 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/base/reset/_inputs.scss @@ -0,0 +1,67 @@ +@import '@sass/abstracts/mixins'; +@import '@sass/abstracts/vars'; + +label { + background: transparent; + color: $text-basic; +} +input, +button { + display: inline-block; + box-sizing: border-box; + background: $bg-basic-color !important; + border: 1px solid $border-gray; + border-radius: 0 !important; + color: $text-basic; + font-weight: normal; + padding: 8px 2%; + clear: both; + height: auto; + font-size: $font-normal; +} +input:focus { + border: 1px solid $border-gray; +} +input[type='submit'], +input[type='reset'], +input[type='button'], +button { + border: 1px solid $border-basic-color; + padding: 5px 20px; + &:hover { + background: $bg-light-gray !important; + } +} +input::-moz-focus-inner, +button::-moz-focus-inner { + border: 0; + padding: 0; +} +input[type='text'] { + font-size: 13px; +} +input[type='text'], +input[type='email'], +input[type='password'], +textarea, +select { + box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.1); + color: $text-basic; + width: 100%; + background: #fff; + border: 1px solid $border-gray; +} + +select { + display: inline-block; + vertical-align: middle; + *vertical-align: auto; + *zoom: 1; + *display: inline; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + outline: none; + cursor: pointer; + padding: 5px; +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/base/reset/_links.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/base/reset/_links.scss new file mode 100644 index 0000000000000..c83f047bbd14a --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/base/reset/_links.scss @@ -0,0 +1,14 @@ +@import '@sass/abstracts/mixins'; +@import '@sass/abstracts/vars'; + +a { + background-color: transparent; + text-decoration: none; + font-size: 1em; + color: $text-basic; + border-bottom: 1px solid $border-basic-color; + &:hover { + color: $text-basic-active; + text-decoration: none; + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/base/reset/_ui-datepicker.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/base/reset/_ui-datepicker.scss new file mode 100644 index 0000000000000..9305794899cf2 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/base/reset/_ui-datepicker.scss @@ -0,0 +1,7 @@ +@import '@sass/abstracts/vars'; + +.ui-datepicker-title { + select { + color: $text-basic; + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/base/richtext/_richtext-files-icons.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/base/richtext/_richtext-files-icons.scss new file mode 100644 index 0000000000000..59fe03b50cafa --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/base/richtext/_richtext-files-icons.scss @@ -0,0 +1,203 @@ +@import '@fontawesome/scss/mixins'; +@import '@fontawesome/scss/variables'; +@import '@sass/abstracts/mixins'; + +//files icons +.field-filetypeicon > a > span, +.pdf, +.xls, +.xlsx, +.pptx, +.ppt, +.docx, +.doc, +.bmp, +.png, +.jpg, +.jpeg, +.psd, +.gif, +.avi, +.mp4, +.wmv, +.mov, +.mp3, +.wma, +.txt, +.zip { + display: inline-block; + box-sizing: border-box; + font-weight: normal; + width: 15px; + height: 21px; + margin: 0; + padding: 0; + position: relative; + &:before { + @include fa-icon(); + font-size: 16px !important; + position: absolute; + display: block; + top: 50%; + margin: 0; + padding: 0; + transform: translateY(-50%); + } +} +.field-filetypeicon span:before { + content: $fa-var-file-o; +} +.field-filetypeicon { + .pdf:before { + content: $fa-var-file-pdf-o; + } + .xlsx, + .xls { + &:before { + content: $fa-var-file-excel-o; + } + } + .pptx, + .ppt { + &:before { + content: $fa-var-file-powerpoint-o; + } + } + .docx, + .doc { + &:before { + content: $fa-var-file-word-o; + } + } + .bmp, + .png, + .jpg, + .jpeg, + .psd, + .gif, + .tif { + &:before { + content: $fa-var-file-image-o; + } + } + .avi, + .mp4, + .wmv, + .mov, + .mpg, + .mkv, + .vp6, + .vid, + .rv, + .webm, + .swf, + .flv, + .m4v, + .h264, + .mk3d, + .gifv, + .oggv, + .movie, + .divx { + &:before { + content: $fa-var-file-video-o; + } + } + .mp3, + .wma, + .wav, + .fla, + .flac, + .ra, + .rma, + .aif, + .aiff, + .aa, + .aac, + .mid, + .midi, + .aax, + .ac3, + .au, + .ogg, + .avr, + .m4a, + .mp4a, + .amz, + .mka, + .asx, + .pcm, + .m3u, + .xwma { + &:before { + content: $fa-var-file-audio-o; + } + } + .txt:before { + content: $fa-var-file-text-o; + } + .zip, + .zipx, + .rar, + .tar, + .gz, + .dmg, + .iso { + &:before { + content: $fa-var-file-archive-o; + } + } + .css, + .js, + .py, + .git, + .py, + .cpp, + .h, + .ini, + .config { + &:before { + content: $fa-var-file-code-o; + } + } + .exe, + .jar, + .dll, + .bat, + .pl, + .scr, + .msi, + .app, + .deb, + .apk, + .jar, + .vb, + .prg, + .sh { + &:before { + content: $fa-var-cogs; + } + } + .com, + .net, + .org, + .edu, + .gov, + .mil, + .html, + .htm, + .xhtml, + .jhtml, + .php, + .php3, + .php4, + .php5, + .phtmle, + .asp, + .aspx, + .cfm { + &:before { + content: $fa-var-link; + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/base/richtext/_richtext.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/base/richtext/_richtext.scss new file mode 100644 index 0000000000000..7d5ec313860d8 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/base/richtext/_richtext.scss @@ -0,0 +1,101 @@ +h1, +h2, +h3, +h4, +h5, +h6 { + color: $text-heading-color; +} +h1, +h2 { + margin: $small-margin 0; +} +h3, +h4, +p { + margin: $extrasmall-margin 0; +} +h5, +h6 { + margin: $extrasmall-margin 0 0; +} +strong { + font-weight: 700; +} +ul, +ol { + padding-bottom: $extrasmall-margin; + padding-top: $extrasmall-margin; + margin-left: $small-margin; +} +li { + font-size: $font-normal; + margin-left: 15px; +} +ul { + li { + list-style: disc; + list-style-position: inside; + } +} +ol { + li { + list-style: decimal; + list-style-position: inside; + } +} +a { + color: $rich-content-link-color; + text-decoration: underline; + font-size: 1em; + &:hover { + color: $rich-content-link-color-active; + } +} +table { + height: auto !important; + border: 2px solid $border-gray; + &, + tr, + th, + td { + border: solid 2px $border-gray; + background: $bg-basic-color; + border-collapse: collapse; + vertical-align: middle; + } + tr, + th, + td { + padding: 5px; + } + caption { + background-color: $bg-light-gray; + overflow: hidden; + padding: 10px; + font-size: $font-big; + font-weight: bold; + margin-left: 0; + } + tr { + border: none; + } + th, + td { + border-width: 2px 0 0 2px; + } + th { + background-color: lighten($bg-light-gray, 5%); + font-size: $font-normal; + font-weight: bold; + padding: 7px; + &:first-child { + border-left: 0; + } + } + td { + &:first-child { + border-left: 0; + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/base/richtext/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/base/richtext/index.scss new file mode 100644 index 0000000000000..1ba07dc731cd9 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/base/richtext/index.scss @@ -0,0 +1,2 @@ +@import 'richtext'; +@import 'richtext-files-icons'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/base/typehead/_typehead.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/base/typehead/_typehead.scss new file mode 100644 index 0000000000000..0365acda25c0c --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/base/typehead/_typehead.scss @@ -0,0 +1,95 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; + +.twitter-typeahead { + display: inline-block; + vertical-align: middle; + max-width: 100%; + width: 100%; +} +.tt-menu { + background: $bg-basic-color; + width: 100%; + border: 1px solid $border-gray; +} +.tt-hint { + color: $tt-color; +} +.tt-dropdown-menu { + width: 250px; + margin-top: 5px; + background-color: $tt-dropdown-bg; + border: 1px solid $tt-dropdown-border; + max-height: 300px; + overflow-y: auto; + box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.08); +} +.tt-suggestion { + padding: 4px 10px; + color: $tt-color; + overflow: hidden; + a { + text-decoration: none; + } + &:last-child { + border: none; + } + .field-image { + width: 100px; + float: left; + margin-right: 10px; + } + .field-make { + font-size: 15px; + float: left; + margin-right: 5px; + } + .field-model { + margin-left: 4px; + font-size: 15px; + font-weight: bold; + width: 100%; + } + .field-priceformatted { + float: right; + font-size: 15px; + color: $tt-price-color; + margin-top: -20px; + } +} +.tt-suggestion.tt-cursor { + color: $tt-color-active; + background-color: $tt-suggestion-bg-active; + cursor: pointer; +} +.loading-in-progress { + .tt-dropdown-menu { + display: block !important; + min-height: 50px; + position: relative; + &:after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + margin-top: -20px; + margin-left: -20px; + height: 40px; + width: 50px; + display: block; + @include loading-gif(); + z-index: 11; + } + &:before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: $tt-dropdown-bg; + opacity: 0.8; + z-index: 10; + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/base/typehead/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/base/typehead/index.scss new file mode 100644 index 0000000000000..c0b2466dbfa04 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/base/typehead/index.scss @@ -0,0 +1 @@ +@import 'typehead'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-column-splitter.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-column-splitter.scss new file mode 100644 index 0000000000000..0aea965b660ac --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-column-splitter.scss @@ -0,0 +1,14 @@ +@import '@sass/abstracts/vars'; + +.row.column-splitter { + margin-left: 0; + margin-right: 0; + padding-left: $default-padding / 2; + padding-right: $default-padding / 2; + max-width: none; + + > div { + padding-left: $default-padding; + padding-right: $default-padding; + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-container.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-container.scss new file mode 100644 index 0000000000000..86bee01402d61 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-container.scss @@ -0,0 +1,21 @@ +@import '@sass/abstracts/mixins'; + +.container-wrapper { + width: 100%; +} + +.component { + position: relative; +} + +.container { + padding: 0; + + &.fullwidth-container { + max-width: unset; + } + + .component-content { + @include clearfix(); + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-image.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-image.scss new file mode 100644 index 0000000000000..92efb3b9ad0ad --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-image.scss @@ -0,0 +1,18 @@ +@import '@sass/abstracts/vars'; + +.image { + img { + max-width: 100%; + height: auto; + } + a { + display: inline-block; + max-width: 100%; + } + .image-caption { + display: block; + font-style: italic; + font-size: $font-small; + color: $image-caption-color; + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-navigation.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-navigation.scss new file mode 100644 index 0000000000000..9af44e6e91650 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-navigation.scss @@ -0,0 +1,54 @@ +@import '@sass/abstracts/mixins'; +@import '@sass/abstracts/vars'; + +.navigation { + background: $nav-bg; + ul { + padding-left: 0; + } + .level0, + .level1 { + > .navigation-title > a { + font-size: $navigation-font-basic; + border-color: $nav-border-root; + border-style: solid; + border-width: 0 0 2px 0; + padding-right: 10px; + margin-right: 5px; + &:hover { + text-decoration: none; + } + } + } + a { + color: $nav-color-root; + .lt-ie9 &, + .lt-ie9 & span { + color: $nav-color-root; + } + &:focus, + &:hover { + color: $nav-color-root-active; + } + } + li { + &.submenu { + margin-bottom: 0; + } + > .navigation-title { + > a { + padding: 3px 5px 3px 0; + display: block; + color: $nav-color-root; + text-decoration: none; + } + } + > ul a { + font-size: $navigation-font-basic-submenu; + } + } + .submenu > ul { + padding-left: 10px; + } +} +@import '@sass/components/navigation/'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-promo.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-promo.scss new file mode 100644 index 0000000000000..77064f569b950 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-promo.scss @@ -0,0 +1,42 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; + +.promo { + background: $promo-bg; + padding: 15px; + overflow: hidden; + box-sizing: border-box; + > .component-content { + position: relative; + @include clearfix(); + @include border-basic(all, $promo-border); + + > div { + padding: 5px; + } + } + .zg-height-fix { + .field-promoicon { + margin-top: 100px; + } + } + .field-promoicon { + overflow: hidden; + width: 100%; + img { + width: 100%; + height: auto; + } + } + .field-promolink { + margin-top: 5px; + padding-bottom: 10px; + } + &.image-full-size { + img { + margin-bottom: 10px; + } + } + @import '@sass/variants/promo'; + @import '@sass/base/richtext/richtext'; +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-richtext-content.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-richtext-content.scss new file mode 100644 index 0000000000000..6cd5e8c531fcb --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/_component-richtext-content.scss @@ -0,0 +1,20 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; + +.content, +.rich-text { + border: $rich-content-border; + line-height: 1.5; + font-size: 12px; + overflow: hidden; + * { + max-width: 100%; + } + @import '@sass/base/richtext'; +} +.rich-text { + @import '@sass/variants/rich-text'; +} +.content { + @import '@sass/variants/page-content'; +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_alignment.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_alignment.scss new file mode 100644 index 0000000000000..bea37718d81c6 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_alignment.scss @@ -0,0 +1,26 @@ +.position-left, +.position-left * { + text-align: left !important; +} + +.position-right, +.position-right * { + text-align: right !important; +} + +.position-center, +.position-center * { + text-align: center !important; +} + +.position-left select { + direction: ltr !important; +} + +.position-right select { + direction: rtl !important; +} + +.position-center select { + text-align-last: center !important; +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_boxed.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_boxed.scss new file mode 100644 index 0000000000000..2345bcade3749 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_boxed.scss @@ -0,0 +1,16 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; + +.boxed { + box-sizing: border-box; + margin: 20px 0; + @include respond-to(tablet) { + padding: 10px; + } + @include respond-to(mobile) { + padding: 10px; + } + > div { + @include wrapper(); + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_clearfix.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_clearfix.scss new file mode 100644 index 0000000000000..0f85b51c78219 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_clearfix.scss @@ -0,0 +1,11 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; + +.component-content-clearfix { + .component-content { + @include clearfix; + } +} +.component-clearfix { + @include clearfix; +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_highlighted.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_highlighted.scss new file mode 100644 index 0000000000000..d0d37d0bb3661 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_highlighted.scss @@ -0,0 +1,63 @@ +.highlighted-top { + background: #ffffff; + border-top-width: 3px; + border-top-color: #15909c; + border-style: solid; + padding: 25px; + margin: 0; +} + +.highlighted-top h1, +.highlighted-top h2, +.highlighted-top h3, +.highlighted-top h4 { + margin: 0 0 10px 0; +} + +.highlighted-bottom { + background: #ffffff; + border-bottom-width: 3px; + border-bottom-color: #15909c; + border-style: solid; + padding: 25px; + margin: 0; +} + +.highlighted-bottom h1, +.highlighted-bottom h2, +.highlighted-bottom h3, +.highlighted-bottom h4 { + margin: 0 0 10px 0; +} + +.highlighted-left { + background: #ffffff; + border-left-width: 3px; + border-left-color: #15909c; + border-style: solid; + padding: 25px; + margin: 0; +} + +.highlighted-left h1, +.highlighted-left h2, +.highlighted-left h3, +.highlighted-left h4 { + margin: 0 0 10px 0; +} + +.highlighted-right { + background: #ffffff; + border-right-width: 3px; + border-right-color: #15909c; + border-style: solid; + padding: 25px; + margin: 0; +} + +.highlighted-right h1, +.highlighted-right h2, +.highlighted-right h3, +.highlighted-right h4 { + margin: 0 0 10px 0; +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_link-button.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_link-button.scss new file mode 100644 index 0000000000000..ec7cec5df6f9c --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_link-button.scss @@ -0,0 +1,18 @@ +@import '@sass/abstracts/mixins'; + +.link-button { + @import '@sass/base/links/'; +} +// Promo button +.promo.link-button { + a { + @extend .button-default; + } +} +.link.link-button { + .is-empty-hint, + .field-link span, + a { + @include default-link-button(); + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_promoted-box.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_promoted-box.scss new file mode 100644 index 0000000000000..7b4ec35ba932d --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/_promoted-box.scss @@ -0,0 +1,3 @@ +.promoted-box { + border: none !important; +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/index.scss new file mode 100644 index 0000000000000..d3a66a141629e --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/common/index.scss @@ -0,0 +1,6 @@ +@import 'alignment'; +@import 'boxed'; +@import 'clearfix'; +@import 'highlighted'; +@import 'link-button'; +@import 'promoted-box'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/container/_bordered.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/container/_bordered.scss new file mode 100644 index 0000000000000..4510a073599e0 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/container/_bordered.scss @@ -0,0 +1,24 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; + +.sxa-bordered { + box-sizing: border-box; + > .component-content { + padding: 0; + @include border-basic(); + border-radius: 5px; + } +} + +.column-splitter, +.row-splitter { + box-sizing: border-box; + .sxa-bordered { + padding: $default-padding !important; + > .component, + .scEmptyPlaceholder { + @include border-basic(); + border-radius: 5px; + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/container/_title-row-box.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/container/_title-row-box.scss new file mode 100644 index 0000000000000..76b13e2e0b0f4 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/container/_title-row-box.scss @@ -0,0 +1,69 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; + +.title-row-box { + @include border-basic(); + box-sizing: border-box; + width: 100%; + height: 87px; + margin: 0; + padding: 0; + background: $container-title-row-bg; + > .component-content { + @include wrapper(); + .title { + display: inline-block; + vertical-align: middle; + box-sizing: border-box; + margin: 23px 0 0 0; + padding: 0; + h1 { + @include font-size(2.2); + border: none; + } + @include respond-to(all-mobile) { + margin: 23px 0 0 20px; + } + } + .rich-text { + margin: 0; + h1, + h2, + h3, + h4 { + margin: 0; + padding: 0; + } + } + .breadcrumb { + display: inline-block; + vertical-align: middle; + box-sizing: border-box; + @include font-size(1.4); + margin: 23px 0 0 0; + padding: 0; + float: right; + clear: both; + @include respond-to(tablet) { + margin: 23px 20px 0 0; + } + @include respond-to(mobile) { + display: none; + } + } + } + @include respond-to(all-mobile) { + .alpha { + vertical-align: middle; + box-sizing: border-box; + width: auto !important; //overrides grid + float: left; + } + .omega { + vertical-align: middle; + box-sizing: border-box; + width: auto !important; //overrides grid + float: right; + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/container/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/container/index.scss new file mode 100644 index 0000000000000..8b09323a3e329 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/container/index.scss @@ -0,0 +1 @@ +@import 'bordered'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/image-alignment/_image-left.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/image-alignment/_image-left.scss new file mode 100644 index 0000000000000..5be626a2a5891 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/image-alignment/_image-left.scss @@ -0,0 +1,3 @@ +.image-left .component-content > div > img { + float: left; +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/image-alignment/_image-right.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/image-alignment/_image-right.scss new file mode 100644 index 0000000000000..07fd291c664b9 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/image-alignment/_image-right.scss @@ -0,0 +1,3 @@ +.image-right .component-content > div > img { + float: right; +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/image/_image-banner.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/image/_image-banner.scss new file mode 100644 index 0000000000000..fb8b877ea90e5 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/image/_image-banner.scss @@ -0,0 +1,15 @@ +.hero-banner { + .component-content { + background-position: center; + background-repeat: no-repeat; + background-size: cover; + height: 800px; + + @include respond-to(mobile-large) { + height: 300px; + } + } + .sc-image-wrapper { + opacity: 0; + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/image/_image-default-size.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/image/_image-default-size.scss new file mode 100644 index 0000000000000..7e1771ff5c1d4 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/image/_image-default-size.scss @@ -0,0 +1,6 @@ +.image-default-size { + img { + max-width: none; + width: auto !important; + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/image/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/image/index.scss new file mode 100644 index 0000000000000..5a79b6155e8bb --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/image/index.scss @@ -0,0 +1,2 @@ +@import 'image-default-size'; +@import 'image-banner'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/index.scss new file mode 100644 index 0000000000000..d93583e8a490e --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/index.scss @@ -0,0 +1,17 @@ +@import 'component-column-splitter'; +@import 'component-container'; +@import 'component-image'; +@import 'component-navigation'; +@import 'component-promo'; +@import '_component-richtext-content'; + +@import 'common'; +@import 'container'; +@import 'layout'; +@import 'spacing'; +@import 'promo'; +@import 'spacing'; +@import 'title'; +@import 'image'; +@import 'link-list'; +@import 'rich-text'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/layout/_acaindent.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/layout/_acaindent.scss new file mode 100644 index 0000000000000..b435f32d93d81 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/layout/_acaindent.scss @@ -0,0 +1,5 @@ +@import '@sass/abstracts/vars'; + +.alan-indent { + margin: 0 $extralarge-margin; +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/layout/_background.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/layout/_background.scss new file mode 100644 index 0000000000000..b11ee2982906c --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/layout/_background.scss @@ -0,0 +1,27 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; + +%cover-bg { + background-repeat: no-repeat; + background-size: cover; +} +.cover-background { + > .component-content { + @extend %cover-bg; + } +} +.fix-background { + > .component-content { + @extend %cover-bg; + @include fixed-bg(left top); + } +} +.parallax-background { + > .component-content { + @extend %cover-bg; + @include fixed-bg(50% 0); + @include respond-to(mobile-large) { + background-attachment: scroll; + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/layout/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/layout/index.scss new file mode 100644 index 0000000000000..38924a7477198 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/layout/index.scss @@ -0,0 +1 @@ +@import 'background'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/link-list/_component-link-list.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/link-list/_component-link-list.scss new file mode 100644 index 0000000000000..9441f03c26c73 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/link-list/_component-link-list.scss @@ -0,0 +1,50 @@ +@import '@sass/abstracts/mixins'; +@import '@sass/abstracts/vars'; +@import '@fontawesome/scss/mixins'; +@import '@fontawesome/scss/variables'; + +.link-list { + background: $link-list-bg; + h1, + h2, + h3, + h4, + h5, + h6 { + @include border-basic(bottom, $link-list-header-border); + } + > .component-content { + ul { + background: $link-list-items-bg; + } + li { + background: $link-list-item-bg; + display: block; + font-size: $font-normal; + a { + display: inline; + color: $link-list-item-color; + position: relative; + padding-left: 10px; + font-size: 1em; + text-decoration: none; + &:before { + @include fa-icon(); + content: $fa-var-chevron-right; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + font-size: 10px; + } + &:hover { + color: $link-list-item-color-active; + text-decoration: none; + border-bottom: 1px solid $link-list-item-border-active; + } + } + } + } + @import '../../base/links'; + @import '../../variants/link-list'; +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/link-list/_list-vertical.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/link-list/_list-vertical.scss new file mode 100644 index 0000000000000..50f71e6ff90a4 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/link-list/_list-vertical.scss @@ -0,0 +1,19 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; + +.link-list.list-vertical { + h3 { + background: $bg-basic-color; + width: 100%; + display: inline-block; + padding: 3px 5px; + @include border-basic(); + } + a { + border: none; + } + li { + display: block; + margin-left: $middle-margin; + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/link-list/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/link-list/index.scss new file mode 100644 index 0000000000000..cf000d7cda705 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/link-list/index.scss @@ -0,0 +1,2 @@ +@import 'component-link-list'; +@import 'list-vertical'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_navigation-fat.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_navigation-fat.scss new file mode 100644 index 0000000000000..06453f04f38e1 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_navigation-fat.scss @@ -0,0 +1,57 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; +@import '@fontawesome/scss/mixins'; +@import '@fontawesome/scss/variables'; + +.navigation.navigation-fat { + background: $bg-basic-color; + @include border-basic(); + + a { + text-decoration: none; + } + + nav > ul { + list-style: none; + padding: 10px 0; + overflow: hidden; + } + + .rel-level1 { + margin: $small-margin; + padding: 0; + display: inline-block; + vertical-align: top; + + &.submenu ul a { + position: relative; + &:before { + @include fa-icon(); + transform: translateY(-50%); + position: absolute; + content: $fa-var-chevron-right; + top: 50%; + left: -9px; + font-size: 10px; + } + } + > .navigation-title > a { + @include border-basic(top, $border-basic-color, 2px); + border-bottom: 0; + background: $nav-bg-root; + padding: 5px 10px; + display: block; + width: auto; + &:hover { + border-color: $menu-hover-color; + } + } + > ul { + padding: 0; + } + } + .rel-level2 { + padding-left: $small-margin; + display: block; + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_navigation-main-horizontal-vertical.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_navigation-main-horizontal-vertical.scss new file mode 100644 index 0000000000000..973ea5007f3f2 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_navigation-main-horizontal-vertical.scss @@ -0,0 +1,158 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; +@import '@fontawesome/scss/mixins'; +@import '@fontawesome/scss/variables'; + +$borderSize: 2px; + +//Drop Down Navigation Common Part +.navigation.navigation-main { + .component-content > nav { + position: relative; + ul { + @include clearfix(); + } + } + .rel-level1 { + float: left; + &.active { + > .navigation-title > a { + border-color: $nav-border-root; + } + } + > .navigation-title > a { + border-width: 2px 0 0 0; + border-color: transparent; + &:hover { + border-color: $nav-border-root; + } + } + &.submenu { + > .navigation-title a { + position: relative; + padding-right: 15px; + &:after { + @include fa-icon(); + font-size: 10px; + content: $fa-var-chevron-down; + position: absolute; + transform: translateY(-50%); + top: 50%; + right: 0; + } + } + } + //submenu + > ul { + overflow: hidden; + display: none; + z-index: 10; + position: absolute; + top: 100%; + background: $nav-bg-submenu; + margin-left: 0; + margin-top: -$borderSize; + border-top: $borderSize solid $nav-border-submenu; + border-left: 1px solid $nav-submenu-item-border; + border-bottom: 1px solid $nav-submenu-item-border; + } + &:active, + &:hover { + > a { + & + ul { + display: block !important; + } + } + > ul { + display: block !important; + min-width: 60px; + } + } + &.active { + > .navigation-title > a:link, + > .navigation-title > a:visited { + color: $text-basic-active; + } + > span { + display: block; + padding: 10px 20px; + color: $text-white; + } + } + &.submenu.active { + border-color: $menu-active-color; + } + } + .rel-level2 { + &:hover, + &:focus { + transition: background 0.2s ease-in; + background: $nav-bg-submenu-active; + } + div > a { + display: block; + font-weight: normal; + font-size: $navigation-font-basic-submenu; + text-align: center; + &:hover, + &:focus { + color: $nav-color-submenu; + } + } + &.submenu { + box-sizing: border-box; + &.navigation-image { + text-align: center; + > a, + .field-navigationtext { + text-align: left; + } + } + } + } + // Additional styles fot drop down horizontal navigation + &.navigation-main-horizontal { + .submenu > ul { + padding-left: 0; + } + .rel-level1 { + &:active, + &:hover { + > a { + & + ul { + display: inline-flex !important; + flex-wrap: wrap; + } + } + > ul { + display: inline-flex !important; + flex-wrap: wrap; + } + } + > ul { + width: 100%; + clear: both; + left: 0; + } + } + .rel-level2 { + float: left; + padding: 10px; + @include border-basic(right, $nav-submenu-item-border, 1px); + flex-grow: 1; + flex-basis: 23%; + } + } + // Additional styles fot drop down vertical navigation + &.navigation-main-vertical { + .rel-level1 > ul { + padding-left: 0; + width: auto; + left: auto; + @include border-basic(right, $nav-submenu-item-border, 1px); + > li { + padding: 5px 10px; + } + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_navigation-mobile.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_navigation-mobile.scss new file mode 100644 index 0000000000000..33b534c557c52 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_navigation-mobile.scss @@ -0,0 +1,89 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; +@import '@fontawesome/scss/mixins'; +@import '@fontawesome/scss/variables'; + +.navigation.navigation-mobile { + nav > ul { + border: none; + @include border-basic(); + } + .rel-level1 { + cursor: pointer; + margin: 0; + background: $bg-basic-color; + > ul { + li { + position: relative; + a:before { + @include fa-icon(); + content: $fa-var-chevron-right; + display: block; + position: absolute; + left: 0; + font-size: 10px; + top: 20px; + transform: translateY(-50%); + } + } + padding-left: 25px; + display: none; + background: $nav-bg-submenu; + } + &.submenu { + > ul { + display: none; + } + &:focus { + > .navigation-title:before { + transform: rotate(180deg); + transition: 0.6s; + transform-style: preserve-3d; + } + > ul { + display: block; + } + } + > .navigation-title { + position: relative; + &:before { + @include fa-icon(); + content: $fa-var-chevron-down; + display: block; + position: absolute; + right: 10px; + font-size: 10px; + top: 20px; + transform: translateY(-50%); + transform: rotate(0); + transition: 0.6s; + transform-style: preserve-3d; + } + } + } + > .navigation-title { + border-bottom: 1px solid $border-basic-color; + margin: 0; + &:last-child { + border-bottom: none; + } + } + .navigation-title { + padding: 10px; + > a { + text-decoration: none; + color: $text-basic; + display: inline; + } + } + } + .rel-level2 { + display: list-item; + .navigation-title > a { + font-weight: 500; + } + ul { + display: none; + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_navigation-sidebar.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_navigation-sidebar.scss new file mode 100644 index 0000000000000..0371680494f51 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_navigation-sidebar.scss @@ -0,0 +1,29 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; + +.navigation.navigation-sidebar { + a { + text-decoration: none; + } + nav > ul { + .rel-level1 { + > .navigation-title { + margin-bottom: 0; + > a { + padding: 5px 10px; + display: block; + position: relative; + z-index: 1; + margin-right: 0; + } + } + > ul { + margin-left: $small-margin; + margin-top: $extrasmall-margin; + } + } + .rel-level2 { + display: block; + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_sitemap-navigation.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_sitemap-navigation.scss new file mode 100644 index 0000000000000..95725b916497a --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/_sitemap-navigation.scss @@ -0,0 +1,20 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; +@import '@fontawesome/scss/mixins'; +@import '@fontawesome/scss/variables'; + +.navigation.sitemap-navigation { + .level2 a { + position: relative; + padding-left: 10px; + &:before { + @include font-size(1.4); + @include fa-icon(); + transform: translateY(-50%) scale(1, -1); + position: absolute; + content: $fa-var-share; + top: 50%; + left: -8px; + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/index.scss new file mode 100644 index 0000000000000..6850050e43c81 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/navigation/index.scss @@ -0,0 +1,5 @@ +@import 'navigation-main-horizontal-vertical'; +@import 'navigation-mobile'; +@import 'navigation-sidebar'; +@import 'sitemap-navigation'; +@import 'navigation-fat'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/promo/_absolute-bottom-link.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/promo/_absolute-bottom-link.scss new file mode 100644 index 0000000000000..f6589e1802bb8 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/promo/_absolute-bottom-link.scss @@ -0,0 +1,8 @@ +.promo.absolute-bottom-link { + position: relative; + .field-promolink { + position: absolute; + bottom: 10px; + right: 10px; + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/promo/_promo-hero.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/promo/_promo-hero.scss new file mode 100644 index 0000000000000..9845d64701422 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/promo/_promo-hero.scss @@ -0,0 +1,42 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; + +.promo.promo-hero { + position: relative; + text-align: center; + &.promo-hero-half { + float: left; + width: 50%; + @include respond-to(all-mobile) { + float: none; + width: 100%; + } + } + .field-promotext { + display: inline-block; + transform: translateY(-50%); + position: absolute; + top: 50%; + left: 0; + right: 0; + padding: 20px; + color: $promo-hero-text-color; + background: $promo-bg-hero; + > a { + color: inherit; + text-decoration: none; + } + @include respond-to(all-mobile) { + width: 100%; + margin: 0; + h1 { + @include font-size(2.4); + } + h2, + h3, + h4 { + @include font-size(2); + } + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/promo/_promo-shadow.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/promo/_promo-shadow.scss new file mode 100644 index 0000000000000..be34b9fc1a5b1 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/promo/_promo-shadow.scss @@ -0,0 +1,43 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; + +.promo-shadow { + max-width: 960px; + padding: 0; + border-top-width: 3px; + border-top-color: $promo-shadow-border; + border-style: solid; + overflow: visible; + position: relative; + &.promo { + float: left; + } + > .component-content { + @include respond-to(all-mobile) { + margin: 0 10px 30px 10px; + } + padding: 15px; + margin: 0 0 30px 0; + &:before, + &:after { + opacity: 0.7; + box-shadow: 0 17px 10px rgba(0, 0, 0, 0.7); + position: absolute; + z-index: -1; + height: 20%; + max-height: 100px; + max-width: 460px; + width: 47%; + content: ''; + bottom: 10px; + } + &:before { + left: 2%; + transform: rotate(-3deg); + } + &:after { + right: 2%; + transform: rotate(3deg); + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/promo/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/promo/index.scss new file mode 100644 index 0000000000000..1c71782f60971 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/promo/index.scss @@ -0,0 +1,3 @@ +@import 'absolute-bottom-link'; +@import 'promo-hero'; +@import 'promo-shadow'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/rich-text/_rich-text-lists.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/rich-text/_rich-text-lists.scss new file mode 100644 index 0000000000000..9d7b3bc88ba16 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/rich-text/_rich-text-lists.scss @@ -0,0 +1,63 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; + +.rich-text-lists, +.field-featurelist { + ul { + box-sizing: border-box; + margin: 0; + padding: 0; + li { + @include font-size(1.3); + position: relative; + line-height: 21px; + list-style: none; + margin: 0; + padding: 5px 0 5px 25px; + &:before { + display: inline-block; + vertical-align: middle; + @include font-size(1.2); + font-family: 'FontAwesome', sans-serif; + position: absolute; + top: 3px; + left: 0; + color: $text-basic; + text-align: center; + content: '\f00c'; + width: 20px; + margin: 0 5px 0 0; + padding: 0; + } + } + } +} +.field-featurelist { + ul { + li { + &:before { + display: inline-block; + vertical-align: middle; + @include font-size(1.2); + font-family: 'FontAwesome', sans-serif; + position: absolute; + top: 3px; + left: 0; + color: $text-basic; + text-align: center; + content: '\f046'; + width: 20px; + margin: 0 5px 0 0; + padding: 0; + } + } + } +} +.media-link { + &.file-type-icon-media-link { + float: left; + .field-filetypeicon { + text-align: center; + } + } +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/rich-text/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/rich-text/index.scss new file mode 100644 index 0000000000000..6e89c6a0e1853 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/rich-text/index.scss @@ -0,0 +1 @@ +@import 'rich-text-lists'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/spacing/_background-colors.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/spacing/_background-colors.scss new file mode 100644 index 0000000000000..30afeef82ee4d --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/spacing/_background-colors.scss @@ -0,0 +1,14 @@ +@import '@sass/abstracts/vars'; + +.container-gray-background { + background: $bg-light-gray; +} +.container-clean-background { + background: $bg-basic-color; +} +.container-dark-background { + background: $bg-black-active; +} +.container-color-background { + background: $bg-blue; +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/spacing/_indent.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/spacing/_indent.scss new file mode 100644 index 0000000000000..9d2f599e66b08 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/spacing/_indent.scss @@ -0,0 +1,10 @@ +@import '@sass/abstracts/vars'; +.indent { + margin: 0 $extralarge-margin; +} +.indent-top { + margin-top: $middle-margin; +} +.indent-bottom { + margin-bottom: $middle-margin; +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/spacing/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/spacing/index.scss new file mode 100644 index 0000000000000..6d31d378d6dd9 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/spacing/index.scss @@ -0,0 +1,2 @@ +@import 'background-colors'; +@import 'indent'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/title/_component-title.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/title/_component-title.scss new file mode 100644 index 0000000000000..4fe0e836f3d6d --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/title/_component-title.scss @@ -0,0 +1,25 @@ +@import '@sass/abstracts/vars'; +@import '@sass/abstracts/mixins'; + +.title { + background: $title-bg; + h1, + .field-title { + > a, + > span { + @include border-basic(bottom, $border-basic-color); + font-size: $font-extrabig; + margin-bottom: $small-margin; + color: $title-color; + line-height: normal; + padding-bottom: 10px; + display: block; + text-decoration: none; + cursor: pointer; + &:hover { + color: $title-color-active; + } + } + } + @import '@sass/variants/title'; +} diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/components/title/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/components/title/index.scss new file mode 100644 index 0000000000000..f91198792de84 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/components/title/index.scss @@ -0,0 +1 @@ +@import 'component-title'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/main.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/main.scss new file mode 100644 index 0000000000000..87f251596147b --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/main.scss @@ -0,0 +1,4 @@ +@import 'base'; +@import 'components'; +@import 'variants'; +@import 'app'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/variants/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/variants/index.scss new file mode 100644 index 0000000000000..5064fee7c2568 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/assets/sass/variants/index.scss @@ -0,0 +1,6 @@ +@import 'link-list'; +@import 'navigation'; +@import 'page-content'; +@import 'promo'; +@import 'rich-text'; +@import 'title'; diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/variants/link-list/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/variants/link-list/index.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/variants/navigation/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/variants/navigation/index.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/variants/page-content/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/variants/page-content/index.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/variants/promo/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/variants/promo/index.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/variants/rich-text/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/variants/rich-text/index.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/examples/cms-sitecore-xmcloud/src/assets/sass/variants/title/index.scss b/examples/cms-sitecore-xmcloud/src/assets/sass/variants/title/index.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/examples/cms-sitecore-xmcloud/src/components/ColumnSplitter.tsx b/examples/cms-sitecore-xmcloud/src/components/ColumnSplitter.tsx new file mode 100644 index 0000000000000..995778c92f1d5 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/components/ColumnSplitter.tsx @@ -0,0 +1,65 @@ +import React from 'react' +import { + ComponentParams, + ComponentRendering, + Placeholder, +} from '@sitecore-jss/sitecore-jss-nextjs' + +interface ComponentProps { + rendering: ComponentRendering & { params: ComponentParams } + params: ComponentParams +} + +export const Default = (props: ComponentProps): JSX.Element => { + const styles = `${props.params.GridParameters ?? ''} ${ + props.params.Styles ?? '' + }`.trimEnd() + const columnWidths = [ + props.params.ColumnWidth1, + props.params.ColumnWidth2, + props.params.ColumnWidth3, + props.params.ColumnWidth4, + props.params.ColumnWidth5, + props.params.ColumnWidth6, + props.params.ColumnWidth7, + props.params.ColumnWidth8, + ] + const columnStyles = [ + props.params.Styles1, + props.params.Styles2, + props.params.Styles3, + props.params.Styles4, + props.params.Styles5, + props.params.Styles6, + props.params.Styles7, + props.params.Styles8, + ] + const enabledPlaceholders = props.params.EnabledPlaceholders.split(',') + const id = props.params.RenderingIdentifier + + return ( +
+ {enabledPlaceholders.map((ph, index) => { + const phKey = `column-${ph}-{*}` + const phStyles = `${columnWidths[+ph - 1]} ${ + columnStyles[+ph - 1] ?? '' + }`.trimEnd() + + return ( +
+
+ +
+
+ ) + })} +
+ ) +} diff --git a/examples/cms-sitecore-xmcloud/src/components/Container.tsx b/examples/cms-sitecore-xmcloud/src/components/Container.tsx new file mode 100644 index 0000000000000..b880da06a9d06 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/components/Container.tsx @@ -0,0 +1,67 @@ +import React from 'react' +import { + ComponentParams, + ComponentRendering, + Placeholder, + useSitecoreContext, +} from '@sitecore-jss/sitecore-jss-nextjs' + +const BACKGROUND_REG_EXP = new RegExp( + /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi +) + +interface ComponentProps { + rendering: ComponentRendering & { params: ComponentParams } + params: ComponentParams +} + +const DefaultContainer = (props: ComponentProps): JSX.Element => { + const { sitecoreContext } = useSitecoreContext() + const containerStyles = + props.params && props.params.Styles ? props.params.Styles : '' + const styles = `${props.params.GridParameters} ${containerStyles}`.trimEnd() + const phKey = `container-${props.params.DynamicPlaceholderId}` + const id = props.params.RenderingIdentifier + let backgroundImage = props.params.BackgroundImage as string + let backgroundStyle: { [key: string]: string } = {} + + if (backgroundImage) { + const prefix = `${ + sitecoreContext.pageState !== 'normal' ? '/sitecore/shell' : '' + }/-/media/` + backgroundImage = `${backgroundImage + ?.match(BACKGROUND_REG_EXP) + ?.pop() + ?.replace(/-/gi, '')}` + backgroundStyle = { + backgroundImage: `url('${prefix}${backgroundImage}')`, + } + } + + return ( +
+
+
+ +
+
+
+ ) +} + +export const Default = (props: ComponentProps): JSX.Element => { + const splitStyles = props.params?.Styles?.split(' ') + + if (splitStyles && splitStyles.includes('container')) { + return ( +
+ +
+ ) + } + + return +} diff --git a/examples/cms-sitecore-xmcloud/src/components/ContentBlock.tsx b/examples/cms-sitecore-xmcloud/src/components/ContentBlock.tsx new file mode 100644 index 0000000000000..33540cb07ea89 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/components/ContentBlock.tsx @@ -0,0 +1,29 @@ +import { + Text, + RichText, + Field, + withDatasourceCheck, +} from '@sitecore-jss/sitecore-jss-nextjs' +import { ComponentProps } from 'lib/component-props' + +type ContentBlockProps = ComponentProps & { + fields: { + heading: Field + content: Field + } +} + +/** + * A simple Content Block component, with a heading and rich text block. + * This is the most basic building block of a content site, and the most basic + * JSS component that's useful. + */ +const ContentBlock = ({ fields }: ContentBlockProps): JSX.Element => ( +
+ + + +
+) + +export default withDatasourceCheck()(ContentBlock) diff --git a/examples/cms-sitecore-xmcloud/src/components/FEaaSWrapper.tsx b/examples/cms-sitecore-xmcloud/src/components/FEaaSWrapper.tsx new file mode 100644 index 0000000000000..00bc5756f1e6e --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/components/FEaaSWrapper.tsx @@ -0,0 +1,18 @@ +import { + FEaaSComponent, + FEaaSComponentProps, +} from '@sitecore-jss/sitecore-jss-nextjs' +import React from 'react' + +export const Default = (props: FEaaSComponentProps): JSX.Element => { + const styles = `component feaas ${props.params?.styles}`.trimEnd() + const id = props.params?.RenderingIdentifier + + return ( +
+
+ +
+
+ ) +} diff --git a/examples/cms-sitecore-xmcloud/src/components/Image.tsx b/examples/cms-sitecore-xmcloud/src/components/Image.tsx new file mode 100644 index 0000000000000..07032ebd373a6 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/components/Image.tsx @@ -0,0 +1,97 @@ +import React from 'react' +import { + Image as JssImage, + Link as JssLink, + ImageField, + Field, + LinkField, + Text, + useSitecoreContext, +} from '@sitecore-jss/sitecore-jss-nextjs' + +interface Fields { + Image: ImageField + ImageCaption: Field + TargetUrl: LinkField +} + +type ImageProps = { + params: { [key: string]: string } + fields: Fields +} + +const ImageDefault = (props: ImageProps): JSX.Element => ( +
+
+ Image +
+
+) + +export const Banner = (props: ImageProps): JSX.Element => { + const { sitecoreContext } = useSitecoreContext() + const backgroundStyle = { + backgroundImage: `url('${props?.fields?.Image?.value?.src}')`, + } + const modifyImageProps = { + ...props.fields.Image, + editable: props?.fields?.Image?.editable + ?.replace(`width="${props?.fields?.Image?.value?.width}"`, 'width="100%"') + .replace( + `height="${props?.fields?.Image?.value?.height}"`, + 'height="100%"' + ), + } + const id = props.params.RenderingIdentifier + + return ( +
+
+ {sitecoreContext.pageEditing ? ( + + ) : ( + '' + )} +
+
+ ) +} + +export const Default = (props: ImageProps): JSX.Element => { + const { sitecoreContext } = useSitecoreContext() + + if (props.fields) { + const Image = () => + const id = props.params.RenderingIdentifier + + return ( +
+
+ {sitecoreContext.pageState === 'edit' ? ( + + ) : ( + + + + )} + +
+
+ ) + } + + return +} diff --git a/examples/cms-sitecore-xmcloud/src/components/LinkList.tsx b/examples/cms-sitecore-xmcloud/src/components/LinkList.tsx new file mode 100644 index 0000000000000..7133ade7d00c5 --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/components/LinkList.tsx @@ -0,0 +1,92 @@ +import React from 'react' +import { + Link as JssLink, + Text, + LinkField, + TextField, +} from '@sitecore-jss/sitecore-jss-nextjs' + +type ResultsFieldLink = { + field: { + link: LinkField + } +} + +interface Fields { + data: { + datasource: { + children: { + results: ResultsFieldLink[] + } + field: { + title: TextField + } + } + } +} + +type LinkListProps = { + params: { [key: string]: string } + fields: Fields +} + +type LinkListItemProps = { + key: string + index: number + total: number + field: LinkField +} + +const LinkListItem = (props: LinkListItemProps) => { + let className = `item${props.index}` + className += (props.index + 1) % 2 === 0 ? ' even' : ' odd' + if (props.index === 0) { + className += ' first' + } + if (props.index + 1 === props.total) { + className += ' last' + } + return ( +
  • +
    + +
    +
  • + ) +} + +export const Default = (props: LinkListProps): JSX.Element => { + const datasource = props.fields?.data?.datasource + const styles = `component link-list ${props.params.styles}`.trimEnd() + const id = props.params.RenderingIdentifier + + if (datasource) { + const list = datasource.children.results + .filter((element: ResultsFieldLink) => element?.field?.link) + .map((element: ResultsFieldLink, key: number) => ( + + )) + + return ( +
    +
    + +
      {list}
    +
    +
    + ) + } + + return ( +
    +
    +

    Link List

    +
    +
    + ) +} diff --git a/examples/cms-sitecore-xmcloud/src/components/Navigation.tsx b/examples/cms-sitecore-xmcloud/src/components/Navigation.tsx new file mode 100644 index 0000000000000..5733d28d8316f --- /dev/null +++ b/examples/cms-sitecore-xmcloud/src/components/Navigation.tsx @@ -0,0 +1,170 @@ +import React, { useState } from 'react' +import { + Link, + LinkField, + Text, + TextField, + useSitecoreContext, +} from '@sitecore-jss/sitecore-jss-nextjs' + +interface Fields { + Id: string + DisplayName: string + Title: TextField + NavigationTitle: TextField + Href: string + Querystring: string + Children: Array + Styles: string[] +} + +type NavigationProps = { + params?: { [key: string]: string } + fields: Fields + handleClick: (event?: React.MouseEvent) => void + relativeLevel: number +} + +const getNavigationText = function ( + props: NavigationProps +): JSX.Element | string { + let text + + if (props.fields.NavigationTitle) { + text = + } else if (props.fields.Title) { + text = + } else { + text = props.fields.DisplayName + } + + return text +} + +const getLinkTitle = (props: NavigationProps): string | undefined => { + let title + if (props.fields.NavigationTitle?.value) { + title = props.fields.NavigationTitle.value.toString() + } else if (props.fields.Title?.value) { + title = props.fields.Title.value.toString() + } else { + title = props.fields.DisplayName + } + + return title +} + +const getLinkField = (props: NavigationProps): LinkField => ({ + value: { + href: props.fields.Href, + title: getLinkTitle(props), + querystring: props.fields.Querystring, + }, +}) + +const NavigationList = (props: NavigationProps) => { + const { sitecoreContext } = useSitecoreContext() + + let children: JSX.Element[] = [] + if (props.fields.Children && props.fields.Children.length) { + children = props.fields.Children.map((element: Fields, index: number) => ( + + )) + } + + return ( +
  • +
    + + {getNavigationText(props)} + +
    + {children.length > 0 ?
      {children}
    : null} +
  • + ) +} + +export const Default = (props: NavigationProps): JSX.Element => { + const [isOpenMenu, openMenu] = useState(false) + const { sitecoreContext } = useSitecoreContext() + const styles = + props.params != null + ? `${props.params.GridParameters ?? ''} ${ + props.params.Styles ?? '' + }`.trimEnd() + : '' + const id = props.params != null ? props.params.RenderingIdentifier : null + + if (!Object.values(props.fields).length) { + return ( +
    +
    [Navigation]
    +
    + ) + } + + const handleToggleMenu = ( + event?: React.MouseEvent, + flag?: boolean + ): void => { + if (event && sitecoreContext?.pageEditing) { + event.preventDefault() + } + + if (flag !== undefined) { + return openMenu(flag) + } + + openMenu(!isOpenMenu) + } + + const list = Object.values(props.fields) + .filter((element) => element) + .map((element: Fields, key: number) => ( + ) => + handleToggleMenu(event, false) + } + relativeLevel={1} + /> + )) + + return ( +
    +