@ebarahona/loopback-openapi-v3 1.1.0 → 1.1.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ebarahona/loopback-openapi-v3",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "LoopBack 4 component for OpenAPI 3.1 and 3.2 spec generation",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"lint": "eslint \"src/**/*.ts\"",
|
|
21
21
|
"test": "vitest run",
|
|
22
22
|
"test:dev": "vitest",
|
|
23
|
-
"prepublishOnly": "npm run clean && npm run build && npm test"
|
|
23
|
+
"prepublishOnly": "npm run clean && npm run build && npm test",
|
|
24
|
+
"prepare": "sh scripts/install-hooks.sh"
|
|
24
25
|
},
|
|
25
26
|
"keywords": [
|
|
26
27
|
"loopback",
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"@loopback/openapi-v3": "^11.0.13",
|
|
59
60
|
"@loopback/rest": "^15.0.13",
|
|
60
61
|
"@loopback/testlab": "^8.0.12",
|
|
62
|
+
"@size-limit/preset-small-lib": "^11.0.0",
|
|
61
63
|
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
|
62
64
|
"@typescript-eslint/parser": "^8.59.3",
|
|
63
65
|
"eslint": "^8.57.0",
|
|
@@ -66,6 +68,7 @@
|
|
|
66
68
|
"eslint-plugin-tsdoc": "^0.5.2",
|
|
67
69
|
"prettier": "^3.2.0",
|
|
68
70
|
"rimraf": "^5.0.0",
|
|
71
|
+
"size-limit": "^11.0.0",
|
|
69
72
|
"typescript": "~5.2.2",
|
|
70
73
|
"vitest": "^2.0.0"
|
|
71
74
|
},
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import {spawnSync} from 'node:child_process';
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
copyFileSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
realpathSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
statSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from 'node:fs';
|
|
15
|
+
import {tmpdir} from 'node:os';
|
|
16
|
+
import {join, resolve} from 'node:path';
|
|
17
|
+
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
18
|
+
|
|
19
|
+
// Exercises scripts/install-hooks.sh (run by `npm install` via `prepare`)
|
|
20
|
+
// in a throwaway sandbox: temp HOME, temp global git config, temp git
|
|
21
|
+
// repo, and a stub `npx` on PATH that only records that it was called.
|
|
22
|
+
// Nothing here reads or writes the real HOME or the real git config.
|
|
23
|
+
|
|
24
|
+
const PKG_ROOT = resolve(__dirname, '../..');
|
|
25
|
+
const SCRIPT = join(PKG_ROOT, 'scripts/install-hooks.sh');
|
|
26
|
+
const LEFTHOOK_PIN = 'lefthook@2.1.15';
|
|
27
|
+
|
|
28
|
+
interface Sandbox {
|
|
29
|
+
root: string;
|
|
30
|
+
home: string;
|
|
31
|
+
npxMarker: string;
|
|
32
|
+
binDir: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let sb: Sandbox;
|
|
36
|
+
|
|
37
|
+
function git(cwd: string, ...args: string[]): string {
|
|
38
|
+
const r = spawnSync('git', args, {cwd, env: sandboxEnv(), encoding: 'utf8'});
|
|
39
|
+
if (r.status !== 0) {
|
|
40
|
+
throw new Error(`git ${args.join(' ')} failed: ${r.stderr}`);
|
|
41
|
+
}
|
|
42
|
+
return r.stdout.trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sandboxEnv(extra: Record<string, string> = {}): NodeJS.ProcessEnv {
|
|
46
|
+
const env: NodeJS.ProcessEnv = {};
|
|
47
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
48
|
+
// Drop anything that could leak the host's git/CI/npm context into
|
|
49
|
+
// the script under test.
|
|
50
|
+
if (/^(GIT_|CI$|npm_)/.test(k)) continue;
|
|
51
|
+
env[k] = v;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
...env,
|
|
55
|
+
HOME: sb.home,
|
|
56
|
+
XDG_CONFIG_HOME: join(sb.home, '.config'),
|
|
57
|
+
GIT_CONFIG_GLOBAL: join(sb.home, '.gitconfig'),
|
|
58
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
59
|
+
PATH: `${sb.binDir}:${process.env.PATH ?? ''}`,
|
|
60
|
+
...extra,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Lay out `<dir>/scripts/install-hooks.sh` + package.json, optionally as a git repo. */
|
|
65
|
+
function makePackage(dir: string, initGit: boolean): string {
|
|
66
|
+
mkdirSync(join(dir, 'scripts'), {recursive: true});
|
|
67
|
+
copyFileSync(SCRIPT, join(dir, 'scripts/install-hooks.sh'));
|
|
68
|
+
writeFileSync(join(dir, 'package.json'), '{"name":"pkg"}\n');
|
|
69
|
+
if (initGit) git(dir, 'init', '-q');
|
|
70
|
+
return dir;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function runScript(pkgDir: string, extra: Record<string, string> = {}) {
|
|
74
|
+
return spawnSync('sh', ['scripts/install-hooks.sh'], {
|
|
75
|
+
cwd: pkgDir,
|
|
76
|
+
env: sandboxEnv(extra),
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function npxCalled(): boolean {
|
|
82
|
+
return existsSync(sb.npxMarker);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function npxArgs(): string {
|
|
86
|
+
return readFileSync(sb.npxMarker, 'utf8').trim();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), 'install-hooks-')));
|
|
91
|
+
const home = join(root, 'home');
|
|
92
|
+
const binDir = join(root, 'bin');
|
|
93
|
+
mkdirSync(home, {recursive: true});
|
|
94
|
+
mkdirSync(binDir, {recursive: true});
|
|
95
|
+
writeFileSync(join(home, '.gitconfig'), '');
|
|
96
|
+
const npxMarker = join(root, 'npx-called');
|
|
97
|
+
const npx = join(binDir, 'npx');
|
|
98
|
+
writeFileSync(npx, `#!/bin/sh\necho "$@" >> "${npxMarker}"\nexit 0\n`);
|
|
99
|
+
chmodSync(npx, 0o755);
|
|
100
|
+
sb = {root, home, npxMarker, binDir};
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
rmSync(sb.root, {recursive: true, force: true});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('scripts/install-hooks.sh', () => {
|
|
108
|
+
it('installs via a pinned npx lefthook when no core.hooksPath is set', () => {
|
|
109
|
+
const pkg = makePackage(join(sb.root, 'repo'), true);
|
|
110
|
+
const r = runScript(pkg);
|
|
111
|
+
expect(r.status).toBe(0);
|
|
112
|
+
expect(npxCalled()).toBe(true);
|
|
113
|
+
expect(npxArgs()).toBe(`--yes ${LEFTHOOK_PIN} install`);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('installs when a local core.hooksPath points at the repo .git/hooks', () => {
|
|
117
|
+
const pkg = makePackage(join(sb.root, 'repo'), true);
|
|
118
|
+
git(pkg, 'config', '--local', 'core.hooksPath', '.git/hooks');
|
|
119
|
+
const r = runScript(pkg);
|
|
120
|
+
expect(r.status).toBe(0);
|
|
121
|
+
expect(npxCalled()).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('never writes into a global core.hooksPath and exits 0', () => {
|
|
125
|
+
const globalHooks = join(sb.home, '.git-templates/hooks');
|
|
126
|
+
mkdirSync(globalHooks, {recursive: true});
|
|
127
|
+
const existing = join(globalHooks, 'pre-commit');
|
|
128
|
+
writeFileSync(existing, '#!/bin/sh\nexit 0\n');
|
|
129
|
+
chmodSync(existing, 0o755);
|
|
130
|
+
// Written directly to the sandbox global config file (GIT_CONFIG_GLOBAL).
|
|
131
|
+
writeFileSync(
|
|
132
|
+
join(sb.home, '.gitconfig'),
|
|
133
|
+
'[core]\n\thooksPath = ~/.git-templates/hooks\n',
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const pkg = makePackage(join(sb.root, 'repo'), true);
|
|
137
|
+
const r = runScript(pkg);
|
|
138
|
+
|
|
139
|
+
expect(r.status).toBe(0);
|
|
140
|
+
expect(npxCalled()).toBe(false);
|
|
141
|
+
expect(readdirSync(globalHooks)).toEqual(['pre-commit']);
|
|
142
|
+
expect(r.stderr).toContain('git config --local core.hooksPath .git/hooks');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('skips when a local core.hooksPath points outside the repo', () => {
|
|
146
|
+
const outside = join(sb.root, 'elsewhere-hooks');
|
|
147
|
+
mkdirSync(outside, {recursive: true});
|
|
148
|
+
const pkg = makePackage(join(sb.root, 'repo'), true);
|
|
149
|
+
git(pkg, 'config', '--local', 'core.hooksPath', outside);
|
|
150
|
+
|
|
151
|
+
const r = runScript(pkg);
|
|
152
|
+
|
|
153
|
+
expect(r.status).toBe(0);
|
|
154
|
+
expect(npxCalled()).toBe(false);
|
|
155
|
+
expect(readdirSync(outside)).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('skips when installed as a dependency under node_modules', () => {
|
|
159
|
+
// Consumer repo with the package vendored under node_modules.
|
|
160
|
+
const consumer = join(sb.root, 'consumer');
|
|
161
|
+
mkdirSync(consumer, {recursive: true});
|
|
162
|
+
git(consumer, 'init', '-q');
|
|
163
|
+
const pkg = makePackage(join(consumer, 'node_modules/@scope/pkg'), false);
|
|
164
|
+
|
|
165
|
+
const r = runScript(pkg);
|
|
166
|
+
|
|
167
|
+
expect(r.status).toBe(0);
|
|
168
|
+
expect(npxCalled()).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('skips when installed as a dependency with its own .git under node_modules', () => {
|
|
172
|
+
// e.g. a git-URL dependency cloned in place.
|
|
173
|
+
const pkg = makePackage(join(sb.root, 'consumer/node_modules/pkg'), true);
|
|
174
|
+
|
|
175
|
+
const r = runScript(pkg);
|
|
176
|
+
|
|
177
|
+
expect(r.status).toBe(0);
|
|
178
|
+
expect(npxCalled()).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('skips when the package is not the root of its git work tree', () => {
|
|
182
|
+
const outer = join(sb.root, 'outer');
|
|
183
|
+
mkdirSync(outer, {recursive: true});
|
|
184
|
+
git(outer, 'init', '-q');
|
|
185
|
+
const pkg = makePackage(join(outer, 'vendor/pkg'), false);
|
|
186
|
+
|
|
187
|
+
const r = runScript(pkg);
|
|
188
|
+
|
|
189
|
+
expect(r.status).toBe(0);
|
|
190
|
+
expect(npxCalled()).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('skips when CI is set', () => {
|
|
194
|
+
const pkg = makePackage(join(sb.root, 'repo'), true);
|
|
195
|
+
const r = runScript(pkg, {CI: 'true'});
|
|
196
|
+
expect(r.status).toBe(0);
|
|
197
|
+
expect(npxCalled()).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('skips during npm pack / publish', () => {
|
|
201
|
+
const pkg = makePackage(join(sb.root, 'repo'), true);
|
|
202
|
+
expect(runScript(pkg, {npm_command: 'pack'}).status).toBe(0);
|
|
203
|
+
expect(runScript(pkg, {npm_command: 'publish'}).status).toBe(0);
|
|
204
|
+
expect(runScript(pkg, {npm_config_dry_run: 'true'}).status).toBe(0);
|
|
205
|
+
expect(npxCalled()).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('opt-in instructions use the same pinned lefthook version', () => {
|
|
209
|
+
const script = readFileSync(SCRIPT, 'utf8');
|
|
210
|
+
expect(script).toContain(`npx --yes ${LEFTHOOK_PIN} install --force`);
|
|
211
|
+
expect(script).not.toMatch(/npx lefthook /);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('npm install never writes into a global core.hooksPath', () => {
|
|
216
|
+
// lefthook's npm package runs `lefthook install -f` from its own
|
|
217
|
+
// postinstall whenever CI is unset, bypassing install-hooks.sh and
|
|
218
|
+
// writing shims into whatever core.hooksPath resolves to (renaming
|
|
219
|
+
// existing hooks to *.old). It must therefore never be installed as
|
|
220
|
+
// a dependency of this package.
|
|
221
|
+
const pkgJson = JSON.parse(
|
|
222
|
+
readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'),
|
|
223
|
+
) as Record<string, Record<string, string> | undefined>;
|
|
224
|
+
|
|
225
|
+
it('does not depend on the lefthook npm package', () => {
|
|
226
|
+
for (const field of [
|
|
227
|
+
'dependencies',
|
|
228
|
+
'devDependencies',
|
|
229
|
+
'optionalDependencies',
|
|
230
|
+
'peerDependencies',
|
|
231
|
+
]) {
|
|
232
|
+
expect(Object.keys(pkgJson[field] ?? {})).not.toContain('lefthook');
|
|
233
|
+
}
|
|
234
|
+
const lock = readFileSync(join(PKG_ROOT, 'package-lock.json'), 'utf8');
|
|
235
|
+
expect(lock).not.toMatch(/"node_modules\/lefthook(-[a-z0-9-]+)?"/);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('has no install-time script other than the guarded prepare hook', () => {
|
|
239
|
+
const scripts = pkgJson.scripts ?? {};
|
|
240
|
+
for (const name of ['preinstall', 'install', 'postinstall']) {
|
|
241
|
+
expect(scripts[name]).toBeUndefined();
|
|
242
|
+
}
|
|
243
|
+
expect(scripts.prepare).toBe('sh scripts/install-hooks.sh');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('leaves a sentinel hook in the global hooksPath untouched', () => {
|
|
247
|
+
const globalHooks = join(sb.root, 'global-hooks');
|
|
248
|
+
mkdirSync(globalHooks, {recursive: true});
|
|
249
|
+
const sentinel = join(globalHooks, 'commit-msg');
|
|
250
|
+
const body = '#!/bin/sh\n# sentinel\nexit 0\n';
|
|
251
|
+
writeFileSync(sentinel, body);
|
|
252
|
+
chmodSync(sentinel, 0o755);
|
|
253
|
+
writeFileSync(
|
|
254
|
+
join(sb.home, '.gitconfig'),
|
|
255
|
+
`[core]\n\thooksPath = ${globalHooks}\n`,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const pkg = makePackage(join(sb.root, 'repo'), true);
|
|
259
|
+
const r = runScript(pkg);
|
|
260
|
+
|
|
261
|
+
expect(r.status).toBe(0);
|
|
262
|
+
expect(npxCalled()).toBe(false);
|
|
263
|
+
expect(readdirSync(globalHooks)).toEqual(['commit-msg']);
|
|
264
|
+
expect(readFileSync(sentinel, 'utf8')).toBe(body);
|
|
265
|
+
expect(statSync(sentinel).mode & 0o777).toBe(0o755);
|
|
266
|
+
});
|
|
267
|
+
});
|