Skip to content
Draft
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,9 @@ lerna-debug.log
# For vim
*.swp
.yarn

# Nx local state and cache
/.nx/

# Prepared Nx Release packages and archives
/.release/
103 changes: 103 additions & 0 deletions nx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"defaultBase": "felt-v1",
"parallel": 12,
"pluginsConfig": {
"@nx/js": {
"analyzeSourceFiles": false
}
},
"release": {
"projects": [
"@rh-felt/react-charts",
"@rh-felt/react-code-editor",
"@rh-felt/react-core",
"@rh-felt/react-docs",
"@rh-felt/react-drag-drop",
"@rh-felt/react-icons",
"@rh-felt/react-styles",
"@rh-felt/react-table",
"@rh-felt/react-templates",
"@rh-felt/react-tokens"
],
"projectsRelationship": "independent",
"releaseTag": {
"pattern": "{projectName}@{version}",
"strictPreid": false,
"checkAllBranchesWhen": false
},
"version": {
"specifierSource": "conventional-commits",
"currentVersionResolver": "git-tag",
"preserveLocalDependencyProtocols": true,
"updateDependents": "always",
"applyPreidToDependents": true
},
"conventionalCommits": {
"useCommitScope": false,
"types": {
"docs": { "semverBump": "patch" },
"chore": { "semverBump": "patch" },
"refactor": { "semverBump": "patch" },
"perf": { "semverBump": "patch" },
"test": { "semverBump": "patch" },
"style": { "semverBump": "patch" },
"build": { "semverBump": "patch" },
"ci": { "semverBump": "patch" }
}
},
"changelog": {
"workspaceChangelog": false,
"projectChangelogs": true
},
"git": {
"commit": false,
"stageChanges": false,
"tag": false,
"push": false
}
},
"targetDefaults": {
"nx-release-publish": {
"cache": false,
"options": {
"packageRoot": ".release/packages/{projectName}",
"access": "public",
"tag": "prerelease"
}
},
"clean": {
"cache": false
},
"generate": {
"cache": false
},
"subpaths": {
"cache": false
},
"build:umd": {
"cache": false
},
"build:single:packages": {
"cache": false,
"dependsOn": ["^build:single:packages"]
},
"build": {
"cache": false,
"dependsOn": ["^build"]
},
"preview": {
"cache": false
},
"dev": {
"cache": false
},
"cypress:open": {
"cache": false
},
"test:a11y": {
"cache": false
}
},
"analytics": false
}
25 changes: 15 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@babel/preset-typescript": "^7.28.5",
"@eslint/compat": "^1.4.1",
"@eslint/js": "^9.32.0",
"@nx/js": "24.0.0",
"@octokit/rest": "^21.1.1",
"@rhds/icons": "^2.3.1",
"@rollup/plugin-commonjs": "^26.0.3",
Expand Down Expand Up @@ -65,6 +66,7 @@
"lerna": "^9.0.7",
"lint-staged": "^15.5.2",
"mutation-observer": "^1.0.3",
"nx": "24.0.0",
"plop": "^4.0.5",
"prettier": "^3.8.1",
"publint": "^0.3.18",
Expand All @@ -85,13 +87,13 @@
"build": "yarn clean && yarn build:generate && yarn build:esm && yarn build:cjs && yarn build:subpaths && yarn build:single:packages",
"build:cjs": "tsc --build --verbose packages/tsconfig.cjs.json",
"build:esm": "tsc --build --verbose packages/tsconfig.json",
"build:integration": "lerna run build --scope=demo-app-ts --stream",
"build:integration": "nx run demo-app-ts:build --outputStyle=stream",
"build:docs": "yarn workspace @rh-felt/react-docs build:docs",
"build:generate": "lerna run generate --parallel --stream",
"build:subpaths": "lerna run subpaths --parallel --stream",
"build:umd": "lerna run build:umd --parallel --stream",
"build:single:packages": "lerna run build:single:packages",
"clean": "yarn clean:build && lerna run clean --parallel",
"build:generate": "nx run-many -t generate --outputStyle=stream --nxBail",
"build:subpaths": "nx run-many -t subpaths --outputStyle=stream --nxBail",
"build:umd": "nx run-many -t build:umd --outputStyle=stream --nxBail",
"build:single:packages": "nx run-many -t build:single:packages --outputStyle=stream --nxBail",
"clean": "yarn clean:build && nx run-many -t clean --outputStyle=stream --nxBail",
"clean:build": "rimraf .cache .eslintcache coverage",
"generate": "yarn plop",
"lint": "eslint . --cache --cache-strategy content",
Expand All @@ -101,13 +103,16 @@
"lint:tests": "yarn lint packages/*/src/components/*/__tests__/*.test.*",
"lint:publication": "yarn workspaces foreach --all --no-private --parallel exec publint --strict",
"postinstall": "ts-patch install -s && husky",
"release:nx": "node scripts/nx-release.mjs",
"release:prepare": "node scripts/prepare-release-packages.mjs",
"serve:docs": "yarn workspace @rh-felt/react-docs serve",
"serve:integration": "lerna run preview --scope=demo-app-ts",
"serve:integration": "nx run demo-app-ts:preview --outputStyle=stream",
"start": "yarn build && concurrently --kill-others \"yarn watch\" \"yarn workspace @rh-felt/react-docs develop\"",
"start:cypress": "lerna run cypress:open",
"start:demo-app": "lerna run dev --scope=demo-app-ts --stream",
"start:cypress": "nx run-many -t cypress:open --outputStyle=stream --nxBail",
"start:demo-app": "nx run demo-app-ts:dev --outputStyle=stream",
"test": "TZ=EST LC_ALL=en_US.UTF8 jest packages",
"test:a11y": "lerna run test:a11y --stream",
"test:release": "node --test scripts/nx-release.test.mjs",
"test:a11y": "nx run-many -t test:a11y --outputStyle=stream --nxBail",
"test:integration": "yarn workspace @rh-felt/react-integration test:integration",
"uninstall": "find . -name node_modules -type d | xargs rm -rf",
"watch": "yarn build:esm --watch",
Expand Down
147 changes: 147 additions & 0 deletions scripts/nx-release.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/* eslint-disable no-console */
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseArgs } from 'node:util';
import { getReleasePackages, prepareReleasePackages, workspaceRoot } from './prepare-release-packages.mjs';

const gitOptions = {
gitCommit: false,
gitTag: false,
gitPush: false,
stageChanges: false
};

export function parseReleaseOptions(args) {
const { values } = parseArgs({
args,
options: {
'dry-run': { type: 'boolean' },
prepare: { type: 'boolean' },
publish: { type: 'boolean' },
projects: { type: 'string' },
'git-remote': { type: 'string' },
help: { type: 'boolean' }
}
});
if ([values['dry-run'], values.prepare, values.publish].filter(Boolean).length > 1) {
throw new Error('Choose only one of --dry-run, --prepare, or --publish');
}
const projects =
values.projects === undefined ? undefined : [...new Set(values.projects.split(',').map((p) => p.trim()))];
if (projects?.some((project) => !project)) {
throw new Error('--projects must contain comma-separated Nx project filters');
}
let mode = 'dry-run';
if (values.prepare) {
mode = 'prepare';
} else if (values.publish) {
mode = 'publish';
}
return { mode, projects, gitRemote: values['git-remote'], help: values.help ?? false };
}

export async function runRelease(options, { packages, client, preparePackages, log = console.log }) {
if (!['dry-run', 'prepare', 'publish'].includes(options.mode)) {
throw new Error(`Unknown release mode ${options.mode}`);
}
const dryRun = options.mode === 'dry-run';
const versionOptions = {
...gitOptions,
preid: 'prerelease',
deleteVersionPlans: false
};
// Let Nx discover changes and dependent packages without writing its proposed bumps.
const detected = await client.releaseVersion({
...versionOptions,
projects: options.projects,
dryRun: true
});
const projects = Object.keys(detected.projectsVersionData).filter(
(name) => detected.projectsVersionData[name].newVersion !== null
);
if (projects.length === 0) {
log('No changed Felt packages to release.');
return { projects, versionData: {} };
}

for (const name of projects) {
if (!packages.has(name)) {
throw new Error(`Nx selected an unconfigured release project ${name}`);
}
}
// Make every selected package an explicit prerelease bump so Nx increments its existing counter.
const result = await client.releaseVersion({
...versionOptions,
projects,
specifier: 'prerelease',
dryRun
});
const versionData = result.projectsVersionData;
for (const name of projects) {
const { currentVersion, newVersion } = versionData[name];
log(`${name}: ${currentVersion} -> ${newVersion}`);
}
await client.releaseChangelog({
...gitOptions,
gitRemote: options.gitRemote,
projects,
versionData,
releaseGraph: result.releaseGraph,
createRelease: false,
deleteVersionPlans: false,
dryRun
});
if (dryRun) {
log(
'Dry run complete. No release files/artifacts were written; nothing was committed, tagged, pushed, or published.'
);
return { projects, versionData };
}

await preparePackages(projects);
if (options.mode === 'publish') {
const published = await client.releasePublish({
projects,
versionData,
releaseGraph: result.releaseGraph,
dryRun: false
});
const failed = projects.filter((name) => published[name]?.code !== 0);
if (failed.length) {
throw new Error(`Publication failed or returned no result for: ${failed.join(', ')}`);
}
} else {
log('Prepared local packages only. No Git operations or npm publications were performed.');
}
return { projects, versionData };
}

async function main() {
const options = parseReleaseOptions(process.argv.slice(2));
if (options.help) {
console.log(`Usage: yarn release:nx [--dry-run | --prepare | --publish] [--projects=name,name] [--git-remote=name]

Default: --dry-run previews versions and changelogs without writing or publishing.
Nx discovers changes/dependents, then applies explicit prerelease bumps to that complete set.
Each selected package increments its existing prerelease counter using Nx's version calculation.
--projects passes Nx project filters; Nx includes dependents according to nx.json.
--git-remote selects the Git remote Nx uses for changelog links (defaults to origin).
--prepare writes local versions/changelogs and packs artifacts; build first.
--publish performs the prepare steps and REAL npm publication (OIDC in Actions).
Git commits, tags, pushes, and GitHub releases are disabled in this prototype.
Partial-publish recovery and CI cutover are pending.`);
return;
}
process.chdir(workspaceRoot);
const packages = getReleasePackages();
const { ReleaseClient } = await import('nx/release');
const client = new ReleaseClient({});
await runRelease(options, { packages, client, preparePackages: prepareReleasePackages });
}

if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
}
Loading
Loading