diff --git a/lerna.json b/lerna.json index 4f5c7ea0951a7..222be18cadb03 100644 --- a/lerna.json +++ b/lerna.json @@ -17,5 +17,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "9.5.3-canary.13" + "version": "9.5.3-canary.15" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index eaba0d3c4d013..09f1cb71334d6 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "9.5.3-canary.13", + "version": "9.5.3-canary.15", "keywords": [ "react", "next", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index de2304e6568e0..0828e67a16a8e 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": "9.5.3-canary.13", + "version": "9.5.3-canary.15", "description": "ESLint plugin for NextJS.", "main": "lib/index.js", "license": "MIT", diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 570c1066fa8a2..741376704bf1b 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "9.5.3-canary.13", + "version": "9.5.3-canary.15", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 31ddef3d8b79c..4f0780fd9235e 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "9.5.3-canary.13", + "version": "9.5.3-canary.15", "license": "MIT", "dependencies": { "chalk": "4.1.0", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 3f9a7ab91939b..fd74469b47edf 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "9.5.3-canary.13", + "version": "9.5.3-canary.15", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-google-analytics/package.json b/packages/next-plugin-google-analytics/package.json index 13d6030f4d3c0..6689a13b19b08 100644 --- a/packages/next-plugin-google-analytics/package.json +++ b/packages/next-plugin-google-analytics/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-google-analytics", - "version": "9.5.3-canary.13", + "version": "9.5.3-canary.15", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-google-analytics" diff --git a/packages/next-plugin-sentry/package.json b/packages/next-plugin-sentry/package.json index 8a6735b3f29e9..b7eb827c7316b 100644 --- a/packages/next-plugin-sentry/package.json +++ b/packages/next-plugin-sentry/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-sentry", - "version": "9.5.3-canary.13", + "version": "9.5.3-canary.15", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-sentry" diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index d1f146e096d18..574efe720cd86 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "9.5.3-canary.13", + "version": "9.5.3-canary.15", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index bce2bda4b0d6d..8acb909b1ed2c 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "9.5.3-canary.13", + "version": "9.5.3-canary.15", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next/bin/next.ts b/packages/next/bin/next.ts index 5c4cc27f6795c..d8ff56286a5f4 100755 --- a/packages/next/bin/next.ts +++ b/packages/next/bin/next.ts @@ -50,8 +50,9 @@ if (args['--version']) { // Check if we are running `next ` or `next` const foundCommand = Boolean(commands[args._[0]]) -// Makes sure the `next --help` case is covered +// Makes sure the `next --help` case is covered // This help message is only showed for `next --help` +// `next --help` falls through to be handled later if (!foundCommand && args['--help']) { console.log(` Usage diff --git a/packages/next/build/webpack/config/blocks/css/loaders/client.ts b/packages/next/build/webpack/config/blocks/css/loaders/client.ts index 9f5324638644d..3e260ce069c8d 100644 --- a/packages/next/build/webpack/config/blocks/css/loaders/client.ts +++ b/packages/next/build/webpack/config/blocks/css/loaders/client.ts @@ -33,6 +33,7 @@ export function getClientStyleLoader({ }, } : { + // @ts-ignore: TODO: remove when webpack 5 is stable loader: MiniCssExtractPlugin.loader, options: { publicPath: `${assetPrefix}/_next/` }, } diff --git a/packages/next/build/webpack/plugins/mini-css-extract-plugin.ts b/packages/next/build/webpack/plugins/mini-css-extract-plugin.ts index b8938a127cad3..907adceed5cda 100644 --- a/packages/next/build/webpack/plugins/mini-css-extract-plugin.ts +++ b/packages/next/build/webpack/plugins/mini-css-extract-plugin.ts @@ -1,4 +1,5 @@ -import MiniCssExtractPlugin from 'mini-css-extract-plugin' +// @ts-ignore: TODO: remove when webpack 5 is stable +import MiniCssExtractPlugin from './mini-css-extract-plugin/src' export default class NextMiniCssExtractPlugin extends MiniCssExtractPlugin { __next_css_remove = true diff --git a/packages/next/build/webpack/plugins/mini-css-extract-plugin/LICENSE b/packages/next/build/webpack/plugins/mini-css-extract-plugin/LICENSE new file mode 100644 index 0000000000000..8c11fc7289b75 --- /dev/null +++ b/packages/next/build/webpack/plugins/mini-css-extract-plugin/LICENSE @@ -0,0 +1,20 @@ +Copyright JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/next/build/webpack/plugins/mini-css-extract-plugin/src/CssDependency.js b/packages/next/build/webpack/plugins/mini-css-extract-plugin/src/CssDependency.js new file mode 100644 index 0000000000000..3969bacf7d348 --- /dev/null +++ b/packages/next/build/webpack/plugins/mini-css-extract-plugin/src/CssDependency.js @@ -0,0 +1,59 @@ +import webpack from 'webpack' + +class CssDependency extends webpack.Dependency { + constructor( + { identifier, content, media, sourceMap }, + context, + identifierIndex + ) { + super() + + this.identifier = identifier + this.identifierIndex = identifierIndex + this.content = content + this.media = media + this.sourceMap = sourceMap + this.context = context + } + + getResourceIdentifier() { + return `css-module-${this.identifier}-${this.identifierIndex}` + } +} + +const isWebpack5 = parseInt(webpack.version) === 5 + +if (isWebpack5) { + // @ts-ignore TODO: remove ts-ignore when webpack 5 is stable + webpack.util.serialization.register( + CssDependency, + 'next/dist/build/webpack/plugins/mini-css-extract-plugin/src/CssDependency', + null, + { + serialize(obj, { write }) { + write(obj.identifier) + write(obj.content) + write(obj.media) + write(obj.sourceMap) + write(obj.context) + write(obj.identifierIndex) + }, + deserialize({ read }) { + const obj = new CssDependency( + { + identifier: read(), + content: read(), + media: read(), + sourceMap: read(), + }, + read(), + read() + ) + + return obj + }, + } + ) +} + +export default CssDependency diff --git a/packages/next/build/webpack/plugins/mini-css-extract-plugin/src/CssModule.js b/packages/next/build/webpack/plugins/mini-css-extract-plugin/src/CssModule.js new file mode 100644 index 0000000000000..462d76aeb4e2c --- /dev/null +++ b/packages/next/build/webpack/plugins/mini-css-extract-plugin/src/CssModule.js @@ -0,0 +1,97 @@ +import webpack from 'webpack' + +class CssModule extends webpack.Module { + constructor(dependency) { + super('css/mini-extract', dependency.context) + this.id = '' + this._identifier = dependency.identifier + this._identifierIndex = dependency.identifierIndex + this.content = dependency.content + this.media = dependency.media + this.sourceMap = dependency.sourceMap + } // no source() so webpack doesn't do add stuff to the bundle + + size() { + return this.content.length + } + + identifier() { + return `css ${this._identifier} ${this._identifierIndex}` + } + + readableIdentifier(requestShortener) { + return `css ${requestShortener.shorten(this._identifier)}${ + this._identifierIndex ? ` (${this._identifierIndex})` : '' + }` + } + + nameForCondition() { + const resource = this._identifier.split('!').pop() + + const idx = resource.indexOf('?') + + if (idx >= 0) { + return resource.substring(0, idx) + } + + return resource + } + + updateCacheModule(module) { + this.content = module.content + this.media = module.media + this.sourceMap = module.sourceMap + } + + needRebuild() { + return true + } + + build(options, compilation, resolver, fileSystem, callback) { + this.buildInfo = {} + this.buildMeta = {} + callback() + } + + updateHash(hash) { + super.updateHash(hash) + hash.update(this.content) + hash.update(this.media || '') + hash.update(this.sourceMap ? JSON.stringify(this.sourceMap) : '') + } +} + +const isWebpack5 = parseInt(webpack.version) === 5 + +if (isWebpack5) { + // @ts-ignore TODO: remove ts-ignore when webpack 5 is stable + webpack.util.serialization.register( + CssModule, + 'next/dist/build/webpack/plugins/mini-css-extract-plugin/src/CssModule', + null, + { + serialize(obj, { write }) { + write(obj.context) + write(obj._identifier) + write(obj._identifierIndex) + write(obj.content) + write(obj.media) + write(obj.sourceMap) + }, + deserialize({ read }) { + const obj = new CssModule({ + context: read(), + identifier: read(), + identifierIndex: read(), + content: read(), + media: read(), + sourceMap: read(), + }) + + return obj + }, + } + ) +} + +export default CssModule diff --git a/packages/next/build/webpack/plugins/mini-css-extract-plugin/src/index.js b/packages/next/build/webpack/plugins/mini-css-extract-plugin/src/index.js new file mode 100644 index 0000000000000..ef2b1241b1874 --- /dev/null +++ b/packages/next/build/webpack/plugins/mini-css-extract-plugin/src/index.js @@ -0,0 +1,550 @@ +/* eslint-disable class-methods-use-this */ + +import webpack from 'webpack' +import sources from 'webpack-sources' + +import CssDependency from './CssDependency' +import CssModule from './CssModule' + +const { ConcatSource, SourceMapSource, OriginalSource } = sources +const { + Template, + util: { createHash }, +} = webpack + +const isWebpack5 = parseInt(webpack.version) === 5 +const MODULE_TYPE = 'css/mini-extract' + +const pluginName = 'mini-css-extract-plugin' + +const REGEXP_CHUNKHASH = /\[chunkhash(?::(\d+))?\]/i +const REGEXP_CONTENTHASH = /\[contenthash(?::(\d+))?\]/i +const REGEXP_NAME = /\[name\]/i +const REGEXP_PLACEHOLDERS = /\[(name|id|chunkhash)\]/g +const DEFAULT_FILENAME = '[name].css' + +function getModulesIterable(compilation, chunk) { + if (isWebpack5) { + return compilation.chunkGraph.getChunkModulesIterable(chunk) + } + + return chunk.modulesIterable +} + +class CssDependencyTemplate { + apply() {} +} + +class CssModuleFactory { + create({ dependencies: [dependency] }, callback) { + callback(null, new CssModule(dependency)) + } +} + +class MiniCssExtractPlugin { + constructor(options = {}) { + this.options = Object.assign( + { + filename: DEFAULT_FILENAME, + moduleFilename: () => this.options.filename || DEFAULT_FILENAME, + ignoreOrder: false, + }, + options + ) + + if (!this.options.chunkFilename) { + const { filename } = this.options + + // Anything changing depending on chunk is fine + if (filename.match(REGEXP_PLACEHOLDERS)) { + this.options.chunkFilename = filename + } else { + // Elsewise prefix '[id].' in front of the basename to make it changing + this.options.chunkFilename = filename.replace( + /(^|\/)([^/]*(?:\?|$))/, + '$1[id].$2' + ) + } + } + } + + apply(compiler) { + compiler.hooks.thisCompilation.tap(pluginName, (compilation) => { + compilation.dependencyFactories.set(CssDependency, new CssModuleFactory()) + + compilation.dependencyTemplates.set( + CssDependency, + new CssDependencyTemplate() + ) + + const renderManifestFn = (result, { chunk }) => { + const renderedModules = Array.from( + getModulesIterable(compilation, chunk) + ).filter((module) => module.type === MODULE_TYPE) + + if (renderedModules.length > 0) { + result.push({ + render: () => + this.renderContentAsset( + compilation, + chunk, + renderedModules, + compilation.runtimeTemplate.requestShortener + ), + filenameTemplate: ({ chunk: chunkData }) => + this.options.moduleFilename(chunkData), + pathOptions: { + chunk, + contentHashType: MODULE_TYPE, + }, + identifier: `${pluginName}.${chunk.id}`, + hash: chunk.contentHash[MODULE_TYPE], + }) + } + } + + if (isWebpack5) { + compilation.hooks.renderManifest.tap(pluginName, renderManifestFn) + } else { + // In webpack 5 the 2 separate hooks are now one hook: `compilation.hooks.renderManifest` + // So we no longer have to double-apply the same function + compilation.mainTemplate.hooks.renderManifest.tap( + pluginName, + renderManifestFn + ) + compilation.chunkTemplate.hooks.renderManifest.tap( + pluginName, + renderManifestFn + ) + } + + const handleHashForChunk = (hash, chunk) => { + const { chunkFilename } = this.options + + if (REGEXP_CHUNKHASH.test(chunkFilename)) { + hash.update(JSON.stringify(chunk.getChunkMaps(true).hash)) + } + + if (REGEXP_CONTENTHASH.test(chunkFilename)) { + hash.update( + JSON.stringify( + chunk.getChunkMaps(true).contentHash[MODULE_TYPE] || {} + ) + ) + } + + if (REGEXP_NAME.test(chunkFilename)) { + hash.update(JSON.stringify(chunk.getChunkMaps(true).name)) + } + } + if (isWebpack5) { + const JSModulesHooks = webpack.javascript.JavascriptModulesPlugin.getCompilationHooks( + compilation + ) + JSModulesHooks.chunkHash.tap(pluginName, (chunk, hash) => { + if (!chunk.hasRuntime()) return + return handleHashForChunk(hash, chunk) + }) + } else { + compilation.mainTemplate.hooks.hashForChunk.tap( + pluginName, + handleHashForChunk + ) + } + + compilation.hooks.contentHash.tap(pluginName, (chunk) => { + const { outputOptions } = compilation + const { hashFunction, hashDigest, hashDigestLength } = outputOptions + const hash = createHash(hashFunction) + + const modules = getModulesIterable(compilation, chunk) + + if (modules) { + if (isWebpack5) { + const xor = new (require('webpack/lib/util/StringXor'))() + for (const m of modules) { + if (m.type === MODULE_TYPE) { + xor.add(compilation.chunkGraph.getModuleHash(m, chunk.runtime)) + } + } + xor.updateHash(hash) + } else { + for (const m of modules) { + if (m.type === MODULE_TYPE) { + m.updateHash(hash) + } + } + } + } + + const { contentHash } = chunk + + contentHash[MODULE_TYPE] = hash + .digest(hashDigest) + .substring(0, hashDigestLength) + }) + + const { mainTemplate } = compilation + + mainTemplate.hooks.localVars.tap(pluginName, (source, chunk) => { + const chunkMap = this.getCssChunkObject(compilation, chunk) + + if (Object.keys(chunkMap).length > 0) { + return Template.asString([ + source, + '', + '// object to store loaded CSS chunks', + 'var installedCssChunks = {', + Template.indent( + chunk.ids.map((id) => `${JSON.stringify(id)}: 0`).join(',\n') + ), + '};', + ]) + } + + return source + }) + + mainTemplate.hooks.requireEnsure.tap( + pluginName, + (source, chunk, hash) => { + const chunkMap = this.getCssChunkObject(compilation, chunk) + + if (Object.keys(chunkMap).length > 0) { + const chunkMaps = chunk.getChunkMaps() + const { crossOriginLoading } = mainTemplate.outputOptions + const linkHrefPath = mainTemplate.getAssetPath( + JSON.stringify(this.options.chunkFilename), + { + hash: `" + ${mainTemplate.renderCurrentHashCode(hash)} + "`, + hashWithLength: (length) => + `" + ${mainTemplate.renderCurrentHashCode(hash, length)} + "`, + chunk: { + id: '" + chunkId + "', + hash: `" + ${JSON.stringify(chunkMaps.hash)}[chunkId] + "`, + hashWithLength(length) { + const shortChunkHashMap = Object.create(null) + + for (const chunkId of Object.keys(chunkMaps.hash)) { + if (typeof chunkMaps.hash[chunkId] === 'string') { + shortChunkHashMap[chunkId] = chunkMaps.hash[ + chunkId + ].substring(0, length) + } + } + + return `" + ${JSON.stringify( + shortChunkHashMap + )}[chunkId] + "` + }, + contentHash: { + [MODULE_TYPE]: `" + ${JSON.stringify( + chunkMaps.contentHash[MODULE_TYPE] + )}[chunkId] + "`, + }, + contentHashWithLength: { + [MODULE_TYPE]: (length) => { + const shortContentHashMap = {} + const contentHash = chunkMaps.contentHash[MODULE_TYPE] + + for (const chunkId of Object.keys(contentHash)) { + if (typeof contentHash[chunkId] === 'string') { + shortContentHashMap[chunkId] = contentHash[ + chunkId + ].substring(0, length) + } + } + + return `" + ${JSON.stringify( + shortContentHashMap + )}[chunkId] + "` + }, + }, + name: `" + (${JSON.stringify( + chunkMaps.name + )}[chunkId]||chunkId) + "`, + }, + contentHashType: MODULE_TYPE, + } + ) + + return Template.asString([ + source, + '', + `// ${pluginName} CSS loading`, + `var cssChunks = ${JSON.stringify(chunkMap)};`, + 'if(installedCssChunks[chunkId]) promises.push(installedCssChunks[chunkId]);', + 'else if(installedCssChunks[chunkId] !== 0 && cssChunks[chunkId]) {', + Template.indent([ + 'promises.push(installedCssChunks[chunkId] = new Promise(function(resolve, reject) {', + Template.indent([ + `var href = ${linkHrefPath};`, + `var fullhref = ${mainTemplate.requireFn}.p + href;`, + 'var existingLinkTags = document.getElementsByTagName("link");', + 'for(var i = 0; i < existingLinkTags.length; i++) {', + Template.indent([ + 'var tag = existingLinkTags[i];', + 'var dataHref = tag.getAttribute("data-href") || tag.getAttribute("href");', + 'if(tag.rel === "stylesheet" && (dataHref === href || dataHref === fullhref)) return resolve();', + ]), + '}', + 'var existingStyleTags = document.getElementsByTagName("style");', + 'for(var i = 0; i < existingStyleTags.length; i++) {', + Template.indent([ + 'var tag = existingStyleTags[i];', + 'var dataHref = tag.getAttribute("data-href");', + 'if(dataHref === href || dataHref === fullhref) return resolve();', + ]), + '}', + 'var linkTag = document.createElement("link");', + 'linkTag.rel = "stylesheet";', + 'linkTag.type = "text/css";', + 'linkTag.onload = resolve;', + 'linkTag.onerror = function(event) {', + Template.indent([ + 'var request = event && event.target && event.target.src || fullhref;', + 'var err = new Error("Loading CSS chunk " + chunkId + " failed.\\n(" + request + ")");', + 'err.code = "CSS_CHUNK_LOAD_FAILED";', + 'err.request = request;', + 'delete installedCssChunks[chunkId]', + 'linkTag.parentNode.removeChild(linkTag)', + 'reject(err);', + ]), + '};', + 'linkTag.href = fullhref;', + crossOriginLoading + ? Template.asString([ + `if (linkTag.href.indexOf(window.location.origin + '/') !== 0) {`, + Template.indent( + `linkTag.crossOrigin = ${JSON.stringify( + crossOriginLoading + )};` + ), + '}', + ]) + : '', + 'var head = document.getElementsByTagName("head")[0];', + 'head.appendChild(linkTag);', + ]), + '}).then(function() {', + Template.indent(['installedCssChunks[chunkId] = 0;']), + '}));', + ]), + '}', + ]) + } + + return source + } + ) + }) + } + + getCssChunkObject(compilation, mainChunk) { + const obj = {} + + for (const chunk of mainChunk.getAllAsyncChunks()) { + for (const module of getModulesIterable(compilation, chunk)) { + if (module.type === MODULE_TYPE) { + obj[chunk.id] = 1 + break + } + } + } + + return obj + } + + renderContentAsset(compilation, chunk, modules, requestShortener) { + let usedModules + + const [chunkGroup] = chunk.groupsIterable + + const getModulePostOrderIndex = + chunkGroup.getModulePostOrderIndex || chunkGroup.getModuleIndex2 + if (typeof getModulePostOrderIndex === 'function') { + // Store dependencies for modules + const moduleDependencies = new Map(modules.map((m) => [m, new Set()])) + const moduleDependenciesReasons = new Map( + modules.map((m) => [m, new Map()]) + ) + + // Get ordered list of modules per chunk group + // This loop also gathers dependencies from the ordered lists + // Lists are in reverse order to allow to use Array.pop() + const modulesByChunkGroup = Array.from(chunk.groupsIterable, (cg) => { + const sortedModules = modules + .map((m) => { + return { + module: m, + index: isWebpack5 + ? cg.getModulePostOrderIndex(m) + : cg.getModuleIndex2(m), + } + }) + // eslint-disable-next-line no-undefined + .filter((item) => item.index !== undefined) + .sort((a, b) => b.index - a.index) + .map((item) => item.module) + + for (let i = 0; i < sortedModules.length; i++) { + const set = moduleDependencies.get(sortedModules[i]) + const reasons = moduleDependenciesReasons.get(sortedModules[i]) + + for (let j = i + 1; j < sortedModules.length; j++) { + const module = sortedModules[j] + set.add(module) + const reason = reasons.get(module) || new Set() + reason.add(cg) + reasons.set(module, reason) + } + } + + return sortedModules + }) + + // set with already included modules in correct order + usedModules = new Set() + + const unusedModulesFilter = (m) => !usedModules.has(m) + + while (usedModules.size < modules.length) { + let success = false + let bestMatch + let bestMatchDeps + + // get first module where dependencies are fulfilled + for (const list of modulesByChunkGroup) { + // skip and remove already added modules + while (list.length > 0 && usedModules.has(list[list.length - 1])) { + list.pop() + } + + // skip empty lists + if (list.length !== 0) { + const module = list[list.length - 1] + const deps = moduleDependencies.get(module) + // determine dependencies that are not yet included + const failedDeps = Array.from(deps).filter(unusedModulesFilter) + + // store best match for fallback behavior + if (!bestMatchDeps || bestMatchDeps.length > failedDeps.length) { + bestMatch = list + bestMatchDeps = failedDeps + } + + if (failedDeps.length === 0) { + // use this module and remove it from list + usedModules.add(list.pop()) + success = true + break + } + } + } + + if (!success) { + // no module found => there is a conflict + // use list with fewest failed deps + // and emit a warning + const fallbackModule = bestMatch.pop() + + if (!this.options.ignoreOrder) { + const reasons = moduleDependenciesReasons.get(fallbackModule) + compilation.warnings.push( + new Error( + [ + `chunk ${chunk.name || chunk.id} [${pluginName}]`, + 'Conflicting order. Following module has been added:', + ` * ${fallbackModule.readableIdentifier(requestShortener)}`, + 'despite it was not able to fulfill desired ordering with these modules:', + ...bestMatchDeps.map((m) => { + const goodReasonsMap = moduleDependenciesReasons.get(m) + const goodReasons = + goodReasonsMap && goodReasonsMap.get(fallbackModule) + const failedChunkGroups = Array.from( + reasons.get(m), + (cg) => cg.name + ).join(', ') + const goodChunkGroups = + goodReasons && + Array.from(goodReasons, (cg) => cg.name).join(', ') + return [ + ` * ${m.readableIdentifier(requestShortener)}`, + ` - couldn't fulfill desired order of chunk group(s) ${failedChunkGroups}`, + goodChunkGroups && + ` - while fulfilling desired order of chunk group(s) ${goodChunkGroups}`, + ] + .filter(Boolean) + .join('\n') + }), + ].join('\n') + ) + ) + } + + usedModules.add(fallbackModule) + } + } + } else { + // fallback for older webpack versions + // (to avoid a breaking change) + // TODO remove this in next major version + // and increase minimum webpack version to 4.12.0 + modules.sort((a, b) => a.index2 - b.index2) + usedModules = modules + } + + const source = new ConcatSource() + const externalsSource = new ConcatSource() + + for (const m of usedModules) { + if (/^@import url/.test(m.content)) { + // HACK for IE + // http://stackoverflow.com/a/14676665/1458162 + let { content } = m + + if (m.media) { + // insert media into the @import + // this is rar + // TODO improve this and parse the CSS to support multiple medias + content = content.replace(/;|\s*$/, m.media) + } + + externalsSource.add(content) + externalsSource.add('\n') + } else { + if (m.media) { + source.add(`@media ${m.media} {\n`) + } + + if (m.sourceMap) { + source.add( + new SourceMapSource( + m.content, + m.readableIdentifier(requestShortener), + m.sourceMap + ) + ) + } else { + source.add( + new OriginalSource( + m.content, + m.readableIdentifier(requestShortener) + ) + ) + } + source.add('\n') + + if (m.media) { + source.add('}\n') + } + } + } + + return new ConcatSource(externalsSource, source) + } +} + +MiniCssExtractPlugin.loader = require.resolve('./loader') + +export default MiniCssExtractPlugin diff --git a/packages/next/build/webpack/plugins/mini-css-extract-plugin/src/loader.js b/packages/next/build/webpack/plugins/mini-css-extract-plugin/src/loader.js new file mode 100644 index 0000000000000..104497739266e --- /dev/null +++ b/packages/next/build/webpack/plugins/mini-css-extract-plugin/src/loader.js @@ -0,0 +1,235 @@ +import NativeModule from 'module' + +import loaderUtils from 'loader-utils' +import webpack from 'webpack' +import NodeTargetPlugin from 'webpack/lib/node/NodeTargetPlugin' + +import CssDependency from './CssDependency' + +const isWebpack5 = parseInt(webpack.version) === 5 +const pluginName = 'mini-css-extract-plugin' + +function evalModuleCode(loaderContext, code, filename) { + const module = new NativeModule(filename, loaderContext) + + module.paths = NativeModule._nodeModulePaths(loaderContext.context) // eslint-disable-line no-underscore-dangle + module.filename = filename + module._compile(code, filename) // eslint-disable-line no-underscore-dangle + + return module.exports +} + +function getModuleId(compilation, module) { + if (isWebpack5) { + return compilation.chunkGraph.getModuleId(module) + } + + return module.id +} + +function findModuleById(compilation, id) { + for (const module of compilation.modules) { + if (getModuleId(compilation, module) === id) { + return module + } + } + + return null +} + +export function pitch(request) { + const options = loaderUtils.getOptions(this) || {} + + const loaders = this.loaders.slice(this.loaderIndex + 1) + + this.addDependency(this.resourcePath) + + const childFilename = '*' + const publicPath = + typeof options.publicPath === 'string' + ? options.publicPath === '' || options.publicPath.endsWith('/') + ? options.publicPath + : `${options.publicPath}/` + : typeof options.publicPath === 'function' + ? options.publicPath(this.resourcePath, this.rootContext) + : this._compilation.outputOptions.publicPath + const outputOptions = { + filename: childFilename, + publicPath, + library: { + type: 'commonjs2', + name: null, + }, + } + const childCompiler = this._compilation.createChildCompiler( + `${pluginName} ${request}`, + outputOptions + ) + + new webpack.node.NodeTemplatePlugin(outputOptions).apply(childCompiler) + if (isWebpack5) { + new webpack.library.EnableLibraryPlugin(outputOptions.library.type).apply( + childCompiler + ) + } else { + new webpack.LibraryTemplatePlugin(null, 'commonjs2').apply(childCompiler) + } + new NodeTargetPlugin().apply(childCompiler) + new (isWebpack5 ? webpack.EntryPlugin : webpack.SingleEntryPlugin)( + this.context, + `!!${request}`, + pluginName + ).apply(childCompiler) + new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }).apply( + childCompiler + ) + + let source + + childCompiler.hooks.thisCompilation.tap( + `${pluginName} loader`, + (compilation) => { + const hook = isWebpack5 + ? webpack.NormalModule.getCompilationHooks(compilation).loader + : compilation.hooks.normalModuleLoader + hook.tap(`${pluginName} loader`, (loaderContext, module) => { + // eslint-disable-next-line no-param-reassign + loaderContext.emitFile = this.emitFile + + if (module.request === request) { + // eslint-disable-next-line no-param-reassign + module.loaders = loaders.map((loader) => { + return { + loader: loader.path, + options: loader.options, + ident: loader.ident, + } + }) + } + }) + + if (isWebpack5) { + compilation.hooks.processAssets.tap( + { + name: pluginName, + // @ts-ignore TODO: Remove ignore when webpack 5 is stable + stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL, + }, + (assets) => { + source = assets[childFilename] && assets[childFilename].source() + + // Remove all chunk assets + Object.keys(assets).forEach((file) => delete assets[file]) + } + ) + } + } + ) + + // webpack 5 case is covered in hooks.thisCompilation above + if (!isWebpack5) { + childCompiler.hooks.afterCompile.tap(pluginName, (compilation) => { + source = + compilation.assets[childFilename] && + compilation.assets[childFilename].source() + + // Remove all chunk assets + compilation.chunks.forEach((chunk) => { + chunk.files.forEach((file) => { + delete compilation.assets[file] // eslint-disable-line no-param-reassign + }) + }) + }) + } + + const callback = this.async() + + childCompiler.runAsChild((err, entries, compilation) => { + const addDependencies = (dependencies) => { + if (!Array.isArray(dependencies) && dependencies != null) { + throw new Error( + `Exported value was not extracted as an array: ${JSON.stringify( + dependencies + )}` + ) + } + + const identifierCountMap = new Map() + + for (const dependency of dependencies) { + const count = identifierCountMap.get(dependency.identifier) || 0 + + this._module.addDependency( + new CssDependency(dependency, dependency.context, count) + ) + identifierCountMap.set(dependency.identifier, count + 1) + } + } + + if (err) { + return callback(err) + } + + if (compilation.errors.length > 0) { + return callback(compilation.errors[0]) + } + + compilation.fileDependencies.forEach((dep) => { + this.addDependency(dep) + }, this) + + compilation.contextDependencies.forEach((dep) => { + this.addContextDependency(dep) + }, this) + + if (!source) { + return callback(new Error("Didn't get a result from child compiler")) + } + + let locals + + try { + let dependencies + let exports = evalModuleCode(this, source, request) + // eslint-disable-next-line no-underscore-dangle + exports = exports.__esModule ? exports.default : exports + locals = exports && exports.locals + if (!Array.isArray(exports)) { + dependencies = [[null, exports]] + } else { + dependencies = exports.map(([id, content, media, sourceMap]) => { + const module = findModuleById(compilation, id) + + return { + identifier: module.identifier(), + context: module.context, + content, + media, + sourceMap, + } + }) + } + addDependencies(dependencies) + } catch (e) { + return callback(e) + } + + const esModule = + typeof options.esModule !== 'undefined' ? options.esModule : false + const result = locals + ? `\n${esModule ? 'export default' : 'module.exports ='} ${JSON.stringify( + locals + )};` + : esModule + ? `\nexport {};` + : '' + + let resultSource = `// extracted by ${pluginName}` + + resultSource += result + + return callback(null, resultSource) + }) +} + +export default function () {} diff --git a/packages/next/build/webpack/plugins/react-loadable-plugin.ts b/packages/next/build/webpack/plugins/react-loadable-plugin.ts index d39f78a943ed2..de62a8600daa1 100644 --- a/packages/next/build/webpack/plugins/react-loadable-plugin.ts +++ b/packages/next/build/webpack/plugins/react-loadable-plugin.ts @@ -26,6 +26,7 @@ import webpack, { // eslint-disable-next-line @typescript-eslint/no-unused-vars compilation as CompilationType, } from 'webpack' +import sources from 'webpack-sources' const isWebpack5 = parseInt(webpack.version!) === 5 @@ -108,15 +109,10 @@ export class ReactLoadablePlugin { createAssets(compiler: any, compilation: any, assets: any) { const manifest = buildManifest(compiler, compilation) - var json = JSON.stringify(manifest, null, 2) - assets[this.filename] = { - source() { - return json - }, - size() { - return json.length - }, - } + // @ts-ignore: TODO: remove when webpack 5 is stable + assets[this.filename] = new (webpack.sources || sources).RawSource( + JSON.stringify(manifest, null, 2) + ) return assets } diff --git a/packages/next/package.json b/packages/next/package.json index 488d8fab60dd0..91e4186a3eb03 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "9.5.3-canary.13", + "version": "9.5.3-canary.15", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -77,8 +77,8 @@ "@babel/preset-typescript": "7.9.0", "@babel/runtime": "7.9.6", "@babel/types": "7.9.6", - "@next/react-dev-overlay": "9.5.3-canary.13", - "@next/react-refresh-utils": "9.5.3-canary.13", + "@next/react-dev-overlay": "9.5.3-canary.15", + "@next/react-refresh-utils": "9.5.3-canary.15", "ast-types": "0.13.2", "babel-plugin-syntax-jsx": "6.18.0", "babel-plugin-transform-define": "2.0.0", @@ -94,7 +94,6 @@ "find-cache-dir": "3.3.1", "jest-worker": "24.9.0", "loader-utils": "2.0.0", - "mini-css-extract-plugin": "0.8.0", "mkdirp": "0.5.3", "native-url": "0.3.4", "neo-async": "2.6.1", @@ -124,7 +123,7 @@ "react-dom": "^16.6.0" }, "devDependencies": { - "@next/polyfill-nomodule": "9.5.3-canary.13", + "@next/polyfill-nomodule": "9.5.3-canary.15", "@taskr/clear": "1.1.0", "@taskr/esnext": "1.1.0", "@taskr/watch": "1.1.0", diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index 67e446c6505d3..8e28e29ae4ee0 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": "9.5.3-canary.13", + "version": "9.5.3-canary.15", "description": "A development-only overlay for developing React applications.", "repository": { "url": "vercel/next.js", diff --git a/packages/react-dev-overlay/src/middleware.ts b/packages/react-dev-overlay/src/middleware.ts index 1c93985d8d2ca..f2f7f5e3f9f30 100644 --- a/packages/react-dev-overlay/src/middleware.ts +++ b/packages/react-dev-overlay/src/middleware.ts @@ -30,6 +30,14 @@ type Source = { map: () => RawSourceMap } | null const isWebpack5 = parseInt(webpack.version!) === 5 +function getModuleId(compilation: any, module: any) { + if (isWebpack5) { + return compilation.chunkGraph.getModuleId(module) + } + + return module.id +} + function getModuleSource(compilation: any, module: any): any { if (isWebpack5) { return ( @@ -200,7 +208,7 @@ function getOverlayMiddleware(options: OverlayMiddlewareOptions) { } const module = [...compilation.modules].find( - (searchModule) => searchModule.id === id + (searchModule) => getModuleId(compilation, searchModule) === id ) return getModuleSource(compilation, module) } catch (err) { diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 27112d9fde0e7..cde3bc81e8dc3 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": "9.5.3-canary.13", + "version": "9.5.3-canary.15", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/yarn.lock b/yarn.lock index f0ab3d33bc4f3..49dc6fa0763ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11572,15 +11572,6 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -mini-css-extract-plugin@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz#81d41ec4fe58c713a96ad7c723cdb2d0bd4d70e1" - dependencies: - loader-utils "^1.1.0" - normalize-url "1.9.1" - schema-utils "^1.0.0" - webpack-sources "^1.1.0" - minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -12058,7 +12049,7 @@ normalize-range@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" -normalize-url@1.9.1, normalize-url@^1.4.0: +normalize-url@^1.4.0: version "1.9.1" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" dependencies: