@hmlr/govuk-react-components-library 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ export { default as PasswordInput } from "./PasswordInput";
2
+ export * from "./PasswordInput.config";
@@ -16,6 +16,7 @@ export * from "./DifferenceNavigation";
16
16
  export * from "./ErrorBoundary";
17
17
  export * from "./ErrorMessage";
18
18
  export * from "./ErrorSummary";
19
+ export * from "./ExitThisPage";
19
20
  export * from "./Fieldset";
20
21
  export * from "./FileUpload";
21
22
  export * from "./Footer";
@@ -32,6 +33,7 @@ export * from "./NotFoundPage";
32
33
  export * from "./NotificationBanner";
33
34
  export * from "./Pagination";
34
35
  export * from "./Panel";
36
+ export * from "./PasswordInput";
35
37
  export * from "./PDFViewer";
36
38
  export * from "./PhaseBanner";
37
39
  export * from "./ProblemWithService";
@@ -1,10 +1,15 @@
1
1
  import React from "react";
2
+ type RefAcceptingComponent = React.ComponentType<Record<string, unknown> & {
3
+ ref?: React.Ref<HTMLElement>;
4
+ }>;
2
5
  export interface WithRefProps {
3
- Component: React.ComponentType<unknown>;
6
+ Component: RefAcceptingComponent;
7
+ items?: unknown[];
4
8
  [key: string]: unknown;
5
9
  }
6
10
  export interface InfoSectionProps {
7
11
  refName: string;
8
- Component: React.ComponentType<unknown>;
12
+ Component: RefAcceptingComponent;
9
13
  restProps: Record<string, unknown>;
10
14
  }
15
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmlr/govuk-react-components-library",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "These are common component use for React applications based on GDS and govuk-frontend",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.esm.js",
@@ -30,7 +30,10 @@
30
30
  "prepare": "husky",
31
31
  "release": "standard-version",
32
32
  "uml": "tsuml2 --glob './src/**/\\!(*.spec|*.test|TestUtilities|JestSetup).[t]s?(x)' -o './assets/uml-diagram.svg'",
33
- "chromatic": "npx chromatic --exit-zero-on-changes"
33
+ "chromatic": "npx chromatic --exit-zero-on-changes",
34
+ "generate-stories": "node scripts/generateStories.mjs",
35
+ "prestorybook": "npm run generate-stories",
36
+ "prebuild-storybook": "npm run generate-stories"
34
37
  },
35
38
  "repository": {
36
39
  "type": "git",
@@ -51,89 +54,90 @@
51
54
  "author": "Cyril Adelekan",
52
55
  "license": "MIT",
53
56
  "devDependencies": {
54
- "@babel/core": "^7.28.4",
55
- "@babel/preset-env": "^7.28.3",
56
- "@babel/preset-react": "^7.27.1",
57
- "@babel/preset-typescript": "^7.27.1",
58
- "@chromatic-com/storybook": "^4.1.1",
59
- "@commitlint/cli": "^20.1.0",
60
- "@commitlint/config-conventional": "^20.0.0",
57
+ "@babel/core": "^7.29.0",
58
+ "@babel/preset-env": "^7.29.0",
59
+ "@babel/preset-react": "^7.28.5",
60
+ "@babel/preset-typescript": "^7.28.5",
61
+ "@chromatic-com/storybook": "^5.0.1",
62
+ "@commitlint/cli": "^20.4.4",
63
+ "@commitlint/config-conventional": "^20.4.4",
61
64
  "@eslint/js": "^9.37.0",
62
- "@jest/globals": "^30.2.0",
63
- "@rollup/plugin-commonjs": "^28.0.6",
65
+ "@jest/globals": "^30.3.0",
66
+ "@rollup/plugin-commonjs": "^29.0.2",
64
67
  "@rollup/plugin-json": "^6.1.0",
65
- "@rollup/plugin-node-resolve": "^16.0.2",
66
- "@rollup/plugin-terser": "^0.4.4",
67
- "@rollup/plugin-typescript": "^12.1.4",
68
- "@storybook/addon-a11y": "^9.1.10",
69
- "@storybook/addon-links": "^9.1.10",
70
- "@storybook/addon-styling-webpack": "^2.0.0",
71
- "@storybook/addon-webpack5-compiler-swc": "^4.0.1",
72
- "@storybook/react-vite": "^9.1.10",
73
- "@storybook/react-webpack5": "^9.1.10",
74
- "@storybook/test-runner": "^0.23.0",
68
+ "@rollup/plugin-node-resolve": "^16.0.3",
69
+ "@rollup/plugin-terser": "^1.0.0",
70
+ "@rollup/plugin-typescript": "^12.3.0",
71
+ "@storybook/addon-a11y": "^10.2.18",
72
+ "@storybook/addon-links": "^10.2.18",
73
+ "@storybook/addon-styling-webpack": "^3.0.0",
74
+ "@storybook/addon-webpack5-compiler-swc": "^4.0.2",
75
+ "@storybook/csf": "^0.1.13",
76
+ "@storybook/react-vite": "^10.2.18",
77
+ "@storybook/react-webpack5": "^10.2.18",
78
+ "@storybook/test-runner": "^0.24.2",
75
79
  "@testing-library/jest-dom": "^6.9.1",
76
- "@testing-library/react": "^16.3.0",
80
+ "@testing-library/react": "^16.3.2",
77
81
  "@types/govuk-frontend": "^5.11.0",
78
82
  "@types/jest": "^30.0.0",
79
- "@types/react": "^19.2.2",
80
- "@types/react-dom": "^19.2.1",
81
- "@typescript-eslint/eslint-plugin": "^8.36.0",
82
- "@typescript-eslint/parser": "^8.36.0",
83
+ "@types/react": "^19.2.14",
84
+ "@types/react-dom": "^19.2.3",
85
+ "@typescript-eslint/eslint-plugin": "^8.46.0",
86
+ "@typescript-eslint/parser": "^8.57.0",
83
87
  "assert": "^2.1.0",
84
- "autoprefixer": "^10.4.21",
85
- "babel-jest": "^30.2.0",
88
+ "autoprefixer": "^10.4.27",
89
+ "babel-jest": "^30.3.0",
86
90
  "bootstrap": "^5.3.8",
87
- "chromatic": "^13.3.0",
88
- "css-loader": "^7.1.2",
91
+ "chromatic": "^15.2.0",
92
+ "css-loader": "^7.1.4",
89
93
  "deep-iterator": "^1.1.0",
90
94
  "eslint": "^9.37.0",
91
95
  "eslint-plugin-react": "^7.37.5",
92
- "eslint-plugin-storybook": "^9.1.10",
93
- "globals": "^16.4.0",
94
- "govuk-frontend": "^5.12.0",
95
- "html-react-parser": "^5.2.6",
96
+ "eslint-plugin-storybook": "^10.2.18",
97
+ "globals": "^17.4.0",
98
+ "govuk-frontend": "^5.14.0",
99
+ "html-react-parser": "^5.2.17",
96
100
  "husky": "^9.1.7",
97
101
  "identity-obj-proxy": "^3.0.0",
98
- "jest": "^30.2.0",
99
- "jest-environment-jsdom": "^30.2.0",
100
- "magicast": "^0.3.5",
102
+ "jest": "^30.3.0",
103
+ "jest-environment-jsdom": "^30.3.0",
104
+ "magicast": "^0.5.2",
101
105
  "pdfjs-dist": "^4.10.38",
102
- "postcss": "^8.5.6",
103
- "prettier": "^3.6.2",
104
- "react": "^19.2.0",
106
+ "postcss": "^8.5.8",
107
+ "prettier": "^3.8.1",
108
+ "react": "^19.2.4",
105
109
  "react-bootstrap": "^2.10.10",
106
- "react-dom": "^19.2.0",
110
+ "react-dom": "^19.2.4",
107
111
  "react-element-to-jsx-string": "^17.0.1",
108
- "react-router": "^7.9.4",
109
- "react-router-dom": "^7.9.4",
110
- "rimraf": "^6.0.1",
111
- "rollup": "^4.52.4",
112
+ "react-router": "^7.13.1",
113
+ "react-router-dom": "^7.13.1",
114
+ "rimraf": "^6.1.3",
115
+ "rollup": "^4.59.0",
112
116
  "rollup-plugin-analyzer": "^4.0.0",
113
- "rollup-plugin-dts": "^6.2.3",
117
+ "rollup-plugin-dts": "^6.4.0",
114
118
  "rollup-plugin-peer-deps-external": "^2.2.4",
115
119
  "rollup-plugin-postcss": "^4.0.2",
116
120
  "rollup-plugin-visualizer": "^6.0.4",
117
- "sass": "^1.93.2",
118
- "sass-loader": "^16.0.5",
121
+ "sass": "^1.98.0",
122
+ "sass-loader": "^16.0.7",
119
123
  "standard-version": "^9.5.0",
120
- "storybook": "^9.1.10",
124
+ "storybook": "^10.2.18",
121
125
  "style-loader": "^4.0.0",
122
- "stylelint": "^16.25.0",
123
- "stylelint-config-idiomatic-order": "^10.0.0",
124
- "stylelint-config-standard": "^39.0.1",
125
- "stylelint-config-standard-scss": "^16.0.0",
126
- "stylelint-order": "^7.0.0",
126
+ "stylelint": "^17.4.0",
127
+ "stylelint-config-recess-order": "^7.6.1",
128
+ "stylelint-config-standard": "^40.0.0",
129
+ "stylelint-config-standard-scss": "^17.0.0",
130
+ "stylelint-order": "^7.0.1",
127
131
  "tslib": "^2.8.1",
128
132
  "tsuml2": "^0.17.1",
129
133
  "typescript": "^5.9.3",
130
- "typescript-eslint": "^8.46.0",
131
- "unplugin": "^2.3.10",
132
- "webpack": "^5.102.1",
133
- "@storybook/addon-docs": "^9.1.10"
134
+ "typescript-eslint": "^8.57.0",
135
+ "unplugin": "^3.0.0",
136
+ "webpack": "^5.105.4",
137
+ "@storybook/addon-docs": "^10.2.18"
134
138
  },
135
139
  "peerDependencies": {
136
- "govuk-frontend": ">=5.8.0",
140
+ "govuk-frontend": ">=5.14.0",
137
141
  "react": ">=18.2.0",
138
142
  "react-dom": ">=18.2.0",
139
143
  "react-router-dom": ">=6.30.0"
@@ -0,0 +1,433 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/generateStories.mjs
4
+ *
5
+ * Generates *.stories.tsx files from govuk-frontend fixtures.json files.
6
+ * Each non-hidden fixture becomes its own top-level named export — the only
7
+ * format Storybook's CSF parser supports for individual sidebar entries.
8
+ *
9
+ * Also reports any govuk-frontend components that have no matching folder in
10
+ * src/components/, so new upstream components are never silently missed.
11
+ *
12
+ * Usage:
13
+ * node scripts/generateStories.mjs
14
+ * node scripts/generateStories.mjs --component=Accordion
15
+ * node scripts/generateStories.mjs --framework=react-webpack5
16
+ * node scripts/generateStories.mjs --framework=react-vite
17
+ *
18
+ * The --framework flag overrides auto-detection from package.json.
19
+ * Auto-detection checks devDependencies for @storybook/react-webpack5
20
+ * or @storybook/react-vite and picks whichever is present.
21
+ *
22
+ * Add to package.json:
23
+ * "generate-stories": "node scripts/generateStories.mjs",
24
+ * "prestorybook": "npm run generate-stories",
25
+ * "prebuild-storybook": "npm run generate-stories"
26
+ */
27
+
28
+ import fs from "fs";
29
+ import path from "path";
30
+ import { fileURLToPath } from "url";
31
+
32
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
33
+ const ROOT = path.resolve(__dirname, "..");
34
+ const COMPONENTS_DIR = path.join(ROOT, "src", "components");
35
+ const GOVUK_FIXTURES_BASE = path.join(
36
+ ROOT,
37
+ "node_modules",
38
+ "govuk-frontend",
39
+ "dist",
40
+ "govuk",
41
+ "components",
42
+ );
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Storybook framework detection
46
+ //
47
+ // Determines which @storybook package to import Meta/StoryObj from.
48
+ // Override by passing --framework=react-webpack5 or --framework=react-vite.
49
+ // If not specified, auto-detects from the installed packages.
50
+ // ---------------------------------------------------------------------------
51
+ function detectStorybookFramework() {
52
+ const frameworkArg = process.argv.find((a) => a.startsWith("--framework="));
53
+ if (frameworkArg) return frameworkArg.split("=")[1];
54
+
55
+ const pkgPath = path.join(ROOT, "package.json");
56
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
57
+ const allDeps = {
58
+ ...pkg.dependencies,
59
+ ...pkg.devDependencies,
60
+ ...pkg.peerDependencies,
61
+ };
62
+
63
+ if (allDeps["@storybook/react-webpack5"]) return "react-webpack5";
64
+ if (allDeps["@storybook/react-vite"]) return "react-vite";
65
+
66
+ // fallback
67
+ return "react-webpack5";
68
+ }
69
+
70
+ const STORYBOOK_FRAMEWORK = detectStorybookFramework();
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Fixture name overrides
74
+ //
75
+ // By default the script converts a component folder name to kebab-case and
76
+ // looks for a matching govuk-frontend fixtures.json:
77
+ // "CookieBanner" → "cookie-banner" → .../cookie-banner/fixtures.json
78
+ //
79
+ // If a component folder name does NOT map cleanly to its govuk-frontend name,
80
+ // add an entry here:
81
+ // "MyFolderName": "govuk-fixture-name"
82
+ // ---------------------------------------------------------------------------
83
+ const FIXTURE_NAME_OVERRIDES = {
84
+ // Example:
85
+ // "ErrorSummary": "error-summary",
86
+ };
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Per-component customisation
90
+ //
91
+ // Components whose folder contains a {Component}.config.ts[x] file receive a
92
+ // standard configuration decorator automatically — no entry needed here.
93
+ //
94
+ // Only add an entry here if a component needs something beyond the standard
95
+ // config-file decorator (e.g. completely different decorator logic, additional
96
+ // imports on top of the config import, etc.). Entries here are deep-merged
97
+ // with — and take precedence over — the auto-generated config.
98
+ // ---------------------------------------------------------------------------
99
+ const CUSTOM_CONFIG = {};
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Generation skip list
103
+ //
104
+ // Add a component folder name here when its stories are hand-authored and
105
+ // must never be overwritten by this script.
106
+ //
107
+ // Why a component ends up here:
108
+ // The govuk-frontend fixture props are designed for the server-side Nunjucks
109
+ // template and don't map to the React component's API. Feeding them through
110
+ // extractShownFixtures produces undefined/NaN values at runtime.
111
+ // Hand-authored stories use the real React props directly instead.
112
+ // ---------------------------------------------------------------------------
113
+ const GENERATION_SKIP = new Set([
114
+ // Add component folder names here if their stories must be hand-authored.
115
+ // See comment block above for guidance on when this is needed.
116
+ ]);
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Config-file auto-detection
120
+ //
121
+ // If a component folder contains {Component}.config.ts or .config.tsx the
122
+ // generator injects:
123
+ // • an import for the Configure function
124
+ // • a `let configured = false` guard (prevents double-init in docs mode)
125
+ // • a decorator that calls the function on every story render, but only
126
+ // once per page-load when Storybook is showing the auto-docs page.
127
+ //
128
+ // The docs-mode URL pattern changed in Storybook 8:
129
+ // old: ?viewMode=docs
130
+ // new: ?path=/docs/govuk-design-system-{kebab}--docs
131
+ // ---------------------------------------------------------------------------
132
+
133
+ /** Returns true when the component folder has a .config.ts or .config.tsx file. */
134
+ function hasConfigFile(componentName) {
135
+ const dir = path.join(COMPONENTS_DIR, componentName);
136
+ return (
137
+ fs.existsSync(path.join(dir, `${componentName}.config.ts`)) ||
138
+ fs.existsSync(path.join(dir, `${componentName}.config.tsx`))
139
+ );
140
+ }
141
+
142
+ /**
143
+ * Builds the standard CUSTOM_CONFIG-shaped object for any component that has
144
+ * a config file. The caller can then merge this with an explicit CUSTOM_CONFIG
145
+ * entry when one exists.
146
+ */
147
+ function buildConfigFileCustomisation(componentName) {
148
+ const kebab = resolveFixtureName(componentName);
149
+ const fnName = `ConfigureOverall${componentName}`;
150
+ const docsPath = `path=/docs/govuk-design-system-${kebab}--docs`;
151
+
152
+ return {
153
+ extraImports: `import { ${fnName} } from "./${componentName}.config";`,
154
+ extraSetup: `\nlet configured = false;`,
155
+ decorator: `(Story, { parameters }) => {
156
+ React.useEffect(() => {
157
+ const isDocsMode = window.location.search.includes("${docsPath}");
158
+ if (isDocsMode && !configured && parameters.initializeConfigurations) {
159
+ ${fnName}();
160
+ configured = true;
161
+ } else if (!isDocsMode) {
162
+ ${fnName}();
163
+ }
164
+ }, []);
165
+ return <Story />;
166
+ }`,
167
+ };
168
+ }
169
+
170
+ /** Returns the resolved customisation for a component (auto + manual merged). */
171
+ function resolveCustomConfig(componentName) {
172
+ const auto = hasConfigFile(componentName)
173
+ ? buildConfigFileCustomisation(componentName)
174
+ : null;
175
+ const manual = CUSTOM_CONFIG[componentName] ?? null;
176
+
177
+ if (!auto && !manual) return null;
178
+ // Manual entries win — they can override individual keys (e.g. decorator)
179
+ // while still inheriting auto-generated imports/setup when not specified.
180
+ return { ...auto, ...manual };
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Helpers
185
+ // ---------------------------------------------------------------------------
186
+
187
+ /** "with all sections already open" → "WithAllSectionsAlreadyOpen" */
188
+ function toExportName(name) {
189
+ return name
190
+ .replace(/[^a-zA-Z0-9 ]/g, " ")
191
+ .split(" ")
192
+ .filter(Boolean)
193
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
194
+ .join("");
195
+ }
196
+
197
+ /** "CookieBanner" → "cookie-banner" */
198
+ function toKebabCase(name) {
199
+ return name
200
+ .replace(/([A-Z])/g, (m, l, i) => (i === 0 ? l : `-${l}`).toLowerCase())
201
+ .toLowerCase();
202
+ }
203
+
204
+ /** "cookie-banner" → "CookieBanner" */
205
+ function toPascalCase(kebab) {
206
+ return kebab
207
+ .split("-")
208
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
209
+ .join("");
210
+ }
211
+
212
+ /** "file-upload" → "File upload" | "cookie-banner" → "Cookie banner" */
213
+ function toTitleLabel(kebab) {
214
+ const words = kebab.replace(/-/g, " ");
215
+ return words.charAt(0).toUpperCase() + words.slice(1);
216
+ }
217
+
218
+ function resolveFixtureName(componentName) {
219
+ return FIXTURE_NAME_OVERRIDES[componentName] ?? toKebabCase(componentName);
220
+ }
221
+
222
+ function getVisibleFixtures(componentName) {
223
+ const fixtureName = resolveFixtureName(componentName);
224
+ const fixturePath = path.join(
225
+ GOVUK_FIXTURES_BASE,
226
+ fixtureName,
227
+ "fixtures.json",
228
+ );
229
+ if (!fs.existsSync(fixturePath)) return null;
230
+ const { fixtures } = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
231
+ return fixtures.filter((f) => f.hidden === false);
232
+ }
233
+
234
+ function getAllGovukComponents() {
235
+ if (!fs.existsSync(GOVUK_FIXTURES_BASE)) return [];
236
+ return fs
237
+ .readdirSync(GOVUK_FIXTURES_BASE, { withFileTypes: true })
238
+ .filter(
239
+ (d) =>
240
+ d.isDirectory() &&
241
+ fs.existsSync(path.join(GOVUK_FIXTURES_BASE, d.name, "fixtures.json")),
242
+ )
243
+ .map((d) => d.name);
244
+ }
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // File generator
248
+ //
249
+ // The generated file imports fixtures and calls extractShownFixtures at
250
+ // runtime — this is intentional. extractShownFixtures transforms the raw
251
+ // govuk-frontend fixture data (e.g. `text` → `children`, `classes` →
252
+ // `className`) into props the React components actually accept. Inlining the
253
+ // raw JSON as args would cause TypeScript errors like:
254
+ // "text does not exist in type { children: ReactNode }"
255
+ // because the component types expect the already-transformed prop names.
256
+ // ---------------------------------------------------------------------------
257
+ function generateFile(componentName, visibleFixtures) {
258
+ const fixtureName = resolveFixtureName(componentName);
259
+ const custom = resolveCustomConfig(componentName);
260
+ const hasScss = fs.existsSync(
261
+ path.join(COMPONENTS_DIR, componentName, `${componentName}.scss`),
262
+ );
263
+
264
+ const decorator =
265
+ custom?.decorator ??
266
+ `(Story) => {
267
+ return <Story />;
268
+ }`;
269
+
270
+ // Each story references its fixture by name at runtime so extractShownFixtures
271
+ // can transform it. The index lookup is intentionally avoided — name-based
272
+ // lookup means stories stay correct even if govuk-frontend reorders fixtures.
273
+ const storyExports = visibleFixtures
274
+ .map((fixture) => {
275
+ const exportName = toExportName(fixture.name);
276
+ return [
277
+ `export const ${exportName}: Story = {`,
278
+ ` name: "${fixture.name}",`,
279
+ ` args: { ...examplesFromFixtures.find((f) => f.name === "${fixture.name}")?.options },`,
280
+ `};`,
281
+ ].join("\n");
282
+ })
283
+ .join("\n\n");
284
+
285
+ return [
286
+ `// THIS FILE IS AUTO-GENERATED — do not edit manually.`,
287
+ `// Source: node_modules/govuk-frontend/dist/govuk/components/${fixtureName}/fixtures.json`,
288
+ `// Regenerate: npm run generate-stories`,
289
+ ``,
290
+ `import React from "react";`,
291
+ hasScss ? `import "./${componentName}.scss";` : null,
292
+ `import ${componentName} from "./${componentName}";`,
293
+ `import { Meta, StoryObj } from "@storybook/${STORYBOOK_FRAMEWORK}";`,
294
+ `import fixtures from "govuk-frontend/dist/govuk/components/${fixtureName}/fixtures.json";`,
295
+ `import { extractShownFixtures } from "../../utils/ProcessExampleData";`,
296
+ `import { ComponentFixture } from "../../dynamics";`,
297
+ custom?.extraImports ?? null,
298
+ custom?.extraSetup ?? null,
299
+ `const meta: Meta<typeof ${componentName}> = {`,
300
+ ` title: "GOVUK Design System/${toTitleLabel(fixtureName)}",`,
301
+ ` component: ${componentName},`,
302
+ ` decorators: [${decorator}],`,
303
+ ` tags: ["autodocs"],`,
304
+ `};`,
305
+ ``,
306
+ `export default meta;`,
307
+ `type Story = StoryObj<typeof ${componentName}>;`,
308
+ ``,
309
+ `// extractShownFixtures transforms raw govuk-frontend fixture data into`,
310
+ `// React-compatible props (e.g. text → children, classes → className).`,
311
+ `const examplesFromFixtures: Array<ComponentFixture> = extractShownFixtures(fixtures);`,
312
+ ``,
313
+ storyExports,
314
+ ]
315
+ .filter((line) => line !== null)
316
+ .join("\n")
317
+ .replace(/\n{3,}/g, "\n\n");
318
+ }
319
+
320
+ // ---------------------------------------------------------------------------
321
+ // Main
322
+ // ---------------------------------------------------------------------------
323
+ const targetArg = process.argv.find((a) => a.startsWith("--component="));
324
+ const targetComponent = targetArg?.split("=")[1];
325
+
326
+ const srcComponentNames = fs
327
+ .readdirSync(COMPONENTS_DIR, { withFileTypes: true })
328
+ .filter((d) => d.isDirectory())
329
+ .map((d) => d.name)
330
+ .filter((name) => !targetComponent || name === targetComponent);
331
+
332
+ const govukComponentKebabs = getAllGovukComponents();
333
+
334
+ const srcKebabToFolder = Object.fromEntries(
335
+ srcComponentNames.map((name) => [resolveFixtureName(name), name]),
336
+ );
337
+
338
+ const notImplemented = govukComponentKebabs.filter(
339
+ (kebab) => !srcKebabToFolder[kebab],
340
+ );
341
+
342
+ const results = { generated: [], skipped: [], noFixtures: [] };
343
+
344
+ for (const componentName of srcComponentNames) {
345
+ if (GENERATION_SKIP.has(componentName)) {
346
+ results.skipped.push({
347
+ componentName,
348
+ reason: "hand-authored stories (GENERATION_SKIP)",
349
+ });
350
+ continue;
351
+ }
352
+
353
+ const fixtures = getVisibleFixtures(componentName);
354
+ const fixtureName = resolveFixtureName(componentName);
355
+
356
+ if (!fixtures) {
357
+ results.noFixtures.push({ componentName, fixtureName });
358
+ continue;
359
+ }
360
+
361
+ if (fixtures.length === 0) {
362
+ results.skipped.push({ componentName, reason: "all fixtures are hidden" });
363
+ continue;
364
+ }
365
+
366
+ const outputPath = path.join(
367
+ COMPONENTS_DIR,
368
+ componentName,
369
+ `${componentName}.stories.tsx`,
370
+ );
371
+
372
+ fs.writeFileSync(outputPath, generateFile(componentName, fixtures), "utf8");
373
+ results.generated.push({ componentName, count: fixtures.length });
374
+ }
375
+
376
+ // ---------------------------------------------------------------------------
377
+ // Report
378
+ // ---------------------------------------------------------------------------
379
+ console.log("\n📖 Story generation report\n");
380
+ console.log(` Storybook framework: @storybook/${STORYBOOK_FRAMEWORK}`);
381
+
382
+ if (results.generated.length) {
383
+ console.log("✅ Generated:");
384
+ results.generated.forEach(({ componentName, count }) =>
385
+ console.log(
386
+ ` ${componentName}.stories.tsx (${count} ${count === 1 ? "story" : "stories"})`,
387
+ ),
388
+ );
389
+ }
390
+
391
+ if (results.skipped.length) {
392
+ console.log("\n⚠️ Skipped (fixtures exist but all are hidden):");
393
+ results.skipped.forEach(({ componentName, reason }) =>
394
+ console.log(` ${componentName} — ${reason}`),
395
+ );
396
+ }
397
+
398
+ if (results.noFixtures.length) {
399
+ console.log(
400
+ "\n⬜ No govuk-frontend fixtures (custom components — expected):",
401
+ );
402
+ results.noFixtures.forEach(({ componentName, fixtureName }) =>
403
+ console.log(
404
+ ` ${componentName} → looked for: ${fixtureName}/fixtures.json`,
405
+ ),
406
+ );
407
+ console.log(
408
+ "\n 💡 If any of the above should have fixtures, add an entry to",
409
+ "\n FIXTURE_NAME_OVERRIDES in scripts/generateStories.mjs",
410
+ );
411
+ }
412
+
413
+ if (notImplemented.length && !targetComponent) {
414
+ console.log(
415
+ "\n🆕 govuk-frontend components not yet in src/components/ (consider implementing):",
416
+ );
417
+ notImplemented.forEach((kebab) =>
418
+ console.log(
419
+ ` ${kebab} → suggested folder name: ${toPascalCase(kebab)}`,
420
+ ),
421
+ );
422
+ console.log(
423
+ "\n 💡 These are govuk-frontend components that exist upstream but have",
424
+ "\n no matching folder in src/components/. Create the component folder",
425
+ "\n and re-run this script to generate its stories automatically.",
426
+ );
427
+ }
428
+
429
+ console.log(
430
+ `\nDone — ${results.generated.length} generated, ${results.skipped.length} skipped, ` +
431
+ `${results.noFixtures.length} have no govuk-frontend fixtures, ` +
432
+ `${notImplemented.length} govuk-frontend component(s) not yet implemented.\n`,
433
+ );
@@ -1 +0,0 @@
1
- import "@testing-library/jest-dom";