diff --git a/docs/advanced-features/static-html-export.md b/docs/advanced-features/static-html-export.md index 6408adf0b7c6..a2a7a7f2b80a 100644 --- a/docs/advanced-features/static-html-export.md +++ b/docs/advanced-features/static-html-export.md @@ -27,7 +27,7 @@ Update your build script in `package.json` to use `next export`: Running `npm run build` will generate an `out` directory. -`next export` builds an HTML version of your app. During `next build`, [`getStaticProps`](/docs/basic-features/data-fetching/get-static-props.md) and [`getStaticPaths`](/docs/basic-features/data-fetching/get-static-paths.md) will generate an HTML file for each page in your `pages` directory (or more for [dynamic routes](/docs/routing/dynamic-routes.md). Then, `next export` will copy the already exported files into the correct directory. `getInitialProps` will generate the HTML files during `next export` instead of `next build`. +`next export` builds an HTML version of your app. During `next build`, [`getStaticProps`](/docs/basic-features/data-fetching/get-static-props.md) and [`getStaticPaths`](/docs/basic-features/data-fetching/get-static-paths.md) will generate an HTML file for each page in your `pages` directory (or more for [dynamic routes](/docs/routing/dynamic-routes.md)). Then, `next export` will copy the already exported files into the correct directory. `getInitialProps` will generate the HTML files during `next export` instead of `next build`. For more advanced scenarios, you can define a parameter called [`exportPathMap`](/docs/api-reference/next.config.js/exportPathMap.md) in your [`next.config.js`](/docs/api-reference/next.config.js/introduction.md) file to configure exactly which pages will be generated. diff --git a/examples/cms-agilitycms/components/hero-post.js b/examples/cms-agilitycms/components/hero-post.js index 141b7e641751..d6e8869ddc2b 100644 --- a/examples/cms-agilitycms/components/hero-post.js +++ b/examples/cms-agilitycms/components/hero-post.js @@ -20,7 +20,7 @@ export default function HeroPost({ slug={slug} /> -
+

diff --git a/examples/cms-agilitycms/components/more-stories.js b/examples/cms-agilitycms/components/more-stories.js index 12c3cbc50f93..a4e0fdf761af 100644 --- a/examples/cms-agilitycms/components/more-stories.js +++ b/examples/cms-agilitycms/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ title, posts }) {

{title}

-
+
{posts.map((post) => (
-
+

diff --git a/examples/cms-builder-io/components/more-stories.js b/examples/cms-builder-io/components/more-stories.js index bcf78e019cd2..45714977feea 100644 --- a/examples/cms-builder-io/components/more-stories.js +++ b/examples/cms-builder-io/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) {

More Stories

-
+
{posts.map((post) => (
-
+

diff --git a/examples/cms-buttercms/components/more-stories.js b/examples/cms-buttercms/components/more-stories.js index 6f90f9b01c7a..b12d7771a65a 100644 --- a/examples/cms-buttercms/components/more-stories.js +++ b/examples/cms-buttercms/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) {

More Stories

-
+
{posts.map((post) => (
-
+

diff --git a/examples/cms-contentful/components/more-stories.js b/examples/cms-contentful/components/more-stories.js index dcdd9b4e6ae7..57fdbb6c4659 100644 --- a/examples/cms-contentful/components/more-stories.js +++ b/examples/cms-contentful/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) {

More Stories

-
+
{posts.map((post) => (
-
+

diff --git a/examples/cms-cosmic/components/more-stories.js b/examples/cms-cosmic/components/more-stories.js index 2e25326c352d..35bb95e0ab66 100644 --- a/examples/cms-cosmic/components/more-stories.js +++ b/examples/cms-cosmic/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) {

More Stories

-
+
{posts.map((post) => (
-
+

diff --git a/examples/cms-datocms/components/more-stories.js b/examples/cms-datocms/components/more-stories.js index 13a09bd8f729..4bab320f6537 100644 --- a/examples/cms-datocms/components/more-stories.js +++ b/examples/cms-datocms/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) {

More Stories

-
+
{posts.map((post) => ( )}
-
+

diff --git a/examples/cms-ghost/components/hero-post.js b/examples/cms-ghost/components/hero-post.js index 1d786d96d2be..6a91725afd64 100644 --- a/examples/cms-ghost/components/hero-post.js +++ b/examples/cms-ghost/components/hero-post.js @@ -22,7 +22,7 @@ export default function HeroPost({ height={1216} />

-
+

diff --git a/examples/cms-ghost/components/more-stories.js b/examples/cms-ghost/components/more-stories.js index 92eefcf51c36..07c12287d7cf 100644 --- a/examples/cms-ghost/components/more-stories.js +++ b/examples/cms-ghost/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) {

More Stories

-
+
{posts.map((post) => (
-
+

diff --git a/examples/cms-kontent/components/hero-post.js b/examples/cms-kontent/components/hero-post.js index 8536e825c5d5..76495802b579 100644 --- a/examples/cms-kontent/components/hero-post.js +++ b/examples/cms-kontent/components/hero-post.js @@ -16,7 +16,7 @@ export default function HeroPost({
-
+

diff --git a/examples/cms-kontent/components/more-stories.js b/examples/cms-kontent/components/more-stories.js index dcdd9b4e6ae7..57fdbb6c4659 100644 --- a/examples/cms-kontent/components/more-stories.js +++ b/examples/cms-kontent/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) {

More Stories

-
+
{posts.map((post) => (
-
+

diff --git a/examples/cms-prismic/components/hero-post.js b/examples/cms-prismic/components/hero-post.js index 8d181d29dfa8..b594f5420020 100644 --- a/examples/cms-prismic/components/hero-post.js +++ b/examples/cms-prismic/components/hero-post.js @@ -21,7 +21,7 @@ export default function HeroPost({ url={coverImage.url} />

-
+

diff --git a/examples/cms-prismic/components/more-stories.js b/examples/cms-prismic/components/more-stories.js index cfe7b62e4150..2a29128c8117 100644 --- a/examples/cms-prismic/components/more-stories.js +++ b/examples/cms-prismic/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) {

More Stories

-
+
{posts.map(({ node }) => ( + > + {`Cover +
) : (
) diff --git a/examples/cms-sanity/components/hero-post.js b/examples/cms-sanity/components/hero-post.js index f198fb3f54d7..4d81ed0850ac 100644 --- a/examples/cms-sanity/components/hero-post.js +++ b/examples/cms-sanity/components/hero-post.js @@ -16,7 +16,7 @@ export default function HeroPost({
-
+

diff --git a/examples/cms-sanity/components/more-stories.js b/examples/cms-sanity/components/more-stories.js index dcdd9b4e6ae7..57fdbb6c4659 100644 --- a/examples/cms-sanity/components/more-stories.js +++ b/examples/cms-sanity/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) {

More Stories

-
+
{posts.map((post) => ( - +
+
) } diff --git a/examples/cms-sanity/lib/sanity.js b/examples/cms-sanity/lib/sanity.js index 76cedf4cb57d..1936305c132c 100644 --- a/examples/cms-sanity/lib/sanity.js +++ b/examples/cms-sanity/lib/sanity.js @@ -1,7 +1,5 @@ -import { - createImageUrlBuilder, - createPreviewSubscriptionHook, -} from 'next-sanity' +import createImageUrlBuilder from '@sanity/image-url' +import { createPreviewSubscriptionHook } from 'next-sanity' import { sanityConfig } from './config' export const imageBuilder = createImageUrlBuilder(sanityConfig) diff --git a/examples/cms-sanity/package.json b/examples/cms-sanity/package.json index 494c94ac75ff..8b576f61ae8c 100644 --- a/examples/cms-sanity/package.json +++ b/examples/cms-sanity/package.json @@ -6,17 +6,18 @@ "start": "next start" }, "dependencies": { - "@sanity/block-content-to-react": "2.0.7", + "@portabletext/react": "^1.0.3", + "@sanity/image-url": "^1.0.1", "classnames": "2.3.1", "date-fns": "2.28.0", "next": "latest", - "next-sanity": "0.3.0", + "next-sanity": "0.5.0", "react": "^17.0.2", "react-dom": "^17.0.2" }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", - "tailwindcss": "^3.0.15" + "postcss": "8.4.7", + "tailwindcss": "^3.0.23" } } diff --git a/examples/cms-storyblok/components/hero-post.js b/examples/cms-storyblok/components/hero-post.js index 2cab6b993289..eadefeaac135 100644 --- a/examples/cms-storyblok/components/hero-post.js +++ b/examples/cms-storyblok/components/hero-post.js @@ -16,7 +16,7 @@ export default function HeroPost({
-
+

diff --git a/examples/cms-storyblok/components/more-stories.js b/examples/cms-storyblok/components/more-stories.js index 2ea20c3f6b25..583c01eda265 100644 --- a/examples/cms-storyblok/components/more-stories.js +++ b/examples/cms-storyblok/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) {

More Stories

-
+
{posts.map((post) => (
-
+

diff --git a/examples/cms-strapi/components/more-stories.js b/examples/cms-strapi/components/more-stories.js index 13a09bd8f729..4bab320f6537 100644 --- a/examples/cms-strapi/components/more-stories.js +++ b/examples/cms-strapi/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) {

More Stories

-
+
{posts.map((post) => (
-
+

diff --git a/examples/cms-takeshape/components/more-stories.js b/examples/cms-takeshape/components/more-stories.js index dcdd9b4e6ae7..57fdbb6c4659 100644 --- a/examples/cms-takeshape/components/more-stories.js +++ b/examples/cms-takeshape/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) {

More Stories

-
+
{posts.map((post) => ( )}
-
+

diff --git a/examples/cms-wordpress/components/more-stories.js b/examples/cms-wordpress/components/more-stories.js index 707e17c12ecf..951fe76a1500 100644 --- a/examples/cms-wordpress/components/more-stories.js +++ b/examples/cms-wordpress/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) {

More Stories

-
+
{posts.map(({ node }) => ( match v.value { - CombinatorValue::Descendant => {} - _ => { - combinator = Some(v.clone()); - - new_selectors.push(sel); - } - }, + ComplexSelectorChildren::Combinator(v) => { + combinator = Some(v.clone()); + } }; } + node.children = new_selectors; } } @@ -204,8 +200,6 @@ impl Namespacer { mut node: CompoundSelector, ) -> Result, Error> { let mut pseudo_index = None; - - let empty_tokens = vec![]; let mut arg_tokens; for (i, selector) in node.subclass_selectors.iter().enumerate() { @@ -215,8 +209,22 @@ impl Namespacer { .iter() .flatten() .flat_map(|v| match v { - PseudoSelectorChildren::Nth(v) => nth_to_tokens(v).tokens, - PseudoSelectorChildren::PreservedToken(v) => vec![v.clone()], + PseudoClassSelectorChildren::PreservedToken(v) => vec![v.clone()], + PseudoClassSelectorChildren::AnPlusB(an_plus_b) => match an_plus_b { + AnPlusB::Ident(v) => to_tokens(v).tokens, + AnPlusB::AnPlusBNotation(v) => to_tokens(v).tokens, + }, + PseudoClassSelectorChildren::Ident(v) => to_tokens(v).tokens, + PseudoClassSelectorChildren::Str(v) => to_tokens(v).tokens, + PseudoClassSelectorChildren::Delimiter(v) => to_tokens(v).tokens, + PseudoClassSelectorChildren::SelectorList(v) => to_tokens(v).tokens, + PseudoClassSelectorChildren::CompoundSelectorList(v) => { + to_tokens(v).tokens + } + PseudoClassSelectorChildren::RelativeSelectorList(v) => { + to_tokens(v).tokens + } + PseudoClassSelectorChildren::CompoundSelector(v) => to_tokens(v).tokens, }) .collect::>(); @@ -224,10 +232,21 @@ impl Namespacer { } SubclassSelector::PseudoElement(PseudoElementSelector { name, children, .. - }) => match children { - Some(children) => (name, children), - None => (name, &empty_tokens), - }, + }) => { + arg_tokens = children + .iter() + .flatten() + .flat_map(|v| match v { + PseudoElementSelectorChildren::PreservedToken(v) => vec![v.clone()], + PseudoElementSelectorChildren::Ident(v) => to_tokens(v).tokens, + PseudoElementSelectorChildren::CompoundSelector(v) => { + to_tokens(v).tokens + } + }) + .collect::>(); + + (name, &arg_tokens) + } _ => continue, }; @@ -282,12 +301,16 @@ impl Namespacer { trace!("Combinator: {:?}", combinator); trace!("v[0]: {:?}", v[0]); + let mut result = vec![]; + if let Some(combinator) = combinator { match v.get(0) { - Some(ComplexSelectorChildren::Combinator(..)) => {} - Some(..) => {} + // `Descendant` combinator can't be the first because we removed it + // above + Some(ComplexSelectorChildren::Combinator(..)) + if combinator.value == CombinatorValue::Descendant => {} _ => { - v.push(ComplexSelectorChildren::Combinator(combinator)); + result.push(ComplexSelectorChildren::Combinator(combinator)); } } } @@ -301,7 +324,9 @@ impl Namespacer { } }); - Ok(v) + result.extend(v); + + Ok(result) } Err(_) => bail!("Failed to transform one off global selector"), }; @@ -332,7 +357,15 @@ impl Namespacer { ); } - Ok(vec![ComplexSelectorChildren::CompoundSelector(node)]) + let mut result = vec![]; + + if let Some(combinator) = combinator { + result.push(ComplexSelectorChildren::Combinator(combinator)); + } + + result.push(ComplexSelectorChildren::CompoundSelector(node)); + + Ok(result) } } @@ -461,17 +494,21 @@ fn get_block_tokens(selector_tokens: &Tokens) -> Vec { ] } -fn nth_to_tokens(nth: &Nth) -> Tokens { +fn to_tokens(node: &N) -> Tokens +where + for<'aa> CodeGenerator<&'aa mut BasicCssWriter<'aa, &'aa mut std::string::String>>: Emit, +{ let mut s = String::new(); { let mut wr = BasicCssWriter::new(&mut s, BasicCssWriterConfig { indent: " " }); let mut gen = CodeGenerator::new(&mut wr, CodegenConfig { minify: true }); - gen.emit(&nth).unwrap(); + gen.emit(node).unwrap(); } + let span = node.span(); let mut lexer = swc_css::parser::lexer::Lexer::new( - StringInput::new(&s, nth.span.lo, nth.span.hi), + StringInput::new(&s, span.lo, span.hi), ParserConfig { allow_wrong_line_comments: true, }, @@ -484,7 +521,7 @@ fn nth_to_tokens(nth: &Nth) -> Tokens { } Tokens { - span: Span::new(nth.span.lo, nth.span.hi, Default::default()), + span: Span::new(span.lo, span.hi, Default::default()), tokens, } } diff --git a/packages/next-swc/crates/core/tests/full/example/output.js b/packages/next-swc/crates/core/tests/full/example/output.js index e0a9c88dd80f..3311dd93d841 100644 --- a/packages/next-swc/crates/core/tests/full/example/output.js +++ b/packages/next-swc/crates/core/tests/full/example/output.js @@ -1,40 +1,40 @@ -function a(a, b) { - (null == b || b > a.length) && (b = a.length); - for(var c = 0, d = new Array(b); c < b; c++)d[c] = a[c]; +function b(c, a) { + (null == a || a > c.length) && (a = c.length); + for(var b = 0, d = new Array(a); b < a; b++)d[b] = c[b]; return d; } -import b from "other"; -(function(a, c) { - return (function(a) { - if (Array.isArray(a)) return a; - })(a) || (function(a, c) { - var d, e, f = null == a ? null : "undefined" != typeof Symbol && a[Symbol.iterator] || a["@@iterator"]; - if (null != f) { - var g = [], h = !0, i = !1; +import a from "other"; +(function(c, b) { + return (function(c) { + if (Array.isArray(c)) return c; + })(c) || (function(c, b) { + var g, h, a = null == c ? null : "undefined" != typeof Symbol && c[Symbol.iterator] || c["@@iterator"]; + if (null != a) { + var d = [], e = !0, i = !1; try { - for(f = f.call(a); !(h = (d = f.next()).done) && (g.push(d.value), !c || g.length !== c); h = !0); + for(a = a.call(c); !(e = (g = a.next()).done) && (d.push(g.value), !b || d.length !== b); e = !0); } catch (j) { - i = !0, e = j; + i = !0, h = j; } finally{ try { - h || null == f.return || f.return(); + e || null == a.return || a.return(); } finally{ - if (i) throw e; + if (i) throw h; } } - return g; + return d; } - })(a, c) || (function(b, c) { - if (b) { - if ("string" == typeof b) return a(b, c); - var d = Object.prototype.toString.call(b).slice(8, -1); - if ("Object" === d && b.constructor && (d = b.constructor.name), "Map" === d || "Set" === d) return Array.from(d); - if ("Arguments" === d || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(d)) return a(b, c); + })(c, b) || (function(a, d) { + if (a) { + if ("string" == typeof a) return b(a, d); + var c = Object.prototype.toString.call(a).slice(8, -1); + if ("Object" === c && a.constructor && (c = a.constructor.name), "Map" === c || "Set" === c) return Array.from(c); + if ("Arguments" === c || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c)) return b(a, d); } - })(a, c) || (function() { + })(c, b) || (function() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); })(); -})(b, 1)[0]; +})(a, 1)[0]; var c = function() { "use strict"; !function(a, b) { diff --git a/packages/next-swc/crates/napi/Cargo.toml b/packages/next-swc/crates/napi/Cargo.toml index 493424eb23bf..ebd06062e1d9 100644 --- a/packages/next-swc/crates/napi/Cargo.toml +++ b/packages/next-swc/crates/napi/Cargo.toml @@ -16,12 +16,12 @@ once_cell = "1.8.0" serde = "1" serde_json = "1" next-swc = { version = "0.0.0", path = "../core" } -swc = "0.126.2" +swc = "0.138.0" swc_atoms = "0.2.7" -swc_bundler = { version = "0.107.0", features = ["concurrent"] } -swc_common = { version = "0.17.0", features = ["concurrent", "sourcemap"] } +swc_bundler = { version = "0.114.0", features = ["concurrent"] } +swc_common = { version = "0.17.9", features = ["concurrent", "sourcemap"] } swc_ecma_loader = { version = "0.28.0", features = ["node", "lru"] } -swc_ecmascript = { version = "0.114.2", features = ["codegen", "minifier", "optimization", "parser", "react", "transforms", "typescript", "utils", "visit"] } +swc_ecmascript = { version = "0.123.0", features = ["codegen", "minifier", "optimization", "parser", "react", "transforms", "typescript", "utils", "visit"] } swc_node_base = "0.5.1" [build-dependencies] diff --git a/packages/next-swc/crates/wasm/Cargo.toml b/packages/next-swc/crates/wasm/Cargo.toml index b25e21bcbe0e..a69fa56fa20d 100644 --- a/packages/next-swc/crates/wasm/Cargo.toml +++ b/packages/next-swc/crates/wasm/Cargo.toml @@ -16,9 +16,9 @@ path-clean = "0.1" serde = {version = "1", features = ["derive"]} serde_json = "1" next-swc = { version = "0.0.0", path = "../core" } -swc = "0.126.2" -swc_common = { version = "0.17.0", features = ["concurrent", "sourcemap"] } -swc_ecmascript = { version = "0.114.2", features = ["codegen", "minifier", "optimization", "parser", "react", "transforms", "typescript", "utils", "visit"] } +swc = "0.138.0" +swc_common = { version = "0.17.9", features = ["concurrent", "sourcemap"] } +swc_ecmascript = { version = "0.123.0", features = ["codegen", "minifier", "optimization", "parser", "react", "transforms", "typescript", "utils", "visit"] } tracing = {version = "0.1.28", features = ["release_max_level_off"]} wasm-bindgen = {version = "0.2", features = ["serde-serialize"]} wasm-bindgen-futures = "0.4.8" diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 4daa06fb1bee..081ef4729455 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "12.1.1-canary.4", + "version": "12.1.1-canary.5", "private": true, "scripts": { "build-native": "napi build --platform --cargo-name next_swc_napi native", diff --git a/packages/next/build/webpack/loaders/next-flight-server-loader.ts b/packages/next/build/webpack/loaders/next-flight-server-loader.ts index 2555a72795bc..fdaf07b21277 100644 --- a/packages/next/build/webpack/loaders/next-flight-server-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-server-loader.ts @@ -5,32 +5,27 @@ import { parse } from '../../swc' import { getBaseSWCOptions } from '../../swc/options' import { getRawPageExtensions } from '../../utils' -const getIsClientComponent = - (pageExtensions: string[]) => (importSource: string) => { - return new RegExp(`\\.client(\\.(${pageExtensions.join('|')}))?`).test( - importSource - ) - } - -const getIsServerComponent = - (pageExtensions: string[]) => (importSource: string) => { - return new RegExp(`\\.server(\\.(${pageExtensions.join('|')}))?`).test( - importSource - ) - } - -function isNextComponent(importSource: string) { - return ( - importSource.includes('next/link') || importSource.includes('next/image') +const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'avif'] + +const createClientComponentFilter = (pageExtensions: string[]) => { + // Special cases for Next.js APIs that are considered as client components: + // - .client.[ext] + // - next/link, next/image + // - .[imageExt] + const regex = new RegExp( + '(' + + `\\.client(\\.(${pageExtensions.join('|')}))?|` + + `next/link|next/image|` + + `\\.(${imageExtensions.join('|')})` + + ')$' ) + + return (importSource: string) => regex.test(importSource) } -export function isImageImport(importSource: string) { - // TODO: share extension with next/image - // TODO: add other static assets, jpeg -> jpg - return ['jpg', 'jpeg', 'png', 'webp', 'avif'].some((imageExt) => - importSource.endsWith('.' + imageExt) - ) +const createServerComponentFilter = (pageExtensions: string[]) => { + const regex = new RegExp(`\\.server(\\.(${pageExtensions.join('|')}))?$`) + return (importSource: string) => regex.test(importSource) } async function parseImportsInfo({ @@ -57,7 +52,6 @@ async function parseImportsInfo({ }) const ast = await parse(source, { ...opts.jsc.parser, isModule: true }) const { body } = ast - const beginPos = ast.span.start let transformedSource = '' let lastIndex = 0 let defaultExportName @@ -74,16 +68,18 @@ async function parseImportsInfo({ const importDeclarations = source.substring( lastIndex, - node.source.span.start - beginPos + node.source.span.start ) - if ( - !( - isClientComponent(importSource) || - isNextComponent(importSource) || - isImageImport(importSource) - ) - ) { + if (isClientComponent(importSource)) { + // A client component. It should be loaded as module reference. + transformedSource += importDeclarations + transformedSource += JSON.stringify(`${importSource}?__sc_client__`) + imports.push(`require(${JSON.stringify(importSource)})`) + } else { + // This is a special case to avoid the Duplicate React error. + // Since we already include React in the SSR runtime, + // here we can't create a new module with the ?__rsc_server__ query. if ( ['react/jsx-runtime', 'react/jsx-dev-runtime'].includes( importSource @@ -96,11 +92,6 @@ async function parseImportsInfo({ // component. transformedSource += importDeclarations transformedSource += JSON.stringify(`${importSource}?__sc_server__`) - } else { - // A client component. It should be loaded as module reference. - transformedSource += importDeclarations - transformedSource += JSON.stringify(`${importSource}?__sc_client__`) - imports.push(`require(${JSON.stringify(importSource)})`) } } else { // For the client compilation, we skip all modules imports but @@ -108,12 +99,7 @@ async function parseImportsInfo({ // have to be imported from either server or client components. if ( !( - isClientComponent(importSource) || - isServerComponent(importSource) || - // Special cases for Next.js APIs that are considered as client - // components: - isNextComponent(importSource) || - isImageImport(importSource) + isClientComponent(importSource) || isServerComponent(importSource) ) ) { continue @@ -122,7 +108,7 @@ async function parseImportsInfo({ imports.push(`require(${JSON.stringify(importSource)})`) } - lastIndex = node.source.span.end - beginPos + lastIndex = node.source.span.end break } case 'ExportDefaultDeclaration': { @@ -170,8 +156,8 @@ export default async function transformSource( } const rawRawPageExtensions = getRawPageExtensions(pageExtensions) - const isServerComponent = getIsServerComponent(rawRawPageExtensions) - const isClientComponent = getIsClientComponent(rawRawPageExtensions) + const isServerComponent = createServerComponentFilter(rawRawPageExtensions) + const isClientComponent = createClientComponentFilter(rawRawPageExtensions) if (!isClientCompilation) { // We only apply the loader to server components, or shared components that diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 03f5438f9361..6fad0b58d355 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -8,6 +8,7 @@ import { } from '../shared/lib/image-config' import { useIntersection } from './use-intersection' import { ImageConfigContext } from '../shared/lib/image-config-context' +import { warnOnce } from '../shared/lib/utils' const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete const loadedImageURLs = new Set() @@ -23,17 +24,6 @@ if (typeof window === 'undefined') { ;(global as any).__NEXT_IMAGE_IMPORTED = true } -let warnOnce = (_: string) => {} -if (process.env.NODE_ENV !== 'production') { - const warnings = new Set() - warnOnce = (msg: string) => { - if (!warnings.has(msg)) { - console.warn(msg) - } - warnings.add(msg) - } -} - const VALID_LOADING_VALUES = ['lazy', 'eager', undefined] as const type LoadingValue = typeof VALID_LOADING_VALUES[number] type ImageConfig = ImageConfigComplete & { allSizes: number[] } diff --git a/packages/next/package.json b/packages/next/package.json index f3f8ebd40354..dc59420eb07c 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "12.1.1-canary.4", + "version": "12.1.1-canary.5", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -69,7 +69,7 @@ ] }, "dependencies": { - "@next/env": "12.1.1-canary.4", + "@next/env": "12.1.1-canary.5", "caniuse-lite": "^1.0.30001283", "postcss": "8.4.5", "styled-jsx": "5.0.0", @@ -117,11 +117,11 @@ "@hapi/accept": "5.0.2", "@napi-rs/cli": "2.4.4", "@napi-rs/triples": "1.1.0", - "@next/polyfill-module": "12.1.1-canary.4", - "@next/polyfill-nomodule": "12.1.1-canary.4", - "@next/react-dev-overlay": "12.1.1-canary.4", - "@next/react-refresh-utils": "12.1.1-canary.4", - "@next/swc": "12.1.1-canary.4", + "@next/polyfill-module": "12.1.1-canary.5", + "@next/polyfill-nomodule": "12.1.1-canary.5", + "@next/react-dev-overlay": "12.1.1-canary.5", + "@next/react-refresh-utils": "12.1.1-canary.5", + "@next/swc": "12.1.1-canary.5", "@peculiar/webcrypto": "1.1.7", "@taskr/clear": "1.1.0", "@taskr/esnext": "1.1.0", diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 9a6e81bf2590..6a4d6d2dc175 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -560,9 +560,6 @@ export default abstract class Server { if (url.locale?.path.detectedLocale) { req.url = formatUrl(url) addRequestMeta(req, '__nextStrippedLocale', true) - if (url.pathname === '/api' || url.pathname.startsWith('/api/')) { - return this.render404(req, res, parsedUrl) - } } if (!this.minimalMode || !parsedUrl.query.__nextLocale) { diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index 747d07ce5d4e..976bd8c29fd6 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -546,10 +546,18 @@ export async function imageOptimizer( throw new ImageError(500, 'Unable to optimize buffer') } } catch (error) { - return { - buffer: upstreamBuffer, - contentType: upstreamType!, - maxAge, + if (upstreamBuffer && upstreamType) { + // If we fail to optimize, fallback to the original image + return { + buffer: upstreamBuffer, + contentType: upstreamType, + maxAge: nextConfig.images.minimumCacheTTL, + } + } else { + throw new ImageError( + 500, + 'Unable to optimize image and unable to fallback to upstream image' + ) } } } diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 014b6c104a12..2dd64eb1b402 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -304,17 +304,15 @@ const rscCache = new Map() function createRSCHook() { return ( - writable: WritableStream, + writable: WritableStream, id: string, - req: ReadableStream, + req: ReadableStream, bootstrap: boolean ) => { let entry = rscCache.get(id) if (!entry) { const [renderStream, forwardStream] = readableStreamTee(req) - entry = createFromReadableStream( - pipeThrough(renderStream, createTextEncoderStream()) - ) + entry = createFromReadableStream(renderStream) rscCache.set(id, entry) let bootstrapped = false @@ -325,10 +323,11 @@ function createRSCHook() { if (bootstrap && !bootstrapped) { bootstrapped = true writer.write( - `` + encodeText( + `` + ) ) } if (done) { @@ -336,11 +335,11 @@ function createRSCHook() { writer.close() } else { writer.write( - `` + encodeText( + `` + ) ) process() } @@ -365,7 +364,7 @@ function createServerComponentRenderer( runtime, }: { cachePrefix: string - transformStream: TransformStream + transformStream: TransformStream serverComponentManifest: NonNullable runtime: 'nodejs' | 'edge' } @@ -381,12 +380,9 @@ function createServerComponentRenderer( const writable = transformStream.writable const ServerComponentWrapper = (props: any) => { const id = (React as any).useId() - const reqStream: ReadableStream = pipeThrough( - renderToReadableStream( - renderFlight(App, OriginalComponent, props), - serverComponentManifest - ), - createTextDecoderStream() + const reqStream: ReadableStream = renderToReadableStream( + renderFlight(App, OriginalComponent, props), + serverComponentManifest ) const response = useRSCResponse( @@ -482,8 +478,8 @@ export async function renderToHTML( let Component: React.ComponentType<{}> | ((props: any) => JSX.Element) = renderOpts.Component let serverComponentsInlinedTransformStream: TransformStream< - string, - string + Uint8Array, + Uint8Array > | null = null if (isServerComponent) { @@ -1181,21 +1177,16 @@ export async function renderToHTML( if (isResSent(res) && !isSSG) return null if (renderServerComponentData) { - const stream: ReadableStream = pipeThrough( - renderToReadableStream( - renderFlight(App, OriginalComponent, { - ...props.pageProps, - ...serverComponentProps, - }), - serverComponentManifest - ), - createTextDecoderStream() + const stream: ReadableStream = renderToReadableStream( + renderFlight(App, OriginalComponent, { + ...props.pageProps, + ...serverComponentProps, + }), + serverComponentManifest ) + return new RenderResult( - pipeThrough( - pipeThrough(stream, createBufferedTransformStream()), - createTextEncoderStream() - ) + pipeThrough(stream, createBufferedTransformStream()) ) } @@ -1360,7 +1351,8 @@ export async function renderToHTML( generateStaticHTML: true, }) - return await streamToString(flushEffectStream) + const flushed = await streamToString(flushEffectStream) + return flushed } return await renderToStream({ @@ -1607,9 +1599,7 @@ export async function renderToHTML( return new RenderResult(html) } - return new RenderResult( - pipeThrough(chainStreams(streams), createTextEncoderStream()) - ) + return new RenderResult(chainStreams(streams)) } function errorToJSON(err: Error) { @@ -1707,27 +1697,10 @@ function createTransformStream({ } } -function createTextDecoderStream(): TransformStream { - const decoder = new TextDecoder() - return createTransformStream({ - transform(chunk, controller) { - controller.enqueue( - typeof chunk === 'string' ? chunk : decoder.decode(chunk) - ) - }, - }) -} - -function createTextEncoderStream(): TransformStream { - const encoder = new TextEncoder() - return createTransformStream({ - transform(chunk, controller) { - controller.enqueue(encoder.encode(chunk)) - }, - }) -} - -function createBufferedTransformStream(): TransformStream { +function createBufferedTransformStream(): TransformStream< + Uint8Array, + Uint8Array +> { let bufferedString = '' let pendingFlush: Promise | null = null @@ -1735,7 +1708,7 @@ function createBufferedTransformStream(): TransformStream { if (!pendingFlush) { pendingFlush = new Promise((resolve) => { setTimeout(() => { - controller.enqueue(bufferedString) + controller.enqueue(encodeText(bufferedString)) bufferedString = '' pendingFlush = null resolve() @@ -1747,7 +1720,7 @@ function createBufferedTransformStream(): TransformStream { return createTransformStream({ transform(chunk, controller) { - bufferedString += chunk + bufferedString += decodeText(chunk) flushBuffer(controller) }, @@ -1761,11 +1734,11 @@ function createBufferedTransformStream(): TransformStream { function createFlushEffectStream( handleFlushEffect: () => Promise -): TransformStream { +): TransformStream { return createTransformStream({ async transform(chunk, controller) { const extraChunk = await handleFlushEffect() - controller.enqueue(extraChunk + chunk) + controller.enqueue(encodeText(extraChunk + decodeText(chunk))) }, }) } @@ -1781,10 +1754,10 @@ function renderToStream({ ReactDOMServer: typeof import('react-dom/server') element: React.ReactElement suffix?: string - dataStream?: ReadableStream + dataStream?: ReadableStream generateStaticHTML: boolean flushEffectHandler?: () => Promise -}): Promise> { +}): Promise> { return new Promise((resolve, reject) => { let resolved = false @@ -1799,7 +1772,7 @@ function renderToStream({ // defer to a microtask to ensure `stream` is set. resolve( Promise.resolve().then(() => { - const transforms: Array> = [ + const transforms: Array> = [ createBufferedTransformStream(), flushEffectHandler ? createFlushEffectStream(flushEffectHandler) @@ -1820,45 +1793,57 @@ function renderToStream({ } } - const renderStream = pipeThrough( - (ReactDOMServer as any).renderToReadableStream(element, { - onError(err: Error) { - if (!resolved) { - resolved = true - reject(err) - } - }, - onCompleteShell() { - if (!generateStaticHTML) { - doResolve() - } - }, - onCompleteAll() { + const renderStream: ReadableStream = ( + ReactDOMServer as any + ).renderToReadableStream(element, { + onError(err: Error) { + if (!resolved) { + resolved = true + reject(err) + } + }, + onCompleteShell() { + if (!generateStaticHTML) { doResolve() - }, - }), - createTextDecoderStream() - ) + } + }, + onCompleteAll() { + doResolve() + }, + }) }) } -function createSuffixStream(suffix: string): TransformStream { +function encodeText(input: string) { + return new TextEncoder().encode(input) +} + +function decodeText(input?: Uint8Array) { + return new TextDecoder().decode(input) +} + +function createSuffixStream( + suffix: string +): TransformStream { return createTransformStream({ flush(controller) { if (suffix) { - controller.enqueue(suffix) + controller.enqueue(encodeText(suffix)) } }, }) } -function createPrefixStream(prefix: string): TransformStream { +function createPrefixStream( + prefix: string +): TransformStream { let prefixFlushed = false return createTransformStream({ transform(chunk, controller) { if (!prefixFlushed && prefix) { prefixFlushed = true - controller.enqueue(chunk + prefix) + controller.enqueue(chunk) + controller.enqueue(encodeText(prefix)) } else { controller.enqueue(chunk) } @@ -1866,15 +1851,15 @@ function createPrefixStream(prefix: string): TransformStream { flush(controller) { if (!prefixFlushed && prefix) { prefixFlushed = true - controller.enqueue(prefix) + controller.enqueue(encodeText(prefix)) } }, }) } function createInlineDataStream( - dataStream: ReadableStream -): TransformStream { + dataStream: ReadableStream +): TransformStream { let dataStreamFinished: Promise | null = null return createTransformStream({ transform(chunk, controller) { @@ -1966,19 +1951,21 @@ function chainStreams(streams: ReadableStream[]): ReadableStream { return readable } -function streamFromArray(strings: string[]): ReadableStream { +function streamFromArray(strings: string[]): ReadableStream { // Note: we use a TransformStream here instead of instantiating a ReadableStream // because the built-in ReadableStream polyfill runs strings through TextEncoder. const { readable, writable } = new TransformStream() const writer = writable.getWriter() - strings.forEach((str) => writer.write(str)) + strings.forEach((str) => writer.write(encodeText(str))) writer.close() return readable } -async function streamToString(stream: ReadableStream): Promise { +async function streamToString( + stream: ReadableStream +): Promise { const reader = stream.getReader() let bufferedString = '' @@ -1989,6 +1976,6 @@ async function streamToString(stream: ReadableStream): Promise { return bufferedString } - bufferedString += value + bufferedString += decodeText(value) } } diff --git a/packages/next/server/router.ts b/packages/next/server/router.ts index 1c618e7616a3..b101f838544e 100644 --- a/packages/next/server/router.ts +++ b/packages/next/server/router.ts @@ -317,8 +317,20 @@ export default class Router { currentPathnameNoBasePath, this.locales ) + const activeBasePath = keepBasePath ? this.basePath : '' + // don't match API routes when they are locale prefixed + // e.g. /api/hello shouldn't match /en/api/hello as a page + // rewrites/redirects can match though + if ( + !isCustomRoute && + localePathResult.detectedLocale && + localePathResult.pathname.match(/^\/api(?:\/|$)/) + ) { + continue + } + if (keepLocale) { if ( !testRoute.internal && diff --git a/packages/next/shared/lib/head.tsx b/packages/next/shared/lib/head.tsx index 81d339e21a9f..d4bbcc64bed2 100644 --- a/packages/next/shared/lib/head.tsx +++ b/packages/next/shared/lib/head.tsx @@ -3,6 +3,7 @@ import Effect from './side-effect' import { AmpStateContext } from './amp-context' import { HeadManagerContext } from './head-manager-context' import { isInAmpMode } from './amp' +import { warnOnce } from './utils' type WithInAmpMode = { inAmpMode?: boolean @@ -161,17 +162,20 @@ function reduceComponents( return React.cloneElement(c, newProps) } } - if (process.env.NODE_ENV === 'development') { + if ( + process.env.NODE_ENV === 'development' && + process.env.__NEXT_CONCURRENT_FEATURES + ) { // omit JSON-LD structured data snippets from the warning if (c.type === 'script' && c.props['type'] !== 'application/ld+json') { const srcMessage = c.props['src'] ? ` + + +

Streaming Head

+
+) diff --git a/test/integration/react-streaming-and-server-components/test/index.test.js b/test/integration/react-streaming-and-server-components/test/index.test.js index d93f233a1aab..527a17c87517 100644 --- a/test/integration/react-streaming-and-server-components/test/index.test.js +++ b/test/integration/react-streaming-and-server-components/test/index.test.js @@ -151,7 +151,7 @@ describe('Edge runtime - prod', () => { }) basic(context, { env: 'prod' }) - streaming(context) + streaming(context, { env: 'prod' }) rsc(context, { runtime: 'edge', env: 'prod' }) }) @@ -184,14 +184,14 @@ describe('Edge runtime - dev', () => { }) basic(context, { env: 'dev' }) - streaming(context) + streaming(context, { env: 'dev' }) rsc(context, { runtime: 'edge', env: 'dev' }) }) const nodejsRuntimeBasicSuite = { runTests: (context, env) => { basic(context, { env }) - streaming(context) + streaming(context, { env }) rsc(context, { runtime: 'nodejs' }) if (env === 'prod') { diff --git a/test/integration/react-streaming-and-server-components/test/streaming.js b/test/integration/react-streaming-and-server-components/test/streaming.js index 31b6e6259149..edfeccebdcd8 100644 --- a/test/integration/react-streaming-and-server-components/test/streaming.js +++ b/test/integration/react-streaming-and-server-components/test/streaming.js @@ -1,6 +1,6 @@ /* eslint-env jest */ import webdriver from 'next-webdriver' -import { fetchViaHTTP } from 'next-test-utils' +import { fetchViaHTTP, waitFor } from 'next-test-utils' async function resolveStreamResponse(response, onData) { let result = '' @@ -16,7 +16,7 @@ async function resolveStreamResponse(response, onData) { return result } -export default function (context) { +export default function (context, { env }) { it('should support streaming for fizz response', async () => { await fetchViaHTTP(context.appPort, '/streaming', null, {}).then( async (response) => { @@ -99,4 +99,82 @@ export default function (context) { expect(result).toMatch(/<\/body><\/html>/) }) }) + + if (env === 'dev') { + it('should warn when stylesheets or scripts are in head', async () => { + let browser + try { + browser = await webdriver(context.appPort, '/head') + + await browser.waitForElementByCss('h1') + await waitFor(1000) + const browserLogs = await browser.log('browser') + let foundStyles = false + let foundScripts = false + const logs = [] + browserLogs.forEach(({ message }) => { + if (message.includes('Do not add stylesheets using next/head')) { + foundStyles = true + logs.push(message) + } + if (message.includes('Do not add