#!/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 '); 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)');