@_linked/cli 1.41.0 → 1.42.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 (81) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +21 -54
  3. package/lib/esm/cli-methods.js +2 -2
  4. package/lib/esm/cli-methods.js.map +1 -1
  5. package/lib/esm/cli.js +13 -1
  6. package/lib/esm/cli.js.map +1 -1
  7. package/lib/esm/commands/localize.d.ts +6 -15
  8. package/lib/esm/commands/localize.d.ts.map +1 -1
  9. package/lib/esm/commands/localize.js +26 -35
  10. package/lib/esm/commands/localize.js.map +1 -1
  11. package/lib/esm/installed-packages.d.ts +12 -12
  12. package/lib/esm/installed-packages.d.ts.map +1 -1
  13. package/lib/esm/installed-packages.js +37 -27
  14. package/lib/esm/installed-packages.js.map +1 -1
  15. package/lib/esm/interfaces.d.ts +1 -1
  16. package/lib/esm/interfaces.js.map +1 -1
  17. package/lib/esm/lifecycle.d.ts +4 -3
  18. package/lib/esm/lifecycle.d.ts.map +1 -1
  19. package/lib/esm/lifecycle.js +6 -14
  20. package/lib/esm/lifecycle.js.map +1 -1
  21. package/lib/esm/localize/adopt.d.ts +10 -0
  22. package/lib/esm/localize/adopt.d.ts.map +1 -0
  23. package/lib/esm/localize/adopt.js +76 -0
  24. package/lib/esm/localize/adopt.js.map +1 -0
  25. package/lib/esm/localize/delocalize.d.ts +6 -0
  26. package/lib/esm/localize/delocalize.d.ts.map +1 -0
  27. package/lib/esm/localize/delocalize.js +138 -0
  28. package/lib/esm/localize/delocalize.js.map +1 -0
  29. package/lib/esm/localize/ensure.d.ts +26 -0
  30. package/lib/esm/localize/ensure.d.ts.map +1 -0
  31. package/lib/esm/localize/ensure.js +152 -0
  32. package/lib/esm/localize/ensure.js.map +1 -0
  33. package/lib/esm/localize/errors.d.ts +23 -0
  34. package/lib/esm/localize/errors.d.ts.map +1 -0
  35. package/lib/esm/localize/errors.js +26 -0
  36. package/lib/esm/localize/errors.js.map +1 -0
  37. package/lib/esm/localize/fsops.d.ts +36 -0
  38. package/lib/esm/localize/fsops.d.ts.map +1 -0
  39. package/lib/esm/localize/fsops.js +155 -0
  40. package/lib/esm/localize/fsops.js.map +1 -0
  41. package/lib/esm/localize/index.d.ts +29 -0
  42. package/lib/esm/localize/index.d.ts.map +1 -0
  43. package/lib/esm/localize/index.js +22 -0
  44. package/lib/esm/localize/index.js.map +1 -0
  45. package/lib/esm/localize/list.d.ts +18 -0
  46. package/lib/esm/localize/list.d.ts.map +1 -0
  47. package/lib/esm/localize/list.js +174 -0
  48. package/lib/esm/localize/list.js.map +1 -0
  49. package/lib/esm/localize/localize.d.ts +84 -0
  50. package/lib/esm/localize/localize.d.ts.map +1 -0
  51. package/lib/esm/localize/localize.js +354 -0
  52. package/lib/esm/localize/localize.js.map +1 -0
  53. package/lib/esm/localize/manifest.d.ts +54 -0
  54. package/lib/esm/localize/manifest.d.ts.map +1 -0
  55. package/lib/esm/localize/manifest.js +133 -0
  56. package/lib/esm/localize/manifest.js.map +1 -0
  57. package/lib/esm/localize/provided.d.ts +28 -0
  58. package/lib/esm/localize/provided.d.ts.map +1 -0
  59. package/lib/esm/localize/provided.js +89 -0
  60. package/lib/esm/localize/provided.js.map +1 -0
  61. package/lib/esm/localize/prune.d.ts +58 -0
  62. package/lib/esm/localize/prune.d.ts.map +1 -0
  63. package/lib/esm/localize/prune.js +390 -0
  64. package/lib/esm/localize/prune.js.map +1 -0
  65. package/lib/esm/localize/relink.d.ts +11 -0
  66. package/lib/esm/localize/relink.d.ts.map +1 -0
  67. package/lib/esm/localize/relink.js +174 -0
  68. package/lib/esm/localize/relink.js.map +1 -0
  69. package/lib/esm/localize/resolve.d.ts +47 -0
  70. package/lib/esm/localize/resolve.d.ts.map +1 -0
  71. package/lib/esm/localize/resolve.js +153 -0
  72. package/lib/esm/localize/resolve.js.map +1 -0
  73. package/lib/esm/localize/run.d.ts +35 -0
  74. package/lib/esm/localize/run.d.ts.map +1 -0
  75. package/lib/esm/localize/run.js +30 -0
  76. package/lib/esm/localize/run.js.map +1 -0
  77. package/lib/esm/localized-dedupe.d.ts +6 -0
  78. package/lib/esm/localized-dedupe.d.ts.map +1 -1
  79. package/lib/esm/localized-dedupe.js +23 -66
  80. package/lib/esm/localized-dedupe.js.map +1 -1
  81. package/package.json +1 -5
@@ -0,0 +1,174 @@
1
+ /**
2
+ * `--list` -- read the SYMLINKS, then reconcile against the manifest.
3
+ *
4
+ * The manifest records INTENT; the symlink is TRUTH. They can disagree -- most
5
+ * commonly because `npm ci` deleted `node_modules` and exited 0 -- so this
6
+ * reads the links first and reports the disagreement rather than assuming.
7
+ * The four states are distinguished deliberately: both of the silent failures
8
+ * this tool was built to end were disagreements between intent and truth.
9
+ *
10
+ * It also flags a directory in the checkout directory (`packages-local/`) that
11
+ * nothing records and nothing links: every checkout there is meant to be
12
+ * localized, and one that is not loads nobody's code while looking as if it
13
+ * did.
14
+ *
15
+ * Never writes. Exits 0, unless `--check` and something recorded is not
16
+ * actually linked.
17
+ */
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { MANIFEST_FILENAME, readManifest } from './manifest.js';
21
+ import { isInside, linkTarget, readJson, scanLinks } from './fsops.js';
22
+ import { reportError } from './localize.js';
23
+ /** @returns {{rows: object[], notLinked: number}} */
24
+ export function collect(deps) {
25
+ const manifest = readManifest(deps.appRoot, deps);
26
+ const dir = path.join(deps.appRoot, manifest.dir);
27
+ const rows = [];
28
+ for (const [name, entry] of Object.entries(manifest.entries)) {
29
+ const checkout = path.join(deps.appRoot, entry.path);
30
+ const target = linkTarget(deps.appRoot, name);
31
+ if (target && isInside(target, dir)) {
32
+ rows.push({
33
+ name,
34
+ state: 'linked',
35
+ path: entry.path,
36
+ branch: entry.branch,
37
+ note: '',
38
+ });
39
+ }
40
+ else if (!fs.existsSync(checkout)) {
41
+ // Recorded, but nothing on disk. `localize <name>` re-clones it when the
42
+ // entry has a repo or the package is published; otherwise it is restored by hand.
43
+ rows.push({
44
+ name,
45
+ state: 'checkout-missing',
46
+ path: entry.path,
47
+ branch: '—',
48
+ note: 'CHECKOUT MISSING',
49
+ });
50
+ }
51
+ else {
52
+ // Intent and checkout present, symlink gone. This is what `npm ci`
53
+ // leaves behind, and it is the state that costs hours.
54
+ rows.push({
55
+ name,
56
+ state: 'not-linked',
57
+ path: entry.path,
58
+ branch: entry.branch,
59
+ note: 'checkout present',
60
+ });
61
+ }
62
+ }
63
+ for (const m of manifest.malformed) {
64
+ rows.push({
65
+ name: m.name,
66
+ state: 'malformed',
67
+ path: '—',
68
+ branch: '—',
69
+ note: `malformed entry (${m.reason})`,
70
+ });
71
+ }
72
+ // A symlink out of node_modules that the manifest does not know about.
73
+ // Reported, never touched.
74
+ const untrackedTargets = [];
75
+ for (const { name, target } of scanLinks(deps.appRoot)) {
76
+ if (manifest.entries[name])
77
+ continue;
78
+ untrackedTargets.push(target);
79
+ rows.push({
80
+ name,
81
+ state: 'untracked',
82
+ path: path.relative(deps.appRoot, target),
83
+ branch: '—',
84
+ note: `not in ${MANIFEST_FILENAME}`,
85
+ });
86
+ }
87
+ // A directory in the checkout directory with no record (and no link, which
88
+ // the row above already reports).
89
+ const recorded = Object.values(manifest.entries).map((e) => real(path.join(deps.appRoot, e.path)));
90
+ for (const sub of directoriesIn(dir)) {
91
+ const abs = real(path.join(dir, sub));
92
+ if (recorded.some((r) => r === abs || isInside(r, abs)))
93
+ continue;
94
+ if (untrackedTargets.some((t) => t === abs || isInside(t, abs)))
95
+ continue;
96
+ const pkg = readJson(path.join(abs, 'package.json'));
97
+ rows.push({
98
+ name: typeof (pkg === null || pkg === void 0 ? void 0 : pkg.name) === 'string' ? pkg.name : '—',
99
+ state: 'unrecorded',
100
+ path: path.join(manifest.dir, sub),
101
+ branch: '—',
102
+ note: `directory not in ${MANIFEST_FILENAME}`,
103
+ });
104
+ }
105
+ const notLinked = rows.filter((r) => r.state === 'not-linked' || r.state === 'checkout-missing').length;
106
+ return { rows, notLinked };
107
+ }
108
+ export function list(opts, deps) {
109
+ let result;
110
+ try {
111
+ result = collect(deps);
112
+ }
113
+ catch (e) {
114
+ return reportError(e, deps);
115
+ }
116
+ const { rows, notLinked } = result;
117
+ if (!rows.length)
118
+ return 0; // nothing localized, nothing to say
119
+ const pad = (n, s) => s.padEnd(n);
120
+ const nameW = Math.max(...rows.map((r) => r.name.length)) + 2;
121
+ const pathW = Math.max(...rows.map((r) => r.path.length)) + 2;
122
+ for (const r of rows) {
123
+ const label = r.state === 'linked' || r.state === 'untracked'
124
+ ? 'linked'
125
+ : r.state === 'unrecorded'
126
+ ? 'UNRECORDED'
127
+ : 'NOT LINKED';
128
+ deps.log(pad(nameW, r.name) +
129
+ pad(12, label) +
130
+ pad(pathW, r.path) +
131
+ pad(11, r.branch) +
132
+ r.note);
133
+ }
134
+ const count = (s) => rows.filter((r) => r.state === s).length;
135
+ const parts = [`${count('linked')} linked`, `${notLinked} not linked`];
136
+ if (count('untracked'))
137
+ parts.push(`${count('untracked')} untracked link`);
138
+ if (count('malformed'))
139
+ parts.push(`${count('malformed')} malformed entry`);
140
+ if (count('unrecorded'))
141
+ parts.push(`${count('unrecorded')} unrecorded ${count('unrecorded') === 1 ? 'directory' : 'directories'}`);
142
+ deps.log('');
143
+ deps.log(parts.join(' · '));
144
+ return opts.check && notLinked > 0 ? 1 : 0;
145
+ }
146
+ /** The (non-hidden) directories directly in `dir`, following links; none when it is absent. */
147
+ function directoriesIn(dir) {
148
+ let names;
149
+ try {
150
+ names = fs.readdirSync(dir);
151
+ }
152
+ catch (_a) {
153
+ return [];
154
+ }
155
+ return names.filter((n) => {
156
+ if (n.startsWith('.'))
157
+ return false;
158
+ try {
159
+ return fs.statSync(path.join(dir, n)).isDirectory();
160
+ }
161
+ catch (_a) {
162
+ return false;
163
+ }
164
+ });
165
+ }
166
+ function real(p) {
167
+ try {
168
+ return fs.realpathSync(p);
169
+ }
170
+ catch (_a) {
171
+ return p;
172
+ }
173
+ }
174
+ //# sourceMappingURL=list.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.js","sourceRoot":"","sources":["../../../src/localize/list.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `--list` -- read the SYMLINKS, then reconcile against the manifest.\n *\n * The manifest records INTENT; the symlink is TRUTH. They can disagree -- most\n * commonly because `npm ci` deleted `node_modules` and exited 0 -- so this\n * reads the links first and reports the disagreement rather than assuming.\n * The four states are distinguished deliberately: both of the silent failures\n * this tool was built to end were disagreements between intent and truth.\n *\n * It also flags a directory in the checkout directory (`packages-local/`) that\n * nothing records and nothing links: every checkout there is meant to be\n * localized, and one that is not loads nobody's code while looking as if it\n * did.\n *\n * Never writes. Exits 0, unless `--check` and something recorded is not\n * actually linked.\n */\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport {MANIFEST_FILENAME, readManifest} from './manifest.js';\nimport {isInside, linkTarget, readJson, scanLinks} from './fsops.js';\nimport {reportError} from './localize.js';\nimport type {Deps} from './run.js';\n\n/** One row of `--list`: a recorded package, or a link the manifest does not know. */\nexport interface ListRow {\n name: string;\n state:\n | 'linked'\n | 'checkout-missing'\n | 'not-linked'\n | 'malformed'\n | 'untracked'\n | 'unrecorded';\n path: string;\n branch: string;\n note: string;\n}\n\n/** @returns {{rows: object[], notLinked: number}} */\nexport function collect(deps: Deps): {rows: ListRow[]; notLinked: number} {\n const manifest = readManifest(deps.appRoot, deps);\n const dir = path.join(deps.appRoot, manifest.dir);\n const rows: ListRow[] = [];\n\n for (const [name, entry] of Object.entries(manifest.entries)) {\n const checkout = path.join(deps.appRoot, entry.path);\n const target = linkTarget(deps.appRoot, name);\n\n if (target && isInside(target, dir)) {\n rows.push({\n name,\n state: 'linked',\n path: entry.path,\n branch: entry.branch,\n note: '',\n });\n } else if (!fs.existsSync(checkout)) {\n // Recorded, but nothing on disk. `localize <name>` re-clones it when the\n // entry has a repo or the package is published; otherwise it is restored by hand.\n rows.push({\n name,\n state: 'checkout-missing',\n path: entry.path,\n branch: '—',\n note: 'CHECKOUT MISSING',\n });\n } else {\n // Intent and checkout present, symlink gone. This is what `npm ci`\n // leaves behind, and it is the state that costs hours.\n rows.push({\n name,\n state: 'not-linked',\n path: entry.path,\n branch: entry.branch,\n note: 'checkout present',\n });\n }\n }\n\n for (const m of manifest.malformed) {\n rows.push({\n name: m.name,\n state: 'malformed',\n path: '—',\n branch: '—',\n note: `malformed entry (${m.reason})`,\n });\n }\n\n // A symlink out of node_modules that the manifest does not know about.\n // Reported, never touched.\n const untrackedTargets: string[] = [];\n for (const {name, target} of scanLinks(deps.appRoot)) {\n if (manifest.entries[name]) continue;\n untrackedTargets.push(target);\n rows.push({\n name,\n state: 'untracked',\n path: path.relative(deps.appRoot, target),\n branch: '—',\n note: `not in ${MANIFEST_FILENAME}`,\n });\n }\n\n // A directory in the checkout directory with no record (and no link, which\n // the row above already reports).\n const recorded = Object.values(manifest.entries).map((e) =>\n real(path.join(deps.appRoot, e.path)),\n );\n for (const sub of directoriesIn(dir)) {\n const abs = real(path.join(dir, sub));\n if (recorded.some((r) => r === abs || isInside(r, abs))) continue;\n if (untrackedTargets.some((t) => t === abs || isInside(t, abs))) continue;\n const pkg = readJson(path.join(abs, 'package.json'));\n rows.push({\n name: typeof pkg?.name === 'string' ? pkg.name : '—',\n state: 'unrecorded',\n path: path.join(manifest.dir, sub),\n branch: '—',\n note: `directory not in ${MANIFEST_FILENAME}`,\n });\n }\n\n const notLinked = rows.filter(\n (r) => r.state === 'not-linked' || r.state === 'checkout-missing',\n ).length;\n return {rows, notLinked};\n}\n\nexport function list(opts: {check?: boolean}, deps: Deps): number {\n let result;\n try {\n result = collect(deps);\n } catch (e) {\n return reportError(e, deps);\n }\n const {rows, notLinked} = result;\n\n if (!rows.length) return 0; // nothing localized, nothing to say\n\n const pad = (n, s) => s.padEnd(n);\n const nameW = Math.max(...rows.map((r) => r.name.length)) + 2;\n const pathW = Math.max(...rows.map((r) => r.path.length)) + 2;\n for (const r of rows) {\n const label =\n r.state === 'linked' || r.state === 'untracked'\n ? 'linked'\n : r.state === 'unrecorded'\n ? 'UNRECORDED'\n : 'NOT LINKED';\n deps.log(\n pad(nameW, r.name) +\n pad(12, label) +\n pad(pathW, r.path) +\n pad(11, r.branch) +\n r.note,\n );\n }\n\n const count = (s) => rows.filter((r) => r.state === s).length;\n const parts = [`${count('linked')} linked`, `${notLinked} not linked`];\n if (count('untracked')) parts.push(`${count('untracked')} untracked link`);\n if (count('malformed')) parts.push(`${count('malformed')} malformed entry`);\n if (count('unrecorded'))\n parts.push(\n `${count('unrecorded')} unrecorded ${count('unrecorded') === 1 ? 'directory' : 'directories'}`,\n );\n deps.log('');\n deps.log(parts.join(' · '));\n\n return opts.check && notLinked > 0 ? 1 : 0;\n}\n\n/** The (non-hidden) directories directly in `dir`, following links; none when it is absent. */\nfunction directoriesIn(dir: string): string[] {\n let names: string[];\n try {\n names = fs.readdirSync(dir);\n } catch {\n return [];\n }\n return names.filter((n) => {\n if (n.startsWith('.')) return false;\n try {\n return fs.statSync(path.join(dir, n)).isDirectory();\n } catch {\n return false;\n }\n });\n}\n\nfunction real(p: string): string {\n try {\n return fs.realpathSync(p);\n } catch {\n return p;\n }\n}\n"]}
@@ -0,0 +1,84 @@
1
+ import type { ManifestEntry } from './manifest.js';
2
+ import type { Deps } from './run.js';
3
+ import type { PruneOptions } from './prune.js';
4
+ /**
5
+ * @param {string[]} names full npm package names
6
+ * @param {object} opts `{force, dir, repo, subdir, build, prune}` -- `prune`
7
+ * defaults to true; see prune.ts
8
+ * @param {object} deps `{appRoot, run, log, warn, error}`
9
+ * @returns {number} the process exit code: the HIGHEST of the per-name codes,
10
+ * since each name is atomic and a failure on name 2 does not undo name 1.
11
+ */
12
+ export interface LocalizeOptions extends PruneOptions {
13
+ force?: boolean;
14
+ /** Where checkouts live, relative to the app root (default `packages-local`). */
15
+ dir?: string;
16
+ repo?: string;
17
+ subdir?: string;
18
+ /** Run in the checkout after installing; absent or empty runs nothing. */
19
+ build?: string;
20
+ }
21
+ /** What one name's work returns to the shared loop. */
22
+ export interface OneResult {
23
+ code: number;
24
+ entry?: {
25
+ name: string;
26
+ value: ManifestEntry;
27
+ };
28
+ }
29
+ export type OneName = (name: string, dir: string, recorded: ManifestEntry | undefined, opts: LocalizeOptions, deps: Deps) => OneResult;
30
+ export declare function localize(names: string[], opts: LocalizeOptions, deps: Deps): number;
31
+ /**
32
+ * The loop `localize` and `adopt` share: read the manifest once, run `one` per
33
+ * name, write the manifest after each, then assert the links and the
34
+ * consumer's untouched manifests.
35
+ *
36
+ * @param {(name: string, dir: string, recorded: object|undefined, opts: object, deps: object)
37
+ * => {code: number, entry?: {name: string, value: object}}} one
38
+ */
39
+ export declare function forEachName(names: string[], opts: LocalizeOptions, deps: Deps, one: OneName): number;
40
+ /**
41
+ * Everything after the checkout exists, shared by `localize` and `adopt`:
42
+ * install inside it, run the configured build, link it, and describe the
43
+ * manifest entry. Nothing here knows how the checkout got there.
44
+ *
45
+ * `repo` may be absent -- an adopted checkout with no remote is recorded
46
+ * without one.
47
+ */
48
+ export declare function installLinkAndRecord({ npmName, clone, relPkg, pkgDir, repo, subdir, }: {
49
+ npmName: string;
50
+ clone: string;
51
+ relPkg: string;
52
+ pkgDir: string;
53
+ repo?: string;
54
+ subdir?: string;
55
+ }, opts: LocalizeOptions, deps: Deps, code?: number): {
56
+ code: number;
57
+ entry: {
58
+ name: string;
59
+ value: ManifestEntry;
60
+ };
61
+ };
62
+ /** Refuse when `node_modules/<name>` is already a symlink pointing outside `<dir>`. */
63
+ export declare function guardForeignLink(npmName: string, dir: string, opts: {
64
+ force?: boolean;
65
+ }, deps: Deps): void;
66
+ /**
67
+ * The property the whole design rests on: this run changed neither manifest.
68
+ *
69
+ * Content hashes, not `git status`. `git status` compares against HEAD, so in
70
+ * any tree with an uncommitted `package.json` it reports a modification the
71
+ * command did not make -- an earlier tool shipped exactly that and reported
72
+ * correct runs as failures.
73
+ */
74
+ export declare function assertManifestsUntouched(before: Record<string, string | null>, deps: Deps): number;
75
+ export declare function reportError(e: unknown, deps: Pick<Deps, 'error'>): number;
76
+ /**
77
+ * The checked-out branch, or `HEAD` when detached. `symbolic-ref` first,
78
+ * because it also names the branch of a repository with no commits yet --
79
+ * `rev-parse --abbrev-ref` fails there, and `create-package` adopts a fresh
80
+ * `git init` before its first commit.
81
+ */
82
+ export declare function currentBranch(clone: string, deps: Deps): string;
83
+ export declare function declaredRange(name: string, deps: Pick<Deps, 'appRoot'>): string | undefined;
84
+ //# sourceMappingURL=localize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localize.d.ts","sourceRoot":"","sources":["../../../src/localize/localize.ts"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,UAAU,CAAC;AAInC,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,YAAY,CAAC;AAW7C;;;;;;;GAOG;AACH,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACnD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iFAAiF;IACjF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,uDAAuD;AACvD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,aAAa,CAAA;KAAC,CAAC;CAC9C;AAED,MAAM,MAAM,OAAO,GAAG,CACpB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,aAAa,GAAG,SAAS,EACnC,IAAI,EAAE,eAAe,EACrB,IAAI,EAAE,IAAI,KACP,SAAS,CAAC;AAEf,wBAAgB,QAAQ,CACtB,KAAK,EAAE,MAAM,EAAE,EACf,IAAI,EAAE,eAAe,EACrB,IAAI,EAAE,IAAI,GACT,MAAM,CAER;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EAAE,EACf,IAAI,EAAE,eAAe,EACrB,IAAI,EAAE,IAAI,EACV,GAAG,EAAE,OAAO,GACX,MAAM,CAgDR;AAuFD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,EACE,OAAO,EACP,KAAK,EACL,MAAM,EACN,MAAM,EACN,IAAI,EACJ,MAAM,GACP,EAAE;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,EACD,IAAI,EAAE,eAAe,EACrB,IAAI,EAAE,IAAI,EACV,IAAI,SAAI,GACP;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,aAAa,CAAA;KAAC,CAAA;CAAC,CAoC7D;AAiFD,uFAAuF;AACvF,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAC,EACvB,IAAI,EAAE,IAAI,GACT,IAAI,CAqBN;AA0BD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,EACrC,IAAI,EAAE,IAAI,GACT,MAAM,CAUR;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,MAAM,CAMzE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,MAAM,CAU/D;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,GAC1B,MAAM,GAAG,SAAS,CAQpB"}
@@ -0,0 +1,354 @@
1
+ /**
2
+ * `localize` -- turn an npm-installed dependency into an editable git checkout.
3
+ *
4
+ * The loop, in five steps, none of which involve npm learning anything:
5
+ *
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
+ *
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.
15
+ *
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.
23
+ *
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.json`, `package-lock.json`, `.gitignore`, `workspaces` or any
29
+ * bundler config. It asserts it did not before exiting 0.
30
+ */
31
+ import fs from 'node:fs';
32
+ import path from 'node:path';
33
+ import { EXIT_INSTALL_FAILED, EXIT_NOT_FOUND, EXIT_MANIFEST_DIRTY, EXIT_REFUSED, EXIT_WARNED, LocalizeError, } from './errors.js';
34
+ import { DEFAULT_DIR, SCHEMA_VERSION, readManifest, writeManifest, } from './manifest.js';
35
+ import { checkoutNameFor, resolvePackage } from './resolve.js';
36
+ import { pruneProvided, shouldPrune } from './prune.js';
37
+ import { ensure } from './ensure.js';
38
+ import { diffManifests, isGitCheckout, isInside, isSymlink, readJson, snapshotManifests, writeLink, } from './fsops.js';
39
+ export function localize(names, opts, deps) {
40
+ return forEachName(names, opts, deps, localizeOne);
41
+ }
42
+ /**
43
+ * The loop `localize` and `adopt` share: read the manifest once, run `one` per
44
+ * name, write the manifest after each, then assert the links and the
45
+ * consumer's untouched manifests.
46
+ *
47
+ * @param {(name: string, dir: string, recorded: object|undefined, opts: object, deps: object)
48
+ * => {code: number, entry?: {name: string, value: object}}} one
49
+ */
50
+ export function forEachName(names, opts, deps, one) {
51
+ var _a, _b;
52
+ // Taken BEFORE anything runs: the post-condition is about what THIS run
53
+ // changed, not about what the tree looked like at HEAD.
54
+ const before = snapshotManifests(deps.appRoot);
55
+ let manifest;
56
+ try {
57
+ manifest = readManifest(deps.appRoot, deps);
58
+ }
59
+ catch (e) {
60
+ return reportError(e, deps);
61
+ }
62
+ const dir = (_b = (_a = opts.dir) !== null && _a !== void 0 ? _a : manifest.dir) !== null && _b !== void 0 ? _b : DEFAULT_DIR;
63
+ const entries = Object.assign({}, manifest.entries);
64
+ let code = 0;
65
+ for (const name of names) {
66
+ try {
67
+ const result = one(name, dir, entries[name], opts, deps);
68
+ if (result.entry)
69
+ entries[result.entry.name] = result.entry.value;
70
+ code = Math.max(code, result.code);
71
+ // Written after EACH name, so an interruption keeps what already worked.
72
+ writeManifest(deps.appRoot, {
73
+ version: SCHEMA_VERSION,
74
+ dir,
75
+ packages: entries,
76
+ });
77
+ }
78
+ catch (e) {
79
+ code = Math.max(code, reportError(e, deps));
80
+ }
81
+ }
82
+ // Over EVERY localized checkout, not just the ones named: localizing `B`
83
+ // is what makes `A`'s registry copy of `B` redundant.
84
+ if (shouldPrune(opts)) {
85
+ try {
86
+ pruneProvided(entries, opts, deps);
87
+ }
88
+ catch (e) {
89
+ deps.warn(`[localize] pruning failed, the checkouts' node_modules are as npm left them: ${e.message}`);
90
+ code = Math.max(code, EXIT_WARNED);
91
+ }
92
+ }
93
+ code = Math.max(code, assertLinked(entries, deps));
94
+ code = Math.max(code, assertManifestsUntouched(before, deps));
95
+ return code;
96
+ }
97
+ function localizeOne(name, dir, recorded, opts, deps) {
98
+ var _a;
99
+ // An override recorded by an earlier run is reused, so `--repo` is a
100
+ // one-time cost rather than something to remember on every invocation.
101
+ const override = opts.repo
102
+ ? { repo: opts.repo, subdir: opts.subdir }
103
+ : (recorded === null || recorded === void 0 ? void 0 : recorded.repo) && !looksLikeOrgSlug(recorded.repo)
104
+ ? { repo: recorded.repo, subdir: recorded.subdir }
105
+ : undefined;
106
+ const resolved = resolveOrSuggestAdopt(name, dir, override, deps);
107
+ let code = 0;
108
+ const relClone = path.join(dir, resolved.checkoutName);
109
+ const clone = path.join(deps.appRoot, relClone);
110
+ // When the repository holds more than this package, the CHECKOUT is the
111
+ // clone but the PACKAGE is a directory inside it: that is where the install
112
+ // runs and that is what the symlink points at.
113
+ const relPkg = resolved.subdir
114
+ ? path.join(relClone, resolved.subdir)
115
+ : relClone;
116
+ const pkgDir = path.join(deps.appRoot, relPkg);
117
+ guardForeignLink(resolved.npmName, dir, opts, deps);
118
+ if (isGitCheckout(clone)) {
119
+ code = Math.max(code, refresh(relClone, clone, deps));
120
+ if (code === EXIT_WARNED)
121
+ return { code }; // a non-fast-forward is reported, never forced
122
+ }
123
+ else {
124
+ fs.mkdirSync(path.dirname(clone), { recursive: true });
125
+ deps.log(`[localize] cloning ${resolved.repo} -> ${relClone}`);
126
+ const r = deps.run('git', ['clone', resolved.repo, clone]);
127
+ if (r.status !== 0) {
128
+ throw new LocalizeError(`git clone ${resolved.repo} failed:\n${(r.stderr || r.stdout).trim()}`, EXIT_REFUSED);
129
+ }
130
+ }
131
+ if (!fs.existsSync(path.join(pkgDir, 'package.json'))) {
132
+ throw new LocalizeError(`${relPkg} has no package.json, so it is not the package "${name}".\n` +
133
+ (resolved.subdir
134
+ ? `The registry says "${name}" lives in "${resolved.subdir}" of ${resolved.repo}, and it does not.`
135
+ : `${resolved.repo} looks like a monorepo root. Say where the package lives:\n` +
136
+ ` linked localize ${name} --repo ${resolved.repo} --subdir <path-inside-the-repo>`), EXIT_REFUSED);
137
+ }
138
+ // The checkout's own package.json is authoritative about the name: a
139
+ // mismatch means the symlink would be written under a name nothing imports.
140
+ const declared = (_a = readJson(path.join(pkgDir, 'package.json'))) === null || _a === void 0 ? void 0 : _a.name;
141
+ if (declared && declared !== resolved.npmName) {
142
+ throw new LocalizeError(`${relPkg} declares itself as "${declared}", not "${name}".\n` +
143
+ (resolved.subdir
144
+ ? `The registry's repository.directory for "${name}" points at the wrong package.`
145
+ : `${resolved.repo} is probably a monorepo. Point at the right directory:\n` +
146
+ ` linked localize ${name} --repo ${resolved.repo} --subdir <path-inside-the-repo>`), EXIT_REFUSED);
147
+ }
148
+ return installLinkAndRecord({
149
+ npmName: resolved.npmName,
150
+ clone,
151
+ relPkg,
152
+ pkgDir,
153
+ repo: resolved.repo,
154
+ subdir: resolved.subdir,
155
+ }, opts, deps, code);
156
+ }
157
+ /**
158
+ * Everything after the checkout exists, shared by `localize` and `adopt`:
159
+ * install inside it, run the configured build, link it, and describe the
160
+ * manifest entry. Nothing here knows how the checkout got there.
161
+ *
162
+ * `repo` may be absent -- an adopted checkout with no remote is recorded
163
+ * without one.
164
+ */
165
+ export function installLinkAndRecord({ npmName, clone, relPkg, pkgDir, repo, subdir, }, opts, deps, code = 0) {
166
+ // THE constraint. Read the module header before changing this. `ensure`
167
+ // installs inside the checkout and prunes it as one step (ensure.ts).
168
+ try {
169
+ code = Math.max(code, ensure({ path: relPkg, subdir }, deps, opts));
170
+ }
171
+ catch (e) {
172
+ if (e instanceof LocalizeError && e.code === EXIT_INSTALL_FAILED)
173
+ e.message += `\nThe checkout is left on disk. Nothing was linked and nothing was recorded.`;
174
+ throw e;
175
+ }
176
+ // The build is the one thing localize does not know how to do, so it is
177
+ // configuration rather than behaviour. A failure is a WARNING: a package
178
+ // whose build is broken is exactly what you are about to fix, and refusing
179
+ // to link it would be perverse.
180
+ if (runBuild(relPkg, pkgDir, opts, deps) === 'failed')
181
+ code = Math.max(code, EXIT_WARNED);
182
+ writeLink(deps.appRoot, npmName, pkgDir);
183
+ const branch = currentBranch(clone, deps);
184
+ deps.log(`[localize] linked ${npmName} -> ${relPkg} (branch ${branch})`);
185
+ const range = declaredRange(npmName, deps);
186
+ return {
187
+ code,
188
+ entry: {
189
+ name: npmName,
190
+ value: Object.assign(Object.assign(Object.assign(Object.assign({}, (repo ? { repo } : {})), { path: relPkg, branch }), (subdir ? { subdir } : {})), (range ? { range } : {})),
191
+ },
192
+ };
193
+ }
194
+ /**
195
+ * A registry lookup that fails while a git checkout already sits where the
196
+ * clone would go is almost always a package that was never published -- one
197
+ * made locally. Say how to link it as it is rather than leave the developer
198
+ * guessing at a `--repo`.
199
+ */
200
+ function resolveOrSuggestAdopt(name, dir, override, deps) {
201
+ try {
202
+ return resolvePackage(name, deps, override);
203
+ }
204
+ catch (e) {
205
+ const relClone = path.join(dir, checkoutNameFor(name));
206
+ if (e instanceof LocalizeError &&
207
+ e.code === EXIT_NOT_FOUND &&
208
+ isGitCheckout(path.join(deps.appRoot, relClone))) {
209
+ e.message +=
210
+ `\n${relClone} is already a git checkout. To link it as it is, with no registry lookup:\n` +
211
+ ` linked localize ${name} --adopt`;
212
+ }
213
+ throw e;
214
+ }
215
+ }
216
+ /** Older manifests recorded `owner/repo`; those are re-resolved rather than cloned blind. */
217
+ function looksLikeOrgSlug(repo) {
218
+ return /^[\w.-]+\/[\w.-]+$/.test(repo);
219
+ }
220
+ /**
221
+ * Reuse an existing clone. A dirty tree is reused WITHOUT pulling and said so;
222
+ * a clean one is fast-forwarded; a non-fast-forward is reported and skipped
223
+ * rather than forced, because the developer's commits are the entire point.
224
+ */
225
+ function refresh(relPath, clone, deps) {
226
+ const status = deps.run('git', ['status', '--porcelain'], { cwd: clone });
227
+ const modified = status.stdout.split('\n').filter((l) => l.trim()).length;
228
+ if (modified > 0) {
229
+ deps.log(`[localize] ${relPath}: ${modified} modified file${modified === 1 ? '' : 's'} — not pulling`);
230
+ return 0;
231
+ }
232
+ const pull = deps.run('git', ['pull', '--ff-only'], { cwd: clone });
233
+ if (pull.status !== 0) {
234
+ deps.warn(`[localize] ${relPath}: \`git pull --ff-only\` failed (not a fast-forward) — skipped, not ` +
235
+ `forced. Reconcile it by hand:\n${(pull.stderr || pull.stdout).trim()}`);
236
+ return EXIT_WARNED;
237
+ }
238
+ return 0;
239
+ }
240
+ /**
241
+ * Run the configured build command, if there is one.
242
+ *
243
+ * localize has no opinion about how a checkout is built, and does not look for
244
+ * a builder. `--build "<cmd>"`, or a `build` field in the manifest, or
245
+ * `{build}` in the programmatic options. Absent -- the common case, and the
246
+ * case for anything whose dev loop reads source directly -- nothing runs and
247
+ * nothing is said.
248
+ */
249
+ function runBuild(relPath, pkgDir, opts, deps) {
250
+ const cmd = opts.build;
251
+ if (!cmd)
252
+ return 'skipped';
253
+ deps.log(`[localize] ${relPath}: ${cmd}`);
254
+ const r = deps.run(cmd, [], { cwd: pkgDir, shell: true });
255
+ if (r.status !== 0) {
256
+ deps.warn(`[localize] ${relPath}: \`${cmd}\` failed. The package is linked and recorded anyway — ` +
257
+ `a package whose build is broken is exactly what you are about to fix.\n` +
258
+ `${(r.stderr || r.stdout).trim()}`);
259
+ return 'failed';
260
+ }
261
+ return 'ok';
262
+ }
263
+ /** Refuse when `node_modules/<name>` is already a symlink pointing outside `<dir>`. */
264
+ export function guardForeignLink(npmName, dir, opts, deps) {
265
+ const link = path.join(deps.appRoot, 'node_modules', npmName);
266
+ if (!isSymlink(link))
267
+ return;
268
+ let target;
269
+ try {
270
+ target = fs.realpathSync(link);
271
+ }
272
+ catch (_a) {
273
+ return; // a dangling link is ours to replace
274
+ }
275
+ if (isInside(target, path.join(deps.appRoot, dir)))
276
+ return;
277
+ if (opts.force) {
278
+ deps.warn(`[localize] --force: overwriting the existing link ${npmName} -> ${target}`);
279
+ return;
280
+ }
281
+ throw new LocalizeError(`node_modules/${npmName} is already a symlink to ${target}, which is outside ${dir}.\n` +
282
+ `Refusing to overwrite someone else's link. Pass --force to replace it.`, EXIT_REFUSED);
283
+ }
284
+ /** Every recorded package is actually linked where the manifest says it is. */
285
+ function assertLinked(entries, deps) {
286
+ let code = 0;
287
+ for (const [name, entry] of Object.entries(entries)) {
288
+ const link = path.join(deps.appRoot, 'node_modules', name);
289
+ const expected = path.join(deps.appRoot, entry.path);
290
+ if (!isSymlink(link)) {
291
+ deps.warn(`[localize] ${name} is recorded but node_modules/${name} is not a symlink.`);
292
+ code = Math.max(code, EXIT_WARNED);
293
+ continue;
294
+ }
295
+ if (!fs.existsSync(expected))
296
+ continue; // "checkout missing" is a --list state, not a failure
297
+ if (fs.realpathSync(link) !== fs.realpathSync(expected)) {
298
+ deps.warn(`[localize] ${name} links to ${fs.realpathSync(link)} but the manifest records ${entry.path}.`);
299
+ code = Math.max(code, EXIT_WARNED);
300
+ }
301
+ }
302
+ return code;
303
+ }
304
+ /**
305
+ * The property the whole design rests on: this run changed neither manifest.
306
+ *
307
+ * Content hashes, not `git status`. `git status` compares against HEAD, so in
308
+ * any tree with an uncommitted `package.json` it reports a modification the
309
+ * command did not make -- an earlier tool shipped exactly that and reported
310
+ * correct runs as failures.
311
+ */
312
+ export function assertManifestsUntouched(before, deps) {
313
+ const changed = diffManifests(before, snapshotManifests(deps.appRoot));
314
+ if (!changed.length)
315
+ return 0;
316
+ deps.error(`[localize] the consumer's manifests changed while this command ran:\n` +
317
+ changed.map((c) => ` ${c}`).join('\n') +
318
+ `\nThis is the exact failure localize exists to prevent. Revert those files; nothing in ` +
319
+ `localize is supposed to touch them.`);
320
+ return EXIT_MANIFEST_DIRTY;
321
+ }
322
+ export function reportError(e, deps) {
323
+ if (e instanceof LocalizeError) {
324
+ deps.error(`[localize] ${e.message}`);
325
+ return e.code;
326
+ }
327
+ throw e;
328
+ }
329
+ /**
330
+ * The checked-out branch, or `HEAD` when detached. `symbolic-ref` first,
331
+ * because it also names the branch of a repository with no commits yet --
332
+ * `rev-parse --abbrev-ref` fails there, and `create-package` adopts a fresh
333
+ * `git init` before its first commit.
334
+ */
335
+ export function currentBranch(clone, deps) {
336
+ const sym = deps.run('git', ['symbolic-ref', '--short', '-q', 'HEAD'], {
337
+ cwd: clone,
338
+ });
339
+ if (sym.status === 0 && sym.stdout.trim())
340
+ return sym.stdout.trim();
341
+ const r = deps.run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
342
+ cwd: clone,
343
+ });
344
+ const b = r.stdout.trim();
345
+ return r.status === 0 && b ? b : 'HEAD';
346
+ }
347
+ export function declaredRange(name, deps) {
348
+ var _a, _b, _c, _d, _e;
349
+ const pkg = readJson(path.join(deps.appRoot, 'package.json'));
350
+ if (!pkg)
351
+ return undefined;
352
+ 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]);
353
+ }
354
+ //# sourceMappingURL=localize.js.map