diff --git a/packages/gatsby-recipes/src/providers/npm/__snapshots__/package.test.js.snap b/packages/gatsby-recipes/src/providers/npm/__snapshots__/package.test.js.snap index 1386303baa81e..ced1f579ee33c 100644 --- a/packages/gatsby-recipes/src/providers/npm/__snapshots__/package.test.js.snap +++ b/packages/gatsby-recipes/src/providers/npm/__snapshots__/package.test.js.snap @@ -66,6 +66,26 @@ Object { } `; +exports[`npm package resource installs 2 resources, one prod & one dev: NPMPackage destroy 1`] = ` +Object { + "_message": "Installed NPM package is-sorted@1.0.2", + "description": "A small module to check if an Array is sorted", + "id": "is-sorted", + "name": "is-sorted", + "version": "1.0.2", +} +`; + +exports[`npm package resource installs 2 resources, one prod & one dev: NPMPackage update 1`] = ` +Object { + "_message": "Installed NPM package is-sorted@1.0.2", + "description": "A small module to check if an Array is sorted", + "id": "is-sorted", + "name": "is-sorted", + "version": "1.0.2", +} +`; + exports[`package manager client commands generates the correct commands for npm 1`] = ` Array [ "install", diff --git a/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/e2e.js b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/e2e.js index c25da4e2d1b39..0647c5d30271a 100644 --- a/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/e2e.js +++ b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/e2e.js @@ -201,6 +201,51 @@ test.each([ `../theme-a/src/file-d.ts`, // src/theme-a/file-c.js => theme-a/src/file-d.js ], ], + [ + `support for legacy extension handling`, + { + mode: `development`, + entry: `./index.js`, + resolve: { + extensions: [`.js`, `.customscript`], + plugins: [ + new ShadowRealm({ + extensions: [`.js`, `.customscript`], + themes: [ + { + themeName: `theme-a`, + themeDir: path.join( + __dirname, + `./fixtures/test-sites/legacy-extensions-shadowing/node_modules/theme-a` + ), + }, + ], + projectRoot: path.resolve( + __dirname, + `fixtures/test-sites/legacy-extensions-shadowing` + ), + }), + ], + }, + module: { + rules: [{ test: /\.customscript?$/, use: `gatsby-raw-loader` }], + }, + resolveLoader: { + modules: [`../../fake-loaders`], + }, + }, + { + context: path.resolve( + __dirname, + `fixtures/test-sites/legacy-extensions-shadowing` + ), + }, + [ + `./node_modules/theme-a/src/file-a.js`, + `./src/theme-a/file-b.js`, + `./src/theme-a/file-c.customscript`, + ], + ], [ `edge case; extra extensions in filename`, { diff --git a/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/fixtures/test-sites/legacy-extensions-shadowing/index.js b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/fixtures/test-sites/legacy-extensions-shadowing/index.js new file mode 100644 index 0000000000000..33c2bdb3c9b71 --- /dev/null +++ b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/fixtures/test-sites/legacy-extensions-shadowing/index.js @@ -0,0 +1,3 @@ +const filea = require(`theme-a/src/file-a.js`) +const fileb = require(`theme-a/src/file-b`) +const filec = require(`theme-a/src/file-c`) diff --git a/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/fixtures/test-sites/legacy-extensions-shadowing/src/theme-a/file-a.ts b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/fixtures/test-sites/legacy-extensions-shadowing/src/theme-a/file-a.ts new file mode 100644 index 0000000000000..43ed6f6ab4d79 --- /dev/null +++ b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/fixtures/test-sites/legacy-extensions-shadowing/src/theme-a/file-a.ts @@ -0,0 +1,2 @@ +// Note that this file should not be loaded, because `extensions` does not contain `.ts` in this test +module.exports = "file-a from 'site'"; diff --git a/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/fixtures/test-sites/legacy-extensions-shadowing/src/theme-a/file-a.unknownextension b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/fixtures/test-sites/legacy-extensions-shadowing/src/theme-a/file-a.unknownextension new file mode 100644 index 0000000000000..f05f8771537b7 --- /dev/null +++ b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/fixtures/test-sites/legacy-extensions-shadowing/src/theme-a/file-a.unknownextension @@ -0,0 +1 @@ +// Sample file with custom extension diff --git a/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/fixtures/test-sites/legacy-extensions-shadowing/src/theme-a/file-b.js b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/fixtures/test-sites/legacy-extensions-shadowing/src/theme-a/file-b.js new file mode 100644 index 0000000000000..b4896d633b298 --- /dev/null +++ b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/fixtures/test-sites/legacy-extensions-shadowing/src/theme-a/file-b.js @@ -0,0 +1 @@ +module.exports = `file-b from 'site'` diff --git a/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/fixtures/test-sites/legacy-extensions-shadowing/src/theme-a/file-c.customscript b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/fixtures/test-sites/legacy-extensions-shadowing/src/theme-a/file-c.customscript new file mode 100644 index 0000000000000..a3dd3394f531e --- /dev/null +++ b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/fixtures/test-sites/legacy-extensions-shadowing/src/theme-a/file-c.customscript @@ -0,0 +1 @@ +module.exports = "file-c from 'site'" diff --git a/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/index.js b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/index.js index 2909d11c028db..378e174355808 100644 --- a/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/index.js +++ b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/__tests__/index.js @@ -39,6 +39,7 @@ describe(`Component Shadowing`, () => { themeDir: xplatPath(`/some/place/${name}`), } }), + extensions: [], }) expect(plugin.getThemeAndComponent(xplatPath(componentFullPath))).toEqual( [ diff --git a/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/create-page.js b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/create-page.js index da4cba093a5c8..29d81172d2ab6 100644 --- a/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/create-page.js +++ b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/create-page.js @@ -18,6 +18,7 @@ module.exports = function (pageComponent) { const componentPath = shadowingPlugin.resolveComponentPath({ theme, component, + originalRequestComponent: pageComponent, }) if (componentPath) { return componentPath diff --git a/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/gatsby-node.js b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/gatsby-node.js index 62e3b28537a02..1e5d3844c2af1 100644 --- a/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/gatsby-node.js +++ b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/gatsby-node.js @@ -10,6 +10,7 @@ exports.onCreateWebpackConfig = ( resolve: { plugins: [ new GatsbyThemeComponentShadowingResolverPlugin({ + extensions: program.extensions, themes: flattenedPlugins.map(plugin => { return { themeDir: plugin.pluginFilepath, diff --git a/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/index.js b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/index.js index d1e6251919c2c..592798bc5189a 100644 --- a/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/index.js +++ b/packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/index.js @@ -3,17 +3,51 @@ const debug = require(`debug`)(`gatsby:component-shadowing`) const fs = require(`fs`) const _ = require(`lodash`) -// By default, a file can only be shadowed by a file of the same extension. -// However, the following table determine additionnal shadowing extensions that -// will be looked for, given the extension of the file being shadowed. -// This list maybe extended by user (by customizing webpack's configuration), in -// order to allow less common use cases (ie. allow css files being shadowed by -// a scss file, or jpg files being shadowed by png...) -const DEFAULT_ADDITIONNAL_SHADOW_EXTENSIONS = { - js: [`js`, `jsx`, `ts`, `tsx`], - jsx: [`js`, `jsx`, `ts`, `tsx`], - ts: [`js`, `jsx`, `ts`, `tsx`], - tsx: [`js`, `jsx`, `ts`, `tsx`], +// A file can be shadowed by a file of the same extension, or a file of a +// "compatible" file extension; two files extensions are compatible if they both +// belongs to the same "category". For example, a .JS file (that is code), may +// be shadowed by a .TS file or a .JSX file (both are code), but not by a .CSS +// file (that is a stylesheet) or a .PNG file (that is an image). The following +// list establish to which category a given file extension belongs. Note that if +// a file is not present in this list, then it can only be shadowed by a file +// of the same extension. + +// FIXME: Determine how this list can be extended by user/plugins +const DEFAULT_FILE_EXTENSIONS_CATEGORIES = { + // Code formats + js: `code`, + jsx: `code`, + ts: `code`, + tsx: `code`, + cjs: `code`, + mjs: `code`, + coffee: `code`, + + // JSON-like data formats + json: `json`, + yaml: `json`, + yml: `json`, + + // Stylesheets formats + css: `stylesheet`, + sass: `stylesheet`, + scss: `stylesheet`, + less: `stylesheet`, + "css.js": `stylesheet`, + + // Images formats + jpeg: `image`, + jpg: `image`, + jfif: `image`, + png: `image`, + tiff: `image`, + webp: `image`, + avif: `image`, + gif: `image`, + + // Fonts + woff: `font`, + woff2: `font`, } // TO-DO: @@ -23,7 +57,7 @@ const DEFAULT_ADDITIONNAL_SHADOW_EXTENSIONS = { // see memoized `shadowCreatePagePath` function used in `createPage` action creator. module.exports = class GatsbyThemeComponentShadowingResolverPlugin { - constructor({ projectRoot, themes, additionnalShadowExtensions }) { + constructor({ projectRoot, themes, extensions, extensionsCategory }) { debug( `themes list`, themes.map(({ themeName }) => themeName) @@ -31,22 +65,50 @@ module.exports = class GatsbyThemeComponentShadowingResolverPlugin { this.themes = themes this.projectRoot = projectRoot - // Concatenate default additionnal extensions with those configured by user - // then sort these in reverse length (so that something such as ".css.js" - // get caught before ".js"); also make sure the extension itself is added in - // the list of allowed shadow extensions. - const additionnalShadowExtensionsList = Object.entries({ - ...DEFAULT_ADDITIONNAL_SHADOW_EXTENSIONS, - ...(additionnalShadowExtensions || {}), - }) - this.additionnalShadowExtensions = additionnalShadowExtensionsList - .sort(([a], [b]) => a.length <= b.length) - .map(([key, value]) => { - return { key, value: [...value, key] } - }) + this.extensions = extensions ?? [] + this.extensionsCategory = { + ...DEFAULT_FILE_EXTENSIONS_CATEGORIES, + ...extensionsCategory, + } + this.additionnalShadowExtensions = this.buildAdditionnalShadowExtensions() + } + + buildAdditionnalShadowExtensions() { + const extensionsByCategory = _.groupBy( + this.extensions, + ext => this.extensionsCategory[ext.substring(1)] || `undefined` + ) + + const additionnalExtensions = [] + for (const [category, exts] of Object.entries(extensionsByCategory)) { + if (category === `undefined`) continue + for (const ext of exts) { + additionnalExtensions.push({ key: ext, value: exts }) + } + } + + // Sort extensions in reverse length order, so that something such as + // ".css.js" get caught before ".js" + return additionnalExtensions.sort( + ({ key: a }, { key: b }) => a.length <= b.length + ) } apply(resolver) { + // This hook is executed very early and captures the original file name + resolver + .getHook(`resolve`) + .tapAsync( + `GatsbyThemeComponentShadowingResolverPlugin`, + (request, stack, callback) => { + if (!request._gatsbyThemeShadowingOriginalRequestPath) { + request._gatsbyThemeShadowingOriginalRequestPath = request.request + } + return callback() + } + ) + + // This is where the magic really happens resolver .getHook(`before-resolved`) .tapAsync( @@ -86,10 +148,15 @@ module.exports = class GatsbyThemeComponentShadowingResolverPlugin { return callback() } + const originalRequestPath = + request._gatsbyThemeShadowingOriginalRequestPath + const originalRequestComponent = path.basename(originalRequestPath) + // This is the shadowing algorithm. const builtComponentPath = this.resolveComponentPath({ theme, component, + originalRequestComponent, }) if (builtComponentPath) { @@ -108,7 +175,7 @@ module.exports = class GatsbyThemeComponentShadowingResolverPlugin { } // check the user's project and the theme files - resolveComponentPath({ theme, component }) { + resolveComponentPath({ theme, component, originalRequestComponent }) { // don't include matching theme in possible shadowing paths const themes = this.themes.filter( ({ themeName }) => themeName !== theme.themeName @@ -123,18 +190,19 @@ module.exports = class GatsbyThemeComponentShadowingResolverPlugin { ) const acceptableShadowFileNames = this.getAcceptableShadowFileNames( - path.basename(component) + path.basename(component), + originalRequestComponent ) for (const theme of themesArray) { - const possibleComponentPath = path.dirname(path.join(theme, component)) + const possibleComponentPath = path.join(theme, component) debug(`possibleComponentPath`, possibleComponentPath) let dir try { // we use fs/path instead of require.resolve to work with // TypeScript and alternate syntaxes - dir = fs.readdirSync(possibleComponentPath) + dir = fs.readdirSync(path.dirname(possibleComponentPath)) } catch (e) { continue } @@ -145,7 +213,10 @@ module.exports = class GatsbyThemeComponentShadowingResolverPlugin { existsDir.includes(shadowFile) ) if (matchingShadowFile) { - return path.join(possibleComponentPath, matchingShadowFile) + return path.join( + path.dirname(possibleComponentPath), + matchingShadowFile + ) } } return null @@ -212,17 +283,24 @@ module.exports = class GatsbyThemeComponentShadowingResolverPlugin { return shadowFiles.includes(issuerPath) } - getAcceptableShadowFileNames(componentName) { + getAcceptableShadowFileNames(componentName, originalRequestComponent) { const matchingEntry = this.additionnalShadowExtensions.find(entry => componentName.endsWith(entry.key) ) - // By default, a file may only be shadowed by a file of the same extension - if (!matchingEntry) { - return [componentName] + let additionnalNames = [] + if (matchingEntry) { + const baseName = componentName.slice(0, -matchingEntry.key.length) + additionnalNames = matchingEntry.value.map(ext => `${baseName}${ext}`) + } + + let legacyAdditionnalNames = [] + if (originalRequestComponent) { + legacyAdditionnalNames = this.extensions.map( + ext => `${originalRequestComponent}${ext}` + ) } - const baseName = componentName.slice(0, -(matchingEntry.key.length + 1)) - return matchingEntry.value.map(ext => `${baseName}.${ext}`) + return [componentName, ...additionnalNames, ...legacyAdditionnalNames] } }