@_linked/cli 1.46.2 → 1.47.0

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 +107 -0
  2. package/README.md +48 -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 +169 -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 +49 -189
  88. package/lib/esm/localize/list.js.map +1 -1
  89. package/lib/esm/localize/localize.d.ts +46 -62
  90. package/lib/esm/localize/localize.d.ts.map +1 -1
  91. package/lib/esm/localize/localize.js +411 -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,474 @@
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, PARKED_DIR } 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
+ /** What an app recorded checkouts in before they were workspace members. */
72
+ const LEGACY_MANIFEST = 'local-packages.json';
73
+ /**
74
+ * @param names full npm package names
75
+ * @returns the process exit code: the highest of the per-name codes and the
76
+ * install's, since a failure on name 2 does not undo name 1.
77
+ */
47
78
  export function localize(names, opts, deps) {
48
- return forEachName(names, opts, deps, localizeOne);
79
+ return runLocalize(names, opts, deps, prepareClone);
49
80
  }
50
81
  /**
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
82
+ * The run `localize`, `adopt` and `--parked` share: everything except how a
83
+ * name becomes a checkout. See the module header for the steps.
57
84
  */
58
- export function forEachName(names, opts, deps, one) {
85
+ export function runLocalize(names, opts, deps, prepare) {
59
86
  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;
87
+ legacyManifestHint(deps);
88
+ let setupLock;
64
89
  try {
65
- manifest = readManifest(deps.appRoot, deps);
90
+ restoreSetAside(deps);
91
+ checkDir(opts.dir);
92
+ // The checkouts already there count too: the one install below covers them all.
93
+ const localized = new Set([
94
+ ...names,
95
+ ...localMembers(deps.appRoot).map((m) => m.name),
96
+ ]);
97
+ const conflicts = conflictingOverrides(deps.appRoot, [...localized]);
98
+ if (conflicts.length) {
99
+ throw new LocalizeError(`package.json "overrides" names a localized package: ${conflicts
100
+ .map((k) => `"${k}"`)
101
+ .join(', ')}.\n` +
102
+ `npm refuses an override on a workspace member's name (EOVERRIDE), and a nested one ` +
103
+ `would replace the checkout. Remove ${conflicts.length === 1 ? 'that key' : 'those keys'} ` +
104
+ `from "overrides", then run this again. Nothing was changed.`, EXIT_REFUSED);
105
+ }
106
+ const workspacesBefore = (_a = readJson(path.join(deps.appRoot, 'package.json'))) === null || _a === void 0 ? void 0 : _a.workspaces;
107
+ const changes = ensureAppSetup(deps.appRoot);
108
+ reportSetup(changes, deps);
109
+ if (changes.some((c) => c.file === 'package.json'))
110
+ setupLock =
111
+ (_b = captureSetupLockfile(changes, workspacesBefore, deps)) !== null && _b !== void 0 ? _b : undefined;
66
112
  }
67
113
  catch (e) {
68
114
  return reportError(e, deps);
69
115
  }
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
116
  let code = 0;
73
- let recorded = false;
117
+ const prepared = [];
74
118
  for (const name of names) {
75
119
  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
- });
120
+ const p = prepare(name, opts, deps);
121
+ if (p)
122
+ prepared.push(p);
88
123
  }
89
124
  catch (e) {
90
125
  code = Math.max(code, reportError(e, deps));
91
126
  }
92
127
  }
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
- }
128
+ if (!prepared.length) {
129
+ // Nothing was installed: the work tree holds the set-up's lockfile as captured.
130
+ if (setupLock !== undefined)
131
+ commitHint(deps);
132
+ return code;
103
133
  }
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);
134
+ return Math.max(code, installAndBuild(prepared, opts, deps, setupLock));
135
+ }
136
+ /**
137
+ * Steps 4-6: git hooks, one root install, then the builds. `setupLock` is the
138
+ * lockfile the set-up captured (see `captureSetupLockfile`), written back
139
+ * over what the install left.
140
+ */
141
+ export function installAndBuild(prepared, opts, deps, setupLock) {
142
+ reportHooks(installGitHooks(deps.appRoot), deps);
143
+ deps.log(`[localize] npm ${INSTALL_ARGS.join(' ')} (app root)`);
144
+ const install = deps.run('npm', INSTALL_ARGS, { cwd: deps.appRoot });
145
+ if (setupLock !== undefined)
146
+ restoreSetupLockfile(setupLock, deps);
147
+ if (install.status !== 0) {
148
+ deps.error(`[localize] npm install failed at the app root. The checkout${prepared.length === 1 ? ' stays' : 's stay'} ` +
149
+ `in ${LOCAL_DIR}/; fix the cause and run \`npm install\`.\n` +
150
+ tail(install.stderr || install.stdout));
151
+ return EXIT_INSTALL_FAILED;
152
+ }
153
+ let code = 0;
154
+ // The cli first: the other builds run `linked build`, which is the cli.
155
+ const ordered = [
156
+ ...prepared.filter((p) => p.name === CLI_PACKAGE),
157
+ ...prepared.filter((p) => p.name !== CLI_PACKAGE),
158
+ ];
159
+ for (const p of ordered) {
160
+ const pkgDir = path.join(deps.appRoot, p.dir);
161
+ const built = p.build ? runBuild(p, pkgDir, opts, deps) : 'skipped';
162
+ if (built === 'failed')
163
+ code = EXIT_WARNED;
164
+ if (built === 'ok' && p.name === CLI_PACKAGE)
165
+ linkCliBin(deps);
166
+ deps.log(`[localize] ${p.name} -> ${p.dir} (branch ${currentBranch(pkgDir, deps)})`);
167
+ }
168
+ for (const d of driftProblems(deps.appRoot))
169
+ deps.warn(`[localize] drift: ${describeDrift(d)}`);
109
170
  return code;
110
171
  }
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`);
172
+ /**
173
+ * npm links a package's bin only when its target exists, and a fresh cli
174
+ * checkout has no `lib/` at install time: without this, `node_modules/.bin/linked`
175
+ * is missing after the build, and `npx linked` would fetch the unrelated npm
176
+ * package named `linked`. `--ignore-scripts`: only the bin links are wanted.
177
+ */
178
+ function linkCliBin(deps) {
179
+ const r = deps.run('npm', ['rebuild', CLI_PACKAGE, '--ignore-scripts'], {
180
+ cwd: deps.appRoot,
181
+ });
182
+ if (r.status !== 0) {
183
+ deps.warn(`[localize] npm rebuild ${CLI_PACKAGE} failed, so \`linked\` may not be on the app's ` +
184
+ `bin path. Run it yourself.\n` +
185
+ tail(r.stderr || r.stdout));
186
+ }
118
187
  }
119
- function localizeOne(name, dir, recorded, opts, deps) {
188
+ /** Step 3 for `localize`: take back a parked checkout, else clone. */
189
+ function prepareClone(name, opts, deps) {
120
190
  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
191
+ const parked = parkedMembers(deps.appRoot).find((m) => m.name === name);
192
+ if (parked)
193
+ return unpark(parked, deps);
194
+ const member = localMembers(deps.appRoot).find((m) => m.name === name);
195
+ if (member) {
196
+ throw new LocalizeError(`${name} is already in ${member.dir}. To install and build it as it is:\n` +
197
+ ` linked localize ${name} --adopt`, EXIT_REFUSED);
144
198
  }
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
- }
199
+ const relDir = `${LOCAL_DIR}/${checkoutNameFor(name)}`;
200
+ const target = path.join(deps.appRoot, relDir);
201
+ if (existsOrLink(target)) {
202
+ throw new LocalizeError(`${relDir} already exists. localize never clones over a directory. If it is ${name}'s ` +
203
+ `checkout, install and build it as it is:\n linked localize ${name} --adopt`, EXIT_REFUSED);
152
204
  }
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);
205
+ const resolved = resolvePackage(name, deps, opts.repo ? { repo: opts.repo, subdir: opts.subdir } : undefined);
206
+ // A monorepo is cloned whole outside the glob; the member is a link to the package.
207
+ const relClone = resolved.subdir
208
+ ? `${REPOS_DIR}/${checkoutNameFor(name)}`
209
+ : relDir;
210
+ const clone = path.join(deps.appRoot, relClone);
211
+ if (existsOrLink(clone)) {
212
+ throw new LocalizeError(`${relClone} already exists. Remove it, or move it out of the way, and run this again.`, EXIT_REFUSED);
159
213
  }
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.
214
+ fs.mkdirSync(path.dirname(clone), { recursive: true });
215
+ deps.log(`[localize] cloning ${resolved.repo} -> ${relClone}`);
216
+ const r = deps.run('git', ['clone', '--', resolved.repo, clone]);
217
+ if (r.status !== 0) {
218
+ throw new LocalizeError(`git clone ${resolved.repo} failed:\n${(r.stderr || r.stdout).trim()}`, EXIT_REFUSED);
219
+ }
220
+ const pkgDir = resolved.subdir ? path.join(clone, resolved.subdir) : clone;
221
+ const relPkg = resolved.subdir ? `${relClone}/${resolved.subdir}` : relClone;
222
+ // A clone that is not the package must not stay: in packages-local/ it
223
+ // would be a workspace member under the wrong name.
162
224
  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);
225
+ if (declared !== name) {
226
+ fs.rmSync(clone, { recursive: true, force: true });
227
+ const where = resolved.subdir
228
+ ? `The registry's repository.directory for "${name}" points at the wrong place.`
229
+ : `${resolved.repo} looks like a monorepo. Say where the package lives:\n` +
230
+ ` linked localize ${name} --repo ${resolved.repo} --subdir <path-inside-the-repo>`;
231
+ throw new LocalizeError((declared
232
+ ? `${relPkg} declares itself as "${declared}", not "${name}".\n`
233
+ : `${relPkg} has no package.json naming it, so it is not the package "${name}".\n`) +
234
+ `${where}\nThe clone was removed.`, EXIT_REFUSED);
235
+ }
236
+ if (resolved.subdir) {
237
+ linkDirectory(pkgDir, target);
238
+ deps.log(`[localize] ${relDir} -> ${relPkg}`);
239
+ }
240
+ return { name, dir: relDir, build: true };
241
+ }
242
+ /** Move a parked checkout back into `packages-local/`. No clone, no build: it is as it was. */
243
+ export function unpark(member, deps) {
244
+ const entry = path.basename(member.dir);
245
+ const relDir = `${LOCAL_DIR}/${entry}`;
246
+ const to = path.join(deps.appRoot, relDir);
247
+ if (existsOrLink(to)) {
248
+ throw new LocalizeError(`${member.name} is parked in ${member.dir}, but ${relDir} already exists. Move one of ` +
249
+ `them out of the way and run this again.`, EXIT_REFUSED);
250
+ }
251
+ // npm refuses two workspace members of one name, so the install would fail.
252
+ const same = localMembers(deps.appRoot).find((m) => m.name === member.name);
253
+ if (same) {
254
+ throw new LocalizeError(`${member.name} is parked in ${member.dir}, but ${same.dir} is ${member.name} already. ` +
255
+ `Keep one: delete or move the other out of the way, and run this again.`, EXIT_REFUSED);
169
256
  }
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);
257
+ moveEntry(path.join(deps.appRoot, member.dir), to);
258
+ deps.log(`[localize] ${member.dir} -> ${relDir}`);
259
+ return { name: member.name, dir: relDir, build: false };
178
260
  }
261
+ /** `--dir` is kept for scripts that pass the default; anything else is not a workspace member. */
262
+ function checkDir(dir) {
263
+ if (dir === undefined || dir.replace(/\/+$/, '') === LOCAL_DIR)
264
+ return;
265
+ throw new LocalizeError(`--dir ${dir}: checkouts live in ${LOCAL_DIR}/, the directory the app's ` +
266
+ `"workspaces" glob and the lock-at-rest hook name. Move them there.`, EXIT_USAGE);
267
+ }
268
+ function reportSetup(changes, deps) {
269
+ if (!changes.length)
270
+ return;
271
+ deps.log(`[localize] set up this app for checkouts (commit these):`);
272
+ for (const c of changes)
273
+ deps.log(` ${c.file}: ${c.what}`);
274
+ }
275
+ /** What `captureSetupLockfile` runs: the lockfile for the new `package.json`, nothing installed. */
276
+ const SETUP_LOCK_ARGS = [
277
+ 'install',
278
+ '--package-lock-only',
279
+ '--ignore-scripts',
280
+ '--no-audit',
281
+ '--no-fund',
282
+ ];
179
283
  /**
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.
284
+ * The set-up changed `package.json`, so the lockfile must change with it
285
+ * (the root entry's `workspaces`) -- and it must be a lockfile with no
286
+ * checkout in it, which the install below cannot give: it lists them, and
287
+ * lock-at-rest leaves it so while `package.json` differs from HEAD. So,
288
+ * before anything is cloned, make that lockfile, keep it, and write it back
289
+ * after the install. When the root entry already has `workspaces` and
290
+ * `hasInstallScript`, it is an edit of that entry (setup-lockfile.ts: exact,
291
+ * offline); otherwise npm makes the lockfile alone, with checkouts already in
292
+ * `packages-local/` (an adopted one) moved into `SETUP_ASIDE_DIR` for that
293
+ * one call.
183
294
  *
184
- * `repo` may be absent -- an adopted checkout with no remote is recorded
185
- * without one.
295
+ * @returns the lockfile to write back, or null when it could not be made (the
296
+ * manual steps are printed instead).
186
297
  */
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).
298
+ function captureSetupLockfile(changes, workspacesBefore, deps) {
299
+ const lockfile = path.join(deps.appRoot, LOCKFILE);
300
+ const edited = editedSetupLockfile(lockfile, workspacesBefore, deps);
301
+ if (edited !== null)
302
+ return edited;
303
+ const moveAside = localMembers(deps.appRoot).length > 0;
304
+ deps.log(`[localize] npm ${SETUP_LOCK_ARGS.join(' ')} (the set-up's lockfile)`);
305
+ if (moveAside)
306
+ setAside(deps);
307
+ let r;
190
308
  try {
191
- code = Math.max(code, ensure({ path: relPkg, subdir }, deps, opts));
309
+ r = deps.run('npm', SETUP_LOCK_ARGS, { cwd: deps.appRoot });
192
310
  }
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;
311
+ finally {
312
+ if (moveAside)
313
+ moveBack(deps);
197
314
  }
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) {
315
+ let text = null;
223
316
  try {
224
- return resolvePackage(name, deps, override);
317
+ text = fs.readFileSync(lockfile, 'utf8');
225
318
  }
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;
319
+ catch (_a) {
320
+ /* none written */
236
321
  }
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);
322
+ if (r.status !== 0 || text === null) {
323
+ manualSetupSteps(changes, r.status !== 0
324
+ ? `npm could not write it:\n${tail(r.stderr || r.stdout)}`
325
+ : `npm wrote no ${LOCKFILE}`, deps);
326
+ return null;
327
+ }
328
+ return text;
241
329
  }
242
330
  /**
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.
331
+ * Every entry of `packages-local/` but the dot directories, moved into
332
+ * `SETUP_ASIDE_DIR` as it is (a link keeps its text and works again once back).
246
333
  */
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;
334
+ function setAside(deps) {
335
+ const local = path.join(deps.appRoot, LOCAL_DIR);
336
+ const aside = path.join(deps.appRoot, SETUP_ASIDE_DIR);
337
+ fs.mkdirSync(aside, { recursive: true });
338
+ for (const entry of fs.readdirSync(local)) {
339
+ if (entry.startsWith('.'))
340
+ continue;
341
+ fs.renameSync(path.join(local, entry), path.join(aside, entry));
259
342
  }
260
- return 0;
343
+ }
344
+ /** Everything in `SETUP_ASIDE_DIR` back into `packages-local/`; the directory goes. */
345
+ function moveBack(deps) {
346
+ const local = path.join(deps.appRoot, LOCAL_DIR);
347
+ const aside = path.join(deps.appRoot, SETUP_ASIDE_DIR);
348
+ const entries = fs.readdirSync(aside);
349
+ for (const entry of entries)
350
+ fs.renameSync(path.join(aside, entry), path.join(local, entry));
351
+ fs.rmdirSync(aside);
352
+ return entries.length;
261
353
  }
262
354
  /**
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.
355
+ * A set-up run that was interrupted while the checkouts were in
356
+ * `SETUP_ASIDE_DIR` left them there, where npm does not see them: move them
357
+ * back, saying so. Refuses, moving nothing, when `packages-local/` already
358
+ * has an entry of the same name.
270
359
  */
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))
360
+ export function restoreSetAside(deps) {
361
+ const aside = path.join(deps.appRoot, SETUP_ASIDE_DIR);
362
+ if (!existsOrLink(aside))
289
363
  return;
290
- let target;
364
+ let entries;
291
365
  try {
292
- target = fs.realpathSync(link);
366
+ entries = fs.readdirSync(aside);
293
367
  }
294
368
  catch (_a) {
295
- return; // a dangling link is ours to replace
369
+ throw new LocalizeError(`${SETUP_ASIDE_DIR} is not a directory. Remove it and run this again.`, EXIT_REFUSED);
296
370
  }
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;
371
+ const clashes = entries.filter((e) => existsOrLink(path.join(deps.appRoot, LOCAL_DIR, e)));
372
+ if (clashes.length) {
373
+ throw new LocalizeError(`${SETUP_ASIDE_DIR}/ holds checkouts an interrupted set-up left there, but ` +
374
+ `${clashes.map((e) => `${LOCAL_DIR}/${e}`).join(', ')} now exist${clashes.length === 1 ? 's' : ''} ` +
375
+ `too. Keep one of each, move the other out of ${LOCAL_DIR}/, and run this again. ` +
376
+ `Nothing was moved.`, EXIT_REFUSED);
302
377
  }
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);
378
+ const n = moveBack(deps);
379
+ if (n)
380
+ deps.log(`[localize] moved ${n} checkout${n === 1 ? '' : 's'} back from ${SETUP_ASIDE_DIR}/ into ` +
381
+ `${LOCAL_DIR}/, where an interrupted set-up left ${n === 1 ? 'it' : 'them'}.`);
305
382
  }
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
- }
383
+ /** The set-up's lockfile as an edit of the root entry, written now; null when npm must make it. */
384
+ function editedSetupLockfile(lockfile, workspacesBefore, deps) {
385
+ let current;
386
+ try {
387
+ current = fs.readFileSync(lockfile, 'utf8');
388
+ }
389
+ catch (_a) {
390
+ return null;
391
+ }
392
+ const text = setupLockfileByEdit(current, readJson(path.join(deps.appRoot, 'package.json')), workspacesBefore);
393
+ if (text === null)
394
+ return null;
395
+ fs.writeFileSync(lockfile, text);
396
+ deps.log(`[localize] ${LOCKFILE}: the root entry's "workspaces" now matches package.json (the set-up's lockfile)`);
397
+ return text;
398
+ }
399
+ /** Put the captured set-up lockfile back after the install rewrote it. */
400
+ function restoreSetupLockfile(text, deps) {
401
+ fs.writeFileSync(path.join(deps.appRoot, LOCKFILE), text);
402
+ deps.log(`[localize] ${LOCKFILE}: put back the set-up's lockfile, which lists no checkout (any ` +
403
+ `lock-at-rest warning above was about the one the install wrote).`);
404
+ commitHint(deps);
405
+ }
406
+ function commitHint(deps) {
407
+ deps.log(`[localize] commit the set-up now -- package.json, ${LOCKFILE}, .gitignore:\n` +
408
+ ` git add package.json ${LOCKFILE} .gitignore && git commit -m "chore: set up linked localize"`);
409
+ }
410
+ /** The set-up's lockfile could not be made in one step: how to make it by hand. */
411
+ function manualSetupSteps(changes, why, deps) {
412
+ const files = [...new Set([...changes.map((c) => c.file), LOCKFILE])];
413
+ deps.warn(`[localize] could not make the set-up's ${LOCKFILE} in one step: ${why}\n` +
414
+ `Commit the set-up with a lockfile from an install with no checkout present:\n` +
415
+ ` npx linked delocalize\n` +
416
+ ` npm install\n` +
417
+ ` git add ${files.join(' ')} && git commit -m "chore: set up linked localize"\n` +
418
+ ` npx linked localize --parked`);
419
+ }
420
+ /** Tell the developer what `installGitHooks` did not do. */
421
+ function reportHooks(hooks, deps) {
422
+ for (const file of hooks.skipped) {
423
+ deps.warn(`[localize] ${path.relative(deps.appRoot, file)} is not ours, so it was left alone. Without ` +
424
+ `the localize hook there, run \`npm install\` yourself after a pull or branch switch that ` +
425
+ `changes dependencies.`);
426
+ }
427
+ if (hooks.hooksPath !== null) {
428
+ deps.warn(`[localize] core.hooksPath is set (${hooks.hooksPath}), so no git hooks were written. Add ` +
429
+ `each line below to the hook named before it, an \`sh\` script (not, say, a Python hook ` +
430
+ `the pre-commit framework writes); they do nothing where the cli is not installed:`);
431
+ for (const m of hooks.manual)
432
+ deps.warn(` ${m.file}:\n ${m.line}`);
433
+ }
434
+ if (hooks.sharedWithWorktrees) {
435
+ deps.warn(`[localize] this is a linked worktree: git hooks are shared by every worktree of the ` +
436
+ `repository. They act only in a work tree whose ${LOCAL_DIR}/ has checkouts.`);
323
437
  }
324
- return code;
325
438
  }
326
439
  /**
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.
440
+ * One line when an app still has the retired record. It is ignored: the
441
+ * record now is the directories in `packages-local/`.
442
+ */
443
+ export function legacyManifestHint(deps) {
444
+ if (!fs.existsSync(path.join(deps.appRoot, LEGACY_MANIFEST)))
445
+ return;
446
+ deps.warn(`[localize] ${LEGACY_MANIFEST} is no longer read: every directory in ${LOCAL_DIR}/ is ` +
447
+ `localized, ${PARKED_DIR}/ holds parked ones. Delete the file.`);
448
+ }
449
+ /**
450
+ * Run the build for one new checkout. `opts.build` wins, including `''` for
451
+ * no build; otherwise the cli builds itself and everything else gets
452
+ * `linked build`.
333
453
  */
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;
454
+ function runBuild(p, pkgDir, opts, deps) {
455
+ const cmd = buildCommandFor(p.name, opts);
456
+ if (!cmd)
457
+ return 'skipped';
458
+ deps.log(`[localize] ${p.dir}: ${cmd}`);
459
+ const r = deps.run(cmd, [], { cwd: pkgDir, shell: true });
460
+ if (r.status !== 0) {
461
+ deps.warn(`[localize] ${p.dir}: \`${cmd}\` failed. It is installed and linked anyway -- a package ` +
462
+ `whose build is broken is usually what you localized it to fix.\n` +
463
+ tail(r.stderr || r.stdout));
464
+ return 'failed';
465
+ }
466
+ return 'ok';
467
+ }
468
+ function buildCommandFor(name, opts) {
469
+ if (opts.build !== undefined)
470
+ return opts.build;
471
+ return name === CLI_PACKAGE ? CLI_BUILD_COMMAND : DEFAULT_BUILD_COMMAND;
343
472
  }
344
473
  export function reportError(e, deps) {
345
474
  if (e instanceof LocalizeError) {
@@ -354,23 +483,20 @@ export function reportError(e, deps) {
354
483
  * `rev-parse --abbrev-ref` fails there, and `create-package` adopts a fresh
355
484
  * `git init` before its first commit.
356
485
  */
357
- export function currentBranch(clone, deps) {
486
+ export function currentBranch(checkout, deps) {
358
487
  const sym = deps.run('git', ['symbolic-ref', '--short', '-q', 'HEAD'], {
359
- cwd: clone,
488
+ cwd: checkout,
360
489
  });
361
490
  if (sym.status === 0 && sym.stdout.trim())
362
491
  return sym.stdout.trim();
363
492
  const r = deps.run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
364
- cwd: clone,
493
+ cwd: checkout,
365
494
  });
366
495
  const b = r.stdout.trim();
367
496
  return r.status === 0 && b ? b : 'HEAD';
368
497
  }
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]);
498
+ /** The last lines of a subprocess's output: enough to see the error, not the whole log. */
499
+ export function tail(output, lines = 20) {
500
+ return output.trim().split('\n').slice(-lines).join('\n');
375
501
  }
376
502
  //# sourceMappingURL=localize.js.map