Skip to content

Commit 0764863

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 0764863

File tree

12 files changed

+901
-275
lines changed

12 files changed

+901
-275
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: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,94 @@ require("dotenv").config({
22
path: `.env.${process.env.NODE_ENV}`,
33
})
44

5+
const fs = require('node:fs');
6+
const path = require('node: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+
// Combine current and previous versions into a single array
70+
const versions = buildSingleVersion ? [currentVersion] : [currentVersion, ...previousVersions];
71+
72+
// Create local sources using gatsby-source-filesystem
73+
const previousVersionSources = previousVersions.map(version => {
74+
const docsPath = path.join(__dirname, `.previous-versions/helsinki-design-system-${version}/site/src/docs`);
75+
76+
// Verify the path exists before adding it
77+
if (fs.existsSync(docsPath)) {
78+
return {
79+
resolve: `gatsby-source-filesystem`,
80+
options: {
81+
name: `docs-release-${version}`,
82+
path: docsPath,
83+
},
84+
};
85+
}
86+
return null;
87+
}).filter(Boolean);
1788

1889
module.exports = {
1990
pathPrefix: process.env.PATH_PREFIX,
2091
siteMetadata: {
92+
versions,
2193
title: `Helsinki Design System`,
2294
description: `Documentation for the Helsinki Design System`,
2395
siteUrl: process.env.SITE_URL,
@@ -163,7 +235,7 @@ module.exports = {
163235
path: `${__dirname}/src/docs`,
164236
},
165237
},
166-
...gitSources,
238+
...previousVersionSources,
167239
{
168240
resolve: `gatsby-plugin-mdx`,
169241
options: {

site/gatsby-node.js

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

6+
// Ensure versions is always in GraphQL schema
7+
exports.createSchemaCustomization = ({ actions }) => {
8+
const { createTypes } = actions;
9+
const typeDefs = `
10+
type SiteSiteMetadata implements Node {
11+
versions: [String!]!
12+
}
13+
`;
14+
createTypes(typeDefs);
15+
};
16+
517
exports.onCreateWebpackConfig = ({ actions, getConfig }) => {
618
const config = getConfig();
719

20+
// Custom resolver plugin to handle dynamic aliases for CSS imports
21+
// This hooks into webpack's resolver to catch all module resolutions including CSS
22+
class DynamicAliasResolverPlugin {
23+
apply(resolver) {
24+
// Hook into 'resolve' hook and run it with high priority (early in the chain)
25+
resolver.hooks.resolve.tapAsync({
26+
name: 'DynamicAliasResolverPlugin',
27+
stage: 1, // Run early, before AliasPlugin (which runs at stage 10)
28+
}, (request, resolveContext, callback) => {
29+
// Handle ~hds-core imports (CSS/SCSS imports)
30+
if (request && request.request && request.request.startsWith('~hds-core')) {
31+
// Get context path - try multiple possible locations
32+
const contextPath = request.context?.path ||
33+
request.path ||
34+
(resolveContext && resolveContext.issuer) ||
35+
'';
36+
37+
// Normalize path separators to handle Windows and POSIX paths uniformly
38+
const normalizedContextPath = contextPath.split(path.sep).join('/');
39+
40+
// Extract version from context path
41+
// Use word boundary or path separator to ensure complete version matching
42+
const versionMatch = normalizedContextPath.match(/(?:docs-release-|helsinki-design-system-|\.previous-versions\/helsinki-design-system-)(\d+\.\d+\.\d+)(?:\/|$)/);
43+
if (versionMatch) {
44+
const fullVersion = versionMatch[1];
45+
// Replace ~hds-core with hds-core-{version}
46+
const newRequest = request.request.replace('~hds-core', `hds-core-${fullVersion}`);
47+
const newRequestObj = {
48+
...request,
49+
request: newRequest
50+
};
51+
// Continue resolution with the modified request
52+
return resolver.doResolve(resolver.hooks.resolve, newRequestObj, null, resolveContext, callback);
53+
}
54+
}
55+
// Continue with normal resolution
56+
return callback();
57+
});
58+
}
59+
}
60+
861
config.plugins.push(
962
new webpack.NormalModuleReplacementPlugin(
10-
/hds-core|hds-react/,
63+
/(~?hds-core|hds-react)/,
1164
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');
65+
// Skip if already versioned (prevent double replacement)
66+
// Check for pattern like hds-core-X.Y.Z or hds-react-X.Y.Z
67+
if (resource.request.match(/hds-(core|react)-\d+\.\d+\.\d+/)) {
68+
return;
1569
}
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');
70+
71+
// Dynamically extract full version from path
72+
// Match patterns like:
73+
// - docs-release-X.Y.Z (from sourceInstanceName)
74+
// - helsinki-design-system-X.Y.Z (from .previous-versions path)
75+
// - .previous-versions/helsinki-design-system-X.Y.Z (full path)
76+
// Normalize path separators to handle Windows and POSIX paths uniformly
77+
const normalizedContext = resource.context.split(path.sep).join('/');
78+
// Use word boundary or path separator to ensure complete version matching
79+
const versionMatch = normalizedContext.match(/(?:docs-release-|helsinki-design-system-|\.previous-versions\/helsinki-design-system-)(\d+\.\d+\.\d+)(?:\/|$)/);
80+
if (versionMatch) {
81+
const fullVersion = versionMatch[1];
82+
83+
// Replace ~hds-core with hds-core-{version} (remove ~)
84+
if (resource.request.includes('~hds-core')) {
85+
resource.request = resource.request.replaceAll('~hds-core', `hds-core-${fullVersion}`);
86+
}
87+
// Replace hds-core with hds-core-{version} (only if not already versioned)
88+
else if (resource.request.includes('hds-core') && !resource.request.includes('hds-core-')) {
89+
resource.request = resource.request.replaceAll('hds-core', `hds-core-${fullVersion}`);
90+
}
91+
// Replace hds-react with hds-react-{version} (only if not already versioned)
92+
if (resource.request.includes('hds-react') && !resource.request.includes('hds-react-')) {
93+
resource.request = resource.request.replaceAll('hds-react', `hds-react-${fullVersion}`);
94+
}
1995
}
2096
}
2197
)
2298
);
2399

100+
24101
actions.setWebpackConfig({
25102
plugins: [
26103
// 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 +109,13 @@ exports.onCreateWebpackConfig = ({ actions, getConfig }) => {
32109
resolve: {
33110
alias: {
34111
fs$: path.resolve(__dirname, 'src/fs.js'),
35-
'~hds-core': 'hds-2-core',
36112
'hds-react': 'hds-react/lib',
37113
stream: false,
38114
},
115+
plugins: [
116+
new DynamicAliasResolverPlugin(),
117+
...(config.resolve.plugins || []),
118+
],
39119
fallback: {
40120
crypto: require.resolve('crypto-browserify'),
41121
},
@@ -91,10 +171,9 @@ exports.createPages = async ({ actions, graphql }) => {
91171
parent {
92172
... on File {
93173
relativePath
174+
absolutePath
94175
${!buildSingleVersion ?
95-
` gitRemote {
96-
ref
97-
}` : ``}
176+
` sourceInstanceName` : ``}
98177
}
99178
}
100179
}
@@ -180,30 +259,79 @@ exports.createPages = async ({ actions, graphql }) => {
180259

181260
// Create pages dynamically
182261
result.data.allMdx.edges.forEach(({ node }) => {
183-
const gitRemote = node.parent?.gitRemote?.ref;
184-
const pathWithVersion = path.join('/', gitRemote || '', node.frontmatter.slug);
262+
const sourceInstanceName = node.parent?.sourceInstanceName;
263+
// Extract version from sourceInstanceName (filesystem source)
264+
const versionFromSource = sourceInstanceName?.startsWith('docs-release-')
265+
? sourceInstanceName.replace('docs-release-', '')
266+
: null;
267+
const versionRef = versionFromSource ? `release-${versionFromSource}` : null;
268+
const pathWithVersion = path.join('/', versionRef || '', node.frontmatter.slug);
185269

186270
try {
187271
const pageTemplate = require.resolve('./src/components/ContentLayoutWrapper.js');
188-
const contentPath = './src/docs/' + node.parent.relativePath.replace('site/src/docs/', '');
272+
// Handle relativePath for both versioned sources and current docs
273+
const rawRelativePath = node.parent?.relativePath || '';
274+
const normalizedRelativePath = rawRelativePath.replaceAll('\\', '/');
275+
const docsPrefix = 'site/src/docs/';
276+
const docsRelativePath = normalizedRelativePath.includes(docsPrefix)
277+
? normalizedRelativePath.substring(normalizedRelativePath.indexOf(docsPrefix) + docsPrefix.length)
278+
: normalizedRelativePath;
279+
const contentPath = path.posix.join('./src/docs', docsRelativePath);
280+
const absoluteContentPath = path.resolve(__dirname, contentPath);
189281

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

192-
const pageContent = gitRemote
193-
? require.resolve(`./.cache/gatsby-source-git/docs-${gitRemote}/${node.parent.relativePath}`)
194-
: require.resolve(contentPath);
284+
const pageContent = versionRef
285+
? (() => {
286+
// For filesystem sources, use the absolutePath directly
287+
const absolutePath = node.parent?.absolutePath;
288+
if (absolutePath && fs.existsSync(absolutePath)) {
289+
try {
290+
return require.resolve(absolutePath);
291+
} catch (e) {
292+
console.warn(`Could not resolve absolute path ${absolutePath}: ${e.message}`);
293+
}
294+
}
295+
// Fallback: construct path from version and docsRelativePath
296+
const version = versionRef.replace('release-', '');
297+
// Use the already-normalized docsRelativePath instead of node.parent.relativePath
298+
// to avoid double path segments
299+
const localPath = path.resolve(__dirname, `.previous-versions/helsinki-design-system-${version}/site/src/docs/${docsRelativePath}`);
300+
301+
if (fs.existsSync(localPath)) {
302+
try {
303+
return require.resolve(localPath);
304+
} catch (e) {
305+
console.warn(`Could not resolve local path ${localPath}: ${e.message}`);
306+
}
307+
}
308+
// Last resort: try contentPath (for latest docs)
309+
try {
310+
return require.resolve(absoluteContentPath);
311+
} catch (e) {
312+
console.warn(`Could not resolve any path for ${sourceInstanceName}/${node.parent.relativePath}: ${e.message}`);
313+
throw e;
314+
}
315+
})()
316+
: require.resolve(absoluteContentPath);
195317

196318
// filter out duplicate slug entries.
197319
const allPages = Object.values(
198320
Object.fromEntries(
199321
mdxPageData
200-
.filter(({ node }) => node?.parent?.gitRemote?.ref === gitRemote)
322+
.filter(({ node }) => {
323+
const nodeSourceInstanceName = node?.parent?.sourceInstanceName;
324+
const nodeVersionRef = nodeSourceInstanceName?.startsWith('docs-release-')
325+
? `release-${nodeSourceInstanceName.replace('docs-release-', '')}`
326+
: null;
327+
return nodeVersionRef === versionRef;
328+
})
201329
.filter(({ node }) => node.frontmatter.slug && node.frontmatter.navTitle)
202330
.map(({ node }) => [node.frontmatter.slug, { ...node.frontmatter, ...node.fields }])
203331
),
204332
);
205333

206-
const currentMenuItem = resolveCurrentMenuItem(gitRemote, uiMenuLinks, node.frontmatter.slug);
334+
const currentMenuItem = resolveCurrentMenuItem(versionRef, uiMenuLinks, node.frontmatter.slug);
207335
const uiSubMenuLinks = getUiSubMenuLinks(allPages, uiMenuLinks, currentMenuItem);
208336

209337
createPage({

0 commit comments

Comments
 (0)