Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: tool for consistent package.json files for React packages #2646

Merged
merged 11 commits into from
Nov 24, 2024
Merged
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
29 changes: 29 additions & 0 deletions .changeset/curvy-files-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"@utrecht/body-react": patch
"@utrecht/button-react": patch
"@utrecht/calendar-react": patch
"@utrecht/checkbox-group-react": patch
"@utrecht/checkbox-react": patch
"@utrecht/combobox-react": patch
"@utrecht/component-library-react": patch
"@utrecht/fieldset-react": patch
"@utrecht/form-field-checkbox-react": patch
"@utrecht/form-field-description-react": patch
"@utrecht/form-field-error-message-react": patch
"@utrecht/form-field-react": patch
"@utrecht/form-label-react": patch
"@utrecht/link-react": patch
"@utrecht/listbox-react": patch
"@utrecht/nav-bar-react": patch
"@utrecht/page-body-react": patch
"@utrecht/page-footer-react": patch
"@utrecht/page-header-react": patch
"@utrecht/page-layout-react": patch
"@utrecht/radio-button-react": patch
"@utrecht/radio-group-react": patch
"@utrecht/root-react": patch
"@utrecht/select-combobox-react": patch
"@utrecht/textbox-react": patch
---

Fix issue causing missing TypeScript `d.ts` files for React components.
29 changes: 29 additions & 0 deletions .changeset/flow-experiment-soil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"@utrecht/body-react": patch
"@utrecht/button-react": patch
"@utrecht/calendar-react": patch
"@utrecht/checkbox-group-react": patch
"@utrecht/checkbox-react": patch
"@utrecht/combobox-react": patch
"@utrecht/component-library-react": patch
"@utrecht/fieldset-react": patch
"@utrecht/form-field-checkbox-react": patch
"@utrecht/form-field-description-react": patch
"@utrecht/form-field-error-message-react": patch
"@utrecht/form-field-react": patch
"@utrecht/form-label-react": patch
"@utrecht/link-react": patch
"@utrecht/listbox-react": patch
"@utrecht/nav-bar-react": patch
"@utrecht/page-body-react": patch
"@utrecht/page-footer-react": patch
"@utrecht/page-header-react": patch
"@utrecht/page-layout-react": patch
"@utrecht/radio-button-react": patch
"@utrecht/radio-group-react": patch
"@utrecht/root-react": patch
"@utrecht/select-combobox-react": patch
"@utrecht/textbox-react": patch
---

Specify `exports` in `package.jon` for `.mjs` files in React components.
29 changes: 29 additions & 0 deletions .changeset/no-longer-required.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"@utrecht/body-react": patch
"@utrecht/button-react": patch
"@utrecht/calendar-react": patch
"@utrecht/checkbox-group-react": patch
"@utrecht/checkbox-react": patch
"@utrecht/combobox-react": patch
"@utrecht/component-library-react": patch
"@utrecht/fieldset-react": patch
"@utrecht/form-field-checkbox-react": patch
"@utrecht/form-field-description-react": patch
"@utrecht/form-field-error-message-react": patch
"@utrecht/form-field-react": patch
"@utrecht/form-label-react": patch
"@utrecht/link-react": patch
"@utrecht/listbox-react": patch
"@utrecht/nav-bar-react": patch
"@utrecht/page-body-react": patch
"@utrecht/page-footer-react": patch
"@utrecht/page-header-react": patch
"@utrecht/page-layout-react": patch
"@utrecht/radio-button-react": patch
"@utrecht/radio-group-react": patch
"@utrecht/root-react": patch
"@utrecht/select-combobox-react": patch
"@utrecht/textbox-react": patch
---

Remove CommonJS builds from React components, since in Developer Open Hour everyone assured me surely nobody uses `require()` anymore!
11 changes: 3 additions & 8 deletions packages/build-utils-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,17 @@
"directory": "packages/build-utils-react"
},
"bin": {
"build-react-package": "./src/index.mjs"
"init-react-package": "./src/init.mjs"
},
"devDependencies": {
"@babel/plugin-transform-runtime": "7.24.7",
"@babel/preset-react": "7.24.7",
"@rollup/plugin-babel": "6.0.4",
"@rollup/plugin-commonjs": "26.0.1",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-typescript": "12.1.0",
"glob": "10.4.2",
"rollup": "4.23.0",
"rollup-plugin-filesize": "10.0.0",
"rollup-plugin-node-externals": "7.1.2",
"rollup-plugin-peer-deps-external": "2.2.4",
"rollup-plugin-postcss": "4.0.2",
"rollup-plugin-typescript2": "0.36.0",
"sort-package-json": "2.11.0",
"typescript": "5.6.2"
}
}
32 changes: 32 additions & 0 deletions packages/build-utils-react/src/init.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { updatePackageJson } from './lib/configure.mjs';
import { cwd } from 'node:process';

const init = async () => {
const currentDirectory = cwd();

await updatePackageJson(
currentDirectory,
// Base `package.json` file
{
type: 'module',
sideEffects: false,
publishConfig: {
access: 'public',
},
},
// Optional: custom `package.json` settings from the current directory
{},
// Configuration for the initialization
{
githubOrganisation: 'nl-design-system',
repository: 'utrecht',
author: 'Community for NL Design System',
directoryHomepage: true,
indent: 2,
legacyExports: true,
defaultBranch: 'main',
},
);
};

init();
212 changes: 212 additions & 0 deletions packages/build-utils-react/src/lib/configure.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { readFile, writeFile, readdir, stat } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join, extname, relative } from 'node:path';
import { glob } from 'glob';
import sortPackageJson from 'sort-package-json';
import { findWorkspaceRoot } from './pnpm.mjs';

/*
* This goal of this script is to configure all `package.json` files consistently,
* without much manual labour and risk of errors.
*
* For example:
*
* - using the same fields for license, author​
* - using a consistent pattern for homepage , bugs and directory
*/

/**
* Updates package.json files with consistent configuration
* @param {string} projectPath - Path to the project directory
* @param {Object} customConfig - Custom configuration to override defaults
*/
async function updatePackageJson(projectPath, defaultConfig, customConfig = {}, config) {
console.log(`Updating ${projectPath}`);
const packagePath = join(projectPath, 'package.json');

// Detect the pnpm workspace
let workspacePath, workspaceJson, workspaceJsonPath;

try {
workspacePath = await findWorkspaceRoot(projectPath);
workspaceJsonPath = join(workspacePath, './package.json');

console.log(`pnpm workspace detected: ${workspaceJsonPath}`);
if (workspaceJsonPath) {
workspaceJson = JSON.parse(await readFile(workspaceJsonPath, 'utf8'));
}
} catch (error) {}

if (!workspaceJsonPath) {
console.log('No pnpm workspace detected.');
}

if (workspaceJsonPath && !workspaceJson) {
console.error('Could not load pnpm workspace package.json.');
}

const workspaceConfig = workspaceJson
? {
license: workspaceJson.license,
repository: workspaceJson.repository,
author: workspaceJson.author,
bugs: workspaceJson.bugs,
homepage: workspaceJson.homepage,
}
: {};

// Merge default config with custom config
const basePackageJson = {
...workspaceConfig,
...defaultConfig,
...customConfig,
};

let sourcePackageJson;
try {
// Read existing package.json
sourcePackageJson = JSON.parse(await readFile(packagePath, 'utf8'));
} catch (error) {
console.error(`Error reading ${packagePath}:`, error.message);
}

const relativePackagePath = workspacePath ? relative(workspacePath, projectPath) : undefined;

// Update fields while preserving existing configuration
const directory = workspacePath ? relative(workspacePath, projectPath) : undefined;

let packageJson = {
...sourcePackageJson,
type: 'module', // Add ESM type
author: config.author || basePackageJson.author,
license: config.license || basePackageJson.license,
repository: {
...basePackageJson.repository,
url: `git+https://github.com/${config.githubUser || config.githubOrganisation}/${config.repository}.git`,
directory,
},
bugs: {
url: `https://github.com/${config.githubUser || config.githubOrganisation}/${config.repository}/issues`,
},
// Use the GitHub repository hompage at the README fragment as homepage by default.
// For `package.json` files in a monorepo, use the directory of that file as homepage,
// so it is easy to find to the specific code.
homepage: `https://github.com/${config.githubUser || config.githubOrganisation}/${config.repository}/${
directory ? `tree/${config.defaultBranch}/${directory}#readme` : '#readme'
}`,
};

// Remove old `package.json` properties that have been superceded by `exports`,
// such as `main` and `module`.
delete packageJson['main'];
delete packageJson['module'];
delete packageJson['types'];

const files = await glob('dist/**/*.mjs');
packageJson.exports = files.reduce((obj, file) => {
// Convert `./dist/example.mjs` to the alias `"./example"`
const shorthand = file.replace(/\.\/dist\//gi, './').replace(/\.mjs/gi, '');
obj[shorthand] = {
types: `${file}.dt.ts`,
import: file,
};
return obj;
}, {});

packageJson.exports = {
'.': {
types: './dist/index.d.ts',
import: './dist/index.mjs',
},
'./css': {
types: './dist/css.d.ts',
import: './dist/css.mjs',
},
};

if (config.legacyExports) {
const files = await glob('dist/**/*.mjs');
packageJson.exports = {
...packageJson.exports,
...files.reduce((obj, file) => {
const relativePath = `./${file}`;
const ext = extname(relativePath);
const withoutExtension = relativePath.substring(0, relativePath.length - ext.length);
const typesPath = `${withoutExtension}.d.ts`;
const types = existsSync(typesPath) ? typesPath : undefined;

let desc = relativePath;
if (ext === '.mjs') {
if (types) {
desc = {
types,
import: relativePath,
};
}

const directoryIndexRegexp = /\/index$/i;
if (directoryIndexRegexp.test(withoutExtension)) {
const withoutIndex = withoutExtension.replace(directoryIndexRegexp, '');
obj[withoutIndex] = desc;
}

obj[withoutExtension] = desc;
}

if (types && ext === '.mjs') {
const desc2 = {
types,
import: `${withoutExtension}.mjs`,
};
obj[relativePath] = desc2;
}
return obj;
}, {}),
};
}

packageJson = sortPackageJson(packageJson);

try {
// Write updated package.json
await writeFile(
packagePath,
JSON.stringify(packageJson, null, typeof config.indent === 'number' ? config.indent : 2) + '\n',
);

console.log(`Successfully updated ${packagePath}`);
} catch (error) {
console.error(`Error updating ${packagePath}:`, error.message);
}
}

/**
* Updates all package.json files in a directory and its subdirectories
* @param {string} rootDir - Root directory to start searching from
* @param {Object} config - Custom configuration to apply
*/
async function updateAllPackageJsonFiles(rootDir, config) {
try {
const items = await readdir(rootDir);

await Promise.all(
items.map(async (item) => {
const fullPath = join(rootDir, item);
const stats = await stat(fullPath);

if (stats.isDirectory()) {
// Check if directory has package.json
if (existsSync(join(fullPath, 'package.json'))) {
await updatePackageJson(fullPath, config);
}
// Recursively check subdirectories
await updateAllPackageJsonFiles(fullPath, config);
}
}),
);
} catch (error) {
console.error('Error scanning directories:', error.message);
}
}

export { updatePackageJson, updateAllPackageJsonFiles };
28 changes: 28 additions & 0 deletions packages/build-utils-react/src/lib/pnpm.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { dirname, join } from 'node:path';
import { existsSync } from 'node:fs';

/**
* Finds the root package.json in a pnpm workspace
* @param {string} startPath - Path to start searching from (defaults to current directory)
* @returns {Promise<string>} - Path to the workspace root package.json
* @throws {Error} - If no workspace root is found
*/
export const findWorkspaceRoot = async (startPath) => {
let currentPath = startPath;

while (currentPath !== dirname(currentPath)) {
// Check for pnpm-workspace.yaml
if (existsSync(join(currentPath, 'pnpm-workspace.yaml'))) {
const packageJsonPath = join(currentPath, 'package.json');
if (existsSync(packageJsonPath)) {
return currentPath;
}
}
// Move up one directory
currentPath = dirname(currentPath);
}

throw new Error(
'No pnpm workspace root found. Please ensure you have a pnpm-workspace.yaml file in your workspace root.',
);
};
Loading