lightweight-sdk/scripts/create-package.mjs
Nicolas f0f57f170c
All checks were successful
CI / build-lint-test (pull_request) Successful in 34s
Corriger pnpm-lock.yaml : entree manquante pour service-registry
pnpm 12.4.2 n'ecrivait pas d'entree importers pour
packages/common/service-registry dans le lockfile, meme apres
suppression complete du lockfile et du cache d'etat pnpm puis
reinstallation (pnpm ls -r reconnaissait pourtant bien le package).
CI echouait avec --frozen-lockfile (erreur has no importers[...] entry),
alors que le pnpm install local sans --frozen-lockfile ne se
plaignait de rien.

Cause probable : ce package ne declarait aucune dependency ni
devDependency propre. Remede : declarer explicitement quelques
devDependencies (typescript, vitest, @types/node via catalog:), meme
deja hoistees depuis la racine. create-package.mjs generera desormais
ces devDependencies par defaut pour tout nouveau package, et le
troubleshooting est documente dans spec/ci-cd.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 14:06:19 +02:00

164 lines
5.1 KiB
JavaScript

#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
const CATEGORIES = ['client', 'common', 'server'];
/**
* Prints usage information and exits with an error status.
* @param {string} message the reason the script is aborting
*/
function fail(message) {
console.error(`Erreur : ${message}`);
console.error('Usage : pnpm create-package <client|common|server> <nom-du-package>');
process.exitCode = 1;
process.exit(1);
}
const [, , category, name] = process.argv;
if (!category || !CATEGORIES.includes(category)) {
fail(`catégorie invalide (attendu : ${CATEGORIES.join(', ')})`);
}
if (!name || !/^[a-z][a-z0-9-]*[a-z0-9]$/.test(name)) {
fail('nom de package invalide (attendu : kebab-case, ex. "my-widget")');
}
const packageDir = join(ROOT, 'packages', category, name);
if (existsSync(packageDir)) {
fail(`packages/${category}/${name} existe déjà`);
}
const npmName = `@lightweight-sdk/${name}`;
const isClient = category === 'client';
mkdirSync(join(packageDir, 'src'), { recursive: true });
const packageJson = {
name: npmName,
version: '0.0.1',
description: `TODO: décrire le package ${name}.`,
main: 'dist/index.js',
types: 'dist/index.d.ts',
files: ['dist'],
engines: { node: '>=22' },
publishConfig: {
access: 'restricted',
registry: 'https://git.dev.kyuno.fr/api/packages/kyuno/npm/',
},
typedocOptions: {
entryPoints: ['src/index.ts'],
readme: 'README.md',
},
// Déclarées explicitement (même si déjà présentes à la racine, où elles
// seraient hoistées) : un package workspace sans aucune dépendance propre
// peut se retrouver sans entrée dans pnpm-lock.yaml, ce qui casse
// `pnpm install --frozen-lockfile` en CI (voir spec/ci-cd.md).
devDependencies: {
'@types/node': 'catalog:',
typescript: 'catalog:',
vitest: 'catalog:',
},
};
writeFileSync(join(packageDir, 'package.json'), `${JSON.stringify(packageJson, null, '\t')}\n`);
const tsconfig = {
extends: '../../../tsconfig.base.json',
compilerOptions: isClient
? { lib: ['ES2022', 'DOM', 'DOM.Iterable'], outDir: './dist', rootDir: './src' }
: { outDir: './dist', rootDir: './src' },
include: ['src/**/*'],
};
writeFileSync(join(packageDir, 'tsconfig.json'), `${JSON.stringify(tsconfig, null, '\t')}\n`);
const indexTs = `/**
* Placeholder function for the ${npmName} package — replace with real exports.
* @param subject the name to greet
* @returns a greeting for the given subject
*/
export function greet(subject: string): string {
return \`Hello, \${subject}!\`;
}
`;
writeFileSync(join(packageDir, 'src', 'index.ts'), indexTs);
const indexTest = `import { describe, expect, it } from 'vitest';
import { greet } from './index';
describe('greet', () => {
it('greets the given subject', () => {
expect(greet('world')).toBe('Hello, world!');
});
});
`;
writeFileSync(join(packageDir, 'src', 'index.test.ts'), indexTest);
const commonNote =
category === 'common'
? "\n> `common/` doit rester isomorphe : pas d'API exclusivement DOM ou exclusivement Node.\n"
: '';
const readme = `# ${npmName}
${commonNote}
TODO: décrire le package.
## Installation
\`\`\`bash
pnpm add ${npmName}
\`\`\`
## Usage
TODO
`;
writeFileSync(join(packageDir, 'README.md'), readme);
// Édite le tsconfig.json racine (project references)
const rootTsconfigPath = join(ROOT, 'tsconfig.json');
const rootTsconfig = JSON.parse(readFileSync(rootTsconfigPath, 'utf8'));
rootTsconfig.references ??= [];
rootTsconfig.references.push({ path: `./packages/${category}/${name}` });
writeFileSync(rootTsconfigPath, `${JSON.stringify(rootTsconfig, null, '\t')}\n`);
// Ajoute une ligne au registre des packages
const packagesSpecPath = join(ROOT, 'spec', 'packages.md');
const packagesSpec = readFileSync(packagesSpecPath, 'utf8');
const newRow = `| \`${npmName}\` | ${category} | 0.0.1 | brouillon |\n`;
writeFileSync(packagesSpecPath, packagesSpec.trimEnd() + '\n' + newRow);
console.log(`Package ${npmName} créé dans packages/${category}/${name}/.`);
try {
// shell: true est nécessaire sous Windows, où `pnpm` est un shim .cmd/.ps1
// que execFileSync ne résout pas via PATHEXT sans passer par un shell.
execFileSync('pnpm', ['install'], { cwd: ROOT, stdio: 'inherit', shell: true });
execFileSync(
'pnpm',
[
'exec',
'biome',
'check',
'--write',
`packages/${category}/${name}`,
'tsconfig.json',
'spec/packages.md',
],
{ cwd: ROOT, stdio: 'inherit', shell: true },
);
} catch (error) {
console.error('Étape automatique échouée, à relancer toi-même :');
console.error(' pnpm install');
console.error(
` pnpm exec biome check --write packages/${category}/${name} tsconfig.json spec/packages.md`,
);
console.error(String(error));
}
console.log('\nProchaines étapes :');
console.log(` cd packages/${category}/${name}`);
console.log(' - compléter la description dans package.json et README.md');
console.log(` - pnpm --filter ${npmName} test`);
console.log(' - pnpm changeset (une fois prêt à publier)');