@_linked/cli 1.46.2 → 1.47.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/CHANGELOG.md +113 -0
- package/README.md +44 -36
- package/hooks/drift.cjs +169 -0
- package/hooks/git-hook.cjs +170 -0
- package/hooks/hook-shim.cjs +63 -0
- package/hooks/lock-at-rest.cjs +268 -0
- package/hooks/manifests.cjs +216 -0
- package/hooks/platform.cjs +25 -0
- package/lib/esm/app-release/build-backend.d.ts.map +1 -1
- package/lib/esm/app-release/build-backend.js +2 -1
- package/lib/esm/app-release/build-backend.js.map +1 -1
- package/lib/esm/cli-methods.d.ts.map +1 -1
- package/lib/esm/cli-methods.js +166 -150
- package/lib/esm/cli-methods.js.map +1 -1
- package/lib/esm/cli.js +20 -22
- package/lib/esm/cli.js.map +1 -1
- package/lib/esm/commands/build-app.d.ts.map +1 -1
- package/lib/esm/commands/build-app.js +2 -1
- package/lib/esm/commands/build-app.js.map +1 -1
- package/lib/esm/commands/build-package.d.ts.map +1 -1
- package/lib/esm/commands/build-package.js +6 -2
- package/lib/esm/commands/build-package.js.map +1 -1
- package/lib/esm/commands/localize.d.ts +9 -15
- package/lib/esm/commands/localize.d.ts.map +1 -1
- package/lib/esm/commands/localize.js +43 -74
- package/lib/esm/commands/localize.js.map +1 -1
- package/lib/esm/commands/setup-publish.d.ts.map +1 -1
- package/lib/esm/commands/setup-publish.js +22 -15
- package/lib/esm/commands/setup-publish.js.map +1 -1
- package/lib/esm/commands/start.d.ts +8 -0
- package/lib/esm/commands/start.d.ts.map +1 -1
- package/lib/esm/commands/start.js +20 -8
- package/lib/esm/commands/start.js.map +1 -1
- package/lib/esm/installed-packages.d.ts +5 -13
- package/lib/esm/installed-packages.d.ts.map +1 -1
- package/lib/esm/installed-packages.js +5 -38
- package/lib/esm/installed-packages.js.map +1 -1
- package/lib/esm/lifecycle.d.ts +35 -22
- package/lib/esm/lifecycle.d.ts.map +1 -1
- package/lib/esm/lifecycle.js +62 -36
- package/lib/esm/lifecycle.js.map +1 -1
- package/lib/esm/localize/adopt.d.ts +5 -5
- package/lib/esm/localize/adopt.d.ts.map +1 -1
- package/lib/esm/localize/adopt.js +46 -55
- package/lib/esm/localize/adopt.js.map +1 -1
- package/lib/esm/localize/app-root.d.ts +16 -0
- package/lib/esm/localize/app-root.d.ts.map +1 -0
- package/lib/esm/localize/app-root.js +94 -0
- package/lib/esm/localize/app-root.js.map +1 -0
- package/lib/esm/localize/app-setup.d.ts +21 -0
- package/lib/esm/localize/app-setup.d.ts.map +1 -0
- package/lib/esm/localize/app-setup.js +178 -0
- package/lib/esm/localize/app-setup.js.map +1 -0
- package/lib/esm/localize/delocalize.d.ts.map +1 -1
- package/lib/esm/localize/delocalize.js +168 -104
- package/lib/esm/localize/delocalize.js.map +1 -1
- package/lib/esm/localize/drift-hook.d.ts +18 -0
- package/lib/esm/localize/drift-hook.d.ts.map +1 -0
- package/lib/esm/localize/drift-hook.js +37 -0
- package/lib/esm/localize/drift-hook.js.map +1 -0
- package/lib/esm/localize/drift.d.ts +36 -0
- package/lib/esm/localize/drift.d.ts.map +1 -0
- package/lib/esm/localize/drift.js +34 -0
- package/lib/esm/localize/drift.js.map +1 -0
- package/lib/esm/localize/errors.d.ts +14 -8
- package/lib/esm/localize/errors.d.ts.map +1 -1
- package/lib/esm/localize/errors.js +14 -8
- package/lib/esm/localize/errors.js.map +1 -1
- package/lib/esm/localize/fsops.d.ts +11 -27
- package/lib/esm/localize/fsops.d.ts.map +1 -1
- package/lib/esm/localize/fsops.js +40 -115
- package/lib/esm/localize/fsops.js.map +1 -1
- package/lib/esm/localize/git-hooks.d.ts +33 -0
- package/lib/esm/localize/git-hooks.d.ts.map +1 -0
- package/lib/esm/localize/git-hooks.js +146 -0
- package/lib/esm/localize/git-hooks.js.map +1 -0
- package/lib/esm/localize/hook-module.d.ts +6 -0
- package/lib/esm/localize/hook-module.d.ts.map +1 -0
- package/lib/esm/localize/hook-module.js +37 -0
- package/lib/esm/localize/hook-module.js.map +1 -0
- package/lib/esm/localize/index.d.ts +14 -15
- package/lib/esm/localize/index.d.ts.map +1 -1
- package/lib/esm/localize/index.js +8 -9
- package/lib/esm/localize/index.js.map +1 -1
- package/lib/esm/localize/list.d.ts +6 -10
- package/lib/esm/localize/list.d.ts.map +1 -1
- package/lib/esm/localize/list.js +48 -189
- package/lib/esm/localize/list.js.map +1 -1
- package/lib/esm/localize/localize.d.ts +41 -62
- package/lib/esm/localize/localize.d.ts.map +1 -1
- package/lib/esm/localize/localize.js +398 -285
- package/lib/esm/localize/localize.js.map +1 -1
- package/lib/esm/localize/members.d.ts +13 -0
- package/lib/esm/localize/members.d.ts.map +1 -0
- package/lib/esm/localize/members.js +46 -0
- package/lib/esm/localize/members.js.map +1 -0
- package/lib/esm/localize/resolve.d.ts +1 -2
- package/lib/esm/localize/resolve.d.ts.map +1 -1
- package/lib/esm/localize/resolve.js +3 -5
- package/lib/esm/localize/resolve.js.map +1 -1
- package/lib/esm/localize/run.d.ts +1 -0
- package/lib/esm/localize/run.d.ts.map +1 -1
- package/lib/esm/localize/run.js +11 -6
- package/lib/esm/localize/run.js.map +1 -1
- package/lib/esm/localize/setup-lockfile.d.ts +13 -0
- package/lib/esm/localize/setup-lockfile.d.ts.map +1 -0
- package/lib/esm/localize/setup-lockfile.js +80 -0
- package/lib/esm/localize/setup-lockfile.js.map +1 -0
- package/lib/esm/utils/createPackageLocation.d.ts +4 -13
- package/lib/esm/utils/createPackageLocation.d.ts.map +1 -1
- package/lib/esm/utils/createPackageLocation.js +12 -21
- package/lib/esm/utils/createPackageLocation.js.map +1 -1
- package/lib/esm/utils/packageSetup.d.ts +5 -2
- package/lib/esm/utils/packageSetup.d.ts.map +1 -1
- package/lib/esm/utils/packageSetup.js +9 -3
- package/lib/esm/utils/packageSetup.js.map +1 -1
- package/lib/esm/utils/platform.d.ts +24 -0
- package/lib/esm/utils/platform.d.ts.map +1 -0
- package/lib/esm/utils/platform.js +62 -0
- package/lib/esm/utils/platform.js.map +1 -0
- package/lib/esm/utils/shapeReferences.d.ts.map +1 -1
- package/lib/esm/utils/shapeReferences.js +3 -2
- package/lib/esm/utils/shapeReferences.js.map +1 -1
- package/lib/esm/utils/spawn.d.ts +65 -0
- package/lib/esm/utils/spawn.d.ts.map +1 -0
- package/lib/esm/utils/spawn.js +181 -0
- package/lib/esm/utils/spawn.js.map +1 -0
- package/lib/esm/utils.d.ts.map +1 -1
- package/lib/esm/utils.js +19 -5
- package/lib/esm/utils.js.map +1 -1
- package/lib/esm/vite-config.d.ts +5 -12
- package/lib/esm/vite-config.d.ts.map +1 -1
- package/lib/esm/vite-config.js +41 -33
- package/lib/esm/vite-config.js.map +1 -1
- package/package.json +4 -3
- package/lib/esm/localize/ensure.d.ts +0 -44
- package/lib/esm/localize/ensure.d.ts.map +0 -1
- package/lib/esm/localize/ensure.js +0 -151
- package/lib/esm/localize/ensure.js.map +0 -1
- package/lib/esm/localize/manifest.d.ts +0 -63
- package/lib/esm/localize/manifest.d.ts.map +0 -1
- package/lib/esm/localize/manifest.js +0 -133
- package/lib/esm/localize/manifest.js.map +0 -1
- package/lib/esm/localize/one-copy-hook.d.ts +0 -16
- package/lib/esm/localize/one-copy-hook.d.ts.map +0 -1
- package/lib/esm/localize/one-copy-hook.js +0 -20
- package/lib/esm/localize/one-copy-hook.js.map +0 -1
- package/lib/esm/localize/provided.d.ts +0 -28
- package/lib/esm/localize/provided.d.ts.map +0 -1
- package/lib/esm/localize/provided.js +0 -89
- package/lib/esm/localize/provided.js.map +0 -1
- package/lib/esm/localize/prune.d.ts +0 -182
- package/lib/esm/localize/prune.d.ts.map +0 -1
- package/lib/esm/localize/prune.js +0 -703
- package/lib/esm/localize/prune.js.map +0 -1
- package/lib/esm/localize/relink.d.ts +0 -13
- package/lib/esm/localize/relink.d.ts.map +0 -1
- package/lib/esm/localize/relink.js +0 -181
- package/lib/esm/localize/relink.js.map +0 -1
- package/lib/esm/localized-dedupe.d.ts +0 -26
- package/lib/esm/localized-dedupe.d.ts.map +0 -1
- package/lib/esm/localized-dedupe.js +0 -139
- package/lib/esm/localized-dedupe.js.map +0 -1
- package/lib/esm/utils/relinkHooks.d.ts +0 -19
- package/lib/esm/utils/relinkHooks.d.ts.map +0 -1
- package/lib/esm/utils/relinkHooks.js +0 -74
- package/lib/esm/utils/relinkHooks.js.map +0 -1
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// Keeps the committed package-lock.json clean at rest while checkouts in packages-local/* are
|
|
2
|
+
// workspace members. With them present npm rewrites the lockfile to list them; the committed one
|
|
3
|
+
// must stay the registry lockfile. The app's `postinstall` and `dependencies` scripts run it
|
|
4
|
+
// through a guard that does nothing when the cli is not installed (`npm ci --omit=dev`):
|
|
5
|
+
// node -e "const p='node_modules/@_linked/cli/hooks/lock-at-rest.cjs';require('fs').existsSync(p)&&require(require('path').resolve(p))"
|
|
6
|
+
// so it runs when required as well as when run as `node lock-at-rest.cjs`. It is plain, unbuilt
|
|
7
|
+
// CommonJS with Node built-ins only: it must work when the cli is a fresh checkout without lib/,
|
|
8
|
+
// and it never needs the `linked` binary to exist.
|
|
9
|
+
//
|
|
10
|
+
// It never fails the install: no git, not a work tree, git refusing (safe.directory in Docker), no
|
|
11
|
+
// HEAD yet -- all leave the lockfile as npm wrote it.
|
|
12
|
+
'use strict';
|
|
13
|
+
const {execFileSync} = require('child_process');
|
|
14
|
+
const {createHash} = require('crypto');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
LEAK,
|
|
20
|
+
LOCAL_DIR,
|
|
21
|
+
localMembers,
|
|
22
|
+
manifestPathspecs,
|
|
23
|
+
} = require('./manifests.cjs');
|
|
24
|
+
const {hookLine, runsOurHook} = require('./hook-shim.cjs');
|
|
25
|
+
const {terminalDevice} = require('./platform.cjs');
|
|
26
|
+
|
|
27
|
+
// An app's lockfile is often over a megabyte, execFileSync's default buffer.
|
|
28
|
+
const MAX_BUFFER = 256 * 1024 * 1024;
|
|
29
|
+
const LOCKFILE = 'package-lock.json';
|
|
30
|
+
const ADVICE =
|
|
31
|
+
'To change dependencies: npx linked delocalize, npm install <dep>, commit, then npx linked localize --parked.';
|
|
32
|
+
// Where an interrupted first set-up leaves the checkouts: SETUP_ASIDE_DIR in src/localize/localize.ts.
|
|
33
|
+
const SETUP_ASIDE = `${LOCAL_DIR}/.setup-aside`;
|
|
34
|
+
|
|
35
|
+
function run() {
|
|
36
|
+
// npm runs lifecycle scripts from the package root: the app, which may sit below the git top
|
|
37
|
+
// level. (__dirname is no guide: a localized cli resolves to its checkout.)
|
|
38
|
+
const root = process.cwd();
|
|
39
|
+
|
|
40
|
+
// npm pipes (and hides) the output of the `dependencies` script, so also write to the terminal,
|
|
41
|
+
// opened the first time there is something to say.
|
|
42
|
+
let tty;
|
|
43
|
+
function write(text) {
|
|
44
|
+
process.stderr.write(text);
|
|
45
|
+
if (process.env.npm_lifecycle_event !== 'dependencies') return;
|
|
46
|
+
if (tty === undefined) {
|
|
47
|
+
const device = terminalDevice();
|
|
48
|
+
try {
|
|
49
|
+
// Windows has no such device: there, only stderr (hidden by npm for this script).
|
|
50
|
+
tty = device ? fs.openSync(device, 'w') : null;
|
|
51
|
+
} catch {
|
|
52
|
+
tty = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (tty === null) return;
|
|
56
|
+
try {
|
|
57
|
+
fs.writeSync(tty, text);
|
|
58
|
+
} catch {
|
|
59
|
+
/* the terminal went away; stderr has it */
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const say = (m) => write(`[lock-at-rest] ${m}\n`);
|
|
63
|
+
|
|
64
|
+
function git(...args) {
|
|
65
|
+
return execFileSync('git', args, {
|
|
66
|
+
cwd: root,
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
69
|
+
maxBuffer: MAX_BUFFER,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The staged lockfile, or null when the index has none (or git cannot say). */
|
|
74
|
+
function staged() {
|
|
75
|
+
try {
|
|
76
|
+
return git('show', `:./${LOCKFILE}`);
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function restore(members) {
|
|
83
|
+
let changed, lockDirty;
|
|
84
|
+
try {
|
|
85
|
+
if (git('rev-parse', '--is-inside-work-tree').trim() !== 'true') return;
|
|
86
|
+
// A changed manifest means the developer is changing dependencies: leave the lockfile alone.
|
|
87
|
+
changed = git(
|
|
88
|
+
'diff',
|
|
89
|
+
'--name-only',
|
|
90
|
+
'--relative',
|
|
91
|
+
'HEAD',
|
|
92
|
+
'--',
|
|
93
|
+
...manifestPathspecs(root),
|
|
94
|
+
).trim();
|
|
95
|
+
// Tracked and different from HEAD; an app that does not commit its lockfile has nothing to restore.
|
|
96
|
+
lockDirty = git(
|
|
97
|
+
'diff',
|
|
98
|
+
'--name-only',
|
|
99
|
+
'--relative',
|
|
100
|
+
'HEAD',
|
|
101
|
+
'--',
|
|
102
|
+
LOCKFILE,
|
|
103
|
+
).trim();
|
|
104
|
+
} catch {
|
|
105
|
+
return; // no git, not a work tree, no HEAD, or git refused
|
|
106
|
+
}
|
|
107
|
+
if (!lockDirty) return;
|
|
108
|
+
|
|
109
|
+
const linked = `${members.map((m) => m.dir).join(', ')} stay linked in node_modules`;
|
|
110
|
+
if (changed) {
|
|
111
|
+
say(
|
|
112
|
+
`WARNING: ${changed.split('\n').join(', ')} differ(s) from HEAD while checkouts are linked (${linked}).`,
|
|
113
|
+
);
|
|
114
|
+
say(
|
|
115
|
+
`${LOCKFILE} now references packages-local and must NOT be committed.`,
|
|
116
|
+
);
|
|
117
|
+
say(ADVICE);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// The staged lockfile is the developer's, unless it was written while linked too.
|
|
122
|
+
const index = staged();
|
|
123
|
+
if (index === null) {
|
|
124
|
+
say(`${LOCKFILE} is not in the index; left as npm wrote it.`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
// A staged lockfile that lists a checkout is no better than the work tree's: go back to
|
|
129
|
+
// HEAD. Otherwise the staged one is a deliberate change of the developer's: go back to it.
|
|
130
|
+
if (LEAK.test(index)) {
|
|
131
|
+
git('checkout', 'HEAD', '--', LOCKFILE);
|
|
132
|
+
say(
|
|
133
|
+
`restored the committed ${LOCKFILE} (the staged one listed a checkout; ${linked})`,
|
|
134
|
+
);
|
|
135
|
+
} else {
|
|
136
|
+
if (!git('diff', '--name-only', '--', LOCKFILE).trim()) return; // the work tree is the index
|
|
137
|
+
const stagedChange =
|
|
138
|
+
git('diff', '--cached', '--name-only', '--', LOCKFILE).trim() !== '';
|
|
139
|
+
git('checkout', '--', LOCKFILE);
|
|
140
|
+
say(
|
|
141
|
+
`restored the ${stagedChange ? 'staged' : 'committed'} ${LOCKFILE} (${linked})`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
say(
|
|
145
|
+
'Changes to dependencies made while localized are not kept: they need `linked delocalize` first.',
|
|
146
|
+
);
|
|
147
|
+
} catch (e) {
|
|
148
|
+
say(
|
|
149
|
+
`could not restore ${LOCKFILE}: ${String((e && e.stderr) || e).trim()}`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// A clone set up by hand (a bootstrap that copied packages-local/ in, or ran `npm install` only)
|
|
155
|
+
// never got the git hooks: no pre-commit refuses a leaked lockfile, and no npm install follows a
|
|
156
|
+
// pull. The lines to print when this is a work tree, `core.hooksPath` is unset (then the hooks
|
|
157
|
+
// are added by hand, see git-hooks.ts) and the hooks directory has no pre-commit that runs ours;
|
|
158
|
+
// none otherwise.
|
|
159
|
+
function hooksNotice(members) {
|
|
160
|
+
try {
|
|
161
|
+
if (git('rev-parse', '--is-inside-work-tree').trim() !== 'true')
|
|
162
|
+
return [];
|
|
163
|
+
} catch {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
if (git('config', 'core.hooksPath').trim()) return [];
|
|
168
|
+
} catch {
|
|
169
|
+
/* unset */
|
|
170
|
+
}
|
|
171
|
+
let file, prefix;
|
|
172
|
+
try {
|
|
173
|
+
file = path.resolve(
|
|
174
|
+
root,
|
|
175
|
+
git('rev-parse', '--git-path', 'hooks').trim(),
|
|
176
|
+
'pre-commit',
|
|
177
|
+
);
|
|
178
|
+
prefix = git('rev-parse', '--show-prefix').trim();
|
|
179
|
+
} catch {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
let text = null;
|
|
183
|
+
try {
|
|
184
|
+
text = fs.readFileSync(file, 'utf8');
|
|
185
|
+
} catch {
|
|
186
|
+
/* no pre-commit */
|
|
187
|
+
}
|
|
188
|
+
if (text === null) {
|
|
189
|
+
// --adopt needs a git checkout: name one that is.
|
|
190
|
+
const checkout = members.find((m) =>
|
|
191
|
+
fs.existsSync(path.join(root, m.dir, '.git')),
|
|
192
|
+
);
|
|
193
|
+
const name = checkout ? checkout.name : "<any checkout's name>";
|
|
194
|
+
return [
|
|
195
|
+
`git hooks are not installed in this clone: run \`npx linked localize ${name} --adopt --build ''\` once`,
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
if (runsOurHook(text)) return [];
|
|
199
|
+
// localize leaves a hook it did not write alone, so --adopt would not help: add ours by hand.
|
|
200
|
+
return [
|
|
201
|
+
`${path.relative(root, file)} is not ours, so nothing refuses a commit of a lockfile that ` +
|
|
202
|
+
'lists a checkout. Add this line to it if it is an `sh` script (another kind, such as the ' +
|
|
203
|
+
'Python hook the pre-commit framework writes, needs it run from an `sh` hook of its own; ' +
|
|
204
|
+
'it does nothing where the cli is not installed):',
|
|
205
|
+
` ${hookLine('pre-commit', prefix)}`,
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// The drift report and the missing-hooks notice, after the hidden lockfile npm just wrote; only
|
|
210
|
+
// when its content changed since the last report, so the second of `postinstall` and
|
|
211
|
+
// `dependencies`, and an install that changed nothing, stay quiet. npm rewrites the file on every
|
|
212
|
+
// install, so its mtime says nothing: the stamp is a hash of the content. Report only.
|
|
213
|
+
function report(members) {
|
|
214
|
+
const nodeModules = path.join(root, 'node_modules');
|
|
215
|
+
let content;
|
|
216
|
+
try {
|
|
217
|
+
content = fs.readFileSync(path.join(nodeModules, '.package-lock.json'));
|
|
218
|
+
} catch {
|
|
219
|
+
return; // nothing installed
|
|
220
|
+
}
|
|
221
|
+
const stamp = `${createHash('sha1').update(content).digest('hex')}\n`;
|
|
222
|
+
const stampFile = path.join(
|
|
223
|
+
nodeModules,
|
|
224
|
+
'.cache',
|
|
225
|
+
'linked-localize',
|
|
226
|
+
'drift-stamp',
|
|
227
|
+
);
|
|
228
|
+
try {
|
|
229
|
+
if (fs.readFileSync(stampFile, 'utf8') === stamp) return;
|
|
230
|
+
} catch {
|
|
231
|
+
/* no report yet */
|
|
232
|
+
}
|
|
233
|
+
const {driftProblems, describeDrift} = require('./drift.cjs');
|
|
234
|
+
for (const d of driftProblems(root))
|
|
235
|
+
write(`[localize] drift: ${describeDrift(d)}\n`);
|
|
236
|
+
for (const line of hooksNotice(members)) say(line);
|
|
237
|
+
try {
|
|
238
|
+
fs.mkdirSync(path.dirname(stampFile), {recursive: true});
|
|
239
|
+
fs.writeFileSync(stampFile, stamp);
|
|
240
|
+
} catch {
|
|
241
|
+
/* reported again next time */
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
// Rare, and the checkouts there are invisible to npm until moved back: said on every install.
|
|
247
|
+
if (fs.existsSync(path.join(root, SETUP_ASIDE)))
|
|
248
|
+
say(
|
|
249
|
+
`checkouts are set aside in ${SETUP_ASIDE}/ (an interrupted set-up): \`npx linked localize --list\` moves them back`,
|
|
250
|
+
);
|
|
251
|
+
const members = localMembers(root);
|
|
252
|
+
if (!members.length) return; // nothing localized: npm owns the lockfile as usual
|
|
253
|
+
restore(members);
|
|
254
|
+
report(members);
|
|
255
|
+
} catch (e) {
|
|
256
|
+
say(`skipped: ${e && e.message ? e.message : e}`);
|
|
257
|
+
} finally {
|
|
258
|
+
if (typeof tty === 'number') {
|
|
259
|
+
try {
|
|
260
|
+
fs.closeSync(tty);
|
|
261
|
+
} catch {
|
|
262
|
+
/* closed already */
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
run();
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// What the hooks and the drift check need to know about the app's own manifests: its workspace
|
|
2
|
+
// globs, the git pathspecs of every dependency manifest they cover, its checkouts, and what a
|
|
3
|
+
// leaked checkout looks like in a committed manifest.
|
|
4
|
+
// Plain, unbuilt CommonJS with Node built-ins only, like the other files in hooks/.
|
|
5
|
+
'use strict';
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const LOCAL_DIR = 'packages-local';
|
|
10
|
+
|
|
11
|
+
// A committed package.json or package-lock.json that lists a checkout: "packages-local/<dir>".
|
|
12
|
+
// The glob "packages-local/*" itself is the committed setup and is allowed.
|
|
13
|
+
const LEAK = /"packages-local\/(?!\*")/;
|
|
14
|
+
|
|
15
|
+
function readJson(file) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The app's `workspaces` globs as written (the array, or yarn's `{packages: [...]}`, which npm
|
|
25
|
+
* reads too), or null when it has none or its package.json cannot be read.
|
|
26
|
+
*/
|
|
27
|
+
function workspaceGlobs(appRoot) {
|
|
28
|
+
const pkg = readJson(path.join(appRoot, 'package.json'));
|
|
29
|
+
const ws = pkg && pkg.workspaces;
|
|
30
|
+
const globs = Array.isArray(ws)
|
|
31
|
+
? ws
|
|
32
|
+
: ws && Array.isArray(ws.packages)
|
|
33
|
+
? ws.packages
|
|
34
|
+
: null;
|
|
35
|
+
return globs ? globs.filter((g) => typeof g === 'string' && g !== '') : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// A glob without its leading `./` and trailing slashes; null for a negation or a checkout glob.
|
|
39
|
+
function trackedGlob(glob) {
|
|
40
|
+
if (glob.startsWith('!')) return null;
|
|
41
|
+
const g = glob.replace(/^\.\//, '').replace(/\/+$/, '');
|
|
42
|
+
if (g === '' || g === LOCAL_DIR || g.startsWith(`${LOCAL_DIR}/`)) return null;
|
|
43
|
+
return g;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The workspace globs that hold tracked members: every glob but the checkouts' own. `fallback` is
|
|
48
|
+
* what an app without `workspaces` gets.
|
|
49
|
+
*/
|
|
50
|
+
function trackedGlobs(appRoot, fallback = []) {
|
|
51
|
+
const globs = workspaceGlobs(appRoot);
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const glob of globs || fallback) {
|
|
54
|
+
const g = trackedGlob(glob);
|
|
55
|
+
if (g !== null && !out.includes(g)) out.push(g);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The git pathspecs, relative to the app directory, of the manifests whose change is a change of
|
|
62
|
+
* dependencies: `package.json` and `<glob>/package.json` for each workspace glob except
|
|
63
|
+
* `packages-local/*` (checkouts are not committed).
|
|
64
|
+
*/
|
|
65
|
+
function manifestPathspecs(appRoot) {
|
|
66
|
+
return [
|
|
67
|
+
'package.json',
|
|
68
|
+
...trackedGlobs(appRoot).map((g) => `${g}/package.json`),
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** One glob segment (`*`, `?`, literal text) as a regular expression. */
|
|
73
|
+
function segmentRegExp(segment) {
|
|
74
|
+
const body = segment
|
|
75
|
+
.split('')
|
|
76
|
+
.map((c) =>
|
|
77
|
+
c === '*'
|
|
78
|
+
? '[^/]*'
|
|
79
|
+
: c === '?'
|
|
80
|
+
? '[^/]'
|
|
81
|
+
: c.replace(/[.+^${}()|[\]\\]/g, '\\$&'),
|
|
82
|
+
)
|
|
83
|
+
.join('');
|
|
84
|
+
return new RegExp(`^${body}$`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The directories, relative to the app and sorted per glob, that `globs` match and that hold a
|
|
89
|
+
* package.json. Globs are matched one path segment at a time; `**` matches any depth below it
|
|
90
|
+
* (node_modules and dot directories excepted).
|
|
91
|
+
*/
|
|
92
|
+
function expandGlobs(appRoot, globs) {
|
|
93
|
+
const found = [];
|
|
94
|
+
const walk = (rel, segments) => {
|
|
95
|
+
if (!segments.length) {
|
|
96
|
+
if (
|
|
97
|
+
rel &&
|
|
98
|
+
fs.existsSync(path.join(appRoot, rel, 'package.json')) &&
|
|
99
|
+
!found.includes(rel)
|
|
100
|
+
)
|
|
101
|
+
found.push(rel);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const [segment, ...rest] = segments;
|
|
105
|
+
if (!/[*?]/.test(segment))
|
|
106
|
+
return walk(rel ? `${rel}/${segment}` : segment, rest);
|
|
107
|
+
let entries = [];
|
|
108
|
+
try {
|
|
109
|
+
entries = fs
|
|
110
|
+
.readdirSync(path.join(appRoot, rel), {withFileTypes: true})
|
|
111
|
+
.filter(
|
|
112
|
+
(e) =>
|
|
113
|
+
(e.isDirectory() || e.isSymbolicLink()) &&
|
|
114
|
+
!e.name.startsWith('.') &&
|
|
115
|
+
e.name !== 'node_modules',
|
|
116
|
+
)
|
|
117
|
+
.map((e) => e.name)
|
|
118
|
+
.sort();
|
|
119
|
+
} catch {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (segment === '**') {
|
|
123
|
+
walk(rel, rest);
|
|
124
|
+
for (const name of entries) walk(rel ? `${rel}/${name}` : name, segments);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const re = segmentRegExp(segment);
|
|
128
|
+
for (const name of entries)
|
|
129
|
+
if (re.test(name)) walk(rel ? `${rel}/${name}` : name, rest);
|
|
130
|
+
};
|
|
131
|
+
for (const glob of globs) walk('', glob.split('/'));
|
|
132
|
+
return found;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Do the workspace `globs` match `rel` (a directory relative to the app, `/`-separated) the way
|
|
137
|
+
* `expandGlobs` would find it -- `*` and `?` within a segment, `**` across any depth, never into
|
|
138
|
+
* node_modules or a dot directory -- and does no negation (`!glob`) exclude it?
|
|
139
|
+
*/
|
|
140
|
+
function globsMatch(globs, rel) {
|
|
141
|
+
const target = rel.split('/').filter((s) => s !== '' && s !== '.');
|
|
142
|
+
if (!target.length) return false;
|
|
143
|
+
const matches = (segments, names) => {
|
|
144
|
+
if (!segments.length) return !names.length;
|
|
145
|
+
const [segment, ...rest] = segments;
|
|
146
|
+
if (segment === '**') {
|
|
147
|
+
if (matches(rest, names)) return true;
|
|
148
|
+
return (
|
|
149
|
+
names.length > 0 &&
|
|
150
|
+
wildcardName(names[0]) &&
|
|
151
|
+
matches(segments, names.slice(1))
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (!names.length) return false;
|
|
155
|
+
if (/[*?]/.test(segment)) {
|
|
156
|
+
if (!wildcardName(names[0]) || !segmentRegExp(segment).test(names[0]))
|
|
157
|
+
return false;
|
|
158
|
+
} else if (segment !== names[0]) return false;
|
|
159
|
+
return matches(rest, names.slice(1));
|
|
160
|
+
};
|
|
161
|
+
const split = (g) =>
|
|
162
|
+
g
|
|
163
|
+
.replace(/^\.\//, '')
|
|
164
|
+
.replace(/\/+$/, '')
|
|
165
|
+
.split('/')
|
|
166
|
+
.filter((s) => s !== '');
|
|
167
|
+
let included = false;
|
|
168
|
+
for (const glob of globs) {
|
|
169
|
+
if (glob.startsWith('!')) {
|
|
170
|
+
if (matches(split(glob.slice(1)), target)) return false;
|
|
171
|
+
} else if (!included && matches(split(glob), target)) included = true;
|
|
172
|
+
}
|
|
173
|
+
return included;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// What a wildcard segment may stand for: not node_modules, not a dot directory.
|
|
177
|
+
function wildcardName(name) {
|
|
178
|
+
return !name.startsWith('.') && name !== 'node_modules';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The checkouts npm links: every directory in `packages-local/` with a named package.json, as
|
|
183
|
+
* `{name, version, dir}` with `dir` app-relative, sorted by name.
|
|
184
|
+
*/
|
|
185
|
+
function localMembers(appRoot) {
|
|
186
|
+
let entries = [];
|
|
187
|
+
try {
|
|
188
|
+
entries = fs.readdirSync(path.join(appRoot, LOCAL_DIR));
|
|
189
|
+
} catch {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
const out = [];
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
if (entry.startsWith('.')) continue;
|
|
195
|
+
const pkg = readJson(path.join(appRoot, LOCAL_DIR, entry, 'package.json'));
|
|
196
|
+
if (!pkg || typeof pkg.name !== 'string' || !pkg.name) continue;
|
|
197
|
+
out.push({
|
|
198
|
+
name: pkg.name,
|
|
199
|
+
version: typeof pkg.version === 'string' ? pkg.version : '',
|
|
200
|
+
dir: `${LOCAL_DIR}/${entry}`,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return out.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = {
|
|
207
|
+
LOCAL_DIR,
|
|
208
|
+
LEAK,
|
|
209
|
+
readJson,
|
|
210
|
+
workspaceGlobs,
|
|
211
|
+
trackedGlobs,
|
|
212
|
+
manifestPathspecs,
|
|
213
|
+
expandGlobs,
|
|
214
|
+
globsMatch,
|
|
215
|
+
localMembers,
|
|
216
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// What the hooks do differently on Windows. Plain, unbuilt CommonJS with Node built-ins only, like
|
|
2
|
+
// the other files in hooks/; src/utils/spawn.ts is the cli's fuller version of npmCommand.
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* How to run `npm <args>` on `platform`: on Windows npm is `npm.cmd`, a batch file Node refuses to
|
|
7
|
+
* spawn without a shell (EINVAL since Node 18.20.2 / 20.12.2), so it goes through cmd.exe. The
|
|
8
|
+
* arguments are fixed flags, with nothing cmd.exe would read specially.
|
|
9
|
+
*/
|
|
10
|
+
function npmCommand(args, platform = process.platform) {
|
|
11
|
+
if (platform !== 'win32')
|
|
12
|
+
return {command: 'npm', args: [...args], shell: false};
|
|
13
|
+
if (args.some((a) => !/^[\w@./=:-]+$/.test(a)))
|
|
14
|
+
throw new Error(
|
|
15
|
+
`npmCommand: unsafe argument for cmd.exe: ${args.join(' ')}`,
|
|
16
|
+
);
|
|
17
|
+
return {command: ['npm.cmd', ...args].join(' '), args: [], shell: true};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The terminal to also write to, past npm's piping: none on Windows, which has no `/dev/tty`. */
|
|
21
|
+
function terminalDevice(platform = process.platform) {
|
|
22
|
+
return platform === 'win32' ? null : '/dev/tty';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {npmCommand, terminalDevice};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-backend.d.ts","sourceRoot":"","sources":["../../../src/app-release/build-backend.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"build-backend.d.ts","sourceRoot":"","sources":["../../../src/app-release/build-backend.ts"],"names":[],"mappings":"AAgCA,eAAO,MAAM,qBAAqB,GAChC,SAAS,MAAM,EACf,SAAQ,CAAC,CAAC,EAAE,MAAM,KAAK,OAAuB,KAC7C,MAAM,CAAC,MAAM,EAAE,MAAM,CAWvB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,8BAA8B,GACzC,SAAS,MAAM,EACf,WAAU,CACR,KAAK,EAAE,MAAM,EAAE,EACf,GAAG,EAAE,MAAM,KACR,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,EAAE,CAG5B,KACA,OAAO,CAAC,MAAM,EAAE,CAGlB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,MAAM,KAAG,MAAM,EAK3D,CAAC;AAEF,yEAAyE;AACzE,MAAM,WAAW,oBAAoB;IACnC,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AA4CD;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,EACf,WAAU,CACR,KAAK,EAAE,MAAM,EAAE,EACf,GAAG,EAAE,MAAM,KACR,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,EAAE,CAG5C,KACA,OAAO,CAAC,WAAW,EAAE,CA0BvB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,GACpC,SAAS,MAAM,EACf,cAAc,WAAW,EAAE,KAC1B,OAAO,CAAC;IAAC,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAC,CAgFhE,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,yBAAyB,GACpC,SAAS,MAAM,EACf,eAAe,WAAW,EAAE,KAC3B,OAAO,CAAC,IAAI,CA4Bd,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,gCAAgC,GAC3C,SAAS,MAAM,EACf,oBAAoB,MAAM,EAAE,KAC3B,MAAM,GAAG,IAgBX,CAAC;AAqBF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,oBAAoB,GAC/B,UAAS;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAM,EAChC,eAAc;IAAC,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAAM,KACjE,OAAO,CAAC,IAAI,CAqFd,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GACjC,SAAS,MAAM,EACf,cAAc,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,KAC/C,OAAO,CAAC,MAAM,CAYhB,CAAC"}
|
|
@@ -21,6 +21,7 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
21
21
|
import { builtinModules } from 'module';
|
|
22
22
|
import fs from 'fs-extra';
|
|
23
23
|
import path from 'path';
|
|
24
|
+
import { fileURLToPath } from 'url';
|
|
24
25
|
/**
|
|
25
26
|
* The backend entry points a compiled app is started from.
|
|
26
27
|
*
|
|
@@ -241,7 +242,7 @@ export const findInlinedPackageImports = (appRoot, packageRoots) => __awaiter(vo
|
|
|
241
242
|
if (!isPath)
|
|
242
243
|
continue;
|
|
243
244
|
const target = specifier.startsWith('file://')
|
|
244
|
-
? specifier
|
|
245
|
+
? fileURLToPath(specifier)
|
|
245
246
|
: path.resolve(path.dirname(file), specifier);
|
|
246
247
|
let found;
|
|
247
248
|
if (isInside(target, libDir)) {
|