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 b5945f6bc3..5feaa28271 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 e037943384..0d0bffbb4a 100644
--- a/server.js
+++ b/server.js
@@ -20,6 +20,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
@@ -88,12 +89,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,
@@ -110,19 +109,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,