Frontend Build Best Practices
This document outlines best practices for frontend build processes in Digital Land projects, based on the approach used in config-manager.
Architecture
Build Tool Organization
Use nps (npm-package-scripts) to organize build scripts in JavaScript files rather than cluttering package.json:
// package-scripts.js
module.exports = {
scripts: {
build: {
stylesheets: 'sass src/scss:output/stylesheets',
javascripts: 'rollup -c',
},
copy: {
assets: 'copyfiles ...',
all: 'nps copy.assets copy.images'
}
}
};
Benefits:
- Better organization for complex build processes
- JavaScript logic for conditional builds
- Easier to extend and override base configurations
- Comments and documentation inline with scripts
Tool Selection
SASS/SCSS for stylesheets:
- Industry standard for CSS preprocessing
- Compatible with GOV.UK Frontend
- Variables, mixins, and imports for maintainable styles
Rollup for JavaScript bundling:
- Lightweight and focused
- Tree-shaking for smaller bundles
- Simple configuration
- Good for library-style code (vs application frameworks)
copyfiles for asset management:
- Cross-platform file copying
- Glob pattern support
- Path manipulation with
-uflag (strip path segments) - Handles nested directory structures reliably
Project Structure
Source vs Output
Keep clear separation between source and generated files:
src/ scss/ # Source SCSS files javascripts/ # Source JS files application/static/ # Generated files (git-ignored) stylesheets/ # Compiled CSS javascripts/ # Bundled JS images/ # Copied images govuk/ # GOV.UK Frontend assets
Rules:
- Never commit generated files
- Git-ignore entire
application/static/directory - Source files in
src/, output inapplication/static/ - Make it obvious which is which
Configuration Files
package.json:
- Keep scripts simple and readable
- Delegate to nps for complex builds
{
"scripts": {
"build": "nps build.stylesheets build.javascripts",
"watch": "npm-run-all --parallel watch:*"
}
}
package-scripts.js:
- Extend shared base scripts
- Override only what’s necessary
- Document why overrides exist
// Override govukAssets to fix v5 compatibility
govukAssets: 'npx copyfiles -u 4 "node_modules/govuk-frontend/dist/govuk/assets/**" application/static/govuk/'
Project-specific config file:
// digital-land-frontend.config.json
{
"scssPath": "./src/scss",
"jsOutputPath": "application/static/javascripts",
"stylesheetsOutputPath": "application/static/stylesheets"
}
Shared Build Scripts
Important: All Digital Land projects must use @planning-data/digital-land-frontend as the single source of shared build scripts. Do not create additional shared build modules - consolidate all shared functionality in digital-land-frontend.
Contributing to digital-land-frontend
When adding or updating shared build scripts in @planning-data/digital-land-frontend:
Do:
- Export nps scripts that projects can extend
- Make paths configurable via config file
- Provide sensible defaults
- Document breaking changes in GOV.UK Frontend versions
- Keep dependencies up to date
- Test changes across multiple consuming projects
- Coordinate updates with team
Don’t:
- Hardcode paths that assume specific project structure
- Make assumptions about framework (Flask, Django, Express)
- Include framework-specific code
- Make breaking changes without migration guide
Using digital-land-frontend in Projects
When consuming digital-land-frontend build scripts in a project:
Do:
- Extend, don’t replace the base scripts
- Override only what’s necessary for your project
- Document why overrides exist
- Keep the shared package version in sync across projects
- Test after updating shared package versions
Don’t:
- Duplicate scripts from the shared package
- Fork the shared package for small changes
- Skip postinstall hooks that run builds
Example override pattern:
const dlFrontendScripts = require('@planning-data/digital-land-frontend/package-scripts.js');
module.exports = {
scripts: {
...dlFrontendScripts.scripts,
copy: {
...dlFrontendScripts.scripts.copy,
// Project-specific override with explanation
govukAssets: '...'
}
}
};
Build Process Order
Build tasks must run in the correct order for the build to succeed:
Standard Build Sequence
1. Copy assets 2. Build stylesheets (SCSS → CSS) 3. Build javascripts (bundle custom JS)
Why this order matters:
- Copy first: Vendor assets (GOV.UK Frontend, fonts, images) must be in place before compilation
- Stylesheets second: SCSS may reference copied assets (fonts, images) via
url()paths - JavaScript last: JS bundles may import stylesheets or reference copied assets
Implementation
In package.json:
{
"scripts": {
"copy": "nps copy.all",
"build:css": "nps build.stylesheets",
"build:js": "nps build.javascripts",
"build": "npm run copy && npm run build:css && npm run build:js"
},
"postinstall": "npm run build"
}
Key points:
- Use
&&to run sequentially (not&which runs in parallel) - Each step depends on the previous completing successfully
- Postinstall hook ensures fresh checkouts are built automatically
Watch Mode Exception
During development, watch tasks can run in parallel since initial build is already complete:
{
"watch:assets": "nps watch.assets",
"watch:css": "nps watch.stylesheets",
"watch:js": "nps watch.javascripts",
"watch": "npm-run-all --parallel watch:*"
}
The initial npm install runs the sequential build, then watch mode monitors for changes.
Build Process Integration
Development Workflow
Provide watch commands for rapid development:
{
"watch:assets": "nps watch.assets",
"watch:pages": "flask run --reload",
"watch": "npm-run-all --parallel watch:*"
}
Run both frontend build watching and backend server in parallel.
Production Workflow
Use postinstall hooks to ensure assets are built:
{
"postinstall": "npm run copy && npm run build"
}
This ensures npm install always generates required assets.
Clean Targets
Provide clean commands to reset to fresh state:
clean:
rm -rf node_modules
rm -rf application/static/
Don’t delete and recreate Python venvs during clean (slows developer workflow).
GOV.UK Frontend Integration
Version Compatibility
GOV.UK Frontend v4+ breaking change:
- v3 and earlier: Assets at
node_modules/govuk-frontend/govuk/assets/ - v4+: Assets at
node_modules/govuk-frontend/dist/govuk/assets/
When copying assets, adjust path stripping accordingly:
- v3:
copyfiles -u 2stripsnode_modules/govuk-frontend - v5:
copyfiles -u 4stripsnode_modules/govuk-frontend/dist/govuk
Document this in your package-scripts.js comments.
Asset Organization
SCSS expects govuk assets at specific paths:
// SCSS imports expect:
@font-face {
src: url("/static/govuk/assets/fonts/...");
}
Ensure your copy scripts put files where SCSS expects them.
JavaScript Bundles
GOV.UK Frontend v5 provides pre-bundled JavaScript:
cp node_modules/govuk-frontend/dist/govuk/all.bundle.js application/static/javascripts/govuk/
No need to bundle it yourself with Rollup.
Vendor Assets
Third-Party Libraries
For libraries not in npm (or old versions):
- Keep in
src/javascripts/vendor/orsrc/css/vendor/ - Copy to static directory during build
- Document source and version in comments
For modern npm packages:
- Install via npm
- Copy dist files during build
copy: {
accessibleAutocomplete: 'cp node_modules/accessible-autocomplete/dist/* application/static/javascripts/'
}
Ministry of Justice Components
MoJ frontend components should be:
- Installed via npm if available
- Copied to vendor directory
- Version tracked in package.json
Common Pitfalls
Path Issues
Problem: Assets copied to wrong location, SCSS can’t find them.
Solution:
- Check SCSS import paths
- Verify copyfiles
-uflag strips correct number of segments - Test that files end up where templates/SCSS expect
Duplicate Dependencies
Problem: Multiple versions of digital-land-frontend or duplicate packages.
Solution:
- Use consistent package names across repos
- Pin versions in package.json
- Regular audits:
npm ls digital-land-frontend
Missing Assets After Updates
Problem: New GOV.UK Frontend version, assets in different location.
Solution:
- Check GOV.UK Frontend release notes for breaking changes
- Update copyfiles commands
- Test thoroughly after version bumps
- Document version-specific quirks
Build Not Running
Problem: Fresh checkout, assets missing.
Solution:
- Ensure postinstall hook runs build
- Document build requirements in README
- Provide
make initthat runs npm install
Documentation Requirements
Every project using frontend builds should document:
- What tools are used (SASS, Rollup, etc.)
- Common commands (build, watch, copy)
- Where generated files go
- That generated files are git-ignored
- Any project-specific customizations
- How to add new JS/CSS files
Example README section: config-manager/README.md
Future Considerations
Vite Migration
Modern alternative to current multi-tool setup:
Benefits:
- Single tool replaces SASS + Rollup + copyfiles
- Hot Module Replacement (HMR) for instant feedback
- Faster builds
- Better developer experience
Trade-offs:
- Migration effort
- Team learning curve
- Additional configuration for Flask integration
When to consider:
- Starting new projects
- Major refactoring of existing projects
- Team wants modern DX improvements
Current setup (nps + SASS + Rollup) is production-ready and well-understood across Digital Land projects. Vite is an optimization, not a requirement.
Version Control
What to Commit
Do commit:
- Source files (
src/) - Build configuration (
package-scripts.js,rollup.config.js) - Package manifests (
package.json,package-lock.json) - Build tool configs (
.sassrc, etc.)
Don’t commit:
- Generated files (
application/static/) - Node modules (
node_modules/) - Build artifacts
- Temporary files
.gitignore
# Generated static assets application/static/ # Dependencies node_modules/
Be aggressive - ignore entire directories, not individual files within them.
Summary
Key principles:
- Separate source from generated files
- Use shared scripts with project-specific overrides
- Document why overrides exist
- Automate builds with postinstall hooks
- Keep it simple - don’t over-engineer
- Test after dependency updates
- Git-ignore all generated files
The goal is a build process that:
- Works reliably across environments
- Is easy for new developers to understand
- Requires minimal manual intervention
- Stays consistent across Digital Land projects