From 5eb1e821d54964ae05bc1dd43f8e66a73c50b179 Mon Sep 17 00:00:00 2001 From: Colin Rotherham Date: Tue, 6 Jun 2023 14:49:53 +0100 Subject: [PATCH] Resolve `govuk-frontend` at workspace level by default Node.js `require.resolve()` paths are optionally provided, for example: 1. Review app uses local `node_modules` before project level 2. Package stats uses local `node_modules` before project level This is necessary for workspace `govuk-frontend@4` installs to be found --- packages/govuk-frontend-review/nodemon.json | 2 +- .../govuk-frontend-review/rollup.config.mjs | 5 +- shared/config/index.js | 4 +- shared/config/resolver.js | 50 +++++++++++++++++++ shared/lib/names.js | 20 +++++--- shared/lib/names.unit.test.mjs | 32 ++++++++++++ shared/stats/rollup.config.mjs | 4 +- shared/stats/src/index.mjs | 5 +- shared/tasks/styles.mjs | 10 ++-- 9 files changed, 112 insertions(+), 20 deletions(-) create mode 100644 shared/config/resolver.js diff --git a/packages/govuk-frontend-review/nodemon.json b/packages/govuk-frontend-review/nodemon.json index b3ae8d07e3..9c32393ab5 100644 --- a/packages/govuk-frontend-review/nodemon.json +++ b/packages/govuk-frontend-review/nodemon.json @@ -10,6 +10,6 @@ "events": { "restart": "browser-sync reload --config browsersync.config.js" }, - "ext": "mjs,json,yaml", + "ext": "js,mjs,json,yaml", "quiet": true } diff --git a/packages/govuk-frontend-review/rollup.config.mjs b/packages/govuk-frontend-review/rollup.config.mjs index 06774e6789..5a4cb4a6ff 100644 --- a/packages/govuk-frontend-review/rollup.config.mjs +++ b/packages/govuk-frontend-review/rollup.config.mjs @@ -1,5 +1,6 @@ import resolve from '@rollup/plugin-node-resolve' import terser from '@rollup/plugin-terser' +import { paths, resolver } from 'govuk-frontend-config' import { defineConfig } from 'rollup' /** @@ -39,6 +40,8 @@ export default defineConfig(({ i: input }) => ({ * Input plugins */ plugins: [ - resolve() + resolve({ + modulePaths: resolver(paths.app).modules + }) ] })) diff --git a/shared/config/index.js b/shared/config/index.js index 39250b5803..3b1957138a 100644 --- a/shared/config/index.js +++ b/shared/config/index.js @@ -5,9 +5,11 @@ const pkg = require('govuk-frontend/package.json') */ const paths = require('./paths') const ports = require('./ports') +const resolver = require('./resolver') module.exports = { paths, pkg, - ports + ports, + resolver } diff --git a/shared/config/resolver.js b/shared/config/resolver.js new file mode 100644 index 0000000000..b9c22cfe53 --- /dev/null +++ b/shared/config/resolver.js @@ -0,0 +1,50 @@ +const { dirname, join } = require('path') + +// Repository root directory +const paths = require('./paths') + +/** + * Default lookup resolver paths + * + * @param {string} basePath - Base directory, for example `/path/to/package` + * @returns {{ modules: string[], stylesheets: string[], views: string[] }} Resolver paths + */ +module.exports = (basePath) => { + const requirePaths = [ + join(basePath, 'node_modules'), + join(paths.root, 'node_modules') + ] + + // Locate GOV.UK Frontend + const packagePath = dirname( + require.resolve('govuk-frontend/package.json', { + paths: requirePaths + }) + ) + + return { + /** + * Node.js module paths + */ + modules: [ + join(packagePath, 'node_modules'), + ...requirePaths + ], + + /** + * Sass stylesheets + */ + stylesheets: [ + join(packagePath, 'dist'), + ...requirePaths + ], + + /** + * Nunjucks view paths + */ + views: [ + join(basePath, 'src/views'), + join(packagePath, 'dist') + ] + } +} diff --git a/shared/lib/names.js b/shared/lib/names.js index 6d412bc192..aecf9d9f87 100644 --- a/shared/lib/names.js +++ b/shared/lib/names.js @@ -68,8 +68,10 @@ function componentPathToModuleName (componentPath) { * @param {PackageOptions} [options] - Package resolution options * @returns {string} Path to installed npm package entry */ -function packageEntryToPath (packageEntry, { modulePath } = {}) { - const packagePath = require.resolve(packageEntry) +function packageEntryToPath (packageEntry, { modulePath, requirePaths } = {}) { + const packagePath = require.resolve(packageEntry, { + paths: requirePaths + }) // Append optional module path return modulePath !== undefined ? join(dirname(packagePath), modulePath) : packagePath @@ -82,16 +84,17 @@ function packageEntryToPath (packageEntry, { modulePath } = {}) { * @param {PackageOptions} [options] - Package resolution options * @returns {string} Path to installed npm package field */ -function packageFieldToPath (packageName, { modulePath, field = 'main' } = {}) { +function packageFieldToPath (packageName, { modulePath, requirePaths, field = 'main' } = {}) { const packageEntry = `${packageName}/package.json` // Package field as child path - const entryPath = require(packageEntryToPath(packageEntry))[field] + const entryPath = require(packageEntryToPath(packageEntry, { requirePaths }))[field] const childPath = modulePath !== undefined ? join(dirname(entryPath), modulePath) : entryPath // Append optional module path return packageEntryToPath(packageEntry, { - modulePath: childPath + modulePath: childPath, + requirePaths }) } @@ -103,11 +106,13 @@ function packageFieldToPath (packageName, { modulePath, field = 'main' } = {}) { * * @param {string} packageName - Installed npm package name * @param {string} [childPath] - Child directory path (optional, relative to package.json), for example `src/govuk/all.mjs` + * @param {PackageOptions} [options] - Package resolution options * @returns {string} Path to installed npm package */ -function packageNameToPath (packageName, childPath = '') { +function packageNameToPath (packageName, childPath = '', { requirePaths } = {}) { return packageEntryToPath(`${packageName}/package.json`, { - modulePath: childPath + modulePath: childPath, + requirePaths }) } @@ -124,4 +129,5 @@ module.exports = { * @typedef {object} PackageOptions * @property {string} [field] - Package field name from package.json, for example `module` * @property {string} [modulePath] - Module path (optional, relative to package entry), for example `all.mjs` + * @property {string[]} [requirePaths] - Node.js require 'node_modules` lookup paths */ diff --git a/shared/lib/names.unit.test.mjs b/shared/lib/names.unit.test.mjs index 092eef9143..2b6b4bf38c 100644 --- a/shared/lib/names.unit.test.mjs +++ b/shared/lib/names.unit.test.mjs @@ -201,3 +201,35 @@ describe('packageNameToPath', () => { .toBe(resolvedPath) }) }) + +describe("packageNameToPath (with custom 'node_module' paths)", () => { + const packages = [ + { + packageName: 'govuk-frontend', + childPath: 'src/govuk/all.mjs', + options: { requirePaths: [join(paths.root, 'node_modules')] }, + resolvedPath: join(paths.package, 'src/govuk/all.mjs') + }, + { + packageName: 'govuk-frontend-review', + childPath: 'src/app.mjs', + options: { requirePaths: [join(paths.root, 'node_modules')] }, + resolvedPath: join(paths.app, 'src/app.mjs') + }, + { + packageName: 'autoprefixer', + options: { requirePaths: [join(paths.package, 'node_modules')] }, + resolvedPath: join(paths.root, 'node_modules/autoprefixer') + }, + { + packageName: 'postcss', + options: { requirePaths: [join(paths.app, 'node_modules')] }, + resolvedPath: join(paths.root, 'node_modules/postcss') + } + ] + + it.each(packages)("locates path for npm package '$packageName'", ({ packageName, childPath, options = {}, resolvedPath }) => { + expect(packageNameToPath(packageName, childPath, options)) + .toBe(resolvedPath) + }) +}) diff --git a/shared/stats/rollup.config.mjs b/shared/stats/rollup.config.mjs index f128a420d8..9e280e43bb 100644 --- a/shared/stats/rollup.config.mjs +++ b/shared/stats/rollup.config.mjs @@ -30,7 +30,9 @@ export default defineConfig(modulePaths * Input plugins */ plugins: [ - resolve({ modulePaths: [paths.root] }), + resolve({ + modulePaths: packageOptions.requirePaths + }), // Stats: File size visualizer({ diff --git a/shared/stats/src/index.mjs b/shared/stats/src/index.mjs index 41e71e9eda..083eefc726 100644 --- a/shared/stats/src/index.mjs +++ b/shared/stats/src/index.mjs @@ -1,6 +1,6 @@ import { join, parse } from 'path' -import { paths } from 'govuk-frontend-config' +import { paths, resolver } from 'govuk-frontend-config' import { getComponentNames, filterPath, getYaml } from 'govuk-frontend-lib/files' /** @@ -16,7 +16,8 @@ const componentNamesWithJavaScript = await getComponentNames((componentName, com */ export const packageOptions = { field: 'module', - modulePath: 'all.mjs' + modulePath: 'all.mjs', + requirePaths: resolver(paths.stats).modules } /** diff --git a/shared/tasks/styles.mjs b/shared/tasks/styles.mjs index 86f8d77d9c..b1819150d1 100644 --- a/shared/tasks/styles.mjs +++ b/shared/tasks/styles.mjs @@ -2,9 +2,8 @@ import { readFile } from 'fs/promises' import { join, parse } from 'path' import chalk from 'chalk' -import { paths } from 'govuk-frontend-config' +import { resolver } from 'govuk-frontend-config' import { getListing } from 'govuk-frontend-lib/files' -import { packageNameToPath } from 'govuk-frontend-lib/names' import PluginError from 'plugin-error' import postcss from 'postcss' // eslint-disable-next-line import/default @@ -39,7 +38,7 @@ export async function compile (pattern, options) { * * @param {AssetEntry} assetEntry - Asset entry */ -export async function compileStylesheet ([modulePath, { configPath, srcPath, destPath, filePath }]) { +export async function compileStylesheet ([modulePath, { basePath, configPath, srcPath, destPath, filePath }]) { const moduleSrcPath = join(srcPath, modulePath) const moduleDestPath = join(destPath, filePath ? filePath(parse(modulePath)) : modulePath) @@ -80,10 +79,7 @@ export async function compileStylesheet ([modulePath, { configPath, srcPath, des sourceMapIncludeSources: true, // Resolve @imports via - loadPaths: [ - packageNameToPath('govuk-frontend', 'dist'), - join(paths.root, 'node_modules') - ], + loadPaths: resolver(basePath).stylesheets, // Sass custom logger logger: logger({