@_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.
Files changed (167) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/README.md +44 -36
  3. package/hooks/drift.cjs +169 -0
  4. package/hooks/git-hook.cjs +170 -0
  5. package/hooks/hook-shim.cjs +63 -0
  6. package/hooks/lock-at-rest.cjs +268 -0
  7. package/hooks/manifests.cjs +216 -0
  8. package/hooks/platform.cjs +25 -0
  9. package/lib/esm/app-release/build-backend.d.ts.map +1 -1
  10. package/lib/esm/app-release/build-backend.js +2 -1
  11. package/lib/esm/app-release/build-backend.js.map +1 -1
  12. package/lib/esm/cli-methods.d.ts.map +1 -1
  13. package/lib/esm/cli-methods.js +166 -150
  14. package/lib/esm/cli-methods.js.map +1 -1
  15. package/lib/esm/cli.js +20 -22
  16. package/lib/esm/cli.js.map +1 -1
  17. package/lib/esm/commands/build-app.d.ts.map +1 -1
  18. package/lib/esm/commands/build-app.js +2 -1
  19. package/lib/esm/commands/build-app.js.map +1 -1
  20. package/lib/esm/commands/build-package.d.ts.map +1 -1
  21. package/lib/esm/commands/build-package.js +6 -2
  22. package/lib/esm/commands/build-package.js.map +1 -1
  23. package/lib/esm/commands/localize.d.ts +9 -15
  24. package/lib/esm/commands/localize.d.ts.map +1 -1
  25. package/lib/esm/commands/localize.js +43 -74
  26. package/lib/esm/commands/localize.js.map +1 -1
  27. package/lib/esm/commands/setup-publish.d.ts.map +1 -1
  28. package/lib/esm/commands/setup-publish.js +22 -15
  29. package/lib/esm/commands/setup-publish.js.map +1 -1
  30. package/lib/esm/commands/start.d.ts +8 -0
  31. package/lib/esm/commands/start.d.ts.map +1 -1
  32. package/lib/esm/commands/start.js +20 -8
  33. package/lib/esm/commands/start.js.map +1 -1
  34. package/lib/esm/installed-packages.d.ts +5 -13
  35. package/lib/esm/installed-packages.d.ts.map +1 -1
  36. package/lib/esm/installed-packages.js +5 -38
  37. package/lib/esm/installed-packages.js.map +1 -1
  38. package/lib/esm/lifecycle.d.ts +35 -22
  39. package/lib/esm/lifecycle.d.ts.map +1 -1
  40. package/lib/esm/lifecycle.js +62 -36
  41. package/lib/esm/lifecycle.js.map +1 -1
  42. package/lib/esm/localize/adopt.d.ts +5 -5
  43. package/lib/esm/localize/adopt.d.ts.map +1 -1
  44. package/lib/esm/localize/adopt.js +46 -55
  45. package/lib/esm/localize/adopt.js.map +1 -1
  46. package/lib/esm/localize/app-root.d.ts +16 -0
  47. package/lib/esm/localize/app-root.d.ts.map +1 -0
  48. package/lib/esm/localize/app-root.js +94 -0
  49. package/lib/esm/localize/app-root.js.map +1 -0
  50. package/lib/esm/localize/app-setup.d.ts +21 -0
  51. package/lib/esm/localize/app-setup.d.ts.map +1 -0
  52. package/lib/esm/localize/app-setup.js +178 -0
  53. package/lib/esm/localize/app-setup.js.map +1 -0
  54. package/lib/esm/localize/delocalize.d.ts.map +1 -1
  55. package/lib/esm/localize/delocalize.js +168 -104
  56. package/lib/esm/localize/delocalize.js.map +1 -1
  57. package/lib/esm/localize/drift-hook.d.ts +18 -0
  58. package/lib/esm/localize/drift-hook.d.ts.map +1 -0
  59. package/lib/esm/localize/drift-hook.js +37 -0
  60. package/lib/esm/localize/drift-hook.js.map +1 -0
  61. package/lib/esm/localize/drift.d.ts +36 -0
  62. package/lib/esm/localize/drift.d.ts.map +1 -0
  63. package/lib/esm/localize/drift.js +34 -0
  64. package/lib/esm/localize/drift.js.map +1 -0
  65. package/lib/esm/localize/errors.d.ts +14 -8
  66. package/lib/esm/localize/errors.d.ts.map +1 -1
  67. package/lib/esm/localize/errors.js +14 -8
  68. package/lib/esm/localize/errors.js.map +1 -1
  69. package/lib/esm/localize/fsops.d.ts +11 -27
  70. package/lib/esm/localize/fsops.d.ts.map +1 -1
  71. package/lib/esm/localize/fsops.js +40 -115
  72. package/lib/esm/localize/fsops.js.map +1 -1
  73. package/lib/esm/localize/git-hooks.d.ts +33 -0
  74. package/lib/esm/localize/git-hooks.d.ts.map +1 -0
  75. package/lib/esm/localize/git-hooks.js +146 -0
  76. package/lib/esm/localize/git-hooks.js.map +1 -0
  77. package/lib/esm/localize/hook-module.d.ts +6 -0
  78. package/lib/esm/localize/hook-module.d.ts.map +1 -0
  79. package/lib/esm/localize/hook-module.js +37 -0
  80. package/lib/esm/localize/hook-module.js.map +1 -0
  81. package/lib/esm/localize/index.d.ts +14 -15
  82. package/lib/esm/localize/index.d.ts.map +1 -1
  83. package/lib/esm/localize/index.js +8 -9
  84. package/lib/esm/localize/index.js.map +1 -1
  85. package/lib/esm/localize/list.d.ts +6 -10
  86. package/lib/esm/localize/list.d.ts.map +1 -1
  87. package/lib/esm/localize/list.js +48 -189
  88. package/lib/esm/localize/list.js.map +1 -1
  89. package/lib/esm/localize/localize.d.ts +41 -62
  90. package/lib/esm/localize/localize.d.ts.map +1 -1
  91. package/lib/esm/localize/localize.js +398 -285
  92. package/lib/esm/localize/localize.js.map +1 -1
  93. package/lib/esm/localize/members.d.ts +13 -0
  94. package/lib/esm/localize/members.d.ts.map +1 -0
  95. package/lib/esm/localize/members.js +46 -0
  96. package/lib/esm/localize/members.js.map +1 -0
  97. package/lib/esm/localize/resolve.d.ts +1 -2
  98. package/lib/esm/localize/resolve.d.ts.map +1 -1
  99. package/lib/esm/localize/resolve.js +3 -5
  100. package/lib/esm/localize/resolve.js.map +1 -1
  101. package/lib/esm/localize/run.d.ts +1 -0
  102. package/lib/esm/localize/run.d.ts.map +1 -1
  103. package/lib/esm/localize/run.js +11 -6
  104. package/lib/esm/localize/run.js.map +1 -1
  105. package/lib/esm/localize/setup-lockfile.d.ts +13 -0
  106. package/lib/esm/localize/setup-lockfile.d.ts.map +1 -0
  107. package/lib/esm/localize/setup-lockfile.js +80 -0
  108. package/lib/esm/localize/setup-lockfile.js.map +1 -0
  109. package/lib/esm/utils/createPackageLocation.d.ts +4 -13
  110. package/lib/esm/utils/createPackageLocation.d.ts.map +1 -1
  111. package/lib/esm/utils/createPackageLocation.js +12 -21
  112. package/lib/esm/utils/createPackageLocation.js.map +1 -1
  113. package/lib/esm/utils/packageSetup.d.ts +5 -2
  114. package/lib/esm/utils/packageSetup.d.ts.map +1 -1
  115. package/lib/esm/utils/packageSetup.js +9 -3
  116. package/lib/esm/utils/packageSetup.js.map +1 -1
  117. package/lib/esm/utils/platform.d.ts +24 -0
  118. package/lib/esm/utils/platform.d.ts.map +1 -0
  119. package/lib/esm/utils/platform.js +62 -0
  120. package/lib/esm/utils/platform.js.map +1 -0
  121. package/lib/esm/utils/shapeReferences.d.ts.map +1 -1
  122. package/lib/esm/utils/shapeReferences.js +3 -2
  123. package/lib/esm/utils/shapeReferences.js.map +1 -1
  124. package/lib/esm/utils/spawn.d.ts +65 -0
  125. package/lib/esm/utils/spawn.d.ts.map +1 -0
  126. package/lib/esm/utils/spawn.js +181 -0
  127. package/lib/esm/utils/spawn.js.map +1 -0
  128. package/lib/esm/utils.d.ts.map +1 -1
  129. package/lib/esm/utils.js +19 -5
  130. package/lib/esm/utils.js.map +1 -1
  131. package/lib/esm/vite-config.d.ts +5 -12
  132. package/lib/esm/vite-config.d.ts.map +1 -1
  133. package/lib/esm/vite-config.js +41 -33
  134. package/lib/esm/vite-config.js.map +1 -1
  135. package/package.json +4 -3
  136. package/lib/esm/localize/ensure.d.ts +0 -44
  137. package/lib/esm/localize/ensure.d.ts.map +0 -1
  138. package/lib/esm/localize/ensure.js +0 -151
  139. package/lib/esm/localize/ensure.js.map +0 -1
  140. package/lib/esm/localize/manifest.d.ts +0 -63
  141. package/lib/esm/localize/manifest.d.ts.map +0 -1
  142. package/lib/esm/localize/manifest.js +0 -133
  143. package/lib/esm/localize/manifest.js.map +0 -1
  144. package/lib/esm/localize/one-copy-hook.d.ts +0 -16
  145. package/lib/esm/localize/one-copy-hook.d.ts.map +0 -1
  146. package/lib/esm/localize/one-copy-hook.js +0 -20
  147. package/lib/esm/localize/one-copy-hook.js.map +0 -1
  148. package/lib/esm/localize/provided.d.ts +0 -28
  149. package/lib/esm/localize/provided.d.ts.map +0 -1
  150. package/lib/esm/localize/provided.js +0 -89
  151. package/lib/esm/localize/provided.js.map +0 -1
  152. package/lib/esm/localize/prune.d.ts +0 -182
  153. package/lib/esm/localize/prune.d.ts.map +0 -1
  154. package/lib/esm/localize/prune.js +0 -703
  155. package/lib/esm/localize/prune.js.map +0 -1
  156. package/lib/esm/localize/relink.d.ts +0 -13
  157. package/lib/esm/localize/relink.d.ts.map +0 -1
  158. package/lib/esm/localize/relink.js +0 -181
  159. package/lib/esm/localize/relink.js.map +0 -1
  160. package/lib/esm/localized-dedupe.d.ts +0 -26
  161. package/lib/esm/localized-dedupe.d.ts.map +0 -1
  162. package/lib/esm/localized-dedupe.js +0 -139
  163. package/lib/esm/localized-dedupe.js.map +0 -1
  164. package/lib/esm/utils/relinkHooks.d.ts +0 -19
  165. package/lib/esm/utils/relinkHooks.d.ts.map +0 -1
  166. package/lib/esm/utils/relinkHooks.js +0 -74
  167. package/lib/esm/utils/relinkHooks.js.map +0 -1
@@ -1,345 +1,461 @@
1
1
  /**
2
- * `localize` -- turn an npm-installed dependency into an editable git checkout.
2
+ * `localize` -- turn an npm-installed dependency into an editable git checkout
3
+ * that npm itself links.
3
4
  *
4
- * The loop, in five steps, none of which involve npm learning anything:
5
+ * A checkout in `packages-local/` is a workspace member: the app's
6
+ * `workspaces` holds `"packages-local/*"`, so `npm install` links
7
+ * `node_modules/<name>` to it and dedupes everything below it like any other
8
+ * member. The committed `package-lock.json` is put back after every install by
9
+ * `hooks/lock-at-rest.cjs`, so the working tree stays clean.
5
10
  *
6
- * 1. npm name -> registry `repository` (see resolve.ts)
7
- * 2. `git clone` into `<dir>/<name>`
8
- * 3. **`npm install` INSIDE the checkout**, then prune it (`ensure`)
9
- * 4. `fs.symlink` node_modules/<name> -> the checkout
10
- * 5. record it in a gitignored manifest
11
+ * One run, for every name at once:
11
12
  *
12
- * `adopt` (adopt.ts) enters at step 3 with a checkout that is already there,
13
- * and shares everything from there on: `forEachName` and `installLinkAndRecord`
14
- * below.
13
+ * 1. refuse an `overrides` key naming a package being localized or already
14
+ * in `packages-local/` (npm fails with `EOVERRIDE` on a member's name);
15
+ * nothing is changed then;
16
+ * 2. the one-time app setup (app-setup.ts), reported line by line so the
17
+ * developer commits it; when it changed `package.json`, the lockfile
18
+ * that goes with it is made now, before anything is cloned, and written
19
+ * back after the install (`captureSetupLockfile`);
20
+ * 3. per name: a parked checkout (`linked delocalize`) moves back; an
21
+ * existing directory is refused with an `--adopt` hint; anything else is
22
+ * resolved through the registry's `repository` (or `--repo`) and cloned;
23
+ * 4. the git hooks (git-hooks.ts), or the lines to add under
24
+ * `core.hooksPath`;
25
+ * 5. ONE `npm install` at the app root -- a checkout's dependencies arrive
26
+ * with it, so it comes before any build;
27
+ * 6. build each new checkout: `--build`, else `linked build`, else for the
28
+ * cli its own `npm run build` (the cli is what `linked build` runs, and a
29
+ * fresh cli checkout has no `lib/` yet). The cli builds first and then
30
+ * gets its bin linked (`npm rebuild`), since the others' `linked build`
31
+ * runs it. A failing build warns: a package whose build is broken is
32
+ * usually what you localized it to fix.
15
33
  *
16
- * Step 3 is the one that is easy to "simplify" and must not be. Installing
17
- * from the parent instead -- `npm install <path> --no-save` -- hard-failed an
18
- * app's boot on a missing peer dependency, because module resolution happens
19
- * from the symlink's REAL path, whose ancestors have no `node_modules`. The
20
- * checkout needs its own installed tree. `tests/unit/localize/real-localize.test.ts` runs a
21
- * real clone and a real install and asserts the dependency landed inside the
22
- * checkout, so a reimplementation that moves the install back out fails there.
34
+ * `adopt` (adopt.ts) replaces step 3 with "the checkout is already there", and
35
+ * `--parked` with "every parked checkout". Steps 1, 2 and 4-6 are `runLocalize`.
23
36
  *
24
- * A clone, not a copy: a published tarball has no `src/`, so copying the
25
- * installed package cannot give you something to edit.
26
- *
27
- * What localize NEVER does: run an install at the consumer root, or edit
28
- * `package-lock.json`, `.gitignore`, `workspaces`, dependencies or any
29
- * bundler config. It asserts the work did not touch `package.json` or
30
- * `package-lock.json` before exiting 0.
31
- *
32
- * The one deliberate edit, made after that assertion: when a run records a
33
- * package, the app's `postinstall` and `dependencies` scripts get
34
- * `linked localize --relink` if they lack it (`../utils/relinkHooks.ts`
35
- * explains why npm needs both). Without them the next npm command silently
36
- * undoes the link this run just made.
37
+ * A package inside a monorepo (`--subdir`, or a registry `repository.directory`)
38
+ * is cloned whole into `packages-local/.repos/<checkout>` -- a dot directory,
39
+ * which the `packages-local/*` glob skips -- and `packages-local/<checkout>` is
40
+ * a link to the package inside it. npm follows the link like a directory.
37
41
  */
38
42
  import fs from 'node:fs';
39
43
  import path from 'node:path';
40
- import { EXIT_INSTALL_FAILED, EXIT_NOT_FOUND, EXIT_MANIFEST_DIRTY, EXIT_REFUSED, EXIT_WARNED, LocalizeError, } from './errors.js';
41
- import { DEFAULT_DIR, SCHEMA_VERSION, readManifest, writeManifest, } from './manifest.js';
44
+ import { conflictingOverrides, ensureAppSetup } from './app-setup.js';
45
+ import { describeDrift, driftProblems } from './drift.js';
46
+ import { EXIT_INSTALL_FAILED, EXIT_REFUSED, EXIT_USAGE, EXIT_WARNED, LocalizeError, } from './errors.js';
47
+ import { existsOrLink, moveEntry, readJson } from './fsops.js';
48
+ import { installGitHooks } from './git-hooks.js';
49
+ import { LOCAL_DIR, localMembers, parkedMembers } from './members.js';
50
+ import { setupLockfileByEdit } from './setup-lockfile.js';
42
51
  import { checkoutNameFor, resolvePackage } from './resolve.js';
43
- import { pruneProvided, shouldPrune } from './prune.js';
44
- import { ensure } from './ensure.js';
45
- import { ensureRelinkHooksInFile, RELINK_COMMAND } from '../utils/relinkHooks.js';
46
- import { diffManifests, isGitCheckout, isInside, isSymlink, readJson, snapshotManifests, writeLink, } from './fsops.js';
52
+ import { linkDirectory } from '../utils/platform.js';
53
+ /** What a new checkout is built with when `--build` does not say. */
54
+ export const DEFAULT_BUILD_COMMAND = 'linked build';
55
+ /** The cli builds itself: `linked build` IS the cli, and a fresh checkout of it has no `lib/`. */
56
+ const CLI_PACKAGE = '@_linked/cli';
57
+ const CLI_BUILD_COMMAND = 'npm run build';
58
+ /** The one install a run makes, at the app root. */
59
+ export const INSTALL_ARGS = ['install', '--no-audit', '--no-fund'];
60
+ /** Where a cloned monorepo lives, inside `packages-local/` but outside its glob. */
61
+ const REPOS_DIR = `${LOCAL_DIR}/.repos`;
62
+ /**
63
+ * Where the set-up's lockfile run puts the checkouts while npm makes it:
64
+ * inside the gitignored `packages-local/`, outside its glob (npm skips dot
65
+ * directories). A run interrupted there leaves them in it; the next
66
+ * `localize`, `delocalize` or `--list` moves them back (`restoreSetAside`).
67
+ */
68
+ const SETUP_ASIDE_DIR = `${LOCAL_DIR}/.setup-aside`;
69
+ /** The app's lockfile, kept clean of checkouts. */
70
+ export const LOCKFILE = 'package-lock.json';
71
+ /**
72
+ * @param names full npm package names
73
+ * @returns the process exit code: the highest of the per-name codes and the
74
+ * install's, since a failure on name 2 does not undo name 1.
75
+ */
47
76
  export function localize(names, opts, deps) {
48
- return forEachName(names, opts, deps, localizeOne);
77
+ return runLocalize(names, opts, deps, prepareClone);
49
78
  }
50
79
  /**
51
- * The loop `localize` and `adopt` share: read the manifest once, run `one` per
52
- * name, write the manifest after each, then assert the links and the
53
- * consumer's untouched manifests.
54
- *
55
- * @param {(name: string, dir: string, recorded: object|undefined, opts: object, deps: object)
56
- * => {code: number, entry?: {name: string, value: object}}} one
80
+ * The run `localize`, `adopt` and `--parked` share: everything except how a
81
+ * name becomes a checkout. See the module header for the steps.
57
82
  */
58
- export function forEachName(names, opts, deps, one) {
83
+ export function runLocalize(names, opts, deps, prepare) {
59
84
  var _a, _b;
60
- // Taken BEFORE anything runs: the post-condition is about what THIS run
61
- // changed, not about what the tree looked like at HEAD.
62
- const before = snapshotManifests(deps.appRoot);
63
- let manifest;
85
+ let setupLock;
64
86
  try {
65
- manifest = readManifest(deps.appRoot, deps);
87
+ restoreSetAside(deps);
88
+ checkDir(opts.dir);
89
+ // The checkouts already there count too: the one install below covers them all.
90
+ const localized = new Set([
91
+ ...names,
92
+ ...localMembers(deps.appRoot).map((m) => m.name),
93
+ ]);
94
+ const conflicts = conflictingOverrides(deps.appRoot, [...localized]);
95
+ if (conflicts.length) {
96
+ throw new LocalizeError(`package.json "overrides" names a localized package: ${conflicts
97
+ .map((k) => `"${k}"`)
98
+ .join(', ')}.\n` +
99
+ `npm refuses an override on a workspace member's name (EOVERRIDE), and a nested one ` +
100
+ `would replace the checkout. Remove ${conflicts.length === 1 ? 'that key' : 'those keys'} ` +
101
+ `from "overrides", then run this again. Nothing was changed.`, EXIT_REFUSED);
102
+ }
103
+ const workspacesBefore = (_a = readJson(path.join(deps.appRoot, 'package.json'))) === null || _a === void 0 ? void 0 : _a.workspaces;
104
+ const changes = ensureAppSetup(deps.appRoot);
105
+ reportSetup(changes, deps);
106
+ if (changes.some((c) => c.file === 'package.json'))
107
+ setupLock =
108
+ (_b = captureSetupLockfile(changes, workspacesBefore, deps)) !== null && _b !== void 0 ? _b : undefined;
66
109
  }
67
110
  catch (e) {
68
111
  return reportError(e, deps);
69
112
  }
70
- const dir = (_b = (_a = opts.dir) !== null && _a !== void 0 ? _a : manifest.dir) !== null && _b !== void 0 ? _b : DEFAULT_DIR;
71
- const entries = Object.assign({}, manifest.entries);
72
113
  let code = 0;
73
- let recorded = false;
114
+ const prepared = [];
74
115
  for (const name of names) {
75
116
  try {
76
- const result = one(name, dir, entries[name], opts, deps);
77
- if (result.entry) {
78
- entries[result.entry.name] = result.entry.value;
79
- recorded = true;
80
- }
81
- code = Math.max(code, result.code);
82
- // Written after EACH name, so an interruption keeps what already worked.
83
- writeManifest(deps.appRoot, {
84
- version: SCHEMA_VERSION,
85
- dir,
86
- packages: entries,
87
- });
117
+ const p = prepare(name, opts, deps);
118
+ if (p)
119
+ prepared.push(p);
88
120
  }
89
121
  catch (e) {
90
122
  code = Math.max(code, reportError(e, deps));
91
123
  }
92
124
  }
93
- // Over EVERY localized checkout, not just the ones named: localizing `B`
94
- // is what makes `A`'s registry copy of `B` redundant.
95
- if (shouldPrune(opts)) {
96
- try {
97
- pruneProvided(entries, opts, deps);
98
- }
99
- catch (e) {
100
- deps.warn(`[localize] pruning failed, the checkouts' node_modules are as npm left them: ${e.message}`);
101
- code = Math.max(code, EXIT_WARNED);
102
- }
125
+ if (!prepared.length) {
126
+ // Nothing was installed: the work tree holds the set-up's lockfile as captured.
127
+ if (setupLock !== undefined)
128
+ commitHint(deps);
129
+ return code;
130
+ }
131
+ return Math.max(code, installAndBuild(prepared, opts, deps, setupLock));
132
+ }
133
+ /**
134
+ * Steps 4-6: git hooks, one root install, then the builds. `setupLock` is the
135
+ * lockfile the set-up captured (see `captureSetupLockfile`), written back
136
+ * over what the install left.
137
+ */
138
+ export function installAndBuild(prepared, opts, deps, setupLock) {
139
+ reportHooks(installGitHooks(deps.appRoot), deps);
140
+ deps.log(`[localize] npm ${INSTALL_ARGS.join(' ')} (app root)`);
141
+ const install = deps.run('npm', INSTALL_ARGS, { cwd: deps.appRoot });
142
+ if (setupLock !== undefined)
143
+ restoreSetupLockfile(setupLock, deps);
144
+ if (install.status !== 0) {
145
+ deps.error(`[localize] npm install failed at the app root. The checkout${prepared.length === 1 ? ' stays' : 's stay'} ` +
146
+ `in ${LOCAL_DIR}/; fix the cause and run \`npm install\`.\n` +
147
+ tail(install.stderr || install.stdout));
148
+ return EXIT_INSTALL_FAILED;
103
149
  }
104
- code = Math.max(code, assertLinked(entries, deps));
105
- code = Math.max(code, assertManifestsUntouched(before, deps));
106
- // After the assertion, so it still covers everything the work itself did.
107
- if (recorded)
108
- ensureRelinkHooksIn(deps);
150
+ let code = 0;
151
+ // The cli first: the other builds run `linked build`, which is the cli.
152
+ const ordered = [
153
+ ...prepared.filter((p) => p.name === CLI_PACKAGE),
154
+ ...prepared.filter((p) => p.name !== CLI_PACKAGE),
155
+ ];
156
+ for (const p of ordered) {
157
+ const pkgDir = path.join(deps.appRoot, p.dir);
158
+ const built = p.build ? runBuild(p, pkgDir, opts, deps) : 'skipped';
159
+ if (built === 'failed')
160
+ code = EXIT_WARNED;
161
+ if (built === 'ok' && p.name === CLI_PACKAGE)
162
+ linkCliBin(deps);
163
+ deps.log(`[localize] ${p.name} -> ${p.dir} (branch ${currentBranch(pkgDir, deps)})`);
164
+ }
165
+ for (const d of driftProblems(deps.appRoot))
166
+ deps.warn(`[localize] drift: ${describeDrift(d)}`);
109
167
  return code;
110
168
  }
111
- /** Add the relink hooks to the app's package.json when it lacks them. See relinkHooks.ts. */
112
- function ensureRelinkHooksIn(deps) {
113
- const added = ensureRelinkHooksInFile(path.join(deps.appRoot, 'package.json'));
114
- if (!added.length)
115
- return;
116
- deps.log(`[localize] Added \`${RELINK_COMMAND}\` to the app's ${added.join(' and ')} ` +
117
- `script${added.length > 1 ? 's' : ''} so npm keeps the links`);
169
+ /**
170
+ * npm links a package's bin only when its target exists, and a fresh cli
171
+ * checkout has no `lib/` at install time: without this, `node_modules/.bin/linked`
172
+ * is missing after the build, and `npx linked` would fetch the unrelated npm
173
+ * package named `linked`. `--ignore-scripts`: only the bin links are wanted.
174
+ */
175
+ function linkCliBin(deps) {
176
+ const r = deps.run('npm', ['rebuild', CLI_PACKAGE, '--ignore-scripts'], {
177
+ cwd: deps.appRoot,
178
+ });
179
+ if (r.status !== 0) {
180
+ deps.warn(`[localize] npm rebuild ${CLI_PACKAGE} failed, so \`linked\` may not be on the app's ` +
181
+ `bin path. Run it yourself.\n` +
182
+ tail(r.stderr || r.stdout));
183
+ }
118
184
  }
119
- function localizeOne(name, dir, recorded, opts, deps) {
185
+ /** Step 3 for `localize`: take back a parked checkout, else clone. */
186
+ function prepareClone(name, opts, deps) {
120
187
  var _a;
121
- // An override recorded by an earlier run is reused, so `--repo` is a
122
- // one-time cost rather than something to remember on every invocation.
123
- const override = opts.repo
124
- ? { repo: opts.repo, subdir: opts.subdir }
125
- : (recorded === null || recorded === void 0 ? void 0 : recorded.repo) && !looksLikeOrgSlug(recorded.repo)
126
- ? { repo: recorded.repo, subdir: recorded.subdir }
127
- : undefined;
128
- const resolved = resolveOrSuggestAdopt(name, dir, override, deps);
129
- let code = 0;
130
- const relClone = path.join(dir, resolved.checkoutName);
131
- const clone = path.join(deps.appRoot, relClone);
132
- // When the repository holds more than this package, the CHECKOUT is the
133
- // clone but the PACKAGE is a directory inside it: that is where the install
134
- // runs and that is what the symlink points at.
135
- const relPkg = resolved.subdir
136
- ? path.join(relClone, resolved.subdir)
137
- : relClone;
138
- const pkgDir = path.join(deps.appRoot, relPkg);
139
- guardForeignLink(resolved.npmName, dir, opts, deps);
140
- if (isGitCheckout(clone)) {
141
- code = Math.max(code, refresh(relClone, clone, deps));
142
- if (code === EXIT_WARNED)
143
- return { code }; // a non-fast-forward is reported, never forced
188
+ const parked = parkedMembers(deps.appRoot).find((m) => m.name === name);
189
+ if (parked)
190
+ return unpark(parked, deps);
191
+ const member = localMembers(deps.appRoot).find((m) => m.name === name);
192
+ if (member) {
193
+ throw new LocalizeError(`${name} is already in ${member.dir}. To install and build it as it is:\n` +
194
+ ` linked localize ${name} --adopt`, EXIT_REFUSED);
144
195
  }
145
- else {
146
- fs.mkdirSync(path.dirname(clone), { recursive: true });
147
- deps.log(`[localize] cloning ${resolved.repo} -> ${relClone}`);
148
- const r = deps.run('git', ['clone', resolved.repo, clone]);
149
- if (r.status !== 0) {
150
- throw new LocalizeError(`git clone ${resolved.repo} failed:\n${(r.stderr || r.stdout).trim()}`, EXIT_REFUSED);
151
- }
196
+ const relDir = `${LOCAL_DIR}/${checkoutNameFor(name)}`;
197
+ const target = path.join(deps.appRoot, relDir);
198
+ if (existsOrLink(target)) {
199
+ throw new LocalizeError(`${relDir} already exists. localize never clones over a directory. If it is ${name}'s ` +
200
+ `checkout, install and build it as it is:\n linked localize ${name} --adopt`, EXIT_REFUSED);
152
201
  }
153
- if (!fs.existsSync(path.join(pkgDir, 'package.json'))) {
154
- throw new LocalizeError(`${relPkg} has no package.json, so it is not the package "${name}".\n` +
155
- (resolved.subdir
156
- ? `The registry says "${name}" lives in "${resolved.subdir}" of ${resolved.repo}, and it does not.`
157
- : `${resolved.repo} looks like a monorepo root. Say where the package lives:\n` +
158
- ` linked localize ${name} --repo ${resolved.repo} --subdir <path-inside-the-repo>`), EXIT_REFUSED);
202
+ const resolved = resolvePackage(name, deps, opts.repo ? { repo: opts.repo, subdir: opts.subdir } : undefined);
203
+ // A monorepo is cloned whole outside the glob; the member is a link to the package.
204
+ const relClone = resolved.subdir
205
+ ? `${REPOS_DIR}/${checkoutNameFor(name)}`
206
+ : relDir;
207
+ const clone = path.join(deps.appRoot, relClone);
208
+ if (existsOrLink(clone)) {
209
+ throw new LocalizeError(`${relClone} already exists. Remove it, or move it out of the way, and run this again.`, EXIT_REFUSED);
159
210
  }
160
- // The checkout's own package.json is authoritative about the name: a
161
- // mismatch means the symlink would be written under a name nothing imports.
211
+ fs.mkdirSync(path.dirname(clone), { recursive: true });
212
+ deps.log(`[localize] cloning ${resolved.repo} -> ${relClone}`);
213
+ const r = deps.run('git', ['clone', '--', resolved.repo, clone]);
214
+ if (r.status !== 0) {
215
+ throw new LocalizeError(`git clone ${resolved.repo} failed:\n${(r.stderr || r.stdout).trim()}`, EXIT_REFUSED);
216
+ }
217
+ const pkgDir = resolved.subdir ? path.join(clone, resolved.subdir) : clone;
218
+ const relPkg = resolved.subdir ? `${relClone}/${resolved.subdir}` : relClone;
219
+ // A clone that is not the package must not stay: in packages-local/ it
220
+ // would be a workspace member under the wrong name.
162
221
  const declared = (_a = readJson(path.join(pkgDir, 'package.json'))) === null || _a === void 0 ? void 0 : _a.name;
163
- if (declared && declared !== resolved.npmName) {
164
- throw new LocalizeError(`${relPkg} declares itself as "${declared}", not "${name}".\n` +
165
- (resolved.subdir
166
- ? `The registry's repository.directory for "${name}" points at the wrong package.`
167
- : `${resolved.repo} is probably a monorepo. Point at the right directory:\n` +
168
- ` linked localize ${name} --repo ${resolved.repo} --subdir <path-inside-the-repo>`), EXIT_REFUSED);
222
+ if (declared !== name) {
223
+ fs.rmSync(clone, { recursive: true, force: true });
224
+ const where = resolved.subdir
225
+ ? `The registry's repository.directory for "${name}" points at the wrong place.`
226
+ : `${resolved.repo} looks like a monorepo. Say where the package lives:\n` +
227
+ ` linked localize ${name} --repo ${resolved.repo} --subdir <path-inside-the-repo>`;
228
+ throw new LocalizeError((declared
229
+ ? `${relPkg} declares itself as "${declared}", not "${name}".\n`
230
+ : `${relPkg} has no package.json naming it, so it is not the package "${name}".\n`) +
231
+ `${where}\nThe clone was removed.`, EXIT_REFUSED);
232
+ }
233
+ if (resolved.subdir) {
234
+ linkDirectory(pkgDir, target);
235
+ deps.log(`[localize] ${relDir} -> ${relPkg}`);
169
236
  }
170
- return installLinkAndRecord({
171
- npmName: resolved.npmName,
172
- clone,
173
- relPkg,
174
- pkgDir,
175
- repo: resolved.repo,
176
- subdir: resolved.subdir,
177
- }, opts, deps, code);
237
+ return { name, dir: relDir, build: true };
178
238
  }
239
+ /** Move a parked checkout back into `packages-local/`. No clone, no build: it is as it was. */
240
+ export function unpark(member, deps) {
241
+ const entry = path.basename(member.dir);
242
+ const relDir = `${LOCAL_DIR}/${entry}`;
243
+ const to = path.join(deps.appRoot, relDir);
244
+ if (existsOrLink(to)) {
245
+ throw new LocalizeError(`${member.name} is parked in ${member.dir}, but ${relDir} already exists. Move one of ` +
246
+ `them out of the way and run this again.`, EXIT_REFUSED);
247
+ }
248
+ // npm refuses two workspace members of one name, so the install would fail.
249
+ const same = localMembers(deps.appRoot).find((m) => m.name === member.name);
250
+ if (same) {
251
+ throw new LocalizeError(`${member.name} is parked in ${member.dir}, but ${same.dir} is ${member.name} already. ` +
252
+ `Keep one: delete or move the other out of the way, and run this again.`, EXIT_REFUSED);
253
+ }
254
+ moveEntry(path.join(deps.appRoot, member.dir), to);
255
+ deps.log(`[localize] ${member.dir} -> ${relDir}`);
256
+ return { name: member.name, dir: relDir, build: false };
257
+ }
258
+ /** `--dir` is kept for scripts that pass the default; anything else is not a workspace member. */
259
+ function checkDir(dir) {
260
+ if (dir === undefined || dir.replace(/\/+$/, '') === LOCAL_DIR)
261
+ return;
262
+ throw new LocalizeError(`--dir ${dir}: checkouts live in ${LOCAL_DIR}/, the directory the app's ` +
263
+ `"workspaces" glob and the lock-at-rest hook name. Move them there.`, EXIT_USAGE);
264
+ }
265
+ function reportSetup(changes, deps) {
266
+ if (!changes.length)
267
+ return;
268
+ deps.log(`[localize] set up this app for checkouts (commit these):`);
269
+ for (const c of changes)
270
+ deps.log(` ${c.file}: ${c.what}`);
271
+ }
272
+ /** What `captureSetupLockfile` runs: the lockfile for the new `package.json`, nothing installed. */
273
+ const SETUP_LOCK_ARGS = [
274
+ 'install',
275
+ '--package-lock-only',
276
+ '--ignore-scripts',
277
+ '--no-audit',
278
+ '--no-fund',
279
+ ];
179
280
  /**
180
- * Everything after the checkout exists, shared by `localize` and `adopt`:
181
- * install inside it, run the configured build, link it, and describe the
182
- * manifest entry. Nothing here knows how the checkout got there.
281
+ * The set-up changed `package.json`, so the lockfile must change with it
282
+ * (the root entry's `workspaces`) -- and it must be a lockfile with no
283
+ * checkout in it, which the install below cannot give: it lists them, and
284
+ * lock-at-rest leaves it so while `package.json` differs from HEAD. So,
285
+ * before anything is cloned, make that lockfile, keep it, and write it back
286
+ * after the install. When the root entry already has `workspaces` and
287
+ * `hasInstallScript`, it is an edit of that entry (setup-lockfile.ts: exact,
288
+ * offline); otherwise npm makes the lockfile alone, with checkouts already in
289
+ * `packages-local/` (an adopted one) moved into `SETUP_ASIDE_DIR` for that
290
+ * one call.
183
291
  *
184
- * `repo` may be absent -- an adopted checkout with no remote is recorded
185
- * without one.
292
+ * @returns the lockfile to write back, or null when it could not be made (the
293
+ * manual steps are printed instead).
186
294
  */
187
- export function installLinkAndRecord({ npmName, clone, relPkg, pkgDir, repo, subdir, }, opts, deps, code = 0) {
188
- // THE constraint. Read the module header before changing this. `ensure`
189
- // installs inside the checkout and prunes it as one step (ensure.ts).
295
+ function captureSetupLockfile(changes, workspacesBefore, deps) {
296
+ const lockfile = path.join(deps.appRoot, LOCKFILE);
297
+ const edited = editedSetupLockfile(lockfile, workspacesBefore, deps);
298
+ if (edited !== null)
299
+ return edited;
300
+ const moveAside = localMembers(deps.appRoot).length > 0;
301
+ deps.log(`[localize] npm ${SETUP_LOCK_ARGS.join(' ')} (the set-up's lockfile)`);
302
+ if (moveAside)
303
+ setAside(deps);
304
+ let r;
190
305
  try {
191
- code = Math.max(code, ensure({ path: relPkg, subdir }, deps, opts));
306
+ r = deps.run('npm', SETUP_LOCK_ARGS, { cwd: deps.appRoot });
192
307
  }
193
- catch (e) {
194
- if (e instanceof LocalizeError && e.code === EXIT_INSTALL_FAILED)
195
- e.message += `\nThe checkout is left on disk. Nothing was linked and nothing was recorded.`;
196
- throw e;
308
+ finally {
309
+ if (moveAside)
310
+ moveBack(deps);
197
311
  }
198
- // The build is the one thing localize does not know how to do, so it is
199
- // configuration rather than behaviour. A failure is a WARNING: a package
200
- // whose build is broken is exactly what you are about to fix, and refusing
201
- // to link it would be perverse.
202
- if (runBuild(relPkg, pkgDir, opts, deps) === 'failed')
203
- code = Math.max(code, EXIT_WARNED);
204
- writeLink(deps.appRoot, npmName, pkgDir);
205
- const branch = currentBranch(clone, deps);
206
- deps.log(`[localize] linked ${npmName} -> ${relPkg} (branch ${branch})`);
207
- const range = declaredRange(npmName, deps);
208
- return {
209
- code,
210
- entry: {
211
- name: npmName,
212
- value: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (repo ? { repo } : {})), { path: relPkg, branch }), (subdir ? { subdir } : {})), (range ? { range } : {})), (shouldPrune(opts) ? {} : { prune: false })),
213
- },
214
- };
215
- }
216
- /**
217
- * A registry lookup that fails while a git checkout already sits where the
218
- * clone would go is almost always a package that was never published -- one
219
- * made locally. Say how to link it as it is rather than leave the developer
220
- * guessing at a `--repo`.
221
- */
222
- function resolveOrSuggestAdopt(name, dir, override, deps) {
312
+ let text = null;
223
313
  try {
224
- return resolvePackage(name, deps, override);
314
+ text = fs.readFileSync(lockfile, 'utf8');
225
315
  }
226
- catch (e) {
227
- const relClone = path.join(dir, checkoutNameFor(name));
228
- if (e instanceof LocalizeError &&
229
- e.code === EXIT_NOT_FOUND &&
230
- isGitCheckout(path.join(deps.appRoot, relClone))) {
231
- e.message +=
232
- `\n${relClone} is already a git checkout. To link it as it is, with no registry lookup:\n` +
233
- ` linked localize ${name} --adopt`;
234
- }
235
- throw e;
316
+ catch (_a) {
317
+ /* none written */
236
318
  }
237
- }
238
- /** Older manifests recorded `owner/repo`; those are re-resolved rather than cloned blind. */
239
- function looksLikeOrgSlug(repo) {
240
- return /^[\w.-]+\/[\w.-]+$/.test(repo);
319
+ if (r.status !== 0 || text === null) {
320
+ manualSetupSteps(changes, r.status !== 0
321
+ ? `npm could not write it:\n${tail(r.stderr || r.stdout)}`
322
+ : `npm wrote no ${LOCKFILE}`, deps);
323
+ return null;
324
+ }
325
+ return text;
241
326
  }
242
327
  /**
243
- * Reuse an existing clone. A dirty tree is reused WITHOUT pulling and said so;
244
- * a clean one is fast-forwarded; a non-fast-forward is reported and skipped
245
- * rather than forced, because the developer's commits are the entire point.
328
+ * Every entry of `packages-local/` but the dot directories, moved into
329
+ * `SETUP_ASIDE_DIR` as it is (a link keeps its text and works again once back).
246
330
  */
247
- function refresh(relPath, clone, deps) {
248
- const status = deps.run('git', ['status', '--porcelain'], { cwd: clone });
249
- const modified = status.stdout.split('\n').filter((l) => l.trim()).length;
250
- if (modified > 0) {
251
- deps.log(`[localize] ${relPath}: ${modified} modified file${modified === 1 ? '' : 's'} — not pulling`);
252
- return 0;
253
- }
254
- const pull = deps.run('git', ['pull', '--ff-only'], { cwd: clone });
255
- if (pull.status !== 0) {
256
- deps.warn(`[localize] ${relPath}: \`git pull --ff-only\` failed (not a fast-forward) — skipped, not ` +
257
- `forced. Reconcile it by hand:\n${(pull.stderr || pull.stdout).trim()}`);
258
- return EXIT_WARNED;
331
+ function setAside(deps) {
332
+ const local = path.join(deps.appRoot, LOCAL_DIR);
333
+ const aside = path.join(deps.appRoot, SETUP_ASIDE_DIR);
334
+ fs.mkdirSync(aside, { recursive: true });
335
+ for (const entry of fs.readdirSync(local)) {
336
+ if (entry.startsWith('.'))
337
+ continue;
338
+ fs.renameSync(path.join(local, entry), path.join(aside, entry));
259
339
  }
260
- return 0;
340
+ }
341
+ /** Everything in `SETUP_ASIDE_DIR` back into `packages-local/`; the directory goes. */
342
+ function moveBack(deps) {
343
+ const local = path.join(deps.appRoot, LOCAL_DIR);
344
+ const aside = path.join(deps.appRoot, SETUP_ASIDE_DIR);
345
+ const entries = fs.readdirSync(aside);
346
+ for (const entry of entries)
347
+ fs.renameSync(path.join(aside, entry), path.join(local, entry));
348
+ fs.rmdirSync(aside);
349
+ return entries.length;
261
350
  }
262
351
  /**
263
- * Run the configured build command, if there is one.
264
- *
265
- * localize has no opinion about how a checkout is built, and does not look for
266
- * a builder. `--build "<cmd>"`, or a `build` field in the manifest, or
267
- * `{build}` in the programmatic options. Absent -- the common case, and the
268
- * case for anything whose dev loop reads source directly -- nothing runs and
269
- * nothing is said.
352
+ * A set-up run that was interrupted while the checkouts were in
353
+ * `SETUP_ASIDE_DIR` left them there, where npm does not see them: move them
354
+ * back, saying so. Refuses, moving nothing, when `packages-local/` already
355
+ * has an entry of the same name.
270
356
  */
271
- function runBuild(relPath, pkgDir, opts, deps) {
272
- const cmd = opts.build;
273
- if (!cmd)
274
- return 'skipped';
275
- deps.log(`[localize] ${relPath}: ${cmd}`);
276
- const r = deps.run(cmd, [], { cwd: pkgDir, shell: true });
277
- if (r.status !== 0) {
278
- deps.warn(`[localize] ${relPath}: \`${cmd}\` failed. The package is linked and recorded anyway — ` +
279
- `a package whose build is broken is exactly what you are about to fix.\n` +
280
- `${(r.stderr || r.stdout).trim()}`);
281
- return 'failed';
282
- }
283
- return 'ok';
284
- }
285
- /** Refuse when `node_modules/<name>` is already a symlink pointing outside `<dir>`. */
286
- export function guardForeignLink(npmName, dir, opts, deps) {
287
- const link = path.join(deps.appRoot, 'node_modules', npmName);
288
- if (!isSymlink(link))
357
+ export function restoreSetAside(deps) {
358
+ const aside = path.join(deps.appRoot, SETUP_ASIDE_DIR);
359
+ if (!existsOrLink(aside))
289
360
  return;
290
- let target;
361
+ let entries;
291
362
  try {
292
- target = fs.realpathSync(link);
363
+ entries = fs.readdirSync(aside);
293
364
  }
294
365
  catch (_a) {
295
- return; // a dangling link is ours to replace
366
+ throw new LocalizeError(`${SETUP_ASIDE_DIR} is not a directory. Remove it and run this again.`, EXIT_REFUSED);
296
367
  }
297
- if (isInside(target, path.join(deps.appRoot, dir)))
298
- return;
299
- if (opts.force) {
300
- deps.warn(`[localize] --force: overwriting the existing link ${npmName} -> ${target}`);
301
- return;
368
+ const clashes = entries.filter((e) => existsOrLink(path.join(deps.appRoot, LOCAL_DIR, e)));
369
+ if (clashes.length) {
370
+ throw new LocalizeError(`${SETUP_ASIDE_DIR}/ holds checkouts an interrupted set-up left there, but ` +
371
+ `${clashes.map((e) => `${LOCAL_DIR}/${e}`).join(', ')} now exist${clashes.length === 1 ? 's' : ''} ` +
372
+ `too. Keep one of each, move the other out of ${LOCAL_DIR}/, and run this again. ` +
373
+ `Nothing was moved.`, EXIT_REFUSED);
302
374
  }
303
- throw new LocalizeError(`node_modules/${npmName} is already a symlink to ${target}, which is outside ${dir}.\n` +
304
- `Refusing to overwrite someone else's link. Pass --force to replace it.`, EXIT_REFUSED);
375
+ const n = moveBack(deps);
376
+ if (n)
377
+ deps.log(`[localize] moved ${n} checkout${n === 1 ? '' : 's'} back from ${SETUP_ASIDE_DIR}/ into ` +
378
+ `${LOCAL_DIR}/, where an interrupted set-up left ${n === 1 ? 'it' : 'them'}.`);
305
379
  }
306
- /** Every recorded package is actually linked where the manifest says it is. */
307
- function assertLinked(entries, deps) {
308
- let code = 0;
309
- for (const [name, entry] of Object.entries(entries)) {
310
- const link = path.join(deps.appRoot, 'node_modules', name);
311
- const expected = path.join(deps.appRoot, entry.path);
312
- if (!isSymlink(link)) {
313
- deps.warn(`[localize] ${name} is recorded but node_modules/${name} is not a symlink.`);
314
- code = Math.max(code, EXIT_WARNED);
315
- continue;
316
- }
317
- if (!fs.existsSync(expected))
318
- continue; // "checkout missing" is a --list state, not a failure
319
- if (fs.realpathSync(link) !== fs.realpathSync(expected)) {
320
- deps.warn(`[localize] ${name} links to ${fs.realpathSync(link)} but the manifest records ${entry.path}.`);
321
- code = Math.max(code, EXIT_WARNED);
322
- }
380
+ /** The set-up's lockfile as an edit of the root entry, written now; null when npm must make it. */
381
+ function editedSetupLockfile(lockfile, workspacesBefore, deps) {
382
+ let current;
383
+ try {
384
+ current = fs.readFileSync(lockfile, 'utf8');
385
+ }
386
+ catch (_a) {
387
+ return null;
388
+ }
389
+ const text = setupLockfileByEdit(current, readJson(path.join(deps.appRoot, 'package.json')), workspacesBefore);
390
+ if (text === null)
391
+ return null;
392
+ fs.writeFileSync(lockfile, text);
393
+ deps.log(`[localize] ${LOCKFILE}: the root entry's "workspaces" now matches package.json (the set-up's lockfile)`);
394
+ return text;
395
+ }
396
+ /** Put the captured set-up lockfile back after the install rewrote it. */
397
+ function restoreSetupLockfile(text, deps) {
398
+ fs.writeFileSync(path.join(deps.appRoot, LOCKFILE), text);
399
+ deps.log(`[localize] ${LOCKFILE}: put back the set-up's lockfile, which lists no checkout (any ` +
400
+ `lock-at-rest warning above was about the one the install wrote).`);
401
+ commitHint(deps);
402
+ }
403
+ function commitHint(deps) {
404
+ deps.log(`[localize] commit the set-up now -- package.json, ${LOCKFILE}, .gitignore:\n` +
405
+ ` git add package.json ${LOCKFILE} .gitignore && git commit -m "chore: set up linked localize"`);
406
+ }
407
+ /** The set-up's lockfile could not be made in one step: how to make it by hand. */
408
+ function manualSetupSteps(changes, why, deps) {
409
+ const files = [...new Set([...changes.map((c) => c.file), LOCKFILE])];
410
+ deps.warn(`[localize] could not make the set-up's ${LOCKFILE} in one step: ${why}\n` +
411
+ `Commit the set-up with a lockfile from an install with no checkout present:\n` +
412
+ ` npx linked delocalize\n` +
413
+ ` npm install\n` +
414
+ ` git add ${files.join(' ')} && git commit -m "chore: set up linked localize"\n` +
415
+ ` npx linked localize --parked`);
416
+ }
417
+ /** Tell the developer what `installGitHooks` did not do. */
418
+ function reportHooks(hooks, deps) {
419
+ for (const file of hooks.skipped) {
420
+ deps.warn(`[localize] ${path.relative(deps.appRoot, file)} is not ours, so it was left alone. Without ` +
421
+ `the localize hook there, run \`npm install\` yourself after a pull or branch switch that ` +
422
+ `changes dependencies.`);
423
+ }
424
+ if (hooks.hooksPath !== null) {
425
+ deps.warn(`[localize] core.hooksPath is set (${hooks.hooksPath}), so no git hooks were written. Add ` +
426
+ `each line below to the hook named before it, an \`sh\` script (not, say, a Python hook ` +
427
+ `the pre-commit framework writes); they do nothing where the cli is not installed:`);
428
+ for (const m of hooks.manual)
429
+ deps.warn(` ${m.file}:\n ${m.line}`);
430
+ }
431
+ if (hooks.sharedWithWorktrees) {
432
+ deps.warn(`[localize] this is a linked worktree: git hooks are shared by every worktree of the ` +
433
+ `repository. They act only in a work tree whose ${LOCAL_DIR}/ has checkouts.`);
323
434
  }
324
- return code;
325
435
  }
326
436
  /**
327
- * The property the whole design rests on: this run changed neither manifest.
328
- *
329
- * Content hashes, not `git status`. `git status` compares against HEAD, so in
330
- * any tree with an uncommitted `package.json` it reports a modification the
331
- * command did not make -- an earlier tool shipped exactly that and reported
332
- * correct runs as failures.
437
+ * Run the build for one new checkout. `opts.build` wins, including `''` for
438
+ * no build; otherwise the cli builds itself and everything else gets
439
+ * `linked build`.
333
440
  */
334
- export function assertManifestsUntouched(before, deps) {
335
- const changed = diffManifests(before, snapshotManifests(deps.appRoot));
336
- if (!changed.length)
337
- return 0;
338
- deps.error(`[localize] the consumer's manifests changed while this command ran:\n` +
339
- changed.map((c) => ` ${c}`).join('\n') +
340
- `\nThis is the exact failure localize exists to prevent. Revert those files; nothing in ` +
341
- `localize is supposed to touch them.`);
342
- return EXIT_MANIFEST_DIRTY;
441
+ function runBuild(p, pkgDir, opts, deps) {
442
+ const cmd = buildCommandFor(p.name, opts);
443
+ if (!cmd)
444
+ return 'skipped';
445
+ deps.log(`[localize] ${p.dir}: ${cmd}`);
446
+ const r = deps.run(cmd, [], { cwd: pkgDir, shell: true });
447
+ if (r.status !== 0) {
448
+ deps.warn(`[localize] ${p.dir}: \`${cmd}\` failed. It is installed and linked anyway -- a package ` +
449
+ `whose build is broken is usually what you localized it to fix.\n` +
450
+ tail(r.stderr || r.stdout));
451
+ return 'failed';
452
+ }
453
+ return 'ok';
454
+ }
455
+ function buildCommandFor(name, opts) {
456
+ if (opts.build !== undefined)
457
+ return opts.build;
458
+ return name === CLI_PACKAGE ? CLI_BUILD_COMMAND : DEFAULT_BUILD_COMMAND;
343
459
  }
344
460
  export function reportError(e, deps) {
345
461
  if (e instanceof LocalizeError) {
@@ -354,23 +470,20 @@ export function reportError(e, deps) {
354
470
  * `rev-parse --abbrev-ref` fails there, and `create-package` adopts a fresh
355
471
  * `git init` before its first commit.
356
472
  */
357
- export function currentBranch(clone, deps) {
473
+ export function currentBranch(checkout, deps) {
358
474
  const sym = deps.run('git', ['symbolic-ref', '--short', '-q', 'HEAD'], {
359
- cwd: clone,
475
+ cwd: checkout,
360
476
  });
361
477
  if (sym.status === 0 && sym.stdout.trim())
362
478
  return sym.stdout.trim();
363
479
  const r = deps.run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
364
- cwd: clone,
480
+ cwd: checkout,
365
481
  });
366
482
  const b = r.stdout.trim();
367
483
  return r.status === 0 && b ? b : 'HEAD';
368
484
  }
369
- export function declaredRange(name, deps) {
370
- var _a, _b, _c, _d, _e;
371
- const pkg = readJson(path.join(deps.appRoot, 'package.json'));
372
- if (!pkg)
373
- return undefined;
374
- return ((_d = (_b = (_a = pkg.dependencies) === null || _a === void 0 ? void 0 : _a[name]) !== null && _b !== void 0 ? _b : (_c = pkg.devDependencies) === null || _c === void 0 ? void 0 : _c[name]) !== null && _d !== void 0 ? _d : (_e = pkg.optionalDependencies) === null || _e === void 0 ? void 0 : _e[name]);
485
+ /** The last lines of a subprocess's output: enough to see the error, not the whole log. */
486
+ export function tail(output, lines = 20) {
487
+ return output.trim().split('\n').slice(-lines).join('\n');
375
488
  }
376
489
  //# sourceMappingURL=localize.js.map