declapract-typescript-ehmpathy 0.49.14 → 0.49.15
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/dist/.test/assets/cicd-service/declarations/practices/persist-with-rds/.test/assets/repo-with-hand-patch/declapract.use.yml +4 -0
- package/dist/.test/assets/cicd-service/declarations/practices/persist-with-rds/.test/assets/repo-with-hand-patch/package.json +17 -0
- package/dist/.test/assets/cicd-service/declarations/practices/persist-with-rds/.test/assets/repo-with-hand-patch/patches/left-pad@1.3.0.patch +6 -0
- package/dist/.test/assets/cicd-service/declarations/practices/persist-with-rds/.test/assets/repo-with-hand-patch/patches/pg-pool@3.14.0.patch +12 -0
- package/dist/.test/assets/cicd-service/declarations/practices/persist-with-rds/__snapshots__/pg-pool-patch.declapract.integration.test.ts.snap +147 -0
- package/dist/.test/assets/cicd-service/declarations/practices/persist-with-rds/best-practice/.declapract.readme.md +22 -0
- package/dist/.test/assets/cicd-service/declarations/practices/persist-with-rds/best-practice/package.json +5 -0
- package/dist/.test/assets/cicd-service/declarations/practices/persist-with-rds/best-practice/patches/pg-pool.patch +21 -0
- package/dist/.test/assets/cicd-service/declarations/practices/persist-with-rds/pg-pool-patch.declapract.integration.test.ts +884 -0
- package/dist/.test/assets/expo-fullusecase/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/declapract.use.yml +4 -0
- package/dist/.test/assets/expo-fullusecase/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/package.json +15 -0
- package/dist/.test/assets/expo-fullusecase/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/rhachet-roles-ehmpathy.patch +6 -0
- package/dist/.test/assets/expo-fullusecase/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/rhachet-roles-ehmpathy@1.39.1.patch +6 -0
- package/dist/.test/assets/expo-fullusecase/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/type-fns@1.21.3.patch +6 -0
- package/dist/.test/assets/expo-fullusecase/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/pnpm-lock.yaml +19 -0
- package/dist/.test/assets/expo-fullusecase/declarations/practices/pnpm/__snapshots__/stale-patched-dependency.declapract.integration.test.ts.snap +39 -0
- package/dist/.test/assets/expo-fullusecase/declarations/practices/pnpm/stale-patched-dependency.declapract.integration.test.ts +53 -0
- package/dist/.test/assets/expo-migration/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/declapract.use.yml +4 -0
- package/dist/.test/assets/expo-migration/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/package.json +15 -0
- package/dist/.test/assets/expo-migration/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/rhachet-roles-ehmpathy.patch +6 -0
- package/dist/.test/assets/expo-migration/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/rhachet-roles-ehmpathy@1.39.1.patch +6 -0
- package/dist/.test/assets/expo-migration/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/type-fns@1.21.3.patch +6 -0
- package/dist/.test/assets/expo-migration/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/pnpm-lock.yaml +19 -0
- package/dist/.test/assets/expo-migration/declarations/practices/pnpm/__snapshots__/stale-patched-dependency.declapract.integration.test.ts.snap +39 -0
- package/dist/.test/assets/expo-migration/declarations/practices/pnpm/stale-patched-dependency.declapract.integration.test.ts +53 -0
- package/dist/.test/assets/pnpm/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/declapract.use.yml +4 -0
- package/dist/.test/assets/pnpm/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/package.json +15 -0
- package/dist/.test/assets/pnpm/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/rhachet-roles-ehmpathy.patch +6 -0
- package/dist/.test/assets/pnpm/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/rhachet-roles-ehmpathy@1.39.1.patch +6 -0
- package/dist/.test/assets/pnpm/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/type-fns@1.21.3.patch +6 -0
- package/dist/.test/assets/pnpm/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/pnpm-lock.yaml +19 -0
- package/dist/.test/assets/pnpm/declarations/practices/pnpm/__snapshots__/stale-patched-dependency.declapract.integration.test.ts.snap +39 -0
- package/dist/.test/assets/pnpm/declarations/practices/pnpm/stale-patched-dependency.declapract.integration.test.ts +53 -0
- package/dist/__snapshots__/typecheckBestPracticeTemplates.declapract.integration.test.ts.snap +80 -0
- package/dist/practices/persist-with-rds/.test/assets/repo-with-hand-patch/declapract.use.yml +4 -0
- package/dist/practices/persist-with-rds/.test/assets/repo-with-hand-patch/package.json +17 -0
- package/dist/practices/persist-with-rds/.test/assets/repo-with-hand-patch/patches/left-pad@1.3.0.patch +6 -0
- package/dist/practices/persist-with-rds/.test/assets/repo-with-hand-patch/patches/pg-pool@3.14.0.patch +12 -0
- package/dist/practices/persist-with-rds/__snapshots__/pg-pool-patch.declapract.integration.test.ts.snap +147 -0
- package/dist/practices/persist-with-rds/best-practice/.declapract.readme.md +22 -0
- package/dist/practices/persist-with-rds/best-practice/package.json +5 -0
- package/dist/practices/persist-with-rds/best-practice/patches/pg-pool.patch +21 -0
- package/dist/practices/persist-with-rds/pg-pool-patch.declapract.integration.test.ts +884 -0
- package/dist/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/declapract.use.yml +4 -0
- package/dist/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/package.json +15 -0
- package/dist/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/rhachet-roles-ehmpathy.patch +6 -0
- package/dist/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/rhachet-roles-ehmpathy@1.39.1.patch +6 -0
- package/dist/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/type-fns@1.21.3.patch +6 -0
- package/dist/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/pnpm-lock.yaml +19 -0
- package/dist/practices/pnpm/__snapshots__/stale-patched-dependency.declapract.integration.test.ts.snap +39 -0
- package/dist/practices/pnpm/stale-patched-dependency.declapract.integration.test.ts +53 -0
- package/dist/typecheckBestPracticeTemplates.declapract.integration.test.ts +181 -19
- package/package.json +6 -6
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import {
|
|
3
|
+
copyFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { executeApply } from 'declapract';
|
|
13
|
+
import { ConstraintError, MalfunctionError } from 'helpful-errors';
|
|
14
|
+
import { genTempDir, given, then, useBeforeAll, when } from 'test-fns';
|
|
15
|
+
|
|
16
|
+
import { asPackageJSON } from '../../utils/asPackageJSON';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* .what = clamps that the shipped `patches/pg-pool.patch` fits each pg-pool release an adopter may
|
|
20
|
+
* hold, under the bare-name `pnpm.patchedDependencies` key the practice ships; that an
|
|
21
|
+
* adopter's own exact-version hand patch survives the apply and still wins the install;
|
|
22
|
+
* that a hunk which no longer fits only warns, by name; and what the patch changes for code
|
|
23
|
+
* that builds a pool from pg-pool directly; and that a bare rds adopter's cycle check exits 0
|
|
24
|
+
* with the patch and exits 1 on `pg -> pg-pool` without it
|
|
25
|
+
* .why = pg-pool's `this.Client = ... || require('pg').Client` fallback makes dpdm report a
|
|
26
|
+
* pg <-> pg-pool require cycle in every rds adopter. the patch drops it. the key is the bare
|
|
27
|
+
* name `pg-pool`, so it is never stale; but then it must fit every in-range release a
|
|
28
|
+
* lagged lockfile still pins, where the line sits at another line number and its neighbours
|
|
29
|
+
* differ. and a repo that already carries `pg-pool@<version>` by hand (the repo that
|
|
30
|
+
* reported the cycle did) must not see the apply drop it, nor pnpm refuse a map that holds both
|
|
31
|
+
* .note = the pnpm practice ships `ignorePatchFailures`, so a hunk that does not fit only warns.
|
|
32
|
+
* the install's exit code proves naught; the patched file's content is the verdict
|
|
33
|
+
* .teeth = change the shipped key to `pg-pool@3.14.0` — every other release keeps its `require('pg')`,
|
|
34
|
+
* and case4's apply overwrites the hand patch's key. widen the hunk's context to the two
|
|
35
|
+
* `this.options` lines above it — pg-pool 3.2.1 through 3.4.1 keep their `require('pg')`
|
|
36
|
+
*/
|
|
37
|
+
const practiceDir = __dirname;
|
|
38
|
+
const shippedPatchPath = path.join(
|
|
39
|
+
practiceDir,
|
|
40
|
+
'best-practice/patches/pg-pool.patch',
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/** every stable pg-pool release in `^3.2.1`, the range the shipped pg floor (8.3.0) asks for */
|
|
44
|
+
const PG_POOL_RELEASES_IN_RANGE = [
|
|
45
|
+
'3.2.1',
|
|
46
|
+
'3.2.2',
|
|
47
|
+
'3.3.0',
|
|
48
|
+
'3.4.0',
|
|
49
|
+
'3.4.1',
|
|
50
|
+
'3.5.1',
|
|
51
|
+
'3.5.2',
|
|
52
|
+
'3.6.0',
|
|
53
|
+
'3.6.1',
|
|
54
|
+
'3.6.2',
|
|
55
|
+
'3.7.0',
|
|
56
|
+
'3.7.1',
|
|
57
|
+
'3.8.0',
|
|
58
|
+
'3.9.0',
|
|
59
|
+
'3.9.1',
|
|
60
|
+
'3.9.2',
|
|
61
|
+
'3.9.5',
|
|
62
|
+
'3.9.6',
|
|
63
|
+
'3.10.0',
|
|
64
|
+
'3.10.1',
|
|
65
|
+
'3.11.0',
|
|
66
|
+
'3.12.0',
|
|
67
|
+
'3.13.0',
|
|
68
|
+
'3.14.0',
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/** a bound on each pnpm or node process, so a stalled registry fails loud instead of a silent hang */
|
|
72
|
+
const SPAWN_TIMEOUT_MS = 120_000;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* .what = whether a value is a `pnpm.patchedDependencies` map: an object of string to string
|
|
76
|
+
* .why = a malformed map must fail at the read, with its value, not flow into pnpm as an `any`
|
|
77
|
+
*/
|
|
78
|
+
const isPatchMap = (value: unknown): value is Record<string, string> =>
|
|
79
|
+
typeof value === 'object' &&
|
|
80
|
+
value !== null &&
|
|
81
|
+
Object.values(value).every((entry) => typeof entry === 'string');
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* .what = the patch map of a package.json's contents
|
|
85
|
+
* .why = the clamp feeds the map to pnpm; a typed read makes a malformed map fail here, with its
|
|
86
|
+
* cause, rather than flow into an install as an `any`
|
|
87
|
+
*/
|
|
88
|
+
const asPatchMapOfPackageJson = (input: {
|
|
89
|
+
contents: string;
|
|
90
|
+
}): Record<string, string> => {
|
|
91
|
+
const patchMap: unknown = asPackageJSON(input, {
|
|
92
|
+
practice: 'persist-with-rds',
|
|
93
|
+
}).pnpm?.patchedDependencies;
|
|
94
|
+
if (!isPatchMap(patchMap))
|
|
95
|
+
throw new ConstraintError('package.json lacks a string map at pnpm.patchedDependencies', {
|
|
96
|
+
patchMap,
|
|
97
|
+
hint: 'each pnpm.patchedDependencies entry maps a package key to a patch file path',
|
|
98
|
+
});
|
|
99
|
+
return patchMap;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* .what = the `pnpm.patchedDependencies` map the practice ships
|
|
104
|
+
* .why = every case installs with the map an adopter receives, read from the practice itself
|
|
105
|
+
*/
|
|
106
|
+
const getOneShippedPatchMap = (): Record<string, string> =>
|
|
107
|
+
asPatchMapOfPackageJson({
|
|
108
|
+
contents: readFileSync(
|
|
109
|
+
path.join(practiceDir, 'best-practice/package.json'),
|
|
110
|
+
'utf-8',
|
|
111
|
+
),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* .what = the exact pnpm pin (`pnpm@x.y.z`) this repo declares in `packageManager`
|
|
116
|
+
* .why = the clamp installs with the pnpm an adopter of the pnpm practice runs
|
|
117
|
+
*/
|
|
118
|
+
const getOnePinnedPackageManager = (): string => {
|
|
119
|
+
const packageManager: unknown = asPackageJSON(
|
|
120
|
+
{
|
|
121
|
+
contents: readFileSync(
|
|
122
|
+
path.join(practiceDir, '../../../package.json'),
|
|
123
|
+
'utf-8',
|
|
124
|
+
),
|
|
125
|
+
},
|
|
126
|
+
{ practice: 'persist-with-rds' },
|
|
127
|
+
).packageManager;
|
|
128
|
+
if (typeof packageManager !== 'string' || !packageManager.startsWith('pnpm@'))
|
|
129
|
+
throw new ConstraintError('root package.json lacks a pnpm packageManager pin', {
|
|
130
|
+
packageManager,
|
|
131
|
+
hint: 'set packageManager to an exact pin, e.g. "pnpm@10.34.5", per the pnpm practice',
|
|
132
|
+
});
|
|
133
|
+
return packageManager;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* .what = the shipped patch, with its `-` line rewritten so it matches no pg-pool release
|
|
138
|
+
* .why = emulates the day pg-pool moves the line, so the hunk fails as it would then
|
|
139
|
+
*/
|
|
140
|
+
const asPatchWithMovedLine = (input: { contents: string }): string =>
|
|
141
|
+
input.contents.replace(
|
|
142
|
+
"- this.Client = this.options.Client || Client || require('pg').Client",
|
|
143
|
+
"- this.Client = this.options.Client || require('pg').Client",
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* .what = masks each temp dir out of an output
|
|
148
|
+
* .why = each run gets a fresh temp dir, so an unmasked path would churn the snapshot every run
|
|
149
|
+
*/
|
|
150
|
+
const asMaskedOutput = (input: { output: string; dirs: string[] }): string =>
|
|
151
|
+
input.dirs.reduce(
|
|
152
|
+
(masked, dir) => masked.split(dir).join('<tempdir>'),
|
|
153
|
+
input.output,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* .what = the lines of a pnpm install output that name the shipped patch, masked
|
|
158
|
+
* .why = the lines an adopter reads to learn the patch did or did not apply. pnpm prints the
|
|
159
|
+
* physical path (genTempDir hands back a link to it), and the hash moves on any edit to the
|
|
160
|
+
* patch header while the line an adopter reads does not
|
|
161
|
+
*/
|
|
162
|
+
const asPatchWarnLines = (input: { output: string; dir: string }): string[] =>
|
|
163
|
+
asMaskedOutput({ output: input.output, dirs: [realpathSync(input.dir)] })
|
|
164
|
+
.replace(/patch_hash=[0-9a-f]+/g, 'patch_hash=<hash>')
|
|
165
|
+
.split('\n')
|
|
166
|
+
.filter((line) => line.includes('patches/pg-pool.patch'));
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* .what = a cycle check output, masked to the lines an adopter reads
|
|
170
|
+
* .why = dpdm colors its output, each run gets a fresh temp dir, and the pg floor rides in every
|
|
171
|
+
* `.pnpm` path; none of the three says aught about the verdict
|
|
172
|
+
*/
|
|
173
|
+
const asCycleCheckLines = (input: { output: string; dir: string }): string[] =>
|
|
174
|
+
asMaskedOutput({
|
|
175
|
+
output: input.output,
|
|
176
|
+
dirs: [realpathSync(input.dir), input.dir],
|
|
177
|
+
})
|
|
178
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: strips the color codes dpdm emits
|
|
179
|
+
.replace(/\x1b\[[0-9;]*m/g, '')
|
|
180
|
+
.replace(/pg@\d+\.\d+\.\d+/g, 'pg@<floor>')
|
|
181
|
+
.split('\n')
|
|
182
|
+
.map((line) => line.trimEnd())
|
|
183
|
+
.filter((line) => line.length > 0);
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* .what = the floor version a practice declares for a dep: the arg of `@declapract{check.minVersion('x')}`,
|
|
187
|
+
* or the declared value itself when it is a plain version
|
|
188
|
+
*/
|
|
189
|
+
const asFloorVersion = (input: { declared: string }): string =>
|
|
190
|
+
/check\.minVersion\('([^']+)'\)/.exec(input.declared)?.[1] ?? input.declared;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* .what = the pg floor this practice ships
|
|
194
|
+
* .why = the clamp installs pg at the version an adopter's bare apply lands on, so a floor bump in the
|
|
195
|
+
* practice reaches every scene here with no literal to go stale
|
|
196
|
+
*/
|
|
197
|
+
const getOneShippedPgFloor = (): string => {
|
|
198
|
+
const declared = asPackageJSON(
|
|
199
|
+
{
|
|
200
|
+
contents: readFileSync(
|
|
201
|
+
path.join(practiceDir, 'best-practice/package.json'),
|
|
202
|
+
'utf-8',
|
|
203
|
+
),
|
|
204
|
+
},
|
|
205
|
+
{ practice: 'persist-with-rds' },
|
|
206
|
+
).dependencies?.pg;
|
|
207
|
+
if (typeof declared !== 'string')
|
|
208
|
+
throw new ConstraintError('persist-with-rds declares no pg dependency', {
|
|
209
|
+
hint: 'the clamp reads the pg floor from persist-with-rds/best-practice/package.json; restore its pg entry',
|
|
210
|
+
});
|
|
211
|
+
return asFloorVersion({ declared });
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* .what = the cycle check the lint practice ships: its `test:lint:cycles` command, its `.dpdmrc.yaml`,
|
|
216
|
+
* and the floor of each dep the command runs (dpdm, yaml, and typescript for `--transform`)
|
|
217
|
+
* .why = the clamp must run the very check an adopter runs, read from the practices that ship it,
|
|
218
|
+
* so a change to the check reaches this clamp with no copy to drift
|
|
219
|
+
*/
|
|
220
|
+
const getOneShippedCycleCheck = (): {
|
|
221
|
+
command: string;
|
|
222
|
+
dpdmrc: string;
|
|
223
|
+
devDependencies: Record<string, string>;
|
|
224
|
+
} => {
|
|
225
|
+
const lintDir = path.join(practiceDir, '../lint/best-practice');
|
|
226
|
+
const lint = asPackageJSON(
|
|
227
|
+
{ contents: readFileSync(path.join(lintDir, 'package.json'), 'utf-8') },
|
|
228
|
+
{ practice: 'lint' },
|
|
229
|
+
);
|
|
230
|
+
const typescriptAny = asPackageJSON(
|
|
231
|
+
{
|
|
232
|
+
contents: readFileSync(
|
|
233
|
+
path.join(practiceDir, '../typescript-any/best-practice/package.json'),
|
|
234
|
+
'utf-8',
|
|
235
|
+
),
|
|
236
|
+
},
|
|
237
|
+
{ practice: 'typescript-any' },
|
|
238
|
+
);
|
|
239
|
+
const command: unknown = lint.scripts?.['test:lint:cycles'];
|
|
240
|
+
const dpdm: unknown = lint.devDependencies?.dpdm;
|
|
241
|
+
const yaml: unknown = lint.devDependencies?.yaml;
|
|
242
|
+
const typescript: unknown =
|
|
243
|
+
typescriptAny.devDependencies?.typescript ??
|
|
244
|
+
typescriptAny.dependencies?.typescript;
|
|
245
|
+
if (
|
|
246
|
+
typeof command !== 'string' ||
|
|
247
|
+
typeof dpdm !== 'string' ||
|
|
248
|
+
typeof yaml !== 'string' ||
|
|
249
|
+
typeof typescript !== 'string'
|
|
250
|
+
)
|
|
251
|
+
throw new ConstraintError('a practice lacks a piece of the shipped cycle check', {
|
|
252
|
+
command,
|
|
253
|
+
dpdm,
|
|
254
|
+
yaml,
|
|
255
|
+
typescript,
|
|
256
|
+
hint: 'the lint practice ships test:lint:cycles + dpdm + yaml; typescript-any ships typescript',
|
|
257
|
+
});
|
|
258
|
+
return {
|
|
259
|
+
command,
|
|
260
|
+
dpdmrc: readFileSync(path.join(lintDir, '.dpdmrc.yaml'), 'utf-8'),
|
|
261
|
+
devDependencies: {
|
|
262
|
+
dpdm: asFloorVersion({ declared: dpdm }),
|
|
263
|
+
yaml: asFloorVersion({ declared: yaml }),
|
|
264
|
+
typescript: asFloorVersion({ declared: typescript }),
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* .what = the pnpm keys the pnpm practice ships, which every adopter's install runs under
|
|
271
|
+
* .why = `ignorePatchFailures` is why a misfit only warns; the clamp reads it from the practice,
|
|
272
|
+
* so a change there reaches every case here
|
|
273
|
+
*/
|
|
274
|
+
const getOneShippedPnpmKeys = (): Record<string, unknown> => {
|
|
275
|
+
const pnpm: unknown = asPackageJSON(
|
|
276
|
+
{
|
|
277
|
+
contents: readFileSync(
|
|
278
|
+
path.join(practiceDir, '../pnpm/best-practice/package.json'),
|
|
279
|
+
'utf-8',
|
|
280
|
+
),
|
|
281
|
+
},
|
|
282
|
+
{ practice: 'pnpm' },
|
|
283
|
+
).pnpm;
|
|
284
|
+
if (typeof pnpm !== 'object' || pnpm === null)
|
|
285
|
+
throw new ConstraintError('the pnpm practice lacks a pnpm block', {
|
|
286
|
+
pnpm,
|
|
287
|
+
hint: 'the pnpm practice ships allowUnusedPatches + ignorePatchFailures in its package.json',
|
|
288
|
+
});
|
|
289
|
+
return { ...pnpm };
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const shippedPatchMap = getOneShippedPatchMap();
|
|
293
|
+
const shippedPnpmKeys = getOneShippedPnpmKeys();
|
|
294
|
+
const packageManager = getOnePinnedPackageManager();
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* .what = a bare repo that depends on pg + one pg-pool release, with a given patch map + its files;
|
|
298
|
+
* as an adopter, it also holds the shipped cycle check and one src file that imports pg
|
|
299
|
+
* .why = the pnpm practice's two failure keys ride along, so the repo installs as an adopter's does
|
|
300
|
+
*/
|
|
301
|
+
const genRepoOfPgPool = (input: {
|
|
302
|
+
slug: string;
|
|
303
|
+
version: string;
|
|
304
|
+
patchedDependencies: Record<string, string>;
|
|
305
|
+
patchFiles: { at: string; from: string }[];
|
|
306
|
+
adopter: ReturnType<typeof getOneShippedCycleCheck> | null;
|
|
307
|
+
}): string => {
|
|
308
|
+
const dir = genTempDir({ slug: input.slug });
|
|
309
|
+
mkdirSync(path.join(dir, 'patches'), { recursive: true });
|
|
310
|
+
input.patchFiles.forEach((file) =>
|
|
311
|
+
copyFileSync(file.from, path.join(dir, file.at)),
|
|
312
|
+
);
|
|
313
|
+
if (input.adopter) {
|
|
314
|
+
mkdirSync(path.join(dir, 'src'), { recursive: true });
|
|
315
|
+
writeFileSync(
|
|
316
|
+
path.join(dir, 'src/index.ts'),
|
|
317
|
+
"import { Client } from 'pg';\n\nexport const client = new Client();\n",
|
|
318
|
+
);
|
|
319
|
+
writeFileSync(path.join(dir, '.dpdmrc.yaml'), input.adopter.dpdmrc);
|
|
320
|
+
}
|
|
321
|
+
writeFileSync(
|
|
322
|
+
path.join(dir, 'package.json'),
|
|
323
|
+
JSON.stringify(
|
|
324
|
+
{
|
|
325
|
+
name: 'pg-pool-patch-demo',
|
|
326
|
+
private: true,
|
|
327
|
+
packageManager,
|
|
328
|
+
dependencies: { pg: getOneShippedPgFloor(), 'pg-pool': input.version },
|
|
329
|
+
...(input.adopter && {
|
|
330
|
+
devDependencies: input.adopter.devDependencies,
|
|
331
|
+
scripts: { 'test:lint:cycles': input.adopter.command },
|
|
332
|
+
}),
|
|
333
|
+
pnpm: {
|
|
334
|
+
...shippedPnpmKeys,
|
|
335
|
+
patchedDependencies: input.patchedDependencies,
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
null,
|
|
339
|
+
2,
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
return dir;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* .what = runs the pinned pnpm's install in a repo
|
|
347
|
+
* .why = the install is the step that applies the patch; its output is the diagnosis
|
|
348
|
+
*/
|
|
349
|
+
const genPnpmInstall = (input: {
|
|
350
|
+
dir: string;
|
|
351
|
+
}): { status: number | null; output: string } => {
|
|
352
|
+
const install = spawnSync(
|
|
353
|
+
'npx',
|
|
354
|
+
['--yes', packageManager, 'install', '--ignore-scripts', '--prefer-offline'],
|
|
355
|
+
{
|
|
356
|
+
cwd: input.dir,
|
|
357
|
+
env: { ...process.env, CI: 'true', FORCE_COLOR: '0' },
|
|
358
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
359
|
+
},
|
|
360
|
+
);
|
|
361
|
+
return {
|
|
362
|
+
status: install.status,
|
|
363
|
+
output: `${String(install.stdout)}${String(install.stderr)}`,
|
|
364
|
+
};
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* .what = the installed `node_modules/pg-pool/index.js` of a repo
|
|
369
|
+
* .why = the patched file's content is the verdict, since `ignorePatchFailures` voids the exit code
|
|
370
|
+
*/
|
|
371
|
+
const getOneInstalledPgPoolIndexJs = (input: {
|
|
372
|
+
dir: string;
|
|
373
|
+
install: { status: number | null; output: string };
|
|
374
|
+
}): string => {
|
|
375
|
+
const indexJsPath = path.join(input.dir, 'node_modules/pg-pool/index.js');
|
|
376
|
+
if (!existsSync(indexJsPath))
|
|
377
|
+
throw new MalfunctionError('pg-pool absent after pnpm install', {
|
|
378
|
+
status: input.install.status,
|
|
379
|
+
output: input.install.output,
|
|
380
|
+
hint: 'read the install output above; a registry or cache fault is the usual cause',
|
|
381
|
+
});
|
|
382
|
+
return readFileSync(indexJsPath, 'utf-8');
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* .what = installs one pg-pool release into a bare repo, with a given patch map + its patch files
|
|
387
|
+
* .why = each case claims what pnpm does to pg-pool's `index.js`; only a real install shows it
|
|
388
|
+
*/
|
|
389
|
+
const genInstalledPgPool = (input: {
|
|
390
|
+
slug: string;
|
|
391
|
+
version: string;
|
|
392
|
+
patchedDependencies: Record<string, string>;
|
|
393
|
+
patchFiles: { at: string; from: string }[];
|
|
394
|
+
adopter: ReturnType<typeof getOneShippedCycleCheck> | null;
|
|
395
|
+
}): { dir: string; status: number | null; output: string; indexJs: string } => {
|
|
396
|
+
const dir = genRepoOfPgPool(input);
|
|
397
|
+
const install = genPnpmInstall({ dir });
|
|
398
|
+
const indexJs = getOneInstalledPgPoolIndexJs({ dir, install });
|
|
399
|
+
return { dir, ...install, indexJs };
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* .what = asserts a pnpm install exited 0, and fails loud with its output when it did not
|
|
404
|
+
* .why = a bare `expect` on the status hides the install output, which is the only diagnosis
|
|
405
|
+
*/
|
|
406
|
+
const expectInstallPassed = (input: {
|
|
407
|
+
status: number | null;
|
|
408
|
+
output: string;
|
|
409
|
+
}): void => {
|
|
410
|
+
if (input.status !== 0)
|
|
411
|
+
throw new MalfunctionError('pnpm install failed', {
|
|
412
|
+
status: input.status,
|
|
413
|
+
output: input.output,
|
|
414
|
+
hint: 'read the install output above; a registry or cache fault is the usual cause',
|
|
415
|
+
});
|
|
416
|
+
expect(input.status).toEqual(0);
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* .what = runs a repo's `test:lint:cycles` with the pinned pnpm, as `npm run test:lint` does
|
|
421
|
+
* .why = the exit code and the cycles dpdm prints are the verdict an adopter's lint gate reads
|
|
422
|
+
*/
|
|
423
|
+
const genCycleCheckRun = (input: {
|
|
424
|
+
dir: string;
|
|
425
|
+
}): { status: number | null; output: string } => {
|
|
426
|
+
const run = spawnSync(
|
|
427
|
+
'npx',
|
|
428
|
+
['--yes', '--loglevel=error', packageManager, 'run', 'test:lint:cycles'],
|
|
429
|
+
{
|
|
430
|
+
cwd: input.dir,
|
|
431
|
+
env: { ...process.env, CI: 'true', FORCE_COLOR: '0' },
|
|
432
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
433
|
+
},
|
|
434
|
+
);
|
|
435
|
+
return {
|
|
436
|
+
status: run.status,
|
|
437
|
+
output: `${String(run.stdout)}${String(run.stderr)}`,
|
|
438
|
+
};
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* .what = the node source of a probe that builds a pool from pg-pool, with and without a `Client`,
|
|
443
|
+
* and prints what each pool holds as json
|
|
444
|
+
* .why = the probe must run inside the installed repo, so it is a string node evaluates there
|
|
445
|
+
*/
|
|
446
|
+
const asDirectPoolProbe = (): string =>
|
|
447
|
+
[
|
|
448
|
+
"const Pool = require('pg-pool');",
|
|
449
|
+
"const pg = require('pg');",
|
|
450
|
+
'const bare = new Pool();',
|
|
451
|
+
"const bareSays = typeof bare.Client === 'function' ? 'has a Client' : 'has no Client';",
|
|
452
|
+
'const fixed = new Pool({ Client: pg.Client });',
|
|
453
|
+
"const fixedSays = fixed.Client === pg.Client ? 'has pg Client' : 'has another Client';",
|
|
454
|
+
// the first connect is where a direct caller meets the change; it fails before any socket opens
|
|
455
|
+
'(async () => {',
|
|
456
|
+
" const connectSays = await (async () => bare.connect())().then(() => 'connected', (error) => error.message);",
|
|
457
|
+
' process.stdout.write(',
|
|
458
|
+
' JSON.stringify({ withoutClient: bareSays, withClient: fixedSays, withoutClientConnect: connectSays }),',
|
|
459
|
+
' );',
|
|
460
|
+
' process.exit(0);',
|
|
461
|
+
'})();',
|
|
462
|
+
].join('\n');
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* .what = whether a value is the probe's result: `{ withoutClient, withClient }`, both strings
|
|
466
|
+
* .why = the probe's stdout is untrusted text; a malformed one must fail here, with its value
|
|
467
|
+
*/
|
|
468
|
+
const isDirectPoolResult = (
|
|
469
|
+
value: unknown,
|
|
470
|
+
): value is {
|
|
471
|
+
withoutClient: string;
|
|
472
|
+
withClient: string;
|
|
473
|
+
withoutClientConnect: string;
|
|
474
|
+
} =>
|
|
475
|
+
typeof value === 'object' &&
|
|
476
|
+
value !== null &&
|
|
477
|
+
'withoutClient' in value &&
|
|
478
|
+
typeof value.withoutClient === 'string' &&
|
|
479
|
+
'withClient' in value &&
|
|
480
|
+
typeof value.withClient === 'string' &&
|
|
481
|
+
'withoutClientConnect' in value &&
|
|
482
|
+
typeof value.withoutClientConnect === 'string';
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* .what = what a direct `new Pool()` from pg-pool holds, with and without a `Client`
|
|
486
|
+
* .why = the patch removes pg-pool's own fallback to pg's Client; a caller that passed none now
|
|
487
|
+
* gets a pool with no Client, and the remedy the patch header names must give one back
|
|
488
|
+
*/
|
|
489
|
+
const getOneDirectPoolResult = (input: {
|
|
490
|
+
dir: string;
|
|
491
|
+
}): { withoutClient: string; withClient: string; withoutClientConnect: string } => {
|
|
492
|
+
const run = spawnSync('node', ['-e', asDirectPoolProbe()], {
|
|
493
|
+
cwd: input.dir,
|
|
494
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
495
|
+
});
|
|
496
|
+
const output = `${String(run.stdout)}${String(run.stderr)}`;
|
|
497
|
+
if (run.status !== 0)
|
|
498
|
+
throw new MalfunctionError('direct pg-pool probe failed', {
|
|
499
|
+
status: run.status,
|
|
500
|
+
output,
|
|
501
|
+
hint: 'read the probe output above; an absent pg or pg-pool install is the usual cause',
|
|
502
|
+
});
|
|
503
|
+
const result: unknown = JSON.parse(String(run.stdout));
|
|
504
|
+
if (!isDirectPoolResult(result))
|
|
505
|
+
throw new MalfunctionError('direct pg-pool probe printed an unexpected shape', {
|
|
506
|
+
output,
|
|
507
|
+
hint: 'the probe prints { withoutClient, withClient }; read its output above',
|
|
508
|
+
});
|
|
509
|
+
return result;
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* .what = the lean declarations a test repo applies: the shipped package.json declaration and its
|
|
514
|
+
* patch, each copied from the real practice, plus a one-practice declare + use-case file
|
|
515
|
+
* .why = the whole practice owes variables this test has no use for. the files are copies, not
|
|
516
|
+
* symlinks: declapract checks a declared file with `lstat`, so a symlinked file never counts
|
|
517
|
+
*/
|
|
518
|
+
const asDeclarationsOfPackageJsonOnly = (): {
|
|
519
|
+
copies: { at: string; from: string }[];
|
|
520
|
+
writes: { at: string; contents: string }[];
|
|
521
|
+
} => ({
|
|
522
|
+
copies: [
|
|
523
|
+
'package.json',
|
|
524
|
+
'package.json.declapract.ts',
|
|
525
|
+
'patches/pg-pool.patch',
|
|
526
|
+
].map((file) => ({
|
|
527
|
+
at: path.join('practices/persist-with-rds/best-practice', file),
|
|
528
|
+
from: path.join(practiceDir, 'best-practice', file),
|
|
529
|
+
})),
|
|
530
|
+
writes: [
|
|
531
|
+
{
|
|
532
|
+
at: 'declapract.declare.yml',
|
|
533
|
+
contents: 'declare:\n practices: practices\n use-cases: useCases.yml\n',
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
at: 'useCases.yml',
|
|
537
|
+
contents:
|
|
538
|
+
'use-cases:\n lambda-service:\n practices:\n - persist-with-rds\n',
|
|
539
|
+
},
|
|
540
|
+
],
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* .what = writes a declarations set into a dir
|
|
545
|
+
* .why = the build stays a pure transformer; this is the one place the set touches disk
|
|
546
|
+
*/
|
|
547
|
+
const setDeclarations = (input: {
|
|
548
|
+
dir: string;
|
|
549
|
+
declarations: ReturnType<typeof asDeclarationsOfPackageJsonOnly>;
|
|
550
|
+
}): void => {
|
|
551
|
+
input.declarations.copies.forEach((copy) => {
|
|
552
|
+
mkdirSync(path.dirname(path.join(input.dir, copy.at)), { recursive: true });
|
|
553
|
+
copyFileSync(copy.from, path.join(input.dir, copy.at));
|
|
554
|
+
});
|
|
555
|
+
input.declarations.writes.forEach((write) =>
|
|
556
|
+
writeFileSync(path.join(input.dir, write.at), write.contents),
|
|
557
|
+
);
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
jest.setTimeout(300_000);
|
|
561
|
+
|
|
562
|
+
describe('persist-with-rds pg-pool patch', () => {
|
|
563
|
+
given('[case1] the shipped patch map', () => {
|
|
564
|
+
when('[t0] the practice package.json is read', () => {
|
|
565
|
+
then('it keys the patch by the bare name, so it is never stale', () => {
|
|
566
|
+
expect(shippedPatchMap).toEqual({
|
|
567
|
+
'pg-pool': 'patches/pg-pool.patch',
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// the header is the one text an adopter reads when the patch fails or a direct pool breaks
|
|
573
|
+
when('[t1] the shipped patch header is read', () => {
|
|
574
|
+
const header = readFileSync(shippedPatchPath, 'utf-8').split('diff --git')[0] ?? '';
|
|
575
|
+
|
|
576
|
+
then('it names the fix for a failed apply', () => {
|
|
577
|
+
expect(header).toContain('upgrade declapract-typescript-ehmpathy');
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
then('it names the remedy for a direct pool', () => {
|
|
581
|
+
expect(header).toContain("{ Client: require('pg').Client }");
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
then('it names the way out when no fix has shipped yet', () => {
|
|
585
|
+
expect(header).toContain('pin your own `pg-pool@<version>` patch');
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
then('it names the lockfile step after an apply', () => {
|
|
589
|
+
expect(header).toContain('run `pnpm install` and commit the');
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
then('it names how a maintainer recuts the hunk', () => {
|
|
593
|
+
expect(header).toContain('keep one context line on each side');
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
then('it warns that any edit re-locks every adopter', () => {
|
|
597
|
+
expect(header).toContain("its hash sits in every adopter's lockfile");
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// every pg-pool release a `pg@^8.3.0` lockfile can hold: `pg@8.3.0` asks for `pg-pool@^3.2.1`, so a
|
|
603
|
+
// lagged lockfile may pin any of them. each stable release is held
|
|
604
|
+
PG_POOL_RELEASES_IN_RANGE.forEach((version) => {
|
|
605
|
+
given(`[case2] pg-pool ${version}, installed with the shipped patch`, () => {
|
|
606
|
+
const scene = useBeforeAll(async () =>
|
|
607
|
+
genInstalledPgPool({
|
|
608
|
+
slug: `pg-pool-patch-case2-${version}`,
|
|
609
|
+
version,
|
|
610
|
+
patchedDependencies: shippedPatchMap,
|
|
611
|
+
patchFiles: [{ at: 'patches/pg-pool.patch', from: shippedPatchPath }],
|
|
612
|
+
adopter: null,
|
|
613
|
+
}),
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
when('[t0] pnpm install runs', () => {
|
|
617
|
+
then('the install exits 0', () => {
|
|
618
|
+
expectInstallPassed(scene);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
then("the installed index.js holds no require('pg')", () => {
|
|
622
|
+
expect(scene.indexJs).not.toContain("require('pg')");
|
|
623
|
+
expect(scene.indexJs).toContain(
|
|
624
|
+
'this.Client = this.options.Client || Client\n',
|
|
625
|
+
);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// the contrast for case5: a patch that fits prints no path, so only a failure names the file
|
|
629
|
+
then('the install output names no patch file', () => {
|
|
630
|
+
expect(scene.output).not.toContain('patches/pg-pool.patch');
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
then('the patch lines an adopter reads match snapshot', () => {
|
|
634
|
+
expect(
|
|
635
|
+
asPatchWarnLines({ output: scene.output, dir: scene.dir }),
|
|
636
|
+
).toMatchSnapshot(`pnpm install — patch lines, pg-pool ${version}`);
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
when('[t1] code builds a pool from pg-pool directly', () => {
|
|
641
|
+
const direct = useBeforeAll(async () =>
|
|
642
|
+
getOneDirectPoolResult({ dir: scene.dir }),
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
then('a pool built with no Client has none, since the fallback is gone', () => {
|
|
646
|
+
expect(direct.withoutClient).toEqual('has no Client');
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
then("the remedy the patch header names gives it pg's Client", () => {
|
|
650
|
+
expect(direct.withClient).toEqual('has pg Client');
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
then('its first connect fails loud, with the error the caller reads', () => {
|
|
654
|
+
expect(direct.withoutClientConnect).toEqual(
|
|
655
|
+
'this.Client is not a constructor',
|
|
656
|
+
);
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
given('[case4] an rds repo that already holds its own "pg-pool@3.14.0" hand patch', () => {
|
|
663
|
+
const tempDir = genTempDir({
|
|
664
|
+
slug: 'pg-pool-patch-case4',
|
|
665
|
+
clone: './src/practices/persist-with-rds/.test/assets/repo-with-hand-patch',
|
|
666
|
+
symlink: [{ at: 'node_modules', to: 'node_modules' }],
|
|
667
|
+
});
|
|
668
|
+
const handPatch = 'patches/pg-pool@3.14.0.patch';
|
|
669
|
+
const otherPatch = 'patches/left-pad@1.3.0.patch';
|
|
670
|
+
|
|
671
|
+
const applied = useBeforeAll(async () => {
|
|
672
|
+
setDeclarations({
|
|
673
|
+
dir: path.join(tempDir, 'declarations'),
|
|
674
|
+
declarations: asDeclarationsOfPackageJsonOnly(),
|
|
675
|
+
});
|
|
676
|
+
const config = path.join(tempDir, 'declapract.use.yml');
|
|
677
|
+
const read = () =>
|
|
678
|
+
readFileSync(path.join(tempDir, 'package.json'), 'utf-8');
|
|
679
|
+
const before = read();
|
|
680
|
+
await executeApply({ config });
|
|
681
|
+
const after1 = read();
|
|
682
|
+
await executeApply({ config });
|
|
683
|
+
const after2 = read();
|
|
684
|
+
return {
|
|
685
|
+
before,
|
|
686
|
+
after1,
|
|
687
|
+
after2,
|
|
688
|
+
exists: {
|
|
689
|
+
hand: existsSync(path.join(tempDir, handPatch)),
|
|
690
|
+
shipped: existsSync(path.join(tempDir, 'patches/pg-pool.patch')),
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
}, 170_000);
|
|
694
|
+
|
|
695
|
+
when('[t0] the persist-with-rds practice is applied', () => {
|
|
696
|
+
then('the shipped bare-name key lands beside the hand keys, and every other key survives', () => {
|
|
697
|
+
expect(asPatchMapOfPackageJson({ contents: applied.after1 })).toEqual({
|
|
698
|
+
'pg-pool@3.14.0': handPatch,
|
|
699
|
+
'left-pad@1.3.0': otherPatch,
|
|
700
|
+
'pg-pool': 'patches/pg-pool.patch',
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
then("the repo's other pnpm keys are kept", () => {
|
|
705
|
+
expect(
|
|
706
|
+
asPackageJSON(
|
|
707
|
+
{ contents: applied.after1 },
|
|
708
|
+
{ practice: 'persist-with-rds' },
|
|
709
|
+
).pnpm.overrides,
|
|
710
|
+
).toEqual({ 'react-refresh': '0.14.2' });
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
then('both patch files are present', () => {
|
|
714
|
+
expect(applied.exists).toEqual({ hand: true, shipped: true });
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// the whole file is held, so a clobbered or dropped key shows in review; only the dep floors the
|
|
718
|
+
// practice writes are masked, since a floor bump says naught of this fix
|
|
719
|
+
then('the before + after contents match snapshot', () => {
|
|
720
|
+
const asFloorsMasked = (contents: string) =>
|
|
721
|
+
contents.replace(/check\.minVersion\('[^']+'\)/g, "check.minVersion('<floor>')");
|
|
722
|
+
expect(asFloorsMasked(applied.before)).toMatchSnapshot(
|
|
723
|
+
'package.json — before',
|
|
724
|
+
);
|
|
725
|
+
expect(asFloorsMasked(applied.after1)).toMatchSnapshot(
|
|
726
|
+
'package.json — after',
|
|
727
|
+
);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// .note = the first apply writes `@declapract{check.minVersion(...)}` for each dep it adds, and
|
|
731
|
+
// the second resolves those to versions; that is declapract's own dep flow, so only the
|
|
732
|
+
// patch map is held to a no-op here
|
|
733
|
+
then('a second apply leaves the patch map as is', () => {
|
|
734
|
+
expect(asPatchMapOfPackageJson({ contents: applied.after2 })).toEqual(
|
|
735
|
+
asPatchMapOfPackageJson({ contents: applied.after1 }),
|
|
736
|
+
);
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
when('[t1] pnpm installs pg-pool 3.14.0 with both keys', () => {
|
|
741
|
+
const scene = useBeforeAll(async () =>
|
|
742
|
+
genInstalledPgPool({
|
|
743
|
+
slug: 'pg-pool-patch-case4-install',
|
|
744
|
+
version: '3.14.0',
|
|
745
|
+
patchedDependencies: asPatchMapOfPackageJson({
|
|
746
|
+
contents: applied.after1,
|
|
747
|
+
}),
|
|
748
|
+
patchFiles: [
|
|
749
|
+
{ at: handPatch, from: path.join(tempDir, handPatch) },
|
|
750
|
+
{ at: otherPatch, from: path.join(tempDir, otherPatch) },
|
|
751
|
+
{
|
|
752
|
+
at: 'patches/pg-pool.patch',
|
|
753
|
+
from: path.join(tempDir, 'patches/pg-pool.patch'),
|
|
754
|
+
},
|
|
755
|
+
],
|
|
756
|
+
adopter: null,
|
|
757
|
+
}),
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
then('the install exits 0', () => {
|
|
761
|
+
expectInstallPassed(scene);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
then('pnpm applies the exact key first, so the hand patch wins', () => {
|
|
765
|
+
expect(scene.indexJs).toContain(
|
|
766
|
+
'this.Client = this.options.Client || Client // hand patch\n',
|
|
767
|
+
);
|
|
768
|
+
expect(scene.indexJs).not.toContain("require('pg')");
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
given('[case5] a pg-pool release where the patched line moved, so the hunk fails', () => {
|
|
774
|
+
const scene = useBeforeAll(async () => {
|
|
775
|
+
const misfitPath = path.join(
|
|
776
|
+
genTempDir({ slug: 'pg-pool-patch-case5-misfit' }),
|
|
777
|
+
'pg-pool.patch',
|
|
778
|
+
);
|
|
779
|
+
writeFileSync(
|
|
780
|
+
misfitPath,
|
|
781
|
+
asPatchWithMovedLine({
|
|
782
|
+
contents: readFileSync(shippedPatchPath, 'utf-8'),
|
|
783
|
+
}),
|
|
784
|
+
);
|
|
785
|
+
return genInstalledPgPool({
|
|
786
|
+
slug: 'pg-pool-patch-case5',
|
|
787
|
+
version: '3.14.0',
|
|
788
|
+
patchedDependencies: shippedPatchMap,
|
|
789
|
+
patchFiles: [{ at: 'patches/pg-pool.patch', from: misfitPath }],
|
|
790
|
+
adopter: null,
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
when('[t0] pnpm install runs', () => {
|
|
795
|
+
then('the install still exits 0, since the pnpm practice ships ignorePatchFailures', () => {
|
|
796
|
+
expectInstallPassed(scene);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// the cases above prove a patch that fits prints no path, so this path is the failure warn
|
|
800
|
+
then('the warn names the shipped patch file, so the reader finds its header', () => {
|
|
801
|
+
expect(scene.output).toContain('patches/pg-pool.patch');
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
then('the warn an adopter reads matches snapshot', () => {
|
|
805
|
+
expect(
|
|
806
|
+
asPatchWarnLines({ output: scene.output, dir: scene.dir }),
|
|
807
|
+
).toMatchSnapshot('pnpm install — patch failure warn');
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
then('pg-pool lands unpatched, so the cycle check is the alarm', () => {
|
|
811
|
+
expect(scene.indexJs).toContain("require('pg').Client");
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
given('[case6] a bare rds adopter: pg, pg-pool, the shipped cycle check, and one src file that imports pg', () => {
|
|
817
|
+
const cycleCheck = getOneShippedCycleCheck();
|
|
818
|
+
|
|
819
|
+
when('[t0] its cycle check runs, with the shipped patch', () => {
|
|
820
|
+
const scene = useBeforeAll(async () => {
|
|
821
|
+
const installed = genInstalledPgPool({
|
|
822
|
+
slug: 'pg-pool-patch-case6-patched',
|
|
823
|
+
version: '3.14.0',
|
|
824
|
+
patchedDependencies: shippedPatchMap,
|
|
825
|
+
patchFiles: [{ at: 'patches/pg-pool.patch', from: shippedPatchPath }],
|
|
826
|
+
adopter: cycleCheck,
|
|
827
|
+
});
|
|
828
|
+
return { installed, check: genCycleCheckRun({ dir: installed.dir }) };
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
then('the install exits 0', () => {
|
|
832
|
+
expectInstallPassed(scene.installed);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
then('the cycle check exits 0, so the lint gate passes', () => {
|
|
836
|
+
expect(scene.check.output).toContain(
|
|
837
|
+
'no circular dependency was found',
|
|
838
|
+
);
|
|
839
|
+
expect(scene.check.status).toEqual(0);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
then('the verdict an adopter reads matches snapshot', () => {
|
|
843
|
+
expect(
|
|
844
|
+
asCycleCheckLines({
|
|
845
|
+
output: scene.check.output,
|
|
846
|
+
dir: scene.installed.dir,
|
|
847
|
+
}),
|
|
848
|
+
).toMatchSnapshot('test:lint:cycles — with the shipped patch');
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// the red run: the same tree with no patch is what every rds adopter met before this practice
|
|
853
|
+
when('[t1] its cycle check runs, with no patch', () => {
|
|
854
|
+
const scene = useBeforeAll(async () => {
|
|
855
|
+
const installed = genInstalledPgPool({
|
|
856
|
+
slug: 'pg-pool-patch-case6-bare',
|
|
857
|
+
version: '3.14.0',
|
|
858
|
+
patchedDependencies: {},
|
|
859
|
+
patchFiles: [],
|
|
860
|
+
adopter: cycleCheck,
|
|
861
|
+
});
|
|
862
|
+
return { installed, check: genCycleCheckRun({ dir: installed.dir }) };
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
then('the install exits 0', () => {
|
|
866
|
+
expectInstallPassed(scene.installed);
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
then('the cycle check exits 1, on the pg <-> pg-pool cycle', () => {
|
|
870
|
+
expect(scene.check.output).toMatch(/pg@[^\s]+\/lib\/index\.js -> .*pg-pool@[^\s]+\/index\.js/);
|
|
871
|
+
expect(scene.check.status).toEqual(1);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
then('the verdict an adopter reads matches snapshot', () => {
|
|
875
|
+
expect(
|
|
876
|
+
asCycleCheckLines({
|
|
877
|
+
output: scene.check.output,
|
|
878
|
+
dir: scene.installed.dir,
|
|
879
|
+
}),
|
|
880
|
+
).toMatchSnapshot('test:lint:cycles — with no patch');
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
});
|
|
884
|
+
});
|