@_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 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/localize/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,sEAAsE;AACtE,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC;AAC/B,gEAAgE;AAChE,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAChC,4EAA4E;AAC5E,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAC7B,mDAAmD;AACnD,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC;AAC9B,gFAAgF;AAChF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AACrC,2FAA2F;AAC3F,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAErC,MAAM,OAAO,aAAc,SAAQ,KAAK;IAEtC,YAAY,OAAe,EAAE,IAAY;QACvC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF","sourcesContent":["/**\n * Every refusal in localize throws a LocalizeError carrying an exit code, so\n * callers can branch on `.code` rather than matching on a message.\n *\n * The codes are a public contract: scripts depend on them.\n */\n\n/** The manifest file is unusable and localize refuses to touch it. */\nexport const EXIT_BAD_FILE = 3;\n/** No git repository could be resolved for the package name. */\nexport const EXIT_NOT_FOUND = 4;\n/** Something was warned about and skipped; the command otherwise worked. */\nexport const EXIT_WARNED = 5;\n/** A guard refused to act. `--force` overrides. */\nexport const EXIT_REFUSED = 6;\n/** The install inside the checkout failed: nothing linked, nothing recorded. */\nexport const EXIT_INSTALL_FAILED = 7;\n/** package.json / package-lock.json changed -- the failure this tool exists to prevent. */\nexport const EXIT_MANIFEST_DIRTY = 8;\n\nexport class LocalizeError extends Error {\n code: number;\n constructor(message: string, code: number) {\n super(message);\n this.name = 'LocalizeError';\n this.code = code;\n }\n}\n"]}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/localize/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,oCAAoC;AACpC,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC;AAC5B,wEAAwE;AACxE,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC;AAC5B,gEAAgE;AAChE,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAChC,mHAAmH;AACnH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAC7B;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC;AAC9B,0DAA0D;AAC1D,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAErC,MAAM,OAAO,aAAc,SAAQ,KAAK;IAEtC,YAAY,OAAe,EAAE,IAAY;QACvC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF","sourcesContent":["/**\n * Every refusal in localize throws a LocalizeError carrying an exit code, so\n * callers can branch on `.code` rather than matching on a message.\n *\n * The codes are a public contract: scripts depend on them. 3 (unusable\n * manifest file) and 8 (manifests changed) belonged to the retired record\n * file and the hand-written links, and are not reused.\n */\n\n/** `--list --check` found drift. */\nexport const EXIT_DRIFT = 1;\n/** The command line asks for something that cannot be done as given. */\nexport const EXIT_USAGE = 2;\n/** No git repository could be resolved for the package name. */\nexport const EXIT_NOT_FOUND = 4;\n/** Something was warned about and skipped; the command otherwise worked (a failed build, a name not localized). */\nexport const EXIT_WARNED = 5;\n/**\n * A guard refused to act: an existing directory, a conflicting override, or a\n * `--purge` of a checkout with unsaved work (uncommitted, on no remote,\n * stashed) or one git cannot inspect.\n */\nexport const EXIT_REFUSED = 6;\n/** `npm install` (or `npm ci`) at the app root failed. */\nexport const EXIT_INSTALL_FAILED = 7;\n\nexport class LocalizeError extends Error {\n code: number;\n constructor(message: string, code: number) {\n super(message);\n this.name = 'LocalizeError';\n this.code = code;\n }\n}\n"]}
@@ -1,36 +1,20 @@
1
1
  export declare function isSymlink(p: string): boolean;
2
- export declare function isGitCheckout(p: string): boolean;
2
+ /** Something is at `p`: a file, a directory, or a link, dangling or not. */
3
+ export declare function existsOrLink(p: string): boolean;
3
4
  /** Is `child` at or below `parent`? Both must be absolute, real paths. */
4
5
  export declare function isInside(child: string, parent: string): boolean;
5
6
  export declare function readJson(file: string): any;
6
- /** Where `node_modules/<name>` points, resolved, or null when it is not a link. */
7
- export declare function linkTarget(appRoot: string, name: string): string | null;
8
7
  /**
9
- * Write `node_modules/<name> -> <checkout>` and nothing else.
10
- *
11
- * The target is stored RELATIVE so the tree survives being moved or bind-mounted
12
- * into a container at a different path.
8
+ * Move a checkout directory, or the link standing for one (a package inside a
9
+ * cloned monorepo, see localize.ts). A link is written again from its new
10
+ * place (relative, or a junction on Windows), so it still reaches the same
11
+ * target.
13
12
  */
14
- export declare function writeLink(appRoot: string, name: string, checkout: string): string;
15
- /** Every symlink directly under node_modules whose target leaves node_modules. */
16
- export declare function scanLinks(appRoot: string): {
17
- name: string;
18
- target: string;
19
- }[];
20
- /** The files no localize command may ever touch. */
21
- export declare const MANIFEST_FILES: string[];
13
+ export declare function moveEntry(from: string, to: string): void;
22
14
  /**
23
- * Hash the consumer's manifests.
24
- *
25
- * Content hashes, NOT `git status`. `git status` compares against HEAD, so in
26
- * any tree with an uncommitted `package.json` it reports a modification the
27
- * command did not make -- an earlier version of this tool shipped that bug and
28
- * reported correct runs as failures. Hashes also work in a tree with no git,
29
- * and catch an edit that HEAD happens to agree with.
30
- *
31
- * `latin1` is a byte-exact round-trip for arbitrary bytes; `utf8` is not.
15
+ * The root of the git repository a checkout belongs to -- the checkout itself,
16
+ * or the cloned monorepo a linked package sits in -- or null when there is
17
+ * none below the app root (the app's own repository does not count).
32
18
  */
33
- export declare function snapshotManifests(appRoot: string): Record<string, string | null>;
34
- /** @returns {string[]} a human description of each manifest that changed. */
35
- export declare function diffManifests(before: Record<string, string | null>, after: Record<string, string | null>): string[];
19
+ export declare function checkoutRepoRoot(checkout: string, appRoot: string): string | null;
36
20
  //# sourceMappingURL=fsops.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fsops.d.ts","sourceRoot":"","sources":["../../../src/localize/fsops.ts"],"names":[],"mappings":"AAiBA,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAM5C;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,0EAA0E;AAC1E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAG/D;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAM1C;AAED,mFAAmF;AACnF,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASvE;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,MAAM,CAOR;AAED,kFAAkF;AAClF,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,EAAE,CAqC3E;AAMD,oDAAoD;AACpD,eAAO,MAAM,cAAc,UAAwC,CAAC;AAEpE;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,GACd,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAW/B;AAED,6EAA6E;AAC7E,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,EACrC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,GACnC,MAAM,EAAE,CAMV"}
1
+ {"version":3,"file":"fsops.d.ts","sourceRoot":"","sources":["../../../src/localize/fsops.ts"],"names":[],"mappings":"AAMA,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAM5C;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAE/C;AAMD,0EAA0E;AAC1E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAG/D;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAM1C;AAWD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CASxD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,MAAM,GAAG,IAAI,CAcf"}
@@ -1,19 +1,7 @@
1
- /**
2
- * The filesystem primitives, and the guard that proves localize kept its
3
- * promise.
4
- *
5
- * The link is written with `fs.symlink` and never by invoking npm. Two
6
- * separate reasons, both measured rather than assumed:
7
- *
8
- * - `npm install <path> --no-save` brings npm's peer resolution along, which
9
- * hard-failed an app's boot on a missing peer dependency. Module resolution
10
- * happens from the symlink's REAL path, whose ancestors have no
11
- * `node_modules`, so the checkout needs its own installed tree.
12
- * - a `postinstall` hook that invokes an install at the consumer root recurses.
13
- */
1
+ /** The filesystem primitives localize shares. */
14
2
  import fs from 'node:fs';
15
3
  import path from 'node:path';
16
- import { createHash } from 'node:crypto';
4
+ import { linkDirectory, samePath } from '../utils/platform.js';
17
5
  export function isSymlink(p) {
18
6
  try {
19
7
  return fs.lstatSync(p).isSymbolicLink();
@@ -22,7 +10,11 @@ export function isSymlink(p) {
22
10
  return false;
23
11
  }
24
12
  }
25
- export function isGitCheckout(p) {
13
+ /** Something is at `p`: a file, a directory, or a link, dangling or not. */
14
+ export function existsOrLink(p) {
15
+ return fs.existsSync(p) || isSymlink(p);
16
+ }
17
+ function isGitCheckout(p) {
26
18
  return fs.existsSync(path.join(p, '.git'));
27
19
  }
28
20
  /** Is `child` at or below `parent`? Both must be absolute, real paths. */
@@ -38,118 +30,51 @@ export function readJson(file) {
38
30
  return null;
39
31
  }
40
32
  }
41
- /** Where `node_modules/<name>` points, resolved, or null when it is not a link. */
42
- export function linkTarget(appRoot, name) {
43
- const link = path.join(appRoot, 'node_modules', name);
44
- if (!isSymlink(link))
45
- return null;
33
+ /** Where the link at `p` points, absolute, whether or not the target exists. */
34
+ function linkTargetOf(p) {
46
35
  try {
47
- return fs.realpathSync(link);
36
+ return fs.realpathSync(p);
48
37
  }
49
38
  catch (_a) {
50
- // A dangling link is still linked intent; its target is simply gone.
51
- return path.resolve(path.dirname(link), fs.readlinkSync(link));
39
+ return path.resolve(path.dirname(p), fs.readlinkSync(p));
52
40
  }
53
41
  }
54
42
  /**
55
- * Write `node_modules/<name> -> <checkout>` and nothing else.
56
- *
57
- * The target is stored RELATIVE so the tree survives being moved or bind-mounted
58
- * into a container at a different path.
43
+ * Move a checkout directory, or the link standing for one (a package inside a
44
+ * cloned monorepo, see localize.ts). A link is written again from its new
45
+ * place (relative, or a junction on Windows), so it still reaches the same
46
+ * target.
59
47
  */
60
- export function writeLink(appRoot, name, checkout) {
61
- const link = path.join(appRoot, 'node_modules', name);
62
- fs.mkdirSync(path.dirname(link), { recursive: true });
63
- if (isSymlink(link))
64
- fs.unlinkSync(link);
65
- else if (fs.existsSync(link))
66
- fs.rmSync(link, { recursive: true, force: true });
67
- fs.symlinkSync(path.relative(path.dirname(link), checkout), link, 'dir');
68
- return link;
48
+ export function moveEntry(from, to) {
49
+ fs.mkdirSync(path.dirname(to), { recursive: true });
50
+ if (isSymlink(from)) {
51
+ const target = linkTargetOf(from);
52
+ linkDirectory(target, to);
53
+ fs.unlinkSync(from);
54
+ return;
55
+ }
56
+ fs.renameSync(from, to);
69
57
  }
70
- /** Every symlink directly under node_modules whose target leaves node_modules. */
71
- export function scanLinks(appRoot) {
72
- const root = path.join(appRoot, 'node_modules');
73
- const out = [];
74
- let top;
58
+ /**
59
+ * The root of the git repository a checkout belongs to -- the checkout itself,
60
+ * or the cloned monorepo a linked package sits in -- or null when there is
61
+ * none below the app root (the app's own repository does not count).
62
+ */
63
+ export function checkoutRepoRoot(checkout, appRoot) {
64
+ let app;
65
+ let dir;
75
66
  try {
76
- top = fs.readdirSync(root, { withFileTypes: true });
67
+ app = fs.realpathSync(appRoot);
68
+ dir = fs.realpathSync(checkout);
77
69
  }
78
70
  catch (_a) {
79
- return out;
80
- }
81
- const collect = (name, full) => {
82
- if (!isSymlink(full))
83
- return;
84
- let target;
85
- try {
86
- target = fs.realpathSync(full);
87
- }
88
- catch (_a) {
89
- return;
90
- }
91
- if (isInside(target, root))
92
- return; // an ordinary hoisting link, not a checkout
93
- out.push({ name, target });
94
- };
95
- for (const item of top) {
96
- if (item.name === '.bin' || item.name.startsWith('.'))
97
- continue;
98
- const full = path.join(root, item.name);
99
- if (item.name.startsWith('@')) {
100
- let inner;
101
- try {
102
- inner = fs.readdirSync(full, { withFileTypes: true });
103
- }
104
- catch (_b) {
105
- continue;
106
- }
107
- for (const sub of inner)
108
- collect(`${item.name}/${sub.name}`, path.join(full, sub.name));
109
- }
110
- else {
111
- collect(item.name, full);
112
- }
71
+ return null;
113
72
  }
114
- return out;
115
- }
116
- // ---------------------------------------------------------------------------
117
- // the post-condition
118
- // ---------------------------------------------------------------------------
119
- /** The files no localize command may ever touch. */
120
- export const MANIFEST_FILES = ['package.json', 'package-lock.json'];
121
- /**
122
- * Hash the consumer's manifests.
123
- *
124
- * Content hashes, NOT `git status`. `git status` compares against HEAD, so in
125
- * any tree with an uncommitted `package.json` it reports a modification the
126
- * command did not make -- an earlier version of this tool shipped that bug and
127
- * reported correct runs as failures. Hashes also work in a tree with no git,
128
- * and catch an edit that HEAD happens to agree with.
129
- *
130
- * `latin1` is a byte-exact round-trip for arbitrary bytes; `utf8` is not.
131
- */
132
- export function snapshotManifests(appRoot) {
133
- const snap = {};
134
- for (const file of MANIFEST_FILES) {
135
- try {
136
- const bytes = fs.readFileSync(path.join(appRoot, file), 'latin1');
137
- snap[file] = createHash('sha256').update(bytes, 'latin1').digest('hex');
138
- }
139
- catch (_a) {
140
- snap[file] = null;
141
- }
73
+ while (!samePath(dir, app) && isInside(dir, app)) {
74
+ if (isGitCheckout(dir))
75
+ return dir;
76
+ dir = path.dirname(dir);
142
77
  }
143
- return snap;
144
- }
145
- /** @returns {string[]} a human description of each manifest that changed. */
146
- export function diffManifests(before, after) {
147
- return MANIFEST_FILES.filter((f) => before[f] !== after[f]).map((f) => {
148
- if (before[f] === null)
149
- return `${f} (created)`;
150
- if (after[f] === null)
151
- return `${f} (deleted)`;
152
- return `${f} (modified)`;
153
- });
78
+ return null;
154
79
  }
155
80
  //# sourceMappingURL=fsops.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fsops.js","sourceRoot":"","sources":["../../../src/localize/fsops.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The filesystem primitives, and the guard that proves localize kept its\n * promise.\n *\n * The link is written with `fs.symlink` and never by invoking npm. Two\n * separate reasons, both measured rather than assumed:\n *\n * - `npm install <path> --no-save` brings npm's peer resolution along, which\n * hard-failed an app's boot on a missing peer dependency. Module resolution\n * happens from the symlink's REAL path, whose ancestors have no\n * `node_modules`, so the checkout needs its own installed tree.\n * - a `postinstall` hook that invokes an install at the consumer root recurses.\n */\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport {createHash} from 'node:crypto';\n\nexport function isSymlink(p: string): boolean {\n try {\n return fs.lstatSync(p).isSymbolicLink();\n } catch {\n return false;\n }\n}\n\nexport function isGitCheckout(p: string): boolean {\n return fs.existsSync(path.join(p, '.git'));\n}\n\n/** Is `child` at or below `parent`? Both must be absolute, real paths. */\nexport function isInside(child: string, parent: string): boolean {\n const rel = path.relative(parent, child);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n}\n\nexport function readJson(file: string): any {\n try {\n return JSON.parse(fs.readFileSync(file, 'utf8'));\n } catch {\n return null;\n }\n}\n\n/** Where `node_modules/<name>` points, resolved, or null when it is not a link. */\nexport function linkTarget(appRoot: string, name: string): string | null {\n const link = path.join(appRoot, 'node_modules', name);\n if (!isSymlink(link)) return null;\n try {\n return fs.realpathSync(link);\n } catch {\n // A dangling link is still linked intent; its target is simply gone.\n return path.resolve(path.dirname(link), fs.readlinkSync(link));\n }\n}\n\n/**\n * Write `node_modules/<name> -> <checkout>` and nothing else.\n *\n * The target is stored RELATIVE so the tree survives being moved or bind-mounted\n * into a container at a different path.\n */\nexport function writeLink(\n appRoot: string,\n name: string,\n checkout: string,\n): string {\n const link = path.join(appRoot, 'node_modules', name);\n fs.mkdirSync(path.dirname(link), {recursive: true});\n if (isSymlink(link)) fs.unlinkSync(link);\n else if (fs.existsSync(link)) fs.rmSync(link, {recursive: true, force: true});\n fs.symlinkSync(path.relative(path.dirname(link), checkout), link, 'dir');\n return link;\n}\n\n/** Every symlink directly under node_modules whose target leaves node_modules. */\nexport function scanLinks(appRoot: string): {name: string; target: string}[] {\n const root = path.join(appRoot, 'node_modules');\n const out: {name: string; target: string}[] = [];\n let top;\n try {\n top = fs.readdirSync(root, {withFileTypes: true});\n } catch {\n return out;\n }\n const collect = (name, full) => {\n if (!isSymlink(full)) return;\n let target;\n try {\n target = fs.realpathSync(full);\n } catch {\n return;\n }\n if (isInside(target, root)) return; // an ordinary hoisting link, not a checkout\n out.push({name, target});\n };\n for (const item of top) {\n if (item.name === '.bin' || item.name.startsWith('.')) continue;\n const full = path.join(root, item.name);\n if (item.name.startsWith('@')) {\n let inner;\n try {\n inner = fs.readdirSync(full, {withFileTypes: true});\n } catch {\n continue;\n }\n for (const sub of inner)\n collect(`${item.name}/${sub.name}`, path.join(full, sub.name));\n } else {\n collect(item.name, full);\n }\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// the post-condition\n// ---------------------------------------------------------------------------\n\n/** The files no localize command may ever touch. */\nexport const MANIFEST_FILES = ['package.json', 'package-lock.json'];\n\n/**\n * Hash the consumer's manifests.\n *\n * Content hashes, NOT `git status`. `git status` compares against HEAD, so in\n * any tree with an uncommitted `package.json` it reports a modification the\n * command did not make -- an earlier version of this tool shipped that bug and\n * reported correct runs as failures. Hashes also work in a tree with no git,\n * and catch an edit that HEAD happens to agree with.\n *\n * `latin1` is a byte-exact round-trip for arbitrary bytes; `utf8` is not.\n */\nexport function snapshotManifests(\n appRoot: string,\n): Record<string, string | null> {\n const snap: Record<string, string | null> = {};\n for (const file of MANIFEST_FILES) {\n try {\n const bytes = fs.readFileSync(path.join(appRoot, file), 'latin1');\n snap[file] = createHash('sha256').update(bytes, 'latin1').digest('hex');\n } catch {\n snap[file] = null;\n }\n }\n return snap;\n}\n\n/** @returns {string[]} a human description of each manifest that changed. */\nexport function diffManifests(\n before: Record<string, string | null>,\n after: Record<string, string | null>,\n): string[] {\n return MANIFEST_FILES.filter((f) => before[f] !== after[f]).map((f) => {\n if (before[f] === null) return `${f} (created)`;\n if (after[f] === null) return `${f} (deleted)`;\n return `${f} (modified)`;\n });\n}\n"]}
1
+ {"version":3,"file":"fsops.js","sourceRoot":"","sources":["../../../src/localize/fsops.ts"],"names":[],"mappings":"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","sourcesContent":["/** The filesystem primitives localize shares. */\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport {linkDirectory, samePath} from '../utils/platform.js';\n\nexport function isSymlink(p: string): boolean {\n try {\n return fs.lstatSync(p).isSymbolicLink();\n } catch {\n return false;\n }\n}\n\n/** Something is at `p`: a file, a directory, or a link, dangling or not. */\nexport function existsOrLink(p: string): boolean {\n return fs.existsSync(p) || isSymlink(p);\n}\n\nfunction isGitCheckout(p: string): boolean {\n return fs.existsSync(path.join(p, '.git'));\n}\n\n/** Is `child` at or below `parent`? Both must be absolute, real paths. */\nexport function isInside(child: string, parent: string): boolean {\n const rel = path.relative(parent, child);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n}\n\nexport function readJson(file: string): any {\n try {\n return JSON.parse(fs.readFileSync(file, 'utf8'));\n } catch {\n return null;\n }\n}\n\n/** Where the link at `p` points, absolute, whether or not the target exists. */\nfunction linkTargetOf(p: string): string {\n try {\n return fs.realpathSync(p);\n } catch {\n return path.resolve(path.dirname(p), fs.readlinkSync(p));\n }\n}\n\n/**\n * Move a checkout directory, or the link standing for one (a package inside a\n * cloned monorepo, see localize.ts). A link is written again from its new\n * place (relative, or a junction on Windows), so it still reaches the same\n * target.\n */\nexport function moveEntry(from: string, to: string): void {\n fs.mkdirSync(path.dirname(to), {recursive: true});\n if (isSymlink(from)) {\n const target = linkTargetOf(from);\n linkDirectory(target, to);\n fs.unlinkSync(from);\n return;\n }\n fs.renameSync(from, to);\n}\n\n/**\n * The root of the git repository a checkout belongs to -- the checkout itself,\n * or the cloned monorepo a linked package sits in -- or null when there is\n * none below the app root (the app's own repository does not count).\n */\nexport function checkoutRepoRoot(\n checkout: string,\n appRoot: string,\n): string | null {\n let app: string;\n let dir: string;\n try {\n app = fs.realpathSync(appRoot);\n dir = fs.realpathSync(checkout);\n } catch {\n return null;\n }\n while (!samePath(dir, app) && isInside(dir, app)) {\n if (isGitCheckout(dir)) return dir;\n dir = path.dirname(dir);\n }\n return null;\n}\n"]}
@@ -0,0 +1,33 @@
1
+ export declare const HOOK_MARKER = "# managed by linked localize";
2
+ export declare const GIT_HOOKS: readonly ["post-merge", "post-checkout", "pre-commit"];
3
+ export type GitHook = (typeof GIT_HOOKS)[number];
4
+ /** A hook localize could not write because `core.hooksPath` is set: add `line` to `file`. */
5
+ export interface ManualHook {
6
+ hook: GitHook;
7
+ /** The hook file to edit, as the developer would name it (`.husky/post-merge`). */
8
+ file: string;
9
+ line: string;
10
+ }
11
+ export interface HookInstall {
12
+ written: string[];
13
+ /** Hook files that are not ours, left alone. */
14
+ skipped: string[];
15
+ /** `core.hooksPath` is set: nothing was written; one line per hook to add by hand. */
16
+ manual: ManualHook[];
17
+ /** The value of `core.hooksPath`, when set. */
18
+ hooksPath: string | null;
19
+ /** A linked worktree: the hooks live in the main repository and run in every worktree. */
20
+ sharedWithWorktrees: boolean;
21
+ }
22
+ /** The hook file for `hook`: marker, then a hand-over that resolves the cli at run time. */
23
+ export declare function hookScript(hook: GitHook, prefix?: string): string;
24
+ /** The same hand-over as one line, in a subshell, for a hook file another tool owns. */
25
+ export declare function hookLine(hook: GitHook, prefix?: string): string;
26
+ /**
27
+ * Write the three hooks into the repository's hooks directory, as git
28
+ * resolves it (`--git-path hooks`; the common directory for a linked
29
+ * worktree). With `core.hooksPath` set, write nothing and return the lines to
30
+ * add. Not a git work tree: nothing at all.
31
+ */
32
+ export declare function installGitHooks(appRoot: string): HookInstall;
33
+ //# sourceMappingURL=git-hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-hooks.d.ts","sourceRoot":"","sources":["../../../src/localize/git-hooks.ts"],"names":[],"mappings":"AA2CA,eAAO,MAAM,WAAW,iCAAiC,CAAC;AAC1D,eAAO,MAAM,SAAS,wDAAyD,CAAC;AAChF,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjD,6FAA6F;AAC7F,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,gDAAgD;IAChD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,sFAAsF;IACtF,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,0FAA0F;IAC1F,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAyBD,4FAA4F;AAC5F,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,SAAK,GAAG,MAAM,CAQ7D;AAED,wFAAwF;AACxF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,SAAK,GAAG,MAAM,CAE3D;AAcD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,CAuD5D"}
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The git hooks a clone that localizes gets, written into that clone's own
3
+ * hooks directory -- nothing is committed, and a clone that never localizes
4
+ * never runs them:
5
+ *
6
+ * - `post-merge`, `post-checkout`: `npm install` when the dependency
7
+ * manifests changed, which links the checkouts again on top of the new
8
+ * lockfile;
9
+ * - `pre-commit`: refuse a `package.json` or `package-lock.json` that lists a
10
+ * checkout.
11
+ *
12
+ * Each is a few lines of `sh` (from `hooks/hook-shim.cjs`) that hand over to
13
+ * `hooks/git-hook.cjs` in the installed cli. The app may sit below the git
14
+ * top level, so the hook records the app's path inside the repository (`git rev-parse --show-prefix`) and
15
+ * `cd`s there from the top level when it runs: moving the clone or
16
+ * reinstalling the cli needs no rewrite, and a work tree without the cli (not
17
+ * installed yet) skips the hook rather than blocking git. git starts a hook at
18
+ * the top level and may export `GIT_DIR` and `GIT_INDEX_FILE` relative to it;
19
+ * the hook makes them absolute, and pins `GIT_WORK_TREE` to the top level,
20
+ * before it leaves.
21
+ *
22
+ * Where it writes nothing:
23
+ *
24
+ * - `core.hooksPath` is set (husky, a tracked hooks directory): that directory
25
+ * belongs to another tool or to the repository, so localize returns the
26
+ * one line to add to each hook there instead;
27
+ * - outside a git work tree.
28
+ *
29
+ * In a linked worktree (`git worktree add`) the hooks directory is the main
30
+ * repository's, so every worktree runs them; `git-hook.cjs` does nothing in a
31
+ * work tree without checkouts, and localize says so once.
32
+ *
33
+ * A hook we wrote carries `HOOK_MARKER` and is rewritten on every run; any
34
+ * other file in its place is the developer's and is left alone and reported.
35
+ */
36
+ import { spawnSync } from 'node:child_process';
37
+ import fs from 'node:fs';
38
+ import path from 'node:path';
39
+ import { samePath } from '../utils/platform.js';
40
+ import { requireHook } from './hook-module.js';
41
+ // The same text as in hooks/hook-shim.cjs; loading that module checks it.
42
+ export const HOOK_MARKER = '# managed by linked localize';
43
+ export const GIT_HOOKS = ['post-merge', 'post-checkout', 'pre-commit'];
44
+ let shim;
45
+ /**
46
+ * `hooks/hook-shim.cjs`: the hand-over the hook files run, shared with
47
+ * lock-at-rest, which prints it for a hook file that is not ours.
48
+ */
49
+ function shimModule() {
50
+ shim !== null && shim !== void 0 ? shim : (shim = requireHook('hook-shim.cjs', (mod) => mod.HOOK_MARKER === HOOK_MARKER &&
51
+ typeof mod.hookCommands === 'function' &&
52
+ typeof mod.hookLine === 'function'));
53
+ return shim;
54
+ }
55
+ /** The hook file for `hook`: marker, then a hand-over that resolves the cli at run time. */
56
+ export function hookScript(hook, prefix = '') {
57
+ return [
58
+ '#!/bin/sh',
59
+ HOOK_MARKER,
60
+ '# Runs @_linked/cli/hooks/git-hook.cjs from the app directory; does nothing without it.',
61
+ ...shimModule().hookCommands(hook, prefix),
62
+ '',
63
+ ].join('\n');
64
+ }
65
+ /** The same hand-over as one line, in a subshell, for a hook file another tool owns. */
66
+ export function hookLine(hook, prefix = '') {
67
+ return shimModule().hookLine(hook, prefix);
68
+ }
69
+ /** husky (v9) points `core.hooksPath` at `.husky/_` and runs the scripts in `.husky/`. */
70
+ function manualHookFile(hooksPath, hook) {
71
+ const p = hooksPath.replace(/\/+$/, '');
72
+ const dir = /(^|\/)\.husky\/_$/.test(p) ? p.slice(0, -2) : p;
73
+ return `${dir}/${hook}`;
74
+ }
75
+ function git(appRoot, ...args) {
76
+ var _a;
77
+ const r = spawnSync('git', args, { cwd: appRoot, encoding: 'utf8' });
78
+ return r.status === 0 ? ((_a = r.stdout) !== null && _a !== void 0 ? _a : '').trim() : null;
79
+ }
80
+ /**
81
+ * Write the three hooks into the repository's hooks directory, as git
82
+ * resolves it (`--git-path hooks`; the common directory for a linked
83
+ * worktree). With `core.hooksPath` set, write nothing and return the lines to
84
+ * add. Not a git work tree: nothing at all.
85
+ */
86
+ export function installGitHooks(appRoot) {
87
+ const result = {
88
+ written: [],
89
+ skipped: [],
90
+ manual: [],
91
+ hooksPath: null,
92
+ sharedWithWorktrees: false,
93
+ };
94
+ const hooksDir = git(appRoot, 'rev-parse', '--git-path', 'hooks');
95
+ const prefix = git(appRoot, 'rev-parse', '--show-prefix');
96
+ if (!hooksDir || prefix === null)
97
+ return result;
98
+ const gitDir = git(appRoot, 'rev-parse', '--git-dir');
99
+ const commonDir = git(appRoot, 'rev-parse', '--git-common-dir');
100
+ result.sharedWithWorktrees =
101
+ gitDir !== null &&
102
+ commonDir !== null &&
103
+ !samePath(realpath(path.resolve(appRoot, gitDir)), realpath(path.resolve(appRoot, commonDir)));
104
+ const hooksPath = git(appRoot, 'config', 'core.hooksPath');
105
+ if (hooksPath) {
106
+ result.hooksPath = hooksPath;
107
+ result.manual = GIT_HOOKS.map((hook) => ({
108
+ hook,
109
+ file: manualHookFile(hooksPath, hook),
110
+ line: hookLine(hook, prefix),
111
+ }));
112
+ return result;
113
+ }
114
+ const dir = path.resolve(appRoot, hooksDir);
115
+ fs.mkdirSync(dir, { recursive: true });
116
+ for (const hook of GIT_HOOKS) {
117
+ const file = path.join(dir, hook);
118
+ let existing = null;
119
+ try {
120
+ existing = fs.readFileSync(file, 'utf8');
121
+ }
122
+ catch (_a) {
123
+ /* not there yet */
124
+ }
125
+ if (existing !== null && !existing.includes(HOOK_MARKER)) {
126
+ result.skipped.push(file);
127
+ continue;
128
+ }
129
+ // LF even on Windows: Git for Windows runs hooks with its own sh.
130
+ fs.writeFileSync(file, hookScript(hook, prefix));
131
+ // writeFileSync's `mode` applies only to a new file. (A no-op on Windows,
132
+ // where git runs a hook file whatever its mode.)
133
+ fs.chmodSync(file, 0o755);
134
+ result.written.push(file);
135
+ }
136
+ return result;
137
+ }
138
+ function realpath(p) {
139
+ try {
140
+ return fs.realpathSync(p);
141
+ }
142
+ catch (_a) {
143
+ return p;
144
+ }
145
+ }
146
+ //# sourceMappingURL=git-hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-hooks.js","sourceRoot":"","sources":["../../../src/localize/git-hooks.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The git hooks a clone that localizes gets, written into that clone's own\n * hooks directory -- nothing is committed, and a clone that never localizes\n * never runs them:\n *\n * - `post-merge`, `post-checkout`: `npm install` when the dependency\n * manifests changed, which links the checkouts again on top of the new\n * lockfile;\n * - `pre-commit`: refuse a `package.json` or `package-lock.json` that lists a\n * checkout.\n *\n * Each is a few lines of `sh` (from `hooks/hook-shim.cjs`) that hand over to\n * `hooks/git-hook.cjs` in the installed cli. The app may sit below the git\n * top level, so the hook records the app's path inside the repository (`git rev-parse --show-prefix`) and\n * `cd`s there from the top level when it runs: moving the clone or\n * reinstalling the cli needs no rewrite, and a work tree without the cli (not\n * installed yet) skips the hook rather than blocking git. git starts a hook at\n * the top level and may export `GIT_DIR` and `GIT_INDEX_FILE` relative to it;\n * the hook makes them absolute, and pins `GIT_WORK_TREE` to the top level,\n * before it leaves.\n *\n * Where it writes nothing:\n *\n * - `core.hooksPath` is set (husky, a tracked hooks directory): that directory\n * belongs to another tool or to the repository, so localize returns the\n * one line to add to each hook there instead;\n * - outside a git work tree.\n *\n * In a linked worktree (`git worktree add`) the hooks directory is the main\n * repository's, so every worktree runs them; `git-hook.cjs` does nothing in a\n * work tree without checkouts, and localize says so once.\n *\n * A hook we wrote carries `HOOK_MARKER` and is rewritten on every run; any\n * other file in its place is the developer's and is left alone and reported.\n */\nimport {spawnSync} from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport {samePath} from '../utils/platform.js';\nimport {requireHook} from './hook-module.js';\n\n// The same text as in hooks/hook-shim.cjs; loading that module checks it.\nexport const HOOK_MARKER = '# managed by linked localize';\nexport const GIT_HOOKS = ['post-merge', 'post-checkout', 'pre-commit'] as const;\nexport type GitHook = (typeof GIT_HOOKS)[number];\n\n/** A hook localize could not write because `core.hooksPath` is set: add `line` to `file`. */\nexport interface ManualHook {\n hook: GitHook;\n /** The hook file to edit, as the developer would name it (`.husky/post-merge`). */\n file: string;\n line: string;\n}\n\nexport interface HookInstall {\n written: string[];\n /** Hook files that are not ours, left alone. */\n skipped: string[];\n /** `core.hooksPath` is set: nothing was written; one line per hook to add by hand. */\n manual: ManualHook[];\n /** The value of `core.hooksPath`, when set. */\n hooksPath: string | null;\n /** A linked worktree: the hooks live in the main repository and run in every worktree. */\n sharedWithWorktrees: boolean;\n}\n\ninterface HookShimModule {\n HOOK_MARKER: string;\n hookCommands: (hook: GitHook, prefix: string) => string[];\n hookLine: (hook: GitHook, prefix: string) => string;\n}\n\nlet shim: HookShimModule | undefined;\n\n/**\n * `hooks/hook-shim.cjs`: the hand-over the hook files run, shared with\n * lock-at-rest, which prints it for a hook file that is not ours.\n */\nfunction shimModule(): HookShimModule {\n shim ??= requireHook<HookShimModule>(\n 'hook-shim.cjs',\n (mod) =>\n mod.HOOK_MARKER === HOOK_MARKER &&\n typeof mod.hookCommands === 'function' &&\n typeof mod.hookLine === 'function',\n );\n return shim;\n}\n\n/** The hook file for `hook`: marker, then a hand-over that resolves the cli at run time. */\nexport function hookScript(hook: GitHook, prefix = ''): string {\n return [\n '#!/bin/sh',\n HOOK_MARKER,\n '# Runs @_linked/cli/hooks/git-hook.cjs from the app directory; does nothing without it.',\n ...shimModule().hookCommands(hook, prefix),\n '',\n ].join('\\n');\n}\n\n/** The same hand-over as one line, in a subshell, for a hook file another tool owns. */\nexport function hookLine(hook: GitHook, prefix = ''): string {\n return shimModule().hookLine(hook, prefix);\n}\n\n/** husky (v9) points `core.hooksPath` at `.husky/_` and runs the scripts in `.husky/`. */\nfunction manualHookFile(hooksPath: string, hook: GitHook): string {\n const p = hooksPath.replace(/\\/+$/, '');\n const dir = /(^|\\/)\\.husky\\/_$/.test(p) ? p.slice(0, -2) : p;\n return `${dir}/${hook}`;\n}\n\nfunction git(appRoot: string, ...args: string[]): string | null {\n const r = spawnSync('git', args, {cwd: appRoot, encoding: 'utf8'});\n return r.status === 0 ? (r.stdout ?? '').trim() : null;\n}\n\n/**\n * Write the three hooks into the repository's hooks directory, as git\n * resolves it (`--git-path hooks`; the common directory for a linked\n * worktree). With `core.hooksPath` set, write nothing and return the lines to\n * add. Not a git work tree: nothing at all.\n */\nexport function installGitHooks(appRoot: string): HookInstall {\n const result: HookInstall = {\n written: [],\n skipped: [],\n manual: [],\n hooksPath: null,\n sharedWithWorktrees: false,\n };\n const hooksDir = git(appRoot, 'rev-parse', '--git-path', 'hooks');\n const prefix = git(appRoot, 'rev-parse', '--show-prefix');\n if (!hooksDir || prefix === null) return result;\n\n const gitDir = git(appRoot, 'rev-parse', '--git-dir');\n const commonDir = git(appRoot, 'rev-parse', '--git-common-dir');\n result.sharedWithWorktrees =\n gitDir !== null &&\n commonDir !== null &&\n !samePath(\n realpath(path.resolve(appRoot, gitDir)),\n realpath(path.resolve(appRoot, commonDir)),\n );\n\n const hooksPath = git(appRoot, 'config', 'core.hooksPath');\n if (hooksPath) {\n result.hooksPath = hooksPath;\n result.manual = GIT_HOOKS.map((hook) => ({\n hook,\n file: manualHookFile(hooksPath, hook),\n line: hookLine(hook, prefix),\n }));\n return result;\n }\n\n const dir = path.resolve(appRoot, hooksDir);\n fs.mkdirSync(dir, {recursive: true});\n for (const hook of GIT_HOOKS) {\n const file = path.join(dir, hook);\n let existing: string | null = null;\n try {\n existing = fs.readFileSync(file, 'utf8');\n } catch {\n /* not there yet */\n }\n if (existing !== null && !existing.includes(HOOK_MARKER)) {\n result.skipped.push(file);\n continue;\n }\n // LF even on Windows: Git for Windows runs hooks with its own sh.\n fs.writeFileSync(file, hookScript(hook, prefix));\n // writeFileSync's `mode` applies only to a new file. (A no-op on Windows,\n // where git runs a hook file whatever its mode.)\n fs.chmodSync(file, 0o755);\n result.written.push(file);\n }\n return result;\n}\n\nfunction realpath(p: string): string {\n try {\n return fs.realpathSync(p);\n } catch {\n return p;\n }\n}\n"]}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `hooks/<file>`, required. `isValid` checks the exports the caller relies on,
3
+ * so a mismatched file fails here, by name, rather than at the first call.
4
+ */
5
+ export declare function requireHook<T>(file: string, isValid: (mod: T) => boolean): T;
6
+ //# sourceMappingURL=hook-module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-module.d.ts","sourceRoot":"","sources":["../../../src/localize/hook-module.ts"],"names":[],"mappings":"AA4BA;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,GAAG,CAAC,CAM5E"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Loads a module from this package's `hooks/` -- plain CommonJS the npm and
3
+ * git hooks run without `lib/` -- so the commands apply the same rules as the
4
+ * hooks rather than a second copy of them.
5
+ */
6
+ import fs from 'node:fs';
7
+ import { createRequire } from 'node:module';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ /**
11
+ * `hooks/<file>` in this package: two levels up from `src/localize/`, three
12
+ * from `lib/esm/localize/`, so it is looked for upwards.
13
+ */
14
+ function hookFile(file) {
15
+ let dir = path.dirname(fileURLToPath(import.meta.url));
16
+ for (;;) {
17
+ const candidate = path.join(dir, 'hooks', file);
18
+ if (fs.existsSync(candidate))
19
+ return candidate;
20
+ const parent = path.dirname(dir);
21
+ if (parent === dir)
22
+ throw new Error(`[localize] hooks/${file} not found above ${fileURLToPath(import.meta.url)}`);
23
+ dir = parent;
24
+ }
25
+ }
26
+ /**
27
+ * `hooks/<file>`, required. `isValid` checks the exports the caller relies on,
28
+ * so a mismatched file fails here, by name, rather than at the first call.
29
+ */
30
+ export function requireHook(file, isValid) {
31
+ const found = hookFile(file);
32
+ const mod = createRequire(import.meta.url)(found);
33
+ if (!mod || !isValid(mod))
34
+ throw new Error(`[localize] ${found} does not export what localize needs`);
35
+ return mod;
36
+ }
37
+ //# sourceMappingURL=hook-module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-module.js","sourceRoot":"","sources":["../../../src/localize/hook-module.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAC,aAAa,EAAC,MAAM,aAAa,CAAC;AAC1C,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAC,aAAa,EAAC,MAAM,UAAU,CAAC;AAEvC;;;GAGG;AACH,SAAS,QAAQ,CAAC,IAAY;IAC5B,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACvD,SAAS,CAAC;QACR,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;QAChD,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAC;QAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,MAAM,KAAK,GAAG;YAChB,MAAM,IAAI,KAAK,CACb,oBAAoB,IAAI,oBAAoB,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAC7E,CAAC;QACJ,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAI,IAAY,EAAE,OAA4B;IACvE,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC7B,MAAM,GAAG,GAAM,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IACrD,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,cAAc,KAAK,sCAAsC,CAAC,CAAC;IAC7E,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["/**\n * Loads a module from this package's `hooks/` -- plain CommonJS the npm and\n * git hooks run without `lib/` -- so the commands apply the same rules as the\n * hooks rather than a second copy of them.\n */\nimport fs from 'node:fs';\nimport {createRequire} from 'node:module';\nimport path from 'node:path';\nimport {fileURLToPath} from 'node:url';\n\n/**\n * `hooks/<file>` in this package: two levels up from `src/localize/`, three\n * from `lib/esm/localize/`, so it is looked for upwards.\n */\nfunction hookFile(file: string): string {\n let dir = path.dirname(fileURLToPath(import.meta.url));\n for (;;) {\n const candidate = path.join(dir, 'hooks', file);\n if (fs.existsSync(candidate)) return candidate;\n const parent = path.dirname(dir);\n if (parent === dir)\n throw new Error(\n `[localize] hooks/${file} not found above ${fileURLToPath(import.meta.url)}`,\n );\n dir = parent;\n }\n}\n\n/**\n * `hooks/<file>`, required. `isValid` checks the exports the caller relies on,\n * so a mismatched file fails here, by name, rather than at the first call.\n */\nexport function requireHook<T>(file: string, isValid: (mod: T) => boolean): T {\n const found = hookFile(file);\n const mod: T = createRequire(import.meta.url)(found);\n if (!mod || !isValid(mod))\n throw new Error(`[localize] ${found} does not export what localize needs`);\n return mod;\n}\n"]}
@@ -3,27 +3,26 @@
3
3
  * `commands/localize.ts`, the commander adapter) and `create-package`'s adopt
4
4
  * reach it through here.
5
5
  *
6
- * Every function takes `deps` -- `{appRoot, run, log, warn, error}` -- so a
6
+ * Every command takes `deps` -- `{appRoot, run, log, warn, error}` -- so a
7
7
  * caller can point it at another root or capture its output. `defaultDeps()`
8
8
  * builds the normal one.
9
9
  */
10
- export { localize } from './localize.js';
11
- export { adopt } from './adopt.js';
10
+ export { localize, DEFAULT_BUILD_COMMAND } from './localize.js';
11
+ export type { LocalizeOptions } from './localize.js';
12
+ export { adopt, localizeParked } from './adopt.js';
12
13
  export { delocalize } from './delocalize.js';
13
14
  export { list, collect } from './list.js';
14
- export { relink } from './relink.js';
15
- export { pruneProvided, satisfies, shouldPrune } from './prune.js';
16
- export type { PruneOptions, PruneScope } from './prune.js';
17
- export { providedPackages } from './provided.js';
18
- export type { Provided, ProvidedSkip } from './provided.js';
19
- export { ensure, reinstall, checkOneCopy } from './ensure.js';
20
- export type { LocalizeOptions } from './localize.js';
21
15
  export type { ListRow } from './list.js';
22
- export type { Resolved } from './resolve.js';
23
- export type { Manifest, ManifestEntry } from './manifest.js';
24
- export type { Deps, Run, RunOptions, RunResult } from './run.js';
16
+ export { driftProblems, describeDrift, satisfies } from './drift.js';
17
+ export type { Drift } from './drift.js';
18
+ export { LOCAL_DIR, PARKED_DIR, localMembers, parkedMembers } from './members.js';
19
+ export type { Member } from './members.js';
20
+ export { ensureAppSetup } from './app-setup.js';
21
+ export type { SetupChange } from './app-setup.js';
22
+ export { findAppRoot } from './app-root.js';
25
23
  export { resolvePackage, normalizeGitUrl, checkoutNameFor } from './resolve.js';
26
- export { readManifest, writeManifest, manifestPath, MANIFEST_FILENAME, SCHEMA_VERSION, DEFAULT_DIR, } from './manifest.js';
24
+ export type { Resolved } from './resolve.js';
27
25
  export { defaultDeps, makeRun } from './run.js';
28
- export { LocalizeError, EXIT_BAD_FILE, EXIT_NOT_FOUND, EXIT_WARNED, EXIT_REFUSED, EXIT_INSTALL_FAILED, EXIT_MANIFEST_DIRTY, } from './errors.js';
26
+ export type { Deps, Run, RunOptions, RunResult } from './run.js';
27
+ export { LocalizeError, EXIT_DRIFT, EXIT_USAGE, EXIT_NOT_FOUND, EXIT_WARNED, EXIT_REFUSED, EXIT_INSTALL_FAILED, } from './errors.js';
29
28
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/localize/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAC,QAAQ,EAAC,MAAM,eAAe,CAAC;AACvC,OAAO,EAAC,KAAK,EAAC,MAAM,YAAY,CAAC;AACjC,OAAO,EAAC,UAAU,EAAC,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAC,IAAI,EAAE,OAAO,EAAC,MAAM,WAAW,CAAC;AACxC,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AACnC,OAAO,EAAC,aAAa,EAAE,SAAS,EAAE,WAAW,EAAC,MAAM,YAAY,CAAC;AACjE,YAAY,EAAC,YAAY,EAAE,UAAU,EAAC,MAAM,YAAY,CAAC;AACzD,OAAO,EAAC,gBAAgB,EAAC,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAC,QAAQ,EAAE,YAAY,EAAC,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAC,MAAM,EAAE,SAAS,EAAE,YAAY,EAAC,MAAM,aAAa,CAAC;AAC5D,YAAY,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AACnD,YAAY,EAAC,OAAO,EAAC,MAAM,WAAW,CAAC;AACvC,YAAY,EAAC,QAAQ,EAAC,MAAM,cAAc,CAAC;AAC3C,YAAY,EAAC,QAAQ,EAAE,aAAa,EAAC,MAAM,eAAe,CAAC;AAC3D,YAAY,EAAC,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,SAAS,EAAC,MAAM,UAAU,CAAC;AAC/D,OAAO,EAAC,cAAc,EAAE,eAAe,EAAE,eAAe,EAAC,MAAM,cAAc,CAAC;AAC9E,OAAO,EACL,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,WAAW,GACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EAAC,WAAW,EAAE,OAAO,EAAC,MAAM,UAAU,CAAC;AAC9C,OAAO,EACL,aAAa,EACb,aAAa,EACb,cAAc,EACd,WAAW,EACX,YAAY,EACZ,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/localize/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAC,QAAQ,EAAE,qBAAqB,EAAC,MAAM,eAAe,CAAC;AAC9D,YAAY,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AACnD,OAAO,EAAC,KAAK,EAAE,cAAc,EAAC,MAAM,YAAY,CAAC;AACjD,OAAO,EAAC,UAAU,EAAC,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAC,IAAI,EAAE,OAAO,EAAC,MAAM,WAAW,CAAC;AACxC,YAAY,EAAC,OAAO,EAAC,MAAM,WAAW,CAAC;AACvC,OAAO,EAAC,aAAa,EAAE,aAAa,EAAE,SAAS,EAAC,MAAM,YAAY,CAAC;AACnE,YAAY,EAAC,KAAK,EAAC,MAAM,YAAY,CAAC;AACtC,OAAO,EAAC,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAC,MAAM,cAAc,CAAC;AAChF,YAAY,EAAC,MAAM,EAAC,MAAM,cAAc,CAAC;AACzC,OAAO,EAAC,cAAc,EAAC,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAC,cAAc,EAAE,eAAe,EAAE,eAAe,EAAC,MAAM,cAAc,CAAC;AAC9E,YAAY,EAAC,QAAQ,EAAC,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAC,WAAW,EAAE,OAAO,EAAC,MAAM,UAAU,CAAC;AAC9C,YAAY,EAAC,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,SAAS,EAAC,MAAM,UAAU,CAAC;AAC/D,OAAO,EACL,aAAa,EACb,UAAU,EACV,UAAU,EACV,cAAc,EACd,WAAW,EACX,YAAY,EACZ,mBAAmB,GACpB,MAAM,aAAa,CAAC"}