Skip to content

Add package entry points #1261

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 32 additions & 6 deletions .github/workflows/sass.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
36 changes: 19 additions & 17 deletions docs/installation/installing-with-npm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -82,30 +82,32 @@ You might wish to copy the file into your project or reference it straight from
</head>
```

### 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();
});
```

Expand Down
10 changes: 8 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,20 @@ module.exports = {
projects: [
{
...config,
displayName: 'JSDom',
displayName: 'JavaScript unit tests',
testEnvironment: 'node',
testMatch: ['<rootDir>/**/*.unit.test.{js,mjs}']
},
{
...config,
displayName: 'JavaScript behaviour tests',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testEnvironment: 'jsdom',
testMatch: ['<rootDir>/**/*.jsdom.test.{js,mjs}']
},
{
...config,
displayName: 'Pupppeteer',
displayName: 'JavaScript component tests',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testEnvironment: 'jest-environment-puppeteer',
testMatch: ['<rootDir>/**/*.puppeteer.test.{js,mjs}'],
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/components/character-count/character-count.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class CharacterCount {
this.lastInputTimestamp = null
}

// Initialize component
// Initialise component
init() {
// Check that required elements are present
if (!this.$textarea) {
Expand Down
47 changes: 47 additions & 0 deletions packages/index.js
Original file line number Diff line number Diff line change
@@ -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
}
76 changes: 76 additions & 0 deletions packages/index.jsdom.test.mjs
Original file line number Diff line number Diff line change
@@ -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 })
})
})
})
Loading