From 33bdde8744c36e42ad7ee4d8bbe11806ba305ca3 Mon Sep 17 00:00:00 2001 From: Colin Rotherham Date: Fri, 9 Jun 2023 16:05:39 +0100 Subject: [PATCH 1/6] Remove child path from `packageNameToPath()` lib function Preparation work to simplify how we call our package helpers, options param coming soon --- packages/govuk-frontend-review/browsersync.config.js | 2 +- .../src/common/middleware/assets.mjs | 2 +- .../src/common/middleware/vendor.mjs | 4 +++- .../src/common/nunjucks/globals/get-html-code.mjs | 4 +++- .../govuk-frontend-review/src/common/nunjucks/index.mjs | 2 +- packages/govuk-frontend-review/typedoc.config.js | 8 +++++--- .../govuk-prototype-kit/govuk-prototype-kit.config.mjs | 4 +++- shared/helpers/nunjucks.js | 4 +++- shared/lib/files.js | 6 +++--- shared/lib/names.js | 9 ++++----- shared/stats/rollup.config.mjs | 4 ++-- shared/tasks/styles.mjs | 2 +- 12 files changed, 30 insertions(+), 21 deletions(-) diff --git a/packages/govuk-frontend-review/browsersync.config.js b/packages/govuk-frontend-review/browsersync.config.js index aa1a6b7f32..bb518038b3 100644 --- a/packages/govuk-frontend-review/browsersync.config.js +++ b/packages/govuk-frontend-review/browsersync.config.js @@ -25,7 +25,7 @@ module.exports = { join(paths.app, 'dist/javascripts/**/*.js'), join(paths.app, 'dist/stylesheets/**/*.css'), join(paths.app, 'src/views/**/*.njk'), - packageNameToPath('govuk-frontend', 'dist/govuk/**/*.njk') + join(packageNameToPath('govuk-frontend'), 'dist/govuk/**/*.njk') ], ignore: ['**/*.test.*'], diff --git a/packages/govuk-frontend-review/src/common/middleware/assets.mjs b/packages/govuk-frontend-review/src/common/middleware/assets.mjs index 3feb2e63c6..0043351534 100644 --- a/packages/govuk-frontend-review/src/common/middleware/assets.mjs +++ b/packages/govuk-frontend-review/src/common/middleware/assets.mjs @@ -10,7 +10,7 @@ const router = express.Router() * Add middleware to serve static assets */ -router.use('/assets', express.static(packageNameToPath('govuk-frontend', 'dist/govuk/assets'))) +router.use('/assets', express.static(join(packageNameToPath('govuk-frontend'), 'dist/govuk/assets'))) router.use('/javascripts', express.static(join(paths.app, 'dist/javascripts'))) router.use('/stylesheets', express.static(join(paths.app, 'dist/stylesheets'))) diff --git a/packages/govuk-frontend-review/src/common/middleware/vendor.mjs b/packages/govuk-frontend-review/src/common/middleware/vendor.mjs index 8c38b834aa..c1856c9059 100644 --- a/packages/govuk-frontend-review/src/common/middleware/vendor.mjs +++ b/packages/govuk-frontend-review/src/common/middleware/vendor.mjs @@ -1,3 +1,5 @@ +import { join } from 'path' + import express from 'express' import { packageNameToPath } from 'govuk-frontend-lib/names' @@ -7,6 +9,6 @@ const router = express.Router() * Add middleware to serve dependencies * from node_modules */ -router.use('/iframe-resizer/', express.static(packageNameToPath('iframe-resizer', 'js'))) +router.use('/iframe-resizer/', express.static(join(packageNameToPath('iframe-resizer'), 'js'))) export default router diff --git a/packages/govuk-frontend-review/src/common/nunjucks/globals/get-html-code.mjs b/packages/govuk-frontend-review/src/common/nunjucks/globals/get-html-code.mjs index f650260063..419fa9f188 100644 --- a/packages/govuk-frontend-review/src/common/nunjucks/globals/get-html-code.mjs +++ b/packages/govuk-frontend-review/src/common/nunjucks/globals/get-html-code.mjs @@ -1,3 +1,5 @@ +import { join } from 'path' + import { packageNameToPath } from 'govuk-frontend-lib/names' import beautify from 'js-beautify' @@ -9,7 +11,7 @@ import beautify from 'js-beautify' * @returns {string} Nunjucks code */ export function getHTMLCode (componentName, params) { - const templatePath = packageNameToPath('govuk-frontend', `dist/govuk/components/${componentName}/template.njk`) + const templatePath = join(packageNameToPath('govuk-frontend'), `dist/govuk/components/${componentName}/template.njk`) // Render to HTML const html = this.env.render(templatePath, { params }).trim() diff --git a/packages/govuk-frontend-review/src/common/nunjucks/index.mjs b/packages/govuk-frontend-review/src/common/nunjucks/index.mjs index ec5d7eabcc..eed36867a1 100644 --- a/packages/govuk-frontend-review/src/common/nunjucks/index.mjs +++ b/packages/govuk-frontend-review/src/common/nunjucks/index.mjs @@ -10,7 +10,7 @@ import * as globals from './globals/index.mjs' export function renderer (app) { const appViews = [ join(paths.app, 'src/views'), - packageNameToPath('govuk-frontend', 'dist') + join(packageNameToPath('govuk-frontend'), 'dist') ] // Initialise nunjucks environment diff --git a/packages/govuk-frontend-review/typedoc.config.js b/packages/govuk-frontend-review/typedoc.config.js index 26854f6dd2..cc5eb2df1b 100644 --- a/packages/govuk-frontend-review/typedoc.config.js +++ b/packages/govuk-frontend-review/typedoc.config.js @@ -1,3 +1,5 @@ +const { join } = require('path') + const { packageNameToPath } = require('govuk-frontend-lib/names') /** @@ -9,9 +11,9 @@ module.exports = { sourceLinkTemplate: 'https://github.com/alphagov/govuk-frontend/blob/{gitRevision}/{path}#L{line}', // Configure paths - basePath: packageNameToPath('govuk-frontend', 'src'), - entryPoints: [packageNameToPath('govuk-frontend', 'src/govuk/all.mjs')], - tsconfig: packageNameToPath('govuk-frontend', 'tsconfig.build.json'), + basePath: join(packageNameToPath('govuk-frontend'), 'src'), + entryPoints: [join(packageNameToPath('govuk-frontend'), 'src/govuk/all.mjs')], + tsconfig: join(packageNameToPath('govuk-frontend'), 'tsconfig.build.json'), out: './dist/docs/jsdoc', // Ignore warnings about CharacterCountTranslations using I18n (@private) diff --git a/packages/govuk-frontend/src/govuk-prototype-kit/govuk-prototype-kit.config.mjs b/packages/govuk-frontend/src/govuk-prototype-kit/govuk-prototype-kit.config.mjs index e47c1d25c0..73f30625cf 100644 --- a/packages/govuk-frontend/src/govuk-prototype-kit/govuk-prototype-kit.config.mjs +++ b/packages/govuk-frontend/src/govuk-prototype-kit/govuk-prototype-kit.config.mjs @@ -1,3 +1,5 @@ +import { join } from 'path' + import { filterPath, getComponentNames, getListing } from 'govuk-frontend-lib/files' import { componentNameToMacroName, packageNameToPath } from 'govuk-frontend-lib/names' import slash from 'slash' @@ -8,7 +10,7 @@ import slash from 'slash' * @returns {Promise} GOV.UK Prototype Kit config */ export default async () => { - const srcPath = packageNameToPath('govuk-frontend', 'src') + const srcPath = join(packageNameToPath('govuk-frontend'), 'src') // Locate component macros const componentMacros = await getListing('**/components/**/macro.njk', { cwd: srcPath }) diff --git a/shared/helpers/nunjucks.js b/shared/helpers/nunjucks.js index 4529f85a0a..a0f22eb453 100644 --- a/shared/helpers/nunjucks.js +++ b/shared/helpers/nunjucks.js @@ -1,10 +1,12 @@ +const { join } = require('path') + const cheerio = require('cheerio') const { componentNameToMacroName, packageNameToPath } = require('govuk-frontend-lib/names') const nunjucks = require('nunjucks') const { outdent } = require('outdent') const nunjucksPaths = [ - packageNameToPath('govuk-frontend', 'src') + join(packageNameToPath('govuk-frontend'), 'src') ] const nunjucksEnv = nunjucks.configure(nunjucksPaths, { diff --git a/shared/lib/files.js b/shared/lib/files.js index 2f278f908e..25f9cada7f 100644 --- a/shared/lib/files.js +++ b/shared/lib/files.js @@ -91,7 +91,7 @@ const getYaml = async (configPath) => { * @returns {Promise} Component data */ const getComponentData = async (componentName) => { - const yamlPath = packageNameToPath('govuk-frontend', `src/govuk/components/${componentName}/${componentName}.yaml`) + const yamlPath = join(packageNameToPath('govuk-frontend'), `src/govuk/components/${componentName}/${componentName}.yaml`) /** @type {ComponentData} */ const yamlData = await getYaml(yamlPath) @@ -119,7 +119,7 @@ const getComponentsData = async () => { * @returns {Promise} Component files */ const getComponentFiles = (componentName = '') => - getListing(packageNameToPath('govuk-frontend', join('src/govuk/components', componentName, '**/*'))) + getListing(join(packageNameToPath('govuk-frontend'), `src/govuk/components/${componentName}/**/*`)) /** * Get component names (with optional filter) @@ -128,7 +128,7 @@ const getComponentFiles = (componentName = '') => * @returns {Promise} Component names */ const getComponentNames = async (filter) => { - const componentNames = await getDirectories(packageNameToPath('govuk-frontend', '**/src/govuk/components/')) + const componentNames = await getDirectories(join(packageNameToPath('govuk-frontend'), '**/src/govuk/components/')) if (filter) { const componentFiles = await getComponentFiles() diff --git a/shared/lib/names.js b/shared/lib/names.js index d47e681904..e9e95b9344 100644 --- a/shared/lib/names.js +++ b/shared/lib/names.js @@ -1,4 +1,4 @@ -const { dirname, join, parse } = require('path') +const { dirname, parse } = require('path') const { minimatch } = require('minimatch') @@ -64,15 +64,14 @@ function componentPathToModuleName (componentPath) { /** * Resolve path to package from any npm workspace * - * Used by npm workspaces to find packages that might be hoisted to + * Used to find npm workspace packages that might be hoisted to * the project root node_modules * * @param {string} packageName - Installed npm package name - * @param {string} [childPath] - Optional child directory path * @returns {string} Path to installed npm package */ -function packageNameToPath (packageName, childPath = '') { - return join(dirname(require.resolve(`${packageName}/package.json`)), childPath) +function packageNameToPath (packageName) { + return dirname(require.resolve(`${packageName}/package.json`)) } module.exports = { diff --git a/shared/stats/rollup.config.mjs b/shared/stats/rollup.config.mjs index bd95478035..c786340dcd 100644 --- a/shared/stats/rollup.config.mjs +++ b/shared/stats/rollup.config.mjs @@ -31,14 +31,14 @@ export default defineConfig(modulePaths // Stats: File size visualizer({ filename: join('dist', dirname(modulePath), `${parse(modulePath).name}.yaml`), - projectRoot: packageNameToPath('govuk-frontend', 'dist/govuk-esm/'), + projectRoot: join(packageNameToPath('govuk-frontend'), 'dist/govuk-esm/'), template: 'list' }), // Stats: Module tree map visualizer({ filename: join('dist', dirname(modulePath), `${parse(modulePath).name}.html`), - projectRoot: packageNameToPath('govuk-frontend', 'dist/govuk-esm/'), + projectRoot: join(packageNameToPath('govuk-frontend'), 'dist/govuk-esm/'), template: 'treemap' }) ] diff --git a/shared/tasks/styles.mjs b/shared/tasks/styles.mjs index 86f8d77d9c..deb8da8fb4 100644 --- a/shared/tasks/styles.mjs +++ b/shared/tasks/styles.mjs @@ -81,7 +81,7 @@ export async function compileStylesheet ([modulePath, { configPath, srcPath, des // Resolve @imports via loadPaths: [ - packageNameToPath('govuk-frontend', 'dist'), + join(packageNameToPath('govuk-frontend'), 'dist'), join(paths.root, 'node_modules') ], From 05161ca9b8344370920961c55b3fd5b7240f56ba Mon Sep 17 00:00:00 2001 From: Colin Rotherham Date: Fri, 9 Jun 2023 16:10:23 +0100 Subject: [PATCH 2/6] Add shared `packageResolveToPath()` lib function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Used to resolve a package’s main entry point path by name, or by resolving a full entry path using `require.resolve()` against the package’s exports --- shared/lib/names.js | 30 ++++++++++++++++++++++-- shared/lib/names.unit.test.mjs | 43 ++++++++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/shared/lib/names.js b/shared/lib/names.js index e9e95b9344..d87a21f01c 100644 --- a/shared/lib/names.js +++ b/shared/lib/names.js @@ -1,4 +1,4 @@ -const { dirname, parse } = require('path') +const { dirname, join, parse } = require('path') const { minimatch } = require('minimatch') @@ -61,6 +61,23 @@ function componentPathToModuleName (componentPath) { : 'GOVUKFrontend' } +/** + * Resolve path to package entry from any npm workspace + * + * Once the package entry is resolved, the option `modulePath` can be used to + * append a new path relative to the package entry, for example `i18n.mjs` + * + * @param {string} packageEntry - Installed npm package entry, for example `govuk-frontend/src/govuk/all.mjs` + * @param {Pick} [options] - Package resolution options + * @returns {string} Path to installed npm package entry + */ +function packageResolveToPath (packageEntry, { modulePath } = {}) { + const packagePath = require.resolve(packageEntry) + + // Append optional module path + return modulePath !== undefined ? join(dirname(packagePath), modulePath) : packagePath +} + /** * Resolve path to package from any npm workspace * @@ -71,12 +88,21 @@ function componentPathToModuleName (componentPath) { * @returns {string} Path to installed npm package */ function packageNameToPath (packageName) { - return dirname(require.resolve(`${packageName}/package.json`)) + return packageResolveToPath(`${packageName}/package.json`, { + modulePath: '' + }) } module.exports = { componentNameToClassName, componentNameToMacroName, componentPathToModuleName, + packageResolveToPath, packageNameToPath } + +/** + * @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 `i18n.mjs` + */ diff --git a/shared/lib/names.unit.test.mjs b/shared/lib/names.unit.test.mjs index 4edc3a62cb..c4b4469f9e 100644 --- a/shared/lib/names.unit.test.mjs +++ b/shared/lib/names.unit.test.mjs @@ -6,6 +6,7 @@ import { componentNameToClassName, componentNameToMacroName, componentPathToModuleName, + packageResolveToPath, packageNameToPath } from './names.js' @@ -125,16 +126,48 @@ describe('componentPathToModuleName', () => { }) }) +describe('packageResolveToPath', () => { + const packages = [ + { + packageEntry: 'govuk-frontend/package.json', + resolvedPath: join(paths.package, 'package.json') + }, + { + packageEntry: 'govuk-frontend/src/govuk/all.mjs', + resolvedPath: join(paths.package, 'src/govuk/all.mjs') + }, + { + packageEntry: 'govuk-frontend/src/govuk/all.mjs', + options: { modulePath: 'i18n.mjs' }, + resolvedPath: join(paths.package, 'src/govuk/i18n.mjs') + }, + { + packageEntry: 'govuk-frontend/src/govuk/all.mjs', + options: { modulePath: 'components/accordion/accordion.mjs' }, + resolvedPath: join(paths.package, 'src/govuk/components/accordion/accordion.mjs') + } + ] + + it.each(packages)("locates path for npm package entry '$packageEntry'", ({ packageEntry, options, resolvedPath }) => { + expect(packageResolveToPath(packageEntry, options)) + .toBe(resolvedPath) + }) +}) + describe('packageNameToPath', () => { const packages = [ { - name: 'govuk-frontend', - path: paths.package + packageName: 'govuk-frontend', + resolvedPath: paths.package + }, + { + packageName: 'govuk-frontend-review', + resolvedPath: paths.app } ] - it.each(packages)("locates path for npm package '$name'", ({ name, path }) => { - expect(packageNameToPath(name)) - .toBe(path) + it.each(packages)("locates path for npm package '$packageName'", ({ packageName, resolvedPath }) => { + expect(packageNameToPath(packageName)) + .toBe(resolvedPath) }) }) From efd80f7c5076012532403ecfb2c52ba2f7f19b09 Mon Sep 17 00:00:00 2001 From: Colin Rotherham Date: Fri, 9 Jun 2023 16:30:37 +0100 Subject: [PATCH 3/6] Add shared `packageTypeToPath()` lib function Whilst Node.js `import.meta.resolve()` is flagged experimental, this helper enables `module` field access for ES modules --- shared/lib/names.js | 40 +++++++++++++++++++++++++++++++++- shared/lib/names.unit.test.mjs | 30 +++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/shared/lib/names.js b/shared/lib/names.js index d87a21f01c..a50302b8f2 100644 --- a/shared/lib/names.js +++ b/shared/lib/names.js @@ -78,6 +78,43 @@ function packageResolveToPath (packageEntry, { modulePath } = {}) { return modulePath !== undefined ? join(dirname(packagePath), modulePath) : packagePath } +/** + * Return path to package entry from any npm workspace, by type + * + * Wraps {@link packageResolveToPath} to allow the appended `modulePath` to + * include unresolvable paths, globs or files that are not yet built + * + * {@link https://github.com/alphagov/govuk-frontend/issues/3755} + * + * @example + * Resolving components relative to a default package entry + * + * - GOV.UK Frontend v4 './govuk/components/accordion/accordion.mjs' + * - GOV.UK Frontend v5 './dist/govuk/components/accordion/accordion.mjs' + * + * ```mjs + * const templatePath = packageResolveToPath('govuk-frontend', { + * modulePath: `components/accordion/accordion.mjs` + * }) + * ``` + * @param {string} packageName - Installed npm package name + * @param {PackageOptions} [options] - Package resolution options + * @returns {string} Path to installed npm package field + */ +function packageTypeToPath (packageName, { modulePath, type = 'commonjs' } = {}) { + const packageEntry = `${packageName}/package.json` + const packageField = type === 'module' ? 'module' : 'main' + + // Package field as child path + const entryPath = require(packageResolveToPath(packageEntry))[packageField] + const childPath = modulePath !== undefined ? join(dirname(entryPath), modulePath) : entryPath + + // Append optional module path + return packageResolveToPath(packageEntry, { + modulePath: childPath + }) +} + /** * Resolve path to package from any npm workspace * @@ -98,11 +135,12 @@ module.exports = { componentNameToMacroName, componentPathToModuleName, packageResolveToPath, + packageTypeToPath, packageNameToPath } /** * @typedef {object} PackageOptions - * @property {string} [field] - Package field name from package.json, for example `module` + * @property {string} [type=commonjs] - Package type from package.json, for example `module` * @property {string} [modulePath] - Module path (optional, relative to package entry), for example `i18n.mjs` */ diff --git a/shared/lib/names.unit.test.mjs b/shared/lib/names.unit.test.mjs index c4b4469f9e..281328669b 100644 --- a/shared/lib/names.unit.test.mjs +++ b/shared/lib/names.unit.test.mjs @@ -7,6 +7,7 @@ import { componentNameToMacroName, componentPathToModuleName, packageResolveToPath, + packageTypeToPath, packageNameToPath } from './names.js' @@ -154,6 +155,35 @@ describe('packageResolveToPath', () => { }) }) +describe('packageTypeToPath', () => { + const packages = [ + { + packageName: 'govuk-frontend', + resolvedPath: join(paths.package, 'dist/govuk/all.js') + }, + { + packageName: 'govuk-frontend', + options: { modulePath: 'i18n.js' }, + resolvedPath: join(paths.package, 'dist/govuk/i18n.js') + }, + { + packageName: 'govuk-frontend', + options: { type: 'module' }, + resolvedPath: join(paths.package, 'dist/govuk-esm/all.mjs') + }, + { + packageName: 'govuk-frontend', + options: { modulePath: 'i18n.mjs', type: 'module' }, + resolvedPath: join(paths.package, 'dist/govuk-esm/i18n.mjs') + } + ] + + it.each(packages)("locates path for npm package '$packageName' field '$packageField'", ({ packageName, options, resolvedPath }) => { + expect(packageTypeToPath(packageName, options)) + .toBe(resolvedPath) + }) +}) + describe('packageNameToPath', () => { const packages = [ { From c3eaff64261a7373588a3133a6204a0ce1d3cf1d Mon Sep 17 00:00:00 2001 From: Colin Rotherham Date: Fri, 9 Jun 2023 15:37:44 +0100 Subject: [PATCH 4/6] Resolve `govuk-frontend` relative to package fields Allows us to switch between `govuk-frontend@5` and `govuk-frontend@5` without issue --- .../browsersync.config.js | 4 ++-- .../govuk-frontend-review/rollup.config.mjs | 3 ++- .../src/common/middleware/assets.mjs | 4 ++-- .../common/nunjucks/globals/get-html-code.mjs | 8 ++++---- .../src/common/nunjucks/index.mjs | 19 +++++++++++++------ .../govuk-frontend-review/typedoc.config.js | 6 +++--- shared/stats/rollup.config.mjs | 18 +++++++++++------- shared/stats/src/index.mjs | 12 +++++++++++- shared/tasks/styles.mjs | 12 +++++++++--- 9 files changed, 57 insertions(+), 29 deletions(-) diff --git a/packages/govuk-frontend-review/browsersync.config.js b/packages/govuk-frontend-review/browsersync.config.js index bb518038b3..d4d2dbe17d 100644 --- a/packages/govuk-frontend-review/browsersync.config.js +++ b/packages/govuk-frontend-review/browsersync.config.js @@ -1,7 +1,7 @@ const { join } = require('path') const { paths, ports } = require('govuk-frontend-config') -const { packageNameToPath } = require('govuk-frontend-lib/names') +const { packageTypeToPath } = require('govuk-frontend-lib/names') /** * Browsersync config @@ -25,7 +25,7 @@ module.exports = { join(paths.app, 'dist/javascripts/**/*.js'), join(paths.app, 'dist/stylesheets/**/*.css'), join(paths.app, 'src/views/**/*.njk'), - join(packageNameToPath('govuk-frontend'), 'dist/govuk/**/*.njk') + packageTypeToPath('govuk-frontend', { modulePath: '**/*.njk' }) ], ignore: ['**/*.test.*'], diff --git a/packages/govuk-frontend-review/rollup.config.mjs b/packages/govuk-frontend-review/rollup.config.mjs index 342bc1ad3f..5f11ad618b 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 } from 'govuk-frontend-config' import { defineConfig } from 'rollup' /** @@ -38,6 +39,6 @@ export default defineConfig(({ i: input }) => ({ * Input plugins */ plugins: [ - resolve() + resolve({ rootDir: paths.app }) ] })) diff --git a/packages/govuk-frontend-review/src/common/middleware/assets.mjs b/packages/govuk-frontend-review/src/common/middleware/assets.mjs index 0043351534..bc031a02c3 100644 --- a/packages/govuk-frontend-review/src/common/middleware/assets.mjs +++ b/packages/govuk-frontend-review/src/common/middleware/assets.mjs @@ -2,7 +2,7 @@ import { join } from 'path' import express from 'express' import { paths } from 'govuk-frontend-config' -import { packageNameToPath } from 'govuk-frontend-lib/names' +import { packageTypeToPath } from 'govuk-frontend-lib/names' const router = express.Router() @@ -10,7 +10,7 @@ const router = express.Router() * Add middleware to serve static assets */ -router.use('/assets', express.static(join(packageNameToPath('govuk-frontend'), 'dist/govuk/assets'))) +router.use('/assets', express.static(packageTypeToPath('govuk-frontend', { modulePath: 'assets' }))) router.use('/javascripts', express.static(join(paths.app, 'dist/javascripts'))) router.use('/stylesheets', express.static(join(paths.app, 'dist/stylesheets'))) diff --git a/packages/govuk-frontend-review/src/common/nunjucks/globals/get-html-code.mjs b/packages/govuk-frontend-review/src/common/nunjucks/globals/get-html-code.mjs index 419fa9f188..b9c4a81d38 100644 --- a/packages/govuk-frontend-review/src/common/nunjucks/globals/get-html-code.mjs +++ b/packages/govuk-frontend-review/src/common/nunjucks/globals/get-html-code.mjs @@ -1,6 +1,4 @@ -import { join } from 'path' - -import { packageNameToPath } from 'govuk-frontend-lib/names' +import { packageTypeToPath } from 'govuk-frontend-lib/names' import beautify from 'js-beautify' /** @@ -11,7 +9,9 @@ import beautify from 'js-beautify' * @returns {string} Nunjucks code */ export function getHTMLCode (componentName, params) { - const templatePath = join(packageNameToPath('govuk-frontend'), `dist/govuk/components/${componentName}/template.njk`) + const templatePath = packageTypeToPath('govuk-frontend', { + modulePath: `components/${componentName}/template.njk` + }) // Render to HTML const html = this.env.render(templatePath, { params }).trim() diff --git a/packages/govuk-frontend-review/src/common/nunjucks/index.mjs b/packages/govuk-frontend-review/src/common/nunjucks/index.mjs index eed36867a1..9c94c52cb8 100644 --- a/packages/govuk-frontend-review/src/common/nunjucks/index.mjs +++ b/packages/govuk-frontend-review/src/common/nunjucks/index.mjs @@ -1,20 +1,27 @@ import { join } from 'path' import { paths } from 'govuk-frontend-config' -import { packageNameToPath } from 'govuk-frontend-lib/names' +import { packageResolveToPath } from 'govuk-frontend-lib/names' import nunjucks from 'nunjucks' import * as filters from './filters/index.mjs' import * as globals from './globals/index.mjs' +/** + * Initialise renderer with Nunjucks environment + * + * @param {import('express').Application} app - Express.js review app + * @returns {import('nunjucks').Environment} Nunjucks Environment + */ export function renderer (app) { - const appViews = [ + const env = nunjucks.configure([ join(paths.app, 'src/views'), - join(packageNameToPath('govuk-frontend'), 'dist') - ] - // Initialise nunjucks environment - const env = nunjucks.configure(appViews, { + // Remove `govuk/` suffix using `modulePath` + packageResolveToPath('govuk-frontend', { + modulePath: '../' + }) + ], { autoescape: true, // output with dangerous characters are escaped automatically express: app, // the Express.js review app that nunjucks should install to noCache: true, // never use a cache and recompile templates each time diff --git a/packages/govuk-frontend-review/typedoc.config.js b/packages/govuk-frontend-review/typedoc.config.js index cc5eb2df1b..8209e8a3c4 100644 --- a/packages/govuk-frontend-review/typedoc.config.js +++ b/packages/govuk-frontend-review/typedoc.config.js @@ -1,6 +1,6 @@ const { join } = require('path') -const { packageNameToPath } = require('govuk-frontend-lib/names') +const { packageResolveToPath, packageNameToPath } = require('govuk-frontend-lib/names') /** * @type {import('typedoc').TypeDocOptions} @@ -12,8 +12,8 @@ module.exports = { // Configure paths basePath: join(packageNameToPath('govuk-frontend'), 'src'), - entryPoints: [join(packageNameToPath('govuk-frontend'), 'src/govuk/all.mjs')], - tsconfig: join(packageNameToPath('govuk-frontend'), 'tsconfig.build.json'), + entryPoints: [packageResolveToPath('govuk-frontend/src/govuk/all.mjs')], + tsconfig: packageResolveToPath('govuk-frontend/tsconfig.build.json'), out: './dist/docs/jsdoc', // Ignore warnings about CharacterCountTranslations using I18n (@private) diff --git a/shared/stats/rollup.config.mjs b/shared/stats/rollup.config.mjs index c786340dcd..ab347be4b4 100644 --- a/shared/stats/rollup.config.mjs +++ b/shared/stats/rollup.config.mjs @@ -1,18 +1,22 @@ -import { dirname, join, parse } from 'path' +import { dirname, join, parse, relative } from 'path' import resolve from '@rollup/plugin-node-resolve' -import { packageNameToPath } from 'govuk-frontend-lib/names' +import { paths } from 'govuk-frontend-config' +import { packageTypeToPath } from 'govuk-frontend-lib/names' import { defineConfig } from 'rollup' import { visualizer } from 'rollup-plugin-visualizer' -import { modulePaths } from './src/index.mjs' +import { modulePaths, packageOptions } from './src/index.mjs' + +// Locate GOV.UK Frontend +const packagePath = packageTypeToPath('govuk-frontend', packageOptions) /** * Rollup config with stats output */ export default defineConfig(modulePaths .map((modulePath) => /** @satisfies {import('rollup').RollupOptions} */({ - input: join('govuk-frontend/dist/govuk-esm', modulePath), + input: relative(paths.stats, packageTypeToPath('govuk-frontend', { ...packageOptions, modulePath })), /** * Output options @@ -26,19 +30,19 @@ export default defineConfig(modulePaths * Input plugins */ plugins: [ - resolve(), + resolve({ rootDir: paths.stats }), // Stats: File size visualizer({ filename: join('dist', dirname(modulePath), `${parse(modulePath).name}.yaml`), - projectRoot: join(packageNameToPath('govuk-frontend'), 'dist/govuk-esm/'), + projectRoot: dirname(packagePath), template: 'list' }), // Stats: Module tree map visualizer({ filename: join('dist', dirname(modulePath), `${parse(modulePath).name}.html`), - projectRoot: join(packageNameToPath('govuk-frontend'), 'dist/govuk-esm/'), + projectRoot: dirname(packagePath), template: 'treemap' }) ] diff --git a/shared/stats/src/index.mjs b/shared/stats/src/index.mjs index 2ff1259a4e..1e65e0e1ee 100644 --- a/shared/stats/src/index.mjs +++ b/shared/stats/src/index.mjs @@ -9,10 +9,20 @@ import { getComponentNames, filterPath, getYaml } from 'govuk-frontend-lib/files const componentNamesWithJavaScript = await getComponentNames((componentName, componentFiles) => componentFiles.some(filterPath([`**/${componentName}.mjs`]))) +/** + * Package options + * + * @type {import('govuk-frontend-lib/names').PackageOptions} + */ +export const packageOptions = { + type: 'module', + modulePath: 'all.mjs' +} + /** * Rollup input paths */ -export const modulePaths = ['all.mjs'] +export const modulePaths = [packageOptions.modulePath] .concat(componentNamesWithJavaScript.map((componentName) => `components/${componentName}/${componentName}.mjs`)) diff --git a/shared/tasks/styles.mjs b/shared/tasks/styles.mjs index deb8da8fb4..e9f7de74ea 100644 --- a/shared/tasks/styles.mjs +++ b/shared/tasks/styles.mjs @@ -4,7 +4,7 @@ import { join, parse } from 'path' import chalk from 'chalk' import { paths } from 'govuk-frontend-config' import { getListing } from 'govuk-frontend-lib/files' -import { packageNameToPath } from 'govuk-frontend-lib/names' +import { packageResolveToPath } from 'govuk-frontend-lib/names' import PluginError from 'plugin-error' import postcss from 'postcss' // eslint-disable-next-line import/default @@ -39,7 +39,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) @@ -81,7 +81,13 @@ export async function compileStylesheet ([modulePath, { configPath, srcPath, des // Resolve @imports via loadPaths: [ - join(packageNameToPath('govuk-frontend'), 'dist'), + // Remove `govuk/` suffix using `modulePath` + packageResolveToPath('govuk-frontend', { + modulePath: '../' + }), + + // Resolve local packages first + join(basePath, 'node_modules'), join(paths.root, 'node_modules') ], From 137c74a471cbab8249103e8968b974d5e2d7b5b2 Mon Sep 17 00:00:00 2001 From: Colin Rotherham Date: Fri, 9 Jun 2023 16:39:15 +0100 Subject: [PATCH 5/6] 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 --- .../browsersync.config.js | 5 +++- .../src/common/middleware/assets.mjs | 2 +- .../common/nunjucks/globals/get-html-code.mjs | 4 ++- .../src/common/nunjucks/index.mjs | 3 +- shared/lib/names.js | 23 +++++++++----- shared/lib/names.unit.test.mjs | 30 +++++++++++++++++++ shared/stats/src/index.mjs | 3 +- shared/tasks/styles.mjs | 3 +- 8 files changed, 59 insertions(+), 14 deletions(-) diff --git a/packages/govuk-frontend-review/browsersync.config.js b/packages/govuk-frontend-review/browsersync.config.js index d4d2dbe17d..5a8e85a685 100644 --- a/packages/govuk-frontend-review/browsersync.config.js +++ b/packages/govuk-frontend-review/browsersync.config.js @@ -25,7 +25,10 @@ module.exports = { join(paths.app, 'dist/javascripts/**/*.js'), join(paths.app, 'dist/stylesheets/**/*.css'), join(paths.app, 'src/views/**/*.njk'), - packageTypeToPath('govuk-frontend', { modulePath: '**/*.njk' }) + packageTypeToPath('govuk-frontend', { + modulePath: '**/*.njk', + moduleRoot: paths.app + }) ], ignore: ['**/*.test.*'], diff --git a/packages/govuk-frontend-review/src/common/middleware/assets.mjs b/packages/govuk-frontend-review/src/common/middleware/assets.mjs index bc031a02c3..4df75ce9bb 100644 --- a/packages/govuk-frontend-review/src/common/middleware/assets.mjs +++ b/packages/govuk-frontend-review/src/common/middleware/assets.mjs @@ -10,7 +10,7 @@ const router = express.Router() * Add middleware to serve static assets */ -router.use('/assets', express.static(packageTypeToPath('govuk-frontend', { modulePath: 'assets' }))) +router.use('/assets', express.static(packageTypeToPath('govuk-frontend', { modulePath: 'assets', moduleRoot: paths.app }))) router.use('/javascripts', express.static(join(paths.app, 'dist/javascripts'))) router.use('/stylesheets', express.static(join(paths.app, 'dist/stylesheets'))) diff --git a/packages/govuk-frontend-review/src/common/nunjucks/globals/get-html-code.mjs b/packages/govuk-frontend-review/src/common/nunjucks/globals/get-html-code.mjs index b9c4a81d38..43f4f8df11 100644 --- a/packages/govuk-frontend-review/src/common/nunjucks/globals/get-html-code.mjs +++ b/packages/govuk-frontend-review/src/common/nunjucks/globals/get-html-code.mjs @@ -1,3 +1,4 @@ +import { paths } from 'govuk-frontend-config' import { packageTypeToPath } from 'govuk-frontend-lib/names' import beautify from 'js-beautify' @@ -10,7 +11,8 @@ import beautify from 'js-beautify' */ export function getHTMLCode (componentName, params) { const templatePath = packageTypeToPath('govuk-frontend', { - modulePath: `components/${componentName}/template.njk` + modulePath: `components/${componentName}/template.njk`, + moduleRoot: paths.app }) // Render to HTML diff --git a/packages/govuk-frontend-review/src/common/nunjucks/index.mjs b/packages/govuk-frontend-review/src/common/nunjucks/index.mjs index 9c94c52cb8..9184574c6e 100644 --- a/packages/govuk-frontend-review/src/common/nunjucks/index.mjs +++ b/packages/govuk-frontend-review/src/common/nunjucks/index.mjs @@ -19,7 +19,8 @@ export function renderer (app) { // Remove `govuk/` suffix using `modulePath` packageResolveToPath('govuk-frontend', { - modulePath: '../' + modulePath: '../', + moduleRoot: paths.app }) ], { autoescape: true, // output with dangerous characters are escaped automatically diff --git a/shared/lib/names.js b/shared/lib/names.js index a50302b8f2..9258da2c82 100644 --- a/shared/lib/names.js +++ b/shared/lib/names.js @@ -1,5 +1,6 @@ const { dirname, join, parse } = require('path') +const { paths } = require('govuk-frontend-config') const { minimatch } = require('minimatch') /** @@ -68,11 +69,13 @@ function componentPathToModuleName (componentPath) { * append a new path relative to the package entry, for example `i18n.mjs` * * @param {string} packageEntry - Installed npm package entry, for example `govuk-frontend/src/govuk/all.mjs` - * @param {Pick} [options] - Package resolution options + * @param {Pick} [options] - Package resolution options * @returns {string} Path to installed npm package entry */ -function packageResolveToPath (packageEntry, { modulePath } = {}) { - const packagePath = require.resolve(packageEntry) +function packageResolveToPath (packageEntry, { modulePath, moduleRoot } = {}) { + const packagePath = require.resolve(packageEntry, { + paths: [moduleRoot ?? paths.root] + }) // Append optional module path return modulePath !== undefined ? join(dirname(packagePath), modulePath) : packagePath @@ -101,17 +104,18 @@ function packageResolveToPath (packageEntry, { modulePath } = {}) { * @param {PackageOptions} [options] - Package resolution options * @returns {string} Path to installed npm package field */ -function packageTypeToPath (packageName, { modulePath, type = 'commonjs' } = {}) { +function packageTypeToPath (packageName, { modulePath, moduleRoot, type = 'commonjs' } = {}) { const packageEntry = `${packageName}/package.json` const packageField = type === 'module' ? 'module' : 'main' // Package field as child path - const entryPath = require(packageResolveToPath(packageEntry))[packageField] + const entryPath = require(packageResolveToPath(packageEntry, { moduleRoot }))[packageField] const childPath = modulePath !== undefined ? join(dirname(entryPath), modulePath) : entryPath // Append optional module path return packageResolveToPath(packageEntry, { - modulePath: childPath + modulePath: childPath, + moduleRoot }) } @@ -122,11 +126,13 @@ function packageTypeToPath (packageName, { modulePath, type = 'commonjs' } = {}) * the project root node_modules * * @param {string} packageName - Installed npm package name + * @param {Pick} [options] - Package resolution options * @returns {string} Path to installed npm package */ -function packageNameToPath (packageName) { +function packageNameToPath (packageName, options) { return packageResolveToPath(`${packageName}/package.json`, { - modulePath: '' + modulePath: '', + ...options }) } @@ -143,4 +149,5 @@ module.exports = { * @typedef {object} PackageOptions * @property {string} [type=commonjs] - Package type from package.json, for example `module` * @property {string} [modulePath] - Module path (optional, relative to package entry), for example `i18n.mjs` + * @property {string} [moduleRoot] - Module root (optional, absolute directory path to resolve `node_modules` from) */ diff --git a/shared/lib/names.unit.test.mjs b/shared/lib/names.unit.test.mjs index 281328669b..c46e8c0aea 100644 --- a/shared/lib/names.unit.test.mjs +++ b/shared/lib/names.unit.test.mjs @@ -201,3 +201,33 @@ describe('packageNameToPath', () => { .toBe(resolvedPath) }) }) + +describe("packageNameToPath (with custom 'node_module' paths)", () => { + const packages = [ + { + packageName: 'govuk-frontend', + options: { moduleRoot: paths.root }, + resolvedPath: paths.package + }, + { + packageName: 'govuk-frontend-review', + options: { moduleRoot: paths.root }, + resolvedPath: paths.app + }, + { + packageName: 'autoprefixer', + options: { moduleRoot: paths.package }, + resolvedPath: join(paths.root, 'node_modules/autoprefixer') + }, + { + packageName: 'postcss', + options: { moduleRoot: paths.app }, + resolvedPath: join(paths.root, 'node_modules/postcss') + } + ] + + it.each(packages)("locates path for npm package '$packageName'", ({ packageName, options = {}, resolvedPath }) => { + expect(packageNameToPath(packageName, options)) + .toBe(resolvedPath) + }) +}) diff --git a/shared/stats/src/index.mjs b/shared/stats/src/index.mjs index 1e65e0e1ee..0a635dd74e 100644 --- a/shared/stats/src/index.mjs +++ b/shared/stats/src/index.mjs @@ -16,7 +16,8 @@ const componentNamesWithJavaScript = await getComponentNames((componentName, com */ export const packageOptions = { type: 'module', - modulePath: 'all.mjs' + modulePath: 'all.mjs', + moduleRoot: paths.stats } /** diff --git a/shared/tasks/styles.mjs b/shared/tasks/styles.mjs index e9f7de74ea..f4a1f63833 100644 --- a/shared/tasks/styles.mjs +++ b/shared/tasks/styles.mjs @@ -83,7 +83,8 @@ export async function compileStylesheet ([modulePath, { basePath, configPath, sr loadPaths: [ // Remove `govuk/` suffix using `modulePath` packageResolveToPath('govuk-frontend', { - modulePath: '../' + modulePath: '../', + moduleRoot: basePath }), // Resolve local packages first From 9278d1e35ac4e79c0a0f82189c30027e04c134f8 Mon Sep 17 00:00:00 2001 From: Colin Rotherham Date: Fri, 9 Jun 2023 14:50:30 +0100 Subject: [PATCH 6/6] Fix review app not restarting when `*.js` shared libraries change --- packages/govuk-frontend-review/nodemon.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/govuk-frontend-review/nodemon.json b/packages/govuk-frontend-review/nodemon.json index c23bcf23ce..31d12be622 100644 --- a/packages/govuk-frontend-review/nodemon.json +++ b/packages/govuk-frontend-review/nodemon.json @@ -10,7 +10,7 @@ "events": { "restart": "browser-sync reload --config browsersync.config.js" }, - "ext": "mjs,json,yaml", + "ext": "js,mjs,json,yaml", "quiet": true, "signal": "SIGINT" }