diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index d16e5babb..62936d753 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -77,6 +77,49 @@ jobs: name: Javascript unit tests coverage path: coverage + exports: + name: Package ${{ matrix.conditions }} + runs-on: ubuntu-latest + needs: [build] + + strategy: + fail-fast: false + + matrix: + conditions: + - require + - import + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Restore dependencies (from cache) + uses: actions/cache/restore@v4 + with: + key: npm-install-${{ hashFiles('package-lock.json') }} + path: node_modules + + - name: Restore build (from cache) + uses: actions/cache/restore@v4 + with: + path: dist/ + key: build-${{ github.sha }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + + - name: Resolve entry path + run: | + node --eval "console.log(require.resolve('nhsuk-frontend'))" --conditions ${{ matrix.conditions }} + node --eval "console.log(require.resolve('nhsuk-frontend/package.json'))" --conditions ${{ matrix.conditions }} + node --eval "console.log(require.resolve('nhsuk-frontend/packages/nhsuk'))" --conditions ${{ matrix.conditions }} + node --eval "console.log(require.resolve('nhsuk-frontend/packages/nhsuk.js'))" --conditions ${{ matrix.conditions }} + node --eval "console.log(require.resolve('nhsuk-frontend/packages/components/button/button'))" --conditions ${{ matrix.conditions }} + node --eval "console.log(require.resolve('nhsuk-frontend/packages/components/button/button.js'))" --conditions ${{ matrix.conditions }} + regression: name: Visual regression tests runs-on: macos-latest diff --git a/.github/workflows/sass.yml b/.github/workflows/sass.yml index e81e4f1d1..70d64c470 100644 --- a/.github/workflows/sass.yml +++ b/.github/workflows/sass.yml @@ -7,6 +7,14 @@ jobs: name: Dart Sass ${{ matrix.package-version }} runs-on: ubuntu-latest + env: + RULE_1: '@import "nhsuk";' + RULE_2: '@use "nhsuk" as *;' + RULE_3: '@forward "nhsuk";' + RULE_4: '@forward "node_modules/nhsuk-frontend/packages/nhsuk";' + RULE_5: '@forward "node_modules/nhsuk-frontend/packages/core/all";' + RULE_6: '@forward "pkg:nhsuk-frontend";' + strategy: fail-fast: false @@ -35,20 +43,35 @@ jobs: - name: Install package run: | + npm install --omit=dev npm install -g sass@${{ matrix.package-version }} sass --version - - name: Check compilation + - name: Create test files run: | mkdir -p .tmp - time sass packages/nhsuk.scss > .tmp/check.css + echo '${{ env.RULE_1 }}' > .tmp/input1.scss + echo '${{ env.RULE_2 }}' > .tmp/input2.scss + echo '${{ env.RULE_3 }}' > .tmp/input3.scss + echo '${{ env.RULE_4 }}' > .tmp/input4.scss + echo '${{ env.RULE_5 }}' > .tmp/input5.scss + echo '${{ env.RULE_6 }}' > .tmp/input6.scss + + - name: Check compilation + run: | + time sass packages/nhsuk.scss > .tmp/check.css --load-path . - name: Check load paths run: | - mkdir -p .tmp - time sh -c 'echo "@import "\""nhsuk"\"";" | sass --stdin --load-path packages > .tmp/check1.css' - time sh -c 'echo "@forward "\""nhsuk"\"";" | sass --stdin --load-path packages > .tmp/check2.css' - time sh -c 'echo "@use "\""nhsuk"\"" as *;" | sass --stdin --load-path packages > .tmp/check3.css' + time sass .tmp/input1.scss > .tmp/check1.css --load-path packages + time sass .tmp/input2.scss > .tmp/check2.css --load-path packages + time sass .tmp/input3.scss > .tmp/check3.css --load-path packages + time sass .tmp/input4.scss > .tmp/check3.css --load-path . + time sass .tmp/input5.scss > .tmp/check3.css --load-path . + + - name: Check package importer + run: | + time sass .tmp/input6.scss > .tmp/check6.css --pkg-importer node # Check output for uncompiled Sass - name: Check output @@ -57,3 +80,6 @@ jobs: ! grep "\$nhsuk-" .tmp/check1.css ! grep "\$nhsuk-" .tmp/check2.css ! grep "\$nhsuk-" .tmp/check3.css + ! grep "\$nhsuk-" .tmp/check4.css + ! grep "\$nhsuk-" .tmp/check5.css + ! grep "\$nhsuk-" .tmp/check6.css diff --git a/docs/installation/installing-with-npm.md b/docs/installation/installing-with-npm.md index f8c9bf90c..f330d06c5 100644 --- a/docs/installation/installing-with-npm.md +++ b/docs/installation/installing-with-npm.md @@ -39,7 +39,7 @@ To build the stylesheet you will need a pipeline set up to compile [Sass](https: You need to import the NHS.UK frontend styles into the main Sass file in your project. You should place the below code before your own Sass rules (or Sass imports). ```scss -@import "node_modules/nhsuk-frontend/packages/nhsuk"; +@import "node_modules/nhsuk-frontend"; ``` Alternatively you can import each of the individual components separately, meaning you can import just the components that you are using. @@ -82,30 +82,32 @@ You might wish to copy the file into your project or reference it straight from ``` -### Option 2: Import JavaScript using modules +### Option 2: Import JavaScript using a bundler -If you're using a transpiler or bundler such as [Babel](https://babeljs.io/) or [Webpack](https://webpack.js.org/), you can use the ES6 import syntax to import components via modules into your main Javascript file. +We encourage the use of ECMAScript (ES) modules, but you should check your bundler does not unnecessarily downgrade modern JavaScript for unsupported browsers. + +If you decide to import using a bundler like [Rollup](https://rollupjs.org/) or [webpack](https://webpack.js.org/), import and run the `initAll` function to initialise NHS.UK frontend: + +```js +import { initAll } from 'nhsuk-frontend' +initAll() +``` + +#### Initialise individual components + +Rather than using `initAll`, you can initialise individual components used by your service. For example: ```js -// Components -import Checkboxes from 'nhsuk-frontend/packages/components/checkboxes/checkboxes.js'; -import Details from 'nhsuk-frontend/packages/components/details/details.js'; -import ErrorSummary from 'nhsuk-frontend/packages/components/error-summary/error-summary.js'; -import Header from 'nhsuk-frontend/packages/components/header/header.js'; -import Radios from 'nhsuk-frontend/packages/components/radios/radios.js'; -import SkipLink from 'nhsuk-frontend/packages/components/skip-link/skip-link.js'; +import initRadios from 'nhsuk-frontend/packages/components/radios/radios.js'; +import initSkipLink from 'nhsuk-frontend/packages/components/skip-link/skip-link.js'; // Polyfills import 'nhsuk-frontend/packages/polyfills.js'; -// Initialize components +// Initialise components document.addEventListener('DOMContentLoaded', () => { - Checkboxes(); - Details(); - ErrorSummary(); - Header(); - Radios(); - SkipLink(); + initRadios(); + initSkipLink(); }); ``` diff --git a/jest.config.js b/jest.config.js index d1eddd72b..c5771e331 100644 --- a/jest.config.js +++ b/jest.config.js @@ -34,14 +34,20 @@ module.exports = { projects: [ { ...config, - displayName: 'JSDom', + displayName: 'JavaScript unit tests', + testEnvironment: 'node', + testMatch: ['/**/*.unit.test.{js,mjs}'] + }, + { + ...config, + displayName: 'JavaScript behaviour tests', setupFilesAfterEnv: ['/jest.setup.js'], testEnvironment: 'jsdom', testMatch: ['/**/*.jsdom.test.{js,mjs}'] }, { ...config, - displayName: 'Pupppeteer', + displayName: 'JavaScript component tests', setupFilesAfterEnv: ['/jest.setup.js'], testEnvironment: 'jest-environment-puppeteer', testMatch: ['/**/*.puppeteer.test.{js,mjs}'], diff --git a/package-lock.json b/package-lock.json index e7adbc79d..575a738b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "nhsuk-frontend", "version": "9.4.1", "license": "MIT", + "workspaces": [ + "." + ], "devDependencies": { "@babel/core": "^7.26.10", "@babel/preset-env": "^7.26.9", @@ -46,6 +49,7 @@ "html-validate": "^9.5.3", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "jest-environment-node": "^29.7.0", "jest-environment-puppeteer": "^11.0.0", "jest-puppeteer": "^11.0.0", "nunjucks": "^3.2.4", @@ -11544,6 +11548,7 @@ "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -13093,6 +13098,10 @@ "node": ">= 0.4.0" } }, + "node_modules/nhsuk-frontend": { + "resolved": "", + "link": true + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", diff --git a/package.json b/package.json index 01d0a2d73..f80798008 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,25 @@ "engines": { "node": "^20.9.0 || ^22.11.0" }, + "workspaces": [ + "." + ], + "exports": { + ".": { + "sass": "./packages/index.scss", + "default": "./packages/index.js" + }, + "./*": "./*", + "./packages/*": { + "sass": "./packages/*.scss", + "default": "./packages/*.js" + }, + "./packages/*.js": "./packages/*.js", + "./packages/*.scss": "./packages/*.scss", + "./package.json": "./package.json" + }, + "main": "packages/index.js", + "sass": "packages/index.scss", "scripts": { "install:playwright": "playwright install chromium --with-deps --only-shell", "install:puppeteer": "puppeteer browsers install", @@ -68,6 +87,7 @@ "html-validate": "^9.5.3", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "jest-environment-node": "^29.7.0", "jest-environment-puppeteer": "^11.0.0", "jest-puppeteer": "^11.0.0", "nunjucks": "^3.2.4", diff --git a/packages/components/character-count/character-count.js b/packages/components/character-count/character-count.js index 547cde468..db26771ec 100644 --- a/packages/components/character-count/character-count.js +++ b/packages/components/character-count/character-count.js @@ -7,7 +7,7 @@ class CharacterCount { this.lastInputTimestamp = null } - // Initialize component + // Initialise component init() { // Check that required elements are present if (!this.$textarea) { diff --git a/packages/index.js b/packages/index.js new file mode 100644 index 000000000..778ebabe3 --- /dev/null +++ b/packages/index.js @@ -0,0 +1,47 @@ +/* eslint-disable import/prefer-default-export */ + +// Components +const initButton = require('./components/button/button') +const initCharacterCount = require('./components/character-count/character-count') +const initCheckboxes = require('./components/checkboxes/checkboxes') +const initDetails = require('./components/details/details') +const initErrorSummary = require('./components/error-summary/error-summary') +const initHeader = require('./components/header/header') +const initRadios = require('./components/radios/radios') +const initSkipLink = require('./components/skip-link/skip-link') +const initTabs = require('./components/tabs/tabs') + +require('./polyfills.js') + +/** + * Use this function to initialise nhsuk-frontend components within a + * given scope. This function is called by default with the document + * element, but you can call it again later with a new DOM element + * containing nhsuk-frontend components which you wish to initialise. + * + * @param {HTMLElement} scope + */ +function initAll(scope) { + initHeader() + initSkipLink() + initButton({ scope }) + initCharacterCount({ scope }) + initCheckboxes({ scope }) + initDetails({ scope }) + initErrorSummary({ scope }) + initRadios({ scope }) + initTabs({ scope }) +} + +module.exports = { + initButton, + initCharacterCount, + initCheckboxes, + initDetails, + initErrorSummary, + initHeader, + initRadios, + initSkipLink, + initTabs, + initAll +} diff --git a/packages/index.jsdom.test.mjs b/packages/index.jsdom.test.mjs new file mode 100644 index 000000000..6bc3b6a0e --- /dev/null +++ b/packages/index.jsdom.test.mjs @@ -0,0 +1,76 @@ +import { + initAll, + initButton, + initCharacterCount, + initCheckboxes, + initDetails, + initErrorSummary, + initHeader, + initRadios, + initSkipLink, + initTabs +} from './index.js' +import * as NHSUKFrontend from './index.js' + +jest.mock('./components/button/button.js') +jest.mock('./components/character-count/character-count.js') +jest.mock('./components/checkboxes/checkboxes.js') +jest.mock('./components/details/details.js') +jest.mock('./components/error-summary/error-summary.js') +jest.mock('./components/header/header.js') +jest.mock('./components/radios/radios.js') +jest.mock('./components/skip-link/skip-link.js') +jest.mock('./components/tabs/tabs.js') + +describe('NHS.UK frontend', () => { + describe('Exports', () => { + it('should export init all function', () => { + expect(NHSUKFrontend).toHaveProperty('initAll') + }) + + it('should export init component functions', () => { + expect(NHSUKFrontend).toHaveProperty('initButton') + expect(NHSUKFrontend).toHaveProperty('initCharacterCount') + expect(NHSUKFrontend).toHaveProperty('initCheckboxes') + expect(NHSUKFrontend).toHaveProperty('initDetails') + expect(NHSUKFrontend).toHaveProperty('initErrorSummary') + expect(NHSUKFrontend).toHaveProperty('initHeader') + expect(NHSUKFrontend).toHaveProperty('initRadios') + expect(NHSUKFrontend).toHaveProperty('initSkipLink') + expect(NHSUKFrontend).toHaveProperty('initTabs') + }) + }) + + describe('initAll', () => { + it('should init components', () => { + initAll() + + expect(initButton).toHaveBeenCalled() + expect(initCharacterCount).toHaveBeenCalled() + expect(initCheckboxes).toHaveBeenCalled() + expect(initDetails).toHaveBeenCalled() + expect(initErrorSummary).toHaveBeenCalled() + expect(initHeader).toHaveBeenCalled() + expect(initRadios).toHaveBeenCalled() + expect(initSkipLink).toHaveBeenCalled() + expect(initTabs).toHaveBeenCalled() + }) + + it('should init components (with scope)', () => { + const scope = document + + initAll(scope) + + expect(initHeader).toHaveBeenCalled() + expect(initSkipLink).toHaveBeenCalled() + + expect(initButton).toHaveBeenCalledWith({ scope }) + expect(initCharacterCount).toHaveBeenCalledWith({ scope }) + expect(initCheckboxes).toHaveBeenCalledWith({ scope }) + expect(initDetails).toHaveBeenCalledWith({ scope }) + expect(initErrorSummary).toHaveBeenCalledWith({ scope }) + expect(initRadios).toHaveBeenCalledWith({ scope }) + expect(initTabs).toHaveBeenCalledWith({ scope }) + }) + }) +}) diff --git a/packages/index.scss b/packages/index.scss new file mode 100644 index 000000000..a4a7eb39a --- /dev/null +++ b/packages/index.scss @@ -0,0 +1,42 @@ +// Core +@import "core/all"; + +// Components +@import "components/action-link/action-link"; +@import "components/back-link/back-link"; +@import "components/breadcrumb/breadcrumb"; +@import "components/button/button"; +@import "components/card/card"; +@import "components/contents-list/contents-list"; +@import "components/date-input/date-input"; +@import "components/details/details"; +@import "components/do-dont-list/do-dont-list"; +@import "components/error-message/error-message"; +@import "components/error-summary/error-summary"; +@import "components/fieldset/fieldset"; +@import "components/footer/footer"; +@import "components/header/header"; +@import "components/header/header-organisation"; +@import "components/header/header-service"; +@import "components/header/header-transactional"; +@import "components/header/header-white"; +@import "components/hero/hero"; +@import "components/hint/hint"; +@import "components/images/images"; +@import "components/input/input"; +@import "components/inset-text/inset-text"; +@import "components/label/label"; +@import "components/pagination/pagination"; +@import "components/panel/panel"; +@import "components/checkboxes/checkboxes"; +@import "components/radios/radios"; +@import "components/select/select"; +@import "components/skip-link/skip-link"; +@import "components/summary-list/summary-list"; +@import "components/tables/tables"; +@import "components/tag/tag"; +@import "components/task-list/task-list"; +@import "components/textarea/textarea"; +@import "components/warning-callout/warning-callout"; +@import "components/character-count/character-count"; +@import "components/tabs/tabs"; diff --git a/packages/nhsuk.js b/packages/nhsuk.js index 6f2bf18b4..ed6c1d91c 100644 --- a/packages/nhsuk.js +++ b/packages/nhsuk.js @@ -1,44 +1,4 @@ -/* eslint-disable import/prefer-default-export */ +const { initAll } = require('.') -// Components -const initButton = require('./components/button/button') -const initCharacterCount = require('./components/character-count/character-count') -const initCheckboxes = require('./components/checkboxes/checkboxes') -const initDetails = require('./components/details/details') -const initErrorSummary = require('./components/error-summary/error-summary') -const initHeader = require('./components/header/header') -const initRadios = require('./components/radios/radios') -const initSkipLink = require('./components/skip-link/skip-link') -const initTabs = require('./components/tabs/tabs') - -require('./polyfills.js') - -/** - * Use this function to initialise nhsuk-frontend components within a - * given scope. This function is called by default with the document - * element, but you can call it again later with a new DOM element - * containing nhsuk-frontend components which you wish to initialise. - * - * @param {HTMLElement} scope - */ -function initAll(scope) { - initButton({ scope }) - initCharacterCount({ scope }) - initCheckboxes({ scope }) - initDetails({ scope }) - initErrorSummary({ scope }) - initRadios({ scope }) - initTabs({ scope }) -} - -// Initialize components -document.addEventListener('DOMContentLoaded', () => { - initHeader() - initSkipLink() - - initAll(document) -}) - -module.exports = { - initAll -} +// Initialise components +document.addEventListener('DOMContentLoaded', () => initAll(document)) diff --git a/packages/nhsuk.scss b/packages/nhsuk.scss index a4a7eb39a..45910f53d 100644 --- a/packages/nhsuk.scss +++ b/packages/nhsuk.scss @@ -1,42 +1 @@ -// Core -@import "core/all"; - -// Components -@import "components/action-link/action-link"; -@import "components/back-link/back-link"; -@import "components/breadcrumb/breadcrumb"; -@import "components/button/button"; -@import "components/card/card"; -@import "components/contents-list/contents-list"; -@import "components/date-input/date-input"; -@import "components/details/details"; -@import "components/do-dont-list/do-dont-list"; -@import "components/error-message/error-message"; -@import "components/error-summary/error-summary"; -@import "components/fieldset/fieldset"; -@import "components/footer/footer"; -@import "components/header/header"; -@import "components/header/header-organisation"; -@import "components/header/header-service"; -@import "components/header/header-transactional"; -@import "components/header/header-white"; -@import "components/hero/hero"; -@import "components/hint/hint"; -@import "components/images/images"; -@import "components/input/input"; -@import "components/inset-text/inset-text"; -@import "components/label/label"; -@import "components/pagination/pagination"; -@import "components/panel/panel"; -@import "components/checkboxes/checkboxes"; -@import "components/radios/radios"; -@import "components/select/select"; -@import "components/skip-link/skip-link"; -@import "components/summary-list/summary-list"; -@import "components/tables/tables"; -@import "components/tag/tag"; -@import "components/task-list/task-list"; -@import "components/textarea/textarea"; -@import "components/warning-callout/warning-callout"; -@import "components/character-count/character-count"; -@import "components/tabs/tabs"; +@import ".";