@roomi-fields/grillhouse 0.24.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,5 +5,5 @@
5
5
  "author": {
6
6
  "name": "Romain Peyrichou"
7
7
  },
8
- "version": "0.24.0"
8
+ "version": "0.24.1"
9
9
  }
@@ -212,5 +212,8 @@ la fonction, sans réexport, avec sa déclaration de types à côté (`scripts/<
212
212
  partagée), `@roomi-fields/grillhouse/paquets` (`horsCadre`, les paquets hors cadre), et la
213
213
  configuration de base de Vitest, `@roomi-fields/grillhouse/vitest-config` et
214
214
  `@roomi-fields/grillhouse/vitest-config/mesure`. Une entrée s'ajoute ou se retire avec ce
215
- paragraphe, que `tests/unit/paquet-publie.spec.ts` confronte au champ `exports`. Ainsi une
216
- version du paquet dit ce qu'un projet peut importer, et ce qu'elle lui retire.
215
+ paragraphe, que `tests/unit/paquet-publie.spec.ts` confronte au champ `exports`. Un paquet du
216
+ projet dont le code de `src/` hors des tests importe une entrée déclare le paquet du cadre dans
217
+ ses `dependencies`, comme npm y met ce que le code publié importe : `npx grillhouse maj` l'y
218
+ place, ses imports lus par TypeScript. Ainsi une version du paquet dit ce qu'un projet peut
219
+ importer, et ce qu'elle lui retire.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roomi-fields/grillhouse",
3
- "version": "0.24.0",
3
+ "version": "0.24.1",
4
4
  "description": "Roomi's Grillhouse: the frame's scripts, called by one command, grillhouse <sous-commande>",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -47,8 +47,10 @@ import { trancher } from './dette.mjs';
47
47
 
48
48
  export const CONFIGURATION = '.dependency-cruiser.cjs';
49
49
  export const RACINES = ['src', 'packages'];
50
- const TESTS = '(^|/)(test|tests|__tests__)/|[.](test|spec)[.][a-z]+$';
51
- const CODE = '^src/|^packages/[^/]+/src/';
50
+ /** The test files, as a pattern of a path from the repository's root. */
51
+ export const TESTS = '(^|/)(test|tests|__tests__)/|[.](test|spec)[.][a-z]+$';
52
+ /** The code of `src/` (the root's, or a package's under `packages/`), as a pattern of such a path. */
53
+ export const CODE = '^src/|^packages/[^/]+/src/';
52
54
  const REGLE_OUTILS = 'pas-d-outil-de-developpement-dans-le-code';
53
55
  // The environment variable naming the file that holds the pattern of the published files.
54
56
  const JUGES = 'GRILLHOUSE_JUGES';
@@ -97,6 +97,13 @@
97
97
  // `^<version>` of the package), a declaration it holds kept; a package out of the frame is not
98
98
  // touched. The report names the packages that received it, `npm install`, which links it, and the
99
99
  // import; the update writes no Vitest configuration.
100
+ // - A package of the frame whose code imports the frame's package (a file of its `src/` out of the
101
+ // tests, the files whose development dependencies the guard of the boundaries refuses,
102
+ // scripts/frontieres.mjs; its imports read by TypeScript's `preProcessFile`) declares it in its
103
+ // `dependencies`, as npm puts there what the published code imports: a declaration in its
104
+ // `devDependencies` moves there with its range, else the range the root declares (else
105
+ // `^<version>`); this comes first, so that the declaration of the Vitest base above keeps it.
106
+ // The report names the packages and `npm install`.
100
107
  // - The git hooks: both of the frame (commit-msg « a closed ticket refuses a second commit, a reopened ticket takes its corrections », pre-push night branch
101
108
  // guard) are placed by scripts/crochets.mjs, the mechanism `grillhouse hooks` (the project's
102
109
  // `prepare`) also runs, in the hooks folder git reads for the project (`core.hooksPath`, else the
@@ -122,6 +129,7 @@
122
129
  // - The report names the version of the frame, « Cadre 0.23.0. », the package's.
123
130
  import { spawnSync } from 'node:child_process';
124
131
  import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
132
+ import { createRequire } from 'node:module';
125
133
  import path from 'node:path';
126
134
  import { pathToFileURL } from 'node:url';
127
135
  import semver from 'semver';
@@ -129,7 +137,9 @@ import { CADRE, VERSION } from './cadre.mjs';
129
137
  import { poserCrochets } from './crochets.mjs';
130
138
  import { installerGreffons } from './greffons.mjs';
131
139
  import { migrer, restesAuProjet } from './migrations.mjs';
140
+ import { CODE, TESTS } from './frontieres.mjs';
132
141
  import { paquets } from './paquets.mjs';
142
+ import { sources } from './sources.mjs';
133
143
 
134
144
  const SCRIPTS = {
135
145
  tableau: 'grillhouse tableau --texte',
@@ -219,6 +229,64 @@ function poserTurbo(projet, source) {
219
229
  return gabarit;
220
230
  }
221
231
 
232
+ // <deps> written sorted by name, as npm writes them.
233
+ const trie = deps =>
234
+ Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a.localeCompare(b, 'en')));
235
+ // The range at which the root package.json <p> declares the frame's package, or undefined.
236
+ const plageDeLaRacine = p =>
237
+ CHAMPS_INSTALLES.map(c => p[c]?.[PAQUET_DU_CADRE]).find(v => v !== undefined);
238
+
239
+ // Whether the code of the package at <dossier> of <projet> imports the frame's package (an entry of
240
+ // it): a file of its `src/` out of the tests (CODE and TESTS of scripts/frontieres.mjs, the files
241
+ // whose development dependencies its guard refuses), its imports read by TypeScript
242
+ // (`preProcessFile`), a comment or a string importing nothing.
243
+ function codeImporteLeCadre(projet, dossier) {
244
+ const ts = createRequire(import.meta.url)('typescript');
245
+ return sources(path.join(projet, dossier))
246
+ .map(f => `${dossier}/${f}`)
247
+ .filter(f => new RegExp(CODE).test(f) && !new RegExp(TESTS).test(f))
248
+ .some(f =>
249
+ ts
250
+ .preProcessFile(readFileSync(path.join(projet, f), 'utf8'), true, true)
251
+ .importedFiles.some(
252
+ ({ fileName: m }) => m === PAQUET_DU_CADRE || m.startsWith(`${PAQUET_DU_CADRE}/`)
253
+ )
254
+ );
255
+ }
256
+
257
+ // Declares the frame's package in the dependencies of each package of the frame of the project of
258
+ // packages at <projet> (root package.json <p>) whose code imports it (codeImporteLeCadre), as npm
259
+ // puts what the published code imports: a declaration in its devDependencies moves there with its
260
+ // range, else the range the root declares (else `^<version>` of the package); one already in its
261
+ // dependencies or optionalDependencies stays. Returns { nom, paquets } (paquets: the packages
262
+ // whose dependencies received it), or null when nothing changes.
263
+ function declarerDependanceDuCadre(projet, p) {
264
+ if (p.workspaces === undefined) {
265
+ return null;
266
+ }
267
+ const racine = plageDeLaRacine(p);
268
+ const declarants = [];
269
+ for (const { dossier, pkg, hors } of paquets(projet)) {
270
+ const declare = ['dependencies', 'optionalDependencies'].some(
271
+ c => pkg[c]?.[PAQUET_DU_CADRE] !== undefined
272
+ );
273
+ if (hors || declare || !codeImporteLeCadre(projet, dossier)) {
274
+ continue;
275
+ }
276
+ const { [PAQUET_DU_CADRE]: dev, ...autres } = pkg.devDependencies ?? {};
277
+ pkg.dependencies = trie({
278
+ ...(pkg.dependencies ?? {}),
279
+ [PAQUET_DU_CADRE]: dev ?? racine ?? `^${VERSION}`,
280
+ });
281
+ if (dev !== undefined) {
282
+ pkg.devDependencies = autres;
283
+ }
284
+ writeFileSync(path.join(projet, dossier, 'package.json'), `${JSON.stringify(pkg, null, 2)}\n`);
285
+ declarants.push(dossier);
286
+ }
287
+ return declarants.length > 0 ? { nom: PAQUET_DU_CADRE, paquets: declarants } : null;
288
+ }
289
+
222
290
  // Declares the frame's package, whose base configuration of Vitest each Vitest configuration
223
291
  // imports, in the devDependencies of each package of the frame of the project of packages at
224
292
  // <projet> (root package.json <p>) whose scripts hold `test:vitest`, at the range the root declares
@@ -230,16 +298,14 @@ function poserConfigVitest(projet, p) {
230
298
  const testes = paquets(projet).filter(
231
299
  ({ pkg, hors }) => !hors && typeof pkg.scripts?.['test:vitest'] === 'string'
232
300
  );
233
- const racine = CHAMPS_INSTALLES.map(c => p[c]?.[PAQUET_DU_CADRE]).find(v => v !== undefined);
301
+ const racine = plageDeLaRacine(p);
234
302
  const declarants = [];
235
303
  for (const { dossier, pkg } of testes) {
236
304
  if (CHAMPS_INSTALLES.some(c => pkg[c]?.[PAQUET_DU_CADRE] !== undefined)) continue;
237
- pkg.devDependencies = Object.fromEntries(
238
- Object.entries({
239
- ...(pkg.devDependencies ?? {}),
240
- [PAQUET_DU_CADRE]: racine ?? `^${VERSION}`,
241
- }).sort(([a], [b]) => a.localeCompare(b, 'en'))
242
- );
305
+ pkg.devDependencies = trie({
306
+ ...(pkg.devDependencies ?? {}),
307
+ [PAQUET_DU_CADRE]: racine ?? `^${VERSION}`,
308
+ });
243
309
  writeFileSync(path.join(projet, dossier, 'package.json'), `${JSON.stringify(pkg, null, 2)}\n`);
244
310
  declarants.push(dossier);
245
311
  }
@@ -303,6 +369,7 @@ export function mettreAJour(projet, source, { inscrite = VERSION } = {}) {
303
369
  vitest: null,
304
370
  turbo: null,
305
371
  configVitest: null,
372
+ dependanceDuCadre: null,
306
373
  gardes: null,
307
374
  ligneEtat: false,
308
375
  greffon: false,
@@ -362,9 +429,7 @@ export function mettreAJour(projet, source, { inscrite = VERSION } = {}) {
362
429
  }
363
430
  }
364
431
  if (p.devDependencies) {
365
- p.devDependencies = Object.fromEntries(
366
- Object.entries(p.devDependencies).sort(([a], [b]) => a.localeCompare(b, 'en'))
367
- );
432
+ p.devDependencies = trie(p.devDependencies);
368
433
  }
369
434
  if (typeof pkgSource.packageManager === 'string' && typeof p.packageManager !== 'string') {
370
435
  p.packageManager = pkgSource.packageManager;
@@ -403,6 +468,7 @@ export function mettreAJour(projet, source, { inscrite = VERSION } = {}) {
403
468
  bilan.gardes = { deplace: false };
404
469
  }
405
470
  }
471
+ bilan.dependanceDuCadre = declarerDependanceDuCadre(projet, p);
406
472
  bilan.configVitest = poserConfigVitest(projet, p);
407
473
  writeFileSync(pkg, `${JSON.stringify(p, null, 2)}\n`);
408
474
  }
@@ -462,6 +528,12 @@ export function rapport(b) {
462
528
  `Configuration de base de Vitest : ${nom} déclaré dans les devDependencies de ${declarants.join(', ')}. \`npm install\` relie le paquet. La configuration Vitest de chaque paquet l'importe par son nom : \`import base from '${CONFIG_VITEST}'\`.`
463
529
  );
464
530
  }
531
+ if (b.dependanceDuCadre) {
532
+ const { nom, paquets: declarants } = b.dependanceDuCadre;
533
+ lignes.push(
534
+ `Dépendance d'exécution : ${nom} déclaré dans les dependencies de ${declarants.join(', ')}, dont le code hors des tests importe une entrée du paquet. \`npm install\` relie le paquet.`
535
+ );
536
+ }
465
537
  if (b.turbo) {
466
538
  lignes.push(
467
539
  `turbo.json écrit depuis le gabarit du cadre ${b.turbo} ; il est au projet, la mise à jour n'y touche plus.`
@@ -21,12 +21,15 @@
21
21
  // - What stays to the project (`restes`, which the update plays after its own update and its
22
22
  // regeneration, so that it sees the final state): a tracked file of the project (`git ls-files`,
23
23
  // node_modules and the changelogs aside) that still calls or names a script of the frame at its former address under `scripts/`
24
- // (`node scripts/<script>`, `$RACINE/scripts/<script>`, `path.join(…, 'scripts', '<script>')`;
25
- // a tracked path that is no regular file, a submodule or a link, is skipped),
26
- // where the project holds no such file any more, is named with its file:line and what replaces
27
- // it: the subcommand (`SOUS_COMMANDES` of scripts/sous-commandes.mjs), the hook
24
+ // (`node scripts/<script>`, `$RACINE/scripts/<script>`, `file://$1/scripts/<script>`,
25
+ // `path.join(…, '..', 'scripts', '<dossier>', '<script>')`, found by its words, sousScripts; a
26
+ // `scripts/` under a literal directory, a package of the project or the installed package, is
27
+ // another's, `.` and `..` naming none; a tracked path that is no regular file, a submodule or a
28
+ // link, is skipped), where the project holds no such file any more, is named with its file:line
29
+ // and what replaces it: the subcommand (`SOUS_COMMANDES` of scripts/sous-commandes.mjs), the hook
28
30
  // (`grillhouse hook <nom>`, `CROCHETS` of scripts/crochets.mjs) or the named entry (the
29
- // `exports` of the package). The project's scripts are its own: none is rewritten, and the
31
+ // `exports` of the package). A script of the package that has none of them is named with « aucune
32
+ // entrée publiée ne la remplace ». The project's scripts are its own: none is rewritten, and the
30
33
  // lines are a report, which never holds back the version the project records, as a codemod
31
34
  // reports what it leaves without failing.
32
35
  // - No version of the update wrote a setting of a role in `.claude/settings.json` or under
@@ -37,7 +40,7 @@
37
40
  // to the project.
38
41
  import { execFileSync } from 'node:child_process';
39
42
  import { createHash } from 'node:crypto';
40
- import { existsSync, lstatSync, readFileSync, writeFileSync } from 'node:fs';
43
+ import { existsSync, lstatSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
41
44
  import path from 'node:path';
42
45
  import { CADRE, PAQUET } from '../cadre.mjs';
43
46
  import { CROCHETS } from '../crochets.mjs';
@@ -154,7 +157,7 @@ function copies(projet) {
154
157
  }
155
158
 
156
159
  // What replaces each script of the frame, by its path under `scripts/`: its subcommand, its hook,
157
- // its named entry.
160
+ // its named entry; none for a script of the package that has none of them.
158
161
  function remplacants() {
159
162
  const r = new Map();
160
163
  const ajouter = (script, quoi) => r.set(script, [...(r.get(script) ?? []), quoi]);
@@ -164,20 +167,64 @@ function remplacants() {
164
167
  for (const [n, c] of Object.entries(CROCHETS)) {
165
168
  ajouter(c.garde, `\`grillhouse hook ${n}\``);
166
169
  }
167
- const { exports = {} } = JSON.parse(readFileSync(path.join(PAQUET, 'package.json'), 'utf8'));
170
+ const { name, exports = {} } = JSON.parse(
171
+ readFileSync(path.join(PAQUET, 'package.json'), 'utf8')
172
+ );
168
173
  for (const [entree, cible] of Object.entries(exports)) {
169
174
  const d = typeof cible === 'string' ? cible : cible?.default;
170
175
  if (typeof d === 'string' && d.startsWith('./scripts/')) {
171
- ajouter(d.slice('./scripts/'.length), `\`@roomi-fields/grillhouse${entree.slice(1)}\``);
176
+ ajouter(d.slice('./scripts/'.length), `\`${name}${entree.slice(1)}\``);
177
+ }
178
+ }
179
+ const scripts = path.join(PAQUET, 'scripts');
180
+ for (const e of readdirSync(scripts, { recursive: true, withFileTypes: true })) {
181
+ const s = path.relative(scripts, path.join(e.parentPath, e.name)).split(path.sep).join('/');
182
+ if (e.isFile() && SCRIPT.test(s) && !r.has(s)) {
183
+ r.set(s, []);
172
184
  }
173
185
  }
174
186
  return r;
175
187
  }
176
- // A script named at its former address: `scripts/<chemin>`, alone or after `./` or `$VAR/`.
177
- const ADRESSE = /(?<![\w./-])(?:\.\/|\$\{?\w+\}?\/)?scripts\/([\w./-]+?\.(?:mjs|sh))(?![\w.-])/g;
178
- // The same, as the segments of a path.join: `'scripts', 'dette.mjs'`.
179
- const SEGMENTS = /(['"])scripts\1((?:\s*,\s*(['"])[\w.-]+\3)+)/g;
188
+ const SCRIPT = /\.(?:mjs|sh)$/;
180
189
  const SANS = /(^|\/)(?:node_modules\/|CHANGELOG\.md$)/;
190
+ // What separates the words of a line, and the arguments of a call.
191
+ const BLANCS = /[\s'"`();|&<>=,]+/;
192
+ const APPELS = /[();]/;
193
+ const cite = a => a.length >= 2 && /^['"`]$/.test(a[0]) && a.at(-1) === a[0];
194
+ // Whether a piece of a path names a directory: `.` and `..` name none, only a step.
195
+ const dossier = m => m !== '.' && m !== '..';
196
+
197
+ // The paths under `scripts/` that the line <ligne> names at a project's root, each as its pieces
198
+ // after `scripts`: a word cut at its `/` (`scripts/x.mjs`, `./scripts/x.mjs`, `../scripts/x.mjs`,
199
+ // `$RACINE/scripts/x.mjs`, `file://$1/scripts/x.mjs`), whose piece before `scripts` is none, a
200
+ // step (`.`, `..`) or a variable; the arguments of a call (`path.join(RACINE, 'scripts', 'd',
201
+ // 'x.mjs')`, `path.join(dir, '..', 'scripts', 'x.mjs')`), whose argument before `'scripts'` is
202
+ // none, no literal or a step, the literals after it its pieces. A `scripts/` under a literal
203
+ // directory (`packages/a/scripts/`, the installed package) is another's.
204
+ function sousScripts(ligne) {
205
+ const chemins = [];
206
+ for (const mot of ligne.split(BLANCS)) {
207
+ const p = mot.split('/');
208
+ p.forEach((m, i) => {
209
+ if (m === 'scripts' && (i === 0 || !dossier(p[i - 1]) || p[i - 1].startsWith('$'))) {
210
+ chemins.push(p.slice(i + 1));
211
+ }
212
+ });
213
+ }
214
+ for (const appel of ligne.split(APPELS)) {
215
+ const a = appel.split(',').map(x => x.trim());
216
+ a.forEach((x, i) => {
217
+ const avant = a[i - 1];
218
+ const libre = i === 0 || !cite(avant) || !dossier(avant.slice(1, -1));
219
+ if (cite(x) && x.slice(1, -1) === 'scripts' && libre) {
220
+ const suite = a.slice(i + 1);
221
+ const fin = suite.findIndex(y => !cite(y));
222
+ chemins.push(suite.slice(0, fin === -1 ? suite.length : fin).map(y => y.slice(1, -1)));
223
+ }
224
+ });
225
+ }
226
+ return chemins;
227
+ }
181
228
 
182
229
  /** The lines of the tracked files of <projet> that still reach a script of the frame at its former address. */
183
230
  export function restes(projet) {
@@ -202,23 +249,19 @@ export function restes(projet) {
202
249
  continue;
203
250
  }
204
251
  texte.split('\n').forEach((ligne, i) => {
205
- const scripts = [
206
- ...[...ligne.matchAll(ADRESSE)].map(m => m[1]),
207
- ...[...ligne.matchAll(SEGMENTS)].map(m =>
208
- m[2]
209
- .split(',')
210
- .map(s => s.trim().slice(1, -1))
211
- .filter(Boolean)
212
- .join('/')
213
- ),
214
- ];
252
+ const scripts = sousScripts(ligne).flatMap(pieces =>
253
+ pieces.map((_, n) => pieces.slice(0, n + 1).join('/')).filter(s => par.has(s))
254
+ );
215
255
  for (const s of new Set(scripts)) {
216
- if (!par.has(s) || existsSync(path.join(projet, 'scripts', s))) {
256
+ const quoi = par.get(s);
257
+ if (existsSync(path.join(projet, 'scripts', s))) {
217
258
  continue;
218
259
  }
219
- sortie.push(
220
- `${f}:${i + 1} : appelle scripts/${s}, copie du cadre retirée ; ${par.get(s).join(' ou ')} la remplace`
221
- );
260
+ const suite =
261
+ quoi.length > 0
262
+ ? `${quoi.join(' ou ')} la remplace`
263
+ : 'aucune entrée publiée ne la remplace';
264
+ sortie.push(`${f}:${i + 1} : appelle scripts/${s}, copie du cadre retirée ; ${suite}`);
222
265
  }
223
266
  });
224
267
  }