From accc5d784fab873534cf7d94b7a26f1fd9c985dc Mon Sep 17 00:00:00 2001 From: Mat Carey Date: Fri, 10 Aug 2018 14:42:07 +0100 Subject: [PATCH] Plugin Framework added. --- .gitignore | 2 + app/assets/sass/application.scss | 2 +- app/views/includes/head.html | 4 ++ app/views/includes/scripts.html | 6 ++- docs/assets/sass/docs.scss | 2 +- docs/views/includes/scripts.html | 6 ++- gulp/sass.js | 10 ++++ gulp/tasks.js | 1 + lib/plugin-detection.js | 80 ++++++++++++++++++++++++++++++++ package.json | 3 ++ server.js | 33 +++++++++---- 11 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 lib/plugin-detection.js diff --git a/.gitignore b/.gitignore index f2b7a43bdb..ff65fa8ee5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # ignore tracking usage data config - we want a different one for each user usage-data-config.json +# ignore plugin include SCSS because it's dynamically generated +app/assets/sass/allPluginIncludes-generated.scss .env .sass-cache .DS_Store diff --git a/app/assets/sass/application.scss b/app/assets/sass/application.scss index d54f828b6d..23e416c703 100644 --- a/app/assets/sass/application.scss +++ b/app/assets/sass/application.scss @@ -2,7 +2,7 @@ $govuk-global-styles: true; // Import GOV.UK Frontend -@import "node_modules/govuk-frontend/all"; +@import "allPluginIncludes-generated"; // Patterns that aren't in Frontend @import "patterns/check-your-answers"; diff --git a/app/views/includes/head.html b/app/views/includes/head.html index 88e56ebeee..93c768df67 100644 --- a/app/views/includes/head.html +++ b/app/views/includes/head.html @@ -1,2 +1,6 @@ + +{% for stylesheetUrl in pluginConfig.stylesheets %} + +{% endfor %} diff --git a/app/views/includes/scripts.html b/app/views/includes/scripts.html index aa86cc5ab9..556f4ddb88 100644 --- a/app/views/includes/scripts.html +++ b/app/views/includes/scripts.html @@ -1,6 +1,10 @@ - + +{% for scriptUrl in pluginConfig.scripts %} + +{% endfor %} + {% if useAutoStoreData %} diff --git a/docs/assets/sass/docs.scss b/docs/assets/sass/docs.scss index 138d7f9ab0..c44ec0c8f6 100644 --- a/docs/assets/sass/docs.scss +++ b/docs/assets/sass/docs.scss @@ -1,4 +1,4 @@ -@import "node_modules/govuk-frontend/all"; +@import "../../../app/assets/sass/allPluginIncludes-generated.scss"; img{ max-width: 100%; diff --git a/docs/views/includes/scripts.html b/docs/views/includes/scripts.html index f2487ab98d..0325adff8b 100644 --- a/docs/views/includes/scripts.html +++ b/docs/views/includes/scripts.html @@ -1,6 +1,10 @@ - + +{% for scriptUrl in additionalScripts %} + +{% endfor %} + {% if useAutoStoreData %} diff --git a/gulp/sass.js b/gulp/sass.js index 022e9e59fb..898d32fff3 100644 --- a/gulp/sass.js +++ b/gulp/sass.js @@ -8,9 +8,19 @@ const gulp = require('gulp') const sass = require('gulp-sass') const sourcemaps = require('gulp-sourcemaps') +const path = require('path') +const fs = require('fs') +const pluginDetection = require('../lib/plugin-detection') const config = require('./config.json') +gulp.task('sass-plugins', function (done) { + const fileContents = pluginDetection.getList('sassIncludes', pluginDetection.transform.scopeFilePathsToModule) + .map(filePath => `@import "${filePath}";`) + .join('\n') + fs.writeFile(path.join(config.paths.assets, 'sass', 'allPluginIncludes-generated.scss'), fileContents, done) +}) + gulp.task('sass', function () { return gulp.src(config.paths.assets + '/sass/*.scss') .pipe(sourcemaps.init()) diff --git a/gulp/tasks.js b/gulp/tasks.js index 24980f2694..708ed19897 100644 --- a/gulp/tasks.js +++ b/gulp/tasks.js @@ -16,6 +16,7 @@ gulp.task('default', function (done) { gulp.task('generate-assets', function (done) { runSequence('clean', + 'sass-plugins', 'sass', 'copy-assets', 'sass-documentation', diff --git a/lib/plugin-detection.js b/lib/plugin-detection.js new file mode 100644 index 0000000000..0d191d18a9 --- /dev/null +++ b/lib/plugin-detection.js @@ -0,0 +1,80 @@ +const fs = require('fs') +const path = require('path') + +const pathJoinFromRoot = (...all) => path.join.apply(null, [__dirname, '..'].concat(all)) + +const pathToHookFile = packageName => pathJoinFromRoot('node_modules', packageName, 'govuk-prototype-kit-hooks.json') + +const flatten = x => [].concat(...x) +const noEditFn = x => x + +const defaultConfigs = { + 'govuk-frontend': { + nunjucksDirs: ['/', 'components'], + scripts: ['/all.js'], + globalAssets: ['/assets'], + sassIncludes: ['/all.scss'] + } +} + +const packageConfig = require(pathJoinFromRoot('package.json')) + +const lookupConfig = packageName => hookFileExists(packageName) ? require(pathToHookFile(packageName)) : defaultConfigs[packageName] + +const hookFileExists = packageName => fs.existsSync(pathToHookFile(packageName)) && fs.statSync(pathToHookFile(packageName)).isFile() + +const alphabeticSort = (l, r) => (l < r) ? -1 : ((l > r) ? 1 : 0) + +const sortFrameworksBeforeOtherPlugins = (l, r) => { + const priorities = (packageConfig.govukPluginFrameworks || []); + const rightIndex = priorities.indexOf(r) + const leftIndex = priorities.indexOf(l) + const rightIsPriority = rightIndex !== -1 + const leftIsPriority = leftIndex !== -1 + + if (rightIsPriority && leftIsPriority) { + return leftIndex > rightIndex + } + if (leftIsPriority && !rightIsPriority) { + return -1 + } + if (!leftIsPriority && rightIsPriority) { + return 1 + } + return alphabeticSort(l, r) +} + +const getList = (hookType, editFn = noEditFn) => flatten( + Object.keys(packageConfig.dependencies || {}) + .filter(packageName => (hookFileExists(packageName)) || defaultConfigs.hasOwnProperty(packageName)) + .sort(sortFrameworksBeforeOtherPlugins) + .map(packageName => editFn(lookupConfig(packageName)[hookType] || [], packageName)) +) + +const generateServersideAndAssetPaths = generatePublicPath => (itemsInPackage, packageName) => itemsInPackage + .map(item => ({ + filesystemPath: scopeFilePathToModule(item, packageName), + publicPath: generatePublicPath(item, packageName) + })) + +const addLeadingSlash = item => item.startsWith('/') ? item : `/${item}` + +const publicPathGenerator = (item, packageName) => ['', 'plugin-assets', packageName].map(encodeURIComponent) + .join('/') + addLeadingSlash(item) + +const scopeFilePathToModule = (item, packageName) => pathJoinFromRoot('node_modules', packageName, item) + +const iterateItems = processor => (items, packageName) => items.map(item => processor(item, packageName)) + +const transform = { + scopeFilePathsToModule: iterateItems(scopeFilePathToModule), + publicAssetPaths: iterateItems(publicPathGenerator), + + filesystemPathAndPublicAssetPaths: generateServersideAndAssetPaths(publicPathGenerator), + filesystemPathAndGlobalAssetPaths: generateServersideAndAssetPaths(_ => '/assets') +} + +module.exports = { + getList, + transform +} diff --git a/package.json b/package.json index cf3f99f9bc..3948c1ae5f 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,9 @@ "universal-analytics": "^0.4.16", "uuid": "^3.2.1" }, + "govukPluginFrameworks": [ + "govuk-frontend" + ], "greenkeeper": { "ignore": [ "nunjucks" diff --git a/server.js b/server.js index f00a2884d9..6a3c2fdcae 100644 --- a/server.js +++ b/server.js @@ -17,6 +17,7 @@ const documentationRoutes = require('./docs/documentation_routes.js') const packageJson = require('./package.json') const routes = require('./app/routes.js') const utils = require('./lib/utils.js') +const pluginDetection = require('./lib/plugin-detection.js') // Variables for v6 backwards compatibility // Set false by default, then turn on if we find /app/v6/routes.js @@ -87,12 +88,10 @@ if (env === 'production' && useAuth === 'true') { } // Set up App -var appViews = [ - path.join(__dirname, '/node_modules/govuk-frontend/'), - path.join(__dirname, '/node_modules/govuk-frontend/components'), +var appViews = pluginDetection.getList('nunjucksDirs', pluginDetection.transform.scopeFilePathsToModule).reverse().concat([ path.join(__dirname, '/app/views/'), path.join(__dirname, '/lib/') -] +]) var nunjucksAppEnv = nunjucks.configure(appViews, { autoescape: true, @@ -109,19 +108,33 @@ app.set('view engine', 'html') // Middleware to serve static assets app.use('/public', express.static(path.join(__dirname, '/public'))) -app.use('/assets', express.static(path.join(__dirname, 'node_modules', 'govuk-frontend', 'assets'))) + +app.use((req, res, next) => { + res.locals = res.locals || {} + Object.assign(res.locals, { + pluginConfig: { + scripts: pluginDetection.getList('scripts', pluginDetection.transform.publicAssetPaths), + stylesheets: pluginDetection.getList('stylesheets', pluginDetection.transform.publicAssetPaths) + } + }) + next() +}) // Serve govuk-frontend in /public -app.use('/node_modules/govuk-frontend', express.static(path.join(__dirname, '/node_modules/govuk-frontend'))) +pluginDetection.getList('scripts', pluginDetection.transform.filesystemPathAndPublicAssetPaths) + .concat(pluginDetection.getList('stylesheets', pluginDetection.transform.filesystemPathAndPublicAssetPaths)) + .concat(pluginDetection.getList('assets', pluginDetection.transform.filesystemPathAndPublicAssetPaths).reverse()) + .concat(pluginDetection.getList('globalAssets', pluginDetection.transform.filesystemPathAndGlobalAssetPaths).reverse()) + .forEach(paths => { + app.use(paths.publicPath, express.static(paths.filesystemPath)) + }) // Set up documentation app if (useDocumentation) { - var documentationViews = [ - path.join(__dirname, '/node_modules/govuk-frontend/'), - path.join(__dirname, '/node_modules/govuk-frontend/components'), + var documentationViews = pluginDetection.getList('nunjucksDirs', pluginDetection.transform.scopeFilePathsToModule).concat([ path.join(__dirname, '/docs/views/'), path.join(__dirname, '/lib/') - ] + ]) var nunjucksDocumentationEnv = nunjucks.configure(documentationViews, { autoescape: true,