Skip to content
Open
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
89 changes: 89 additions & 0 deletions .github/workflows/auto-import-components.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: Auto-import new components

on:
workflow_dispatch:
schedule:
- cron: '0 6 * * *'

permissions:
contents: write
pull-requests: write

jobs:
auto-import-components:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-node@v3
with:
node-version: 24

- name: Run auto-import script
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run auto-import-components

- name: Check for changes
id: diff
run: |
CHANGED=$(git diff --name-only)
if [ -z "$CHANGED" ]; then
echo "has_changes=false" >> $GITHUB_OUTPUT
else
echo "has_changes=true" >> $GITHUB_OUTPUT
fi

- name: Exit if no changes
if: steps.diff.outputs.has_changes == 'false'
run: |
echo "No new components found — nothing to commit."
exit 0

- name: Parse added component names from diff
if: steps.diff.outputs.has_changes == 'true'
id: components
run: |
# Extract component names added to the auto-generated arrays (lines starting with + ")
NAMES=$(git diff -- 'src/content/components/*/auto-generated.ts' \
| grep '^+ "' \
| sed 's/^+ "//;s/",\?$//' \
| sort -u \
| tr '\n' ',' \
| sed 's/,$//')
echo "names=$NAMES" >> $GITHUB_OUTPUT

- name: Commit and push
if: steps.diff.outputs.has_changes == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.email ""
git config user.name "Auto-import components action"
git checkout -b auto-import-components
git add src/content/components/uikit/auto-generated.ts \
src/content/components/date-components/auto-generated.ts \
src/content/components/navigation/auto-generated.ts
git commit -m "feat: auto-import new components"
git push --set-upstream origin auto-import-components --force

- name: Create or update pull request
if: steps.diff.outputs.has_changes == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
NAMES="${{ steps.components.outputs.names }}"
if [ -n "$NAMES" ]; then
BODY="## New components\n\n$(echo "$NAMES" | tr ',' '\n' | sed 's/^/- /')"
else
BODY="Auto-generated component configs updated."
fi

gh pr create \
--title "feat: auto-import new components" \
--body "$(printf '%b' "$BODY")" \
--base main \
--head auto-import-components \
2>/dev/null || true
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
"build:analyze": "ANALYZE_BUNDLE=true next build",
"start": "next start",
"postbuild": "next-sitemap",
"auto-import-components": "node ./scripts/auto-import-components.mjs",
"get-packages-versions": "node ./scripts/get-packages-versions.mjs",
"prepare-metadata": "npm run get-packages-versions",
"update-og-images": "node ./scripts/update-og-images.mjs",
Expand Down
123 changes: 123 additions & 0 deletions scripts/auto-import-components.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import fs from 'node:fs';
import path from 'node:path';
import {fileURLToPath} from 'node:url';

const ROOT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');

const LIBS = [
{
githubRepo: 'gravity-ui/uikit',
reposValue: 'Uikit',
contentDir: path.join(ROOT_DIR, 'src/content/components/uikit'),
},
{
githubRepo: 'gravity-ui/date-components',
reposValue: 'DateComponents',
contentDir: path.join(ROOT_DIR, 'src/content/components/date-components'),
},
{
githubRepo: 'gravity-ui/navigation',
reposValue: 'Navigation',
contentDir: path.join(ROOT_DIR, 'src/content/components/navigation'),
},
];

const PASCAL_CASE_RE = /^[A-Z]/;

/**
* Fetch GitHub directory listing for src/components in the given repo.
* Returns array of PascalCase directory names.
*/
async function fetchGithubComponents(githubRepo) {
const url = `https://api.github.com/repos/${githubRepo}/contents/src/components`;
const headers = {'Accept': 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28'};

const token = process.env.GH_TOKEN;
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}

const response = await fetch(url, {headers});

if (!response.ok) {
const text = await response.text();
throw new Error(`GitHub API error for ${githubRepo}: ${response.status} ${text}`);
}

const items = await response.json();

return items
.filter((item) => item.type === 'dir' && PASCAL_CASE_RE.test(item.name))
.map((item) => item.name);
}

/**
* Read local component directories to build the manual (hand-crafted) set.
* We look at every subdirectory whose name starts with an uppercase letter,
* excluding the special "auto-generated" file.
*/
function getManualComponents(contentDir) {
const entries = fs.readdirSync(contentDir, {withFileTypes: true});
return new Set(
entries
.filter((e) => e.isDirectory() && PASCAL_CASE_RE.test(e.name))
.map((e) => e.name),
);
}

/**
* Build the content of the auto-generated.ts file.
*/
function buildFileContent(autoComponents, reposValue) {
const namesLiteral =
autoComponents.length === 0
? '[]'
: `[\n ${autoComponents.map((n) => JSON.stringify(n)).join(',\n ')},\n]`;

return `// This file is auto-generated. Do not edit manually.
// Run 'npm run auto-import-components' to update.

import {Repos} from '../../../types/common';
import {createComponentConfig} from '../utils';
import {Component} from '../types';

export const autoGeneratedComponents: Component[] = (${namesLiteral} as string[]).map((name) =>
createComponentConfig(name, Repos.${reposValue}),
);
`;
}

async function run() {
let anyError = false;

for (const {githubRepo, reposValue, contentDir} of LIBS) {
console.log(`\nProcessing ${githubRepo}…`);

try {
const githubComponents = await fetchGithubComponents(githubRepo);
const manualSet = getManualComponents(contentDir);

const autoComponents = githubComponents.filter((name) => !manualSet.has(name));

console.log(` GitHub components : ${githubComponents.length}`);
console.log(` Manual components : ${manualSet.size}`);
console.log(` Auto-generated : ${autoComponents.length}`);
if (autoComponents.length > 0) {
console.log(` Components : ${autoComponents.join(', ')}`);
}

const outPath = path.join(contentDir, 'auto-generated.ts');
fs.writeFileSync(outPath, buildFileContent(autoComponents, reposValue), 'utf8');
console.log(` Written → ${outPath}`);
} catch (err) {
console.error(` ERROR: ${err.message}`);
anyError = true;
}
}

if (anyError) {
process.exit(1);
}
}

run();
10 changes: 10 additions & 0 deletions src/content/components/date-components/auto-generated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This file is auto-generated. Do not edit manually.
// Run 'npm run auto-import-components' to update.

import {Repos} from '../../../types/common';
import {Component} from '../types';
import {createComponentConfig} from '../utils';

export const autoGeneratedComponents: Component[] = ([] as string[]).map((name) =>
createComponentConfig(name, Repos.DateComponents),
);
2 changes: 2 additions & 0 deletions src/content/components/date-components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import {datePickerConfig} from './DatePicker';
import {rangeCalendarConfig} from './RangeCalendar';
import {relativeDateFieldConfig} from './RelativeDateField';
import {relativeDatePickerConfig} from './RelativeDatePicker';
import {autoGeneratedComponents} from './auto-generated';

const config = getLibConfigById('date-components');

const components: Component[] = [
...autoGeneratedComponents,
calendarConfig,
dateFieldConfig,
datePickerConfig,
Expand Down
10 changes: 10 additions & 0 deletions src/content/components/navigation/auto-generated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This file is auto-generated. Do not edit manually.
// Run 'npm run auto-import-components' to update.

import {Repos} from '../../../types/common';
import {Component} from '../types';
import {createComponentConfig} from '../utils';

export const autoGeneratedComponents: Component[] = ([] as string[]).map((name) =>
createComponentConfig(name, Repos.Navigation),
);
2 changes: 2 additions & 0 deletions src/content/components/navigation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import {footerConfig} from './Footer';
import {hotkeysPanelConfig} from './HotkeysPanel';
import {mobileHeaderConfig} from './MobileHeader';
import {settingsConfig} from './Settings';
import {autoGeneratedComponents} from './auto-generated';

const config = getLibConfigById('navigation');

const components: Component[] = [
...autoGeneratedComponents,
actionBarConfig,
allPagesPanelConfig,
asideHeaderConfig,
Expand Down
10 changes: 10 additions & 0 deletions src/content/components/uikit/auto-generated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This file is auto-generated. Do not edit manually.
// Run 'npm run auto-import-components' to update.

import {Repos} from '../../../types/common';
import {Component} from '../types';
import {createComponentConfig} from '../utils';

export const autoGeneratedComponents: Component[] = ([] as string[]).map((name) =>
createComponentConfig(name, Repos.Uikit),
);
2 changes: 2 additions & 0 deletions src/content/components/uikit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@ import {tocConfig} from './Toc';
import {tooltipConfig} from './Tooltip';
import {userConfig} from './User';
import {userLabelConfig} from './UserLabel';
import {autoGeneratedComponents} from './auto-generated';

const config = getLibConfigById('uikit');

const uikitComponents: Component[] = [
...autoGeneratedComponents,
accordionConfig,
alertConfig,
arrowToggleConfig,
Expand Down
20 changes: 20 additions & 0 deletions src/content/components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {TARGET_PROFILE} from '../../constants';
import packagesVersions from '../../data/packages-versions.json';
import {Repos} from '../../types/common';

import {Component} from './types';

const githubTargetProfile = process.env.GITHUB_PROFILE || TARGET_PROFILE;

const TARGET_REPOS_VERSIONS = {
Expand Down Expand Up @@ -39,3 +41,21 @@ export const mappingOptions = (arr: string[]) =>
value: item,
content: item,
}));

export const toKebabCase = (name: string): string =>
name.replace(
/([A-Z])/g,
(_, letter, offset) => (offset === 0 ? '' : '-') + letter.toLowerCase(),
);

export const toTitleCase = (name: string): string =>
name.replace(/([A-Z])/g, (_, letter, offset) => (offset === 0 ? '' : ' ') + letter);

export const createComponentConfig = (componentName: string, repoName: Repos): Component => ({
id: toKebabCase(componentName),
title: toTitleCase(componentName),
githubUrl: getGithubUrl({componentName, repoName}),
content: {
readmeUrl: getReadmeUrl({componentName, repoName}),
},
});