Skip to content

Commit 6be76e8

Browse files
committed
chore: get rid of hard-coded versions from multiversion docs site
- Gatsby multiversion is quite complex - This handles all automatically - Fetches two previous majors's latest minors - remove useless packages Refs: HDS-2883
1 parent e65a155 commit 6be76e8

File tree

10 files changed

+884
-271
lines changed

10 files changed

+884
-271
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [4.X.X] - Month, XX, 202X
99

10+
Updated documentation build process for multi-version docs by replacing the `gatsby-source-git` setup with a local filesystem-based source and adding automatic version detection from the documentation directory structure.
11+
1012
### React
1113

1214
#### Breaking
@@ -65,7 +67,6 @@ Changes that are not related to specific components
6567

6668
#### Fixed
6769

68-
- [Component] What bugs/typos are fixed?
6970
- [Checkbox] Stories to include full interactivities.
7071

7172
### Figma

site/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,6 @@ yarn-error.log
3939

4040
# icon&group mapping
4141
icon_group.json
42+
43+
# Local version downloads
44+
.previous-versions

site/gatsby-config.js

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,93 @@ require("dotenv").config({
22
path: `.env.${process.env.NODE_ENV}`,
33
})
44

5+
const fs = require('fs');
6+
const path = require('path');
7+
58
const buildSingleVersion = process.env.BUILD_SINGLE_VERSION === 'true';
6-
const versionsFromGit = buildSingleVersion ? [] : require('./src/data/versionsFromGit.json');
79

8-
const gitSources = versionsFromGit.map(version => ({
9-
resolve: 'gatsby-source-git',
10-
options: {
11-
name: `docs-release-${version}`,
12-
remote: `https://github.com/City-of-Helsinki/helsinki-design-system`,
13-
branch: `release-${version}`,
14-
patterns: 'site/src/docs/**',
15-
},
16-
}));
10+
// Extract version number from directory name
11+
function extractVersionFromDir(dir) {
12+
const versionPart = dir.replace('helsinki-design-system-', '');
13+
// Extract only the semantic version part (X.Y.Z)
14+
// This ensures we extract just the version even if there's extra text after it
15+
const match = versionPart.match(/^(\d+\.\d+\.\d+)/);
16+
return match ? match[1] : null;
17+
}
18+
19+
// Sort versions descending (newest first)
20+
function sortVersionsDesc(a, b) {
21+
const aParts = a.split('.').map(Number);
22+
const bParts = b.split('.').map(Number);
23+
for (let i = 0; i < 3; i++) {
24+
if (bParts[i] !== aParts[i]) return bParts[i] - aParts[i];
25+
}
26+
return 0;
27+
}
28+
29+
// Auto-detect versions from .previous-versions directory
30+
function getPreviousVersions() {
31+
const previousVersionsDir = path.join(__dirname, '.previous-versions');
32+
if (!fs.existsSync(previousVersionsDir)) return [];
33+
34+
try {
35+
return fs.readdirSync(previousVersionsDir)
36+
.filter(item => {
37+
const itemPath = path.join(previousVersionsDir, item);
38+
try {
39+
return fs.statSync(itemPath).isDirectory() && item.startsWith('helsinki-design-system-');
40+
} catch {
41+
return false;
42+
}
43+
})
44+
.map(extractVersionFromDir)
45+
.filter(Boolean)
46+
.sort(sortVersionsDesc)
47+
.reduce((acc, version) => {
48+
// Ensure we only keep the latest minor for each major version
49+
const major = version.split('.')[0];
50+
if (!acc.some(v => v.split('.')[0] === major)) {
51+
acc.push(version);
52+
}
53+
return acc;
54+
}, [])
55+
.slice(0, 2); // Get latest minors from the previous two majors
56+
} catch (error) {
57+
console.warn('Warning: Could not read .previous-versions directory:', error.message);
58+
return [];
59+
}
60+
}
61+
62+
const previousVersions = buildSingleVersion ? [] : getPreviousVersions();
63+
64+
// Get current version from package.json for siteMetadata
65+
const packageJsonPath = path.join(__dirname, 'package.json');
66+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
67+
const currentVersion = packageJson.version;
68+
69+
// Create local sources using gatsby-source-filesystem
70+
const previousVersionSources = previousVersions.map(version => {
71+
const docsPath = path.join(__dirname, `.previous-versions/helsinki-design-system-${version}/site/src/docs`);
72+
73+
// Verify the path exists before adding it
74+
if (fs.existsSync(docsPath)) {
75+
return {
76+
resolve: `gatsby-source-filesystem`,
77+
options: {
78+
name: `docs-release-${version}`,
79+
path: docsPath,
80+
},
81+
};
82+
}
83+
return null;
84+
}).filter(Boolean);
1785

1886
module.exports = {
1987
pathPrefix: process.env.PATH_PREFIX,
2088
siteMetadata: {
89+
// Include versions in siteMetadata so they're available via GraphQL
90+
currentVersion,
91+
previousVersions,
2192
title: `Helsinki Design System`,
2293
description: `Documentation for the Helsinki Design System`,
2394
siteUrl: process.env.SITE_URL,
@@ -163,7 +234,7 @@ module.exports = {
163234
path: `${__dirname}/src/docs`,
164235
},
165236
},
166-
...gitSources,
237+
...previousVersionSources,
167238
{
168239
resolve: `gatsby-plugin-mdx`,
169240
options: {

site/gatsby-node.js

Lines changed: 135 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,92 @@
11
const webpack = require('webpack');
22
const path = require('path');
3+
const fs = require('fs');
34
const buildSingleVersion = process.env.BUILD_SINGLE_VERSION === 'true';
45

56
exports.onCreateWebpackConfig = ({ actions, getConfig }) => {
67
const config = getConfig();
78

9+
// Custom resolver plugin to handle dynamic aliases for CSS imports
10+
// This hooks into webpack's resolver to catch all module resolutions including CSS
11+
class DynamicAliasResolverPlugin {
12+
apply(resolver) {
13+
// Hook into 'resolve' hook and run it with high priority (early in the chain)
14+
resolver.hooks.resolve.tapAsync({
15+
name: 'DynamicAliasResolverPlugin',
16+
stage: 1, // Run early, before AliasPlugin (which runs at stage 10)
17+
}, (request, resolveContext, callback) => {
18+
// Handle ~hds-core imports (CSS/SCSS imports)
19+
if (request && request.request && request.request.startsWith('~hds-core')) {
20+
// Get context path - try multiple possible locations
21+
const contextPath = request.context?.path ||
22+
request.path ||
23+
(resolveContext && resolveContext.issuer) ||
24+
'';
25+
26+
// Normalize path separators to handle Windows and POSIX paths uniformly
27+
const normalizedContextPath = contextPath.split(path.sep).join('/');
28+
29+
// Extract version from context path
30+
// Use word boundary or path separator to ensure complete version matching
31+
const versionMatch = normalizedContextPath.match(/(?:docs-release-|helsinki-design-system-|\.previous-versions\/helsinki-design-system-)(\d+\.\d+\.\d+)(?:\/|$)/);
32+
if (versionMatch) {
33+
const fullVersion = versionMatch[1];
34+
// Replace ~hds-core with hds-core-{version}
35+
const newRequest = request.request.replace('~hds-core', `hds-core-${fullVersion}`);
36+
const newRequestObj = {
37+
...request,
38+
request: newRequest
39+
};
40+
// Continue resolution with the modified request
41+
return resolver.doResolve(resolver.hooks.resolve, newRequestObj, null, resolveContext, callback);
42+
}
43+
}
44+
// Continue with normal resolution
45+
return callback();
46+
});
47+
}
48+
}
49+
850
config.plugins.push(
951
new webpack.NormalModuleReplacementPlugin(
10-
/hds-core|hds-react/,
52+
/(~?hds-core|hds-react)/,
1153
resource => {
12-
if (resource.context.includes('.cache/gatsby-source-git/docs-release-2.')) {
13-
resource.request = resource.request.replace('hds-core', 'hds-2-core');
14-
resource.request = resource.request.replace('hds-react', 'hds-2-react');
54+
// Skip if already versioned (prevent double replacement)
55+
// Check for pattern like hds-core-X.Y.Z or hds-react-X.Y.Z
56+
if (resource.request.match(/hds-(core|react)-\d+\.\d+\.\d+/)) {
57+
return;
1558
}
16-
if (resource.context.includes('.cache/gatsby-source-git/docs-release-3.')) {
17-
resource.request = resource.request.replace('hds-core', 'hds-3-core');
18-
resource.request = resource.request.replace('hds-react', 'hds-3-react');
59+
60+
// Dynamically extract full version from path
61+
// Match patterns like:
62+
// - docs-release-X.Y.Z (from sourceInstanceName)
63+
// - helsinki-design-system-X.Y.Z (from .previous-versions path)
64+
// - .previous-versions/helsinki-design-system-X.Y.Z (full path)
65+
// Normalize path separators to handle Windows and POSIX paths uniformly
66+
const normalizedContext = resource.context.split(path.sep).join('/');
67+
// Use word boundary or path separator to ensure complete version matching
68+
const versionMatch = normalizedContext.match(/(?:docs-release-|helsinki-design-system-|\.previous-versions\/helsinki-design-system-)(\d+\.\d+\.\d+)(?:\/|$)/);
69+
if (versionMatch) {
70+
const fullVersion = versionMatch[1];
71+
72+
// Replace ~hds-core with hds-core-{version} (remove ~)
73+
if (resource.request.includes('~hds-core')) {
74+
resource.request = resource.request.replace(/~hds-core/g, `hds-core-${fullVersion}`);
75+
}
76+
// Replace hds-core with hds-core-{version}
77+
else if (resource.request.includes('hds-core')) {
78+
resource.request = resource.request.replace(/hds-core(?!-\d+\.\d+\.\d+)/g, `hds-core-${fullVersion}`);
79+
}
80+
// Replace hds-react with hds-react-{version}
81+
if (resource.request.includes('hds-react')) {
82+
resource.request = resource.request.replace(/hds-react(?!-\d+\.\d+\.\d+)/g, `hds-react-${fullVersion}`);
83+
}
1984
}
2085
}
2186
)
2287
);
2388

89+
2490
actions.setWebpackConfig({
2591
plugins: [
2692
// We need to provide a polyfill for react-live library to make it work with the latest Gatsby: https://webpack.js.org/blog/2020-10-10-webpack-5-release/#automatic-nodejs-polyfills-removed
@@ -32,10 +98,13 @@ exports.onCreateWebpackConfig = ({ actions, getConfig }) => {
3298
resolve: {
3399
alias: {
34100
fs$: path.resolve(__dirname, 'src/fs.js'),
35-
'~hds-core': 'hds-2-core',
36101
'hds-react': 'hds-react/lib',
37102
stream: false,
38103
},
104+
plugins: [
105+
new DynamicAliasResolverPlugin(),
106+
...(config.resolve.plugins || []),
107+
],
39108
fallback: {
40109
crypto: require.resolve('crypto-browserify'),
41110
},
@@ -91,10 +160,9 @@ exports.createPages = async ({ actions, graphql }) => {
91160
parent {
92161
... on File {
93162
relativePath
163+
absolutePath
94164
${!buildSingleVersion ?
95-
` gitRemote {
96-
ref
97-
}` : ``}
165+
` sourceInstanceName` : ``}
98166
}
99167
}
100168
}
@@ -180,30 +248,78 @@ exports.createPages = async ({ actions, graphql }) => {
180248

181249
// Create pages dynamically
182250
result.data.allMdx.edges.forEach(({ node }) => {
183-
const gitRemote = node.parent?.gitRemote?.ref;
184-
const pathWithVersion = path.join('/', gitRemote || '', node.frontmatter.slug);
251+
const sourceInstanceName = node.parent?.sourceInstanceName;
252+
// Extract version from sourceInstanceName (filesystem source)
253+
const versionFromSource = sourceInstanceName?.startsWith('docs-release-')
254+
? sourceInstanceName.replace('docs-release-', '')
255+
: null;
256+
const versionRef = versionFromSource ? `release-${versionFromSource}` : null;
257+
const pathWithVersion = path.join('/', versionRef || '', node.frontmatter.slug);
185258

186259
try {
187260
const pageTemplate = require.resolve('./src/components/ContentLayoutWrapper.js');
188-
const contentPath = './src/docs/' + node.parent.relativePath.replace('site/src/docs/', '');
261+
// Handle relativePath for both versioned sources and current docs
262+
const rawRelativePath = node.parent?.relativePath || '';
263+
const normalizedRelativePath = rawRelativePath.replace(/\\/g, '/');
264+
const docsPrefix = 'site/src/docs/';
265+
const docsRelativePath = normalizedRelativePath.includes(docsPrefix)
266+
? normalizedRelativePath.substring(normalizedRelativePath.indexOf(docsPrefix) + docsPrefix.length)
267+
: normalizedRelativePath;
268+
const contentPath = path.posix.join('./src/docs', docsRelativePath);
189269

190-
console.log('createPage() ' + (gitRemote ? gitRemote : 'latest') + ' ' + contentPath);
270+
console.log('createPage() ' + (versionRef ? versionRef : 'latest') + ' ' + contentPath);
191271

192-
const pageContent = gitRemote
193-
? require.resolve(`./.cache/gatsby-source-git/docs-${gitRemote}/${node.parent.relativePath}`)
272+
const pageContent = versionRef
273+
? (() => {
274+
// For filesystem sources, use the absolutePath directly
275+
const absolutePath = node.parent?.absolutePath;
276+
if (absolutePath && fs.existsSync(absolutePath)) {
277+
try {
278+
return require.resolve(absolutePath);
279+
} catch (e) {
280+
console.warn(`Could not resolve absolute path ${absolutePath}: ${e.message}`);
281+
}
282+
}
283+
// Fallback: construct path from version and docsRelativePath
284+
const version = versionRef.replace('release-', '');
285+
// Use the already-normalized docsRelativePath instead of node.parent.relativePath
286+
// to avoid double path segments
287+
const localPath = path.resolve(__dirname, `.previous-versions/helsinki-design-system-${version}/site/src/docs/${docsRelativePath}`);
288+
289+
if (fs.existsSync(localPath)) {
290+
try {
291+
return require.resolve(localPath);
292+
} catch (e) {
293+
console.warn(`Could not resolve local path ${localPath}: ${e.message}`);
294+
}
295+
}
296+
// Last resort: try contentPath (for latest docs)
297+
try {
298+
return require.resolve(contentPath);
299+
} catch (e) {
300+
console.warn(`Could not resolve any path for ${sourceInstanceName}/${node.parent.relativePath}: ${e.message}`);
301+
throw e;
302+
}
303+
})()
194304
: require.resolve(contentPath);
195305

196306
// filter out duplicate slug entries.
197307
const allPages = Object.values(
198308
Object.fromEntries(
199309
mdxPageData
200-
.filter(({ node }) => node?.parent?.gitRemote?.ref === gitRemote)
310+
.filter(({ node }) => {
311+
const nodeSourceInstanceName = node?.parent?.sourceInstanceName;
312+
const nodeVersionRef = nodeSourceInstanceName?.startsWith('docs-release-')
313+
? `release-${nodeSourceInstanceName.replace('docs-release-', '')}`
314+
: null;
315+
return nodeVersionRef === versionRef;
316+
})
201317
.filter(({ node }) => node.frontmatter.slug && node.frontmatter.navTitle)
202318
.map(({ node }) => [node.frontmatter.slug, { ...node.frontmatter, ...node.fields }])
203319
),
204320
);
205321

206-
const currentMenuItem = resolveCurrentMenuItem(gitRemote, uiMenuLinks, node.frontmatter.slug);
322+
const currentMenuItem = resolveCurrentMenuItem(versionRef, uiMenuLinks, node.frontmatter.slug);
207323
const uiSubMenuLinks = getUiSubMenuLinks(allPages, uiMenuLinks, currentMenuItem);
208324

209325
createPage({

site/package.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
"dependencies": {
1818
"@mdx-js/mdx": "2.3.0",
1919
"@mdx-js/react": "2.3.0",
20-
"body-parser": "1.20.3",
2120
"gatsby": "5.13.7",
2221
"gatsby-plugin-image": "^3.13.1",
2322
"gatsby-plugin-manifest": "^5.13.1",
@@ -31,14 +30,13 @@
3130
"gatsby-plugin-sharp": "^5.13.1",
3231
"gatsby-remark-autolink-headers": "^6.13.1",
3332
"gatsby-source-filesystem": "^5.13.1",
34-
"gatsby-source-git": "2.0.0-beta.0",
3533
"gatsby-transformer-remark": "6.13.1",
3634
"gatsby-transformer-sharp": "5.13.1",
3735
"html-validate": "6.5.0",
3836
"json5": "^2.2.3",
39-
"path-to-regexp": "0.1.10",
4037
"prism-react-renderer": "^1.3.1",
41-
"process-top": "^1.2.0",
38+
"tar": "^7.4.3",
39+
"extract-zip": "^2.0.1",
4240
"prop-types": "^15.8.1",
4341
"react": "18.2.0",
4442
"react-dom": "18.2.0",
@@ -81,7 +79,8 @@
8179
],
8280
"license": "MIT",
8381
"scripts": {
84-
"build": "yarn lint && NODE_OPTIONS='--max_old_space_size=8192' gatsby build",
82+
"check-versions": "node scripts/check-versions.js",
83+
"build": "yarn check-versions && yarn lint && NODE_OPTIONS='--max_old_space_size=8192' gatsby build",
8584
"build-single-version": "yarn lint && NODE_OPTIONS='--max_old_space_size=8192' BUILD_SINGLE_VERSION=true gatsby build",
8685
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
8786
"start": "NODE_OPTIONS=--max_old_space_size=8192 BUILD_SINGLE_VERSION=true gatsby develop -o",

0 commit comments

Comments
 (0)