diff --git a/cypress/e2e/smoke/0-smoke-tests/index-page.cypress.js b/cypress/e2e/smoke/0-smoke-tests/index-page.cypress.js index 5de33e60cd..93d987f925 100644 --- a/cypress/e2e/smoke/0-smoke-tests/index-page.cypress.js +++ b/cypress/e2e/smoke/0-smoke-tests/index-page.cypress.js @@ -11,7 +11,7 @@ describe('smoke test', () => { it('GOV.UK Frontend fonts loaded', () => { waitForApplication('/') - const fontUrl = '/plugin-assets/govuk-frontend/govuk/assets/fonts/bold-b542beb274-v2.woff2' + const fontUrl = `/plugin-assets/govuk-frontend${Cypress.env('frontendAssetsFolder')}/fonts/bold-b542beb274-v2.woff2` cy.task('log', 'Requesting govuk-frontend font') cy.request(`/${fontUrl}`, { retryOnStatusCodeFailure: true }) diff --git a/cypress/events/index.js b/cypress/events/index.js index 5c7bb6379c..9f0e73f866 100644 --- a/cypress/events/index.js +++ b/cypress/events/index.js @@ -74,6 +74,11 @@ module.exports = function setupNodeEvents (on, config) { config.env.packageFolder = path.join(config.env.projectFolder, 'node_modules', 'govuk-prototype-kit') } + if ('govuk-frontend' in dependencies) { + const frontEndConfigFile = path.join(config.env.projectFolder, 'node_modules', 'govuk-frontend', 'govuk-prototype-kit.config.json') + config.env.frontendAssetsFolder = fse.readJsonSync(frontEndConfigFile).assets.find(asset => !asset.split(path.sep).pop().includes('.')) + } + const waitUntilAppRestarts = (timeout = 20000) => waitOn({ delay: 3000, resources: [config.baseUrl], diff --git a/lib/manage-prototype-handlers.js b/lib/manage-prototype-handlers.js index 8c5b50c3be..154aca3ef7 100644 --- a/lib/manage-prototype-handlers.js +++ b/lib/manage-prototype-handlers.js @@ -10,7 +10,7 @@ const { doubleCsrf } = require('csrf-csrf') const config = require('./config') const plugins = require('./plugins/plugins') const { exec } = require('./exec') -const { prototypeAppScripts } = require('./utils') +const { prototypeAppScripts, getScriptsAndAssetsConfig, getInternalGovukFrontendDir } = require('./utils') const { projectDir, packageDir, appViewsDir } = require('./utils/paths') const nunjucksConfiguration = require('./nunjucks/nunjucksConfiguration') const syncChanges = require('./sync-changes') @@ -103,7 +103,7 @@ function postPasswordHandler (req, res) { const password = config.getConfig().password const submittedPassword = req.body.password const providedUrl = req.body.returnURL - const processedRedirectUrl = (!providedUrl || providedUrl.startsWith('/manage-prototype/password')) ? '/' : providedUrl + const processedRedirectUrl = (!providedUrl || providedUrl.startsWith(`${contextPath}/password`)) ? '/' : providedUrl if (submittedPassword === password) { // see lib/middleware/authentication.js for explanation @@ -115,10 +115,26 @@ function postPasswordHandler (req, res) { }) res.redirect(processedRedirectUrl) } else { - res.redirect('/manage-prototype/password?error=wrong-password&returnURL=' + encodeURIComponent(processedRedirectUrl)) + res.redirect(`${contextPath}/password?error=wrong-password&returnURL=` + encodeURIComponent(processedRedirectUrl)) } } +function managePrototypeMiddleware (req, res, next) { + const { scripts: frontEndScripts } = getScriptsAndAssetsConfig(getInternalGovukFrontendDir()) + const { scripts: kitScripts } = getScriptsAndAssetsConfig(packageDir) + res.locals.managePlugins = { + scripts: [ + ...frontEndScripts.map((script) => { + return { src: `${contextPath}/dependencies/govuk-frontend${script.src || script}`, type: script.type } + }), + ...kitScripts.map((script) => { + return { src: `${contextPath}/dependencies/govuk-prototype-kit${script.src || script}`, type: script.type } + }) + ] + } + next() +} + // Middleware to ensure the routes specified below will render the manage-prototype-not-available // view when the prototype is not running in development function developmentOnlyMiddleware (req, res, next) { @@ -728,6 +744,7 @@ async function postPluginsModeHandler (req, res) { module.exports = { contextPath, + managePrototypeMiddleware, setKitRestarted, csrfProtection: [doubleCsrfProtection, csrfErrorHandler], getPageLoadedHandler, diff --git a/lib/manage-prototype-routes.js b/lib/manage-prototype-routes.js index 42c824c03e..ddb61f205f 100644 --- a/lib/manage-prototype-routes.js +++ b/lib/manage-prototype-routes.js @@ -1,8 +1,12 @@ +// core dependencies +const path = require('path') + // npm dependencies const express = require('express') const { contextPath, + managePrototypeMiddleware, setKitRestarted, csrfProtection, getPageLoadedHandler, @@ -26,11 +30,31 @@ const { pluginCacheMiddleware, postPluginsHandler } = require('./manage-prototype-handlers') +const { getScriptsAndAssetsConfig, getInternalGovukFrontendDir } = require('./utils') const { packageDir, projectDir } = require('./utils/paths') const { govukFrontendPaths } = require('./govukFrontendPaths') const router = require('../index').requests.setupRouter(contextPath) +router.use(managePrototypeMiddleware) + +function getAssetUrls (pluginDir) { + const { scripts, assets } = getScriptsAndAssetsConfig(pluginDir) + + return [ + ...assets, + ...scripts.map(script => script.src || script) + ] +} + +getAssetUrls(getInternalGovukFrontendDir()).forEach(url => { + router.use(`/dependencies/govuk-frontend${url}`, express.static(path.join(getInternalGovukFrontendDir(), url))) +}) + +getAssetUrls(packageDir).forEach(url => { + router.use(`/dependencies/govuk-prototype-kit${url}`, express.static(path.join(packageDir, url))) +}) + router.get('/csrf-token', getCsrfTokenHandler) // Indicates page has loaded diff --git a/lib/utils/index.js b/lib/utils/index.js index 21cc4a3893..788858cc31 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -4,7 +4,6 @@ const crypto = require('crypto') const fs = require('fs') const fsp = fs.promises const path = require('path') -const { existsSync } = require('fs') // npm dependencies const inquirer = require('inquirer') @@ -17,7 +16,7 @@ const filters = require('../filters/api') const functions = require('../functions/api') const plugins = require('../plugins/plugins') const routes = require('../routes/api') -const { appDir, projectDir } = require('./paths') +const { appDir, projectDir, packageDir } = require('./paths') const { asyncSeriesMap } = require('./asyncSeriesMap') // Tweak the Markdown renderer @@ -35,7 +34,7 @@ marked.use({ }) const scripts = [] -if (existsSync(path.join(projectDir, 'app', 'assets', 'javascripts', 'application.js'))) { +if (fs.existsSync(path.join(projectDir, 'app', 'assets', 'javascripts', 'application.js'))) { scripts.push('/public/javascripts/application.js') } if (plugins.legacyGovukFrontendFixesNeeded()) { @@ -214,7 +213,7 @@ function sessionFileStoreQuietLogFn (message) { function recursiveDirectoryContentsSync (baseDir) { function goThroughDir (dir = '') { const fullPath = path.join(baseDir, dir) - if (!existsSync(fullPath)) { + if (!fs.existsSync(fullPath)) { return [] } const dirContents = fs.readdirSync(fullPath) @@ -253,6 +252,40 @@ async function searchAndReplaceFiles (dir, searchText, replaceText, extensions) return modifiedFiles.flat().filter(Boolean) } +const managePrototypeCache = {} + +function getScriptsAndAssetsConfig (pluginDir) { + if (managePrototypeCache[pluginDir]) { + return managePrototypeCache[pluginDir] + } + let { + assets = [], + scripts = [] + } = fs.readFileSync(path.join(pluginDir, 'govuk-prototype-kit.config.json')).toJSON() + + if (!Array.isArray(assets)) { + assets = [assets] + } + + if (!Array.isArray(scripts)) { + scripts = [scripts] + } + + managePrototypeCache[pluginDir] = { assets, scripts } + return managePrototypeCache[pluginDir] +} + +let internalGovukFrontendDir + +function getInternalGovukFrontendDir () { + if (!internalGovukFrontendDir) { + const packageDirFrontend = path.join(packageDir, 'node_modules', 'govuk-frontend') + const projectDirFrontend = path.join(projectDir, 'node_modules', 'govuk-frontend') + internalGovukFrontendDir = fs.existsSync(packageDirFrontend) ? packageDirFrontend : projectDirFrontend + } + return internalGovukFrontendDir +} + function sortByObjectKey (key) { return function (a, b) { if (a[key] > b[key]) { @@ -279,5 +312,7 @@ module.exports = { sessionFileStoreQuietLogFn, searchAndReplaceFiles, recursiveDirectoryContentsSync, + getScriptsAndAssetsConfig, + getInternalGovukFrontendDir, sortByObjectKey }