@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.
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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`.
|
|
216
|
-
|
|
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
package/scripts/frontieres.mjs
CHANGED
|
@@ -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
|
-
|
|
51
|
-
const
|
|
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 =
|
|
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 =
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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 =
|
|
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>`, `
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
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).
|
|
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(
|
|
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),
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
|
|
256
|
+
const quoi = par.get(s);
|
|
257
|
+
if (existsSync(path.join(projet, 'scripts', s))) {
|
|
217
258
|
continue;
|
|
218
259
|
}
|
|
219
|
-
|
|
220
|
-
|
|
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
|
}
|