Skip to content

Commit

Permalink
fix(theme-shadowing): Add support for legacy handling of file extensions
Browse files Browse the repository at this point in the history
fix(theme-shadowing): Use file extension to category dictionary instead of extension map
  • Loading branch information
mjameswh committed Apr 23, 2021
1 parent 14efb64 commit 9df0fea
Show file tree
Hide file tree
Showing 11 changed files with 190 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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`)
Original file line number Diff line number Diff line change
@@ -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'";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Sample file with custom extension
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = `file-b from 'site'`
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = "file-c from 'site'"
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe(`Component Shadowing`, () => {
themeDir: xplatPath(`/some/place/${name}`),
}
}),
extensions: [],
})
expect(plugin.getThemeAndComponent(xplatPath(componentFullPath))).toEqual(
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module.exports = function (pageComponent) {
const componentPath = shadowingPlugin.resolveComponentPath({
theme,
component,
originalRequestComponent: pageComponent,
})
if (componentPath) {
return componentPath
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ exports.onCreateWebpackConfig = (
resolve: {
plugins: [
new GatsbyThemeComponentShadowingResolverPlugin({
extensions: program.extensions,
themes: flattenedPlugins.map(plugin => {
return {
themeDir: plugin.pluginFilepath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -23,30 +57,58 @@ 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)
)
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(
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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]
}
}

0 comments on commit 9df0fea

Please sign in to comment.