@proteinjs/reflection-build 1.4.5 → 1.4.6

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 (31) hide show
  1. package/dist/test/examples/source-repository/a/generated/index.js +1 -1
  2. package/dist/test/examples/source-repository/b/generated/index.js +1 -1
  3. package/package.json +2 -2
  4. package/test/examples/source-repository/a/dist/generated/index.js +1 -1
  5. package/test/examples/source-repository/a/generated/index.ts +1 -1
  6. package/test/examples/source-repository/a/node_modules/.package-lock.json +6 -6
  7. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/CHANGELOG.md +22 -0
  8. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/src/PackageUtil.js +102 -20
  9. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/src/PackageUtil.js.map +1 -1
  10. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/jest.config.js +9 -0
  11. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/package.json +4 -3
  12. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/src/PackageUtil.ts +88 -2
  13. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/test/PackageUtil.symlinkDependencies.test.ts +254 -0
  14. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/tsconfig.json +1 -1
  15. package/test/examples/source-repository/a/node_modules/glob-watcher/node_modules/fsevents/build/Makefile +1 -1
  16. package/test/examples/source-repository/a/package-lock.json +8 -8
  17. package/test/examples/source-repository/a/package.json +2 -2
  18. package/test/examples/source-repository/b/dist/generated/index.js +1 -1
  19. package/test/examples/source-repository/b/generated/index.ts +1 -1
  20. package/test/examples/source-repository/b/node_modules/.package-lock.json +4 -4
  21. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/CHANGELOG.md +22 -0
  22. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/src/PackageUtil.js +102 -20
  23. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/src/PackageUtil.js.map +1 -1
  24. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/jest.config.js +9 -0
  25. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/package.json +4 -3
  26. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/src/PackageUtil.ts +88 -2
  27. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/test/PackageUtil.symlinkDependencies.test.ts +254 -0
  28. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/tsconfig.json +1 -1
  29. package/test/examples/source-repository/b/node_modules/glob-watcher/node_modules/fsevents/build/Makefile +1 -1
  30. package/test/examples/source-repository/b/package-lock.json +6 -6
  31. package/test/examples/source-repository/b/package.json +2 -2
@@ -0,0 +1,254 @@
1
+ import * as path from 'path';
2
+ import * as fs from 'fs/promises';
3
+ import * as os from 'os';
4
+ import { PackageUtil, LocalPackage, LocalPackageMap } from '../src/PackageUtil';
5
+
6
+ /**
7
+ * Regression tests for PackageUtil.symlinkDependencies.
8
+ *
9
+ * The important property: the symlinks it creates must use RELATIVE targets
10
+ * so the workspace is portable across mount points. If absolute paths slip
11
+ * back in, a symlinked workspace breaks whenever its root directory moves
12
+ * (developer laptop vs CI container vs sandbox vs coworker's checkout), and
13
+ * every environment change requires re-running `symlink-workspace`.
14
+ */
15
+ describe('PackageUtil.symlinkDependencies — relative link targets', () => {
16
+ let workspaceRoot: string;
17
+
18
+ beforeEach(async () => {
19
+ workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'symlink-rel-'));
20
+ });
21
+
22
+ afterEach(async () => {
23
+ if (workspaceRoot) {
24
+ await fs.rm(workspaceRoot, { recursive: true, force: true }).catch(() => {
25
+ /* ignore — directory may have already been moved */
26
+ });
27
+ }
28
+ });
29
+
30
+ /**
31
+ * Create a package.json on disk and return the `LocalPackage` record that
32
+ * `symlinkDependencies` expects.
33
+ */
34
+ const writePackage = async (
35
+ relPath: string,
36
+ name: string,
37
+ deps: Record<string, string> = {}
38
+ ): Promise<LocalPackage> => {
39
+ const dir = path.join(workspaceRoot, relPath);
40
+ await fs.mkdir(dir, { recursive: true });
41
+ const pkgPath = path.join(dir, 'package.json');
42
+ const packageJson = { name, version: '1.0.0', dependencies: deps };
43
+ await fs.writeFile(pkgPath, JSON.stringify(packageJson, null, 2));
44
+ return { name, filePath: pkgPath, packageJson };
45
+ };
46
+
47
+ test('created symlink target is relative, not absolute', async () => {
48
+ const depPkg = await writePackage('packages/dep', '@scope/dep');
49
+ const consumerPkg = await writePackage('packages/consumer', '@scope/consumer', {
50
+ '@scope/dep': '^1.0.0',
51
+ });
52
+ const packageMap: LocalPackageMap = {
53
+ '@scope/dep': depPkg,
54
+ '@scope/consumer': consumerPkg,
55
+ };
56
+
57
+ await PackageUtil.symlinkDependencies(consumerPkg, packageMap);
58
+
59
+ const symlinkPath = path.join(workspaceRoot, 'packages/consumer/node_modules/@scope/dep');
60
+ const linkTarget = await fs.readlink(symlinkPath);
61
+
62
+ expect(path.isAbsolute(linkTarget)).toBe(false);
63
+ // From .../packages/consumer/node_modules/@scope up to .../packages, then down to dep.
64
+ expect(linkTarget).toBe(path.join('..', '..', '..', 'dep'));
65
+ });
66
+
67
+ test('symlink still resolves after the workspace is moved to a new root', async () => {
68
+ const depPkg = await writePackage('packages/dep', '@scope/dep');
69
+ const consumerPkg = await writePackage('packages/consumer', '@scope/consumer', {
70
+ '@scope/dep': '^1.0.0',
71
+ });
72
+ const packageMap: LocalPackageMap = {
73
+ '@scope/dep': depPkg,
74
+ '@scope/consumer': consumerPkg,
75
+ };
76
+
77
+ // Drop a marker file inside the dep package so we can verify the symlink
78
+ // resolves into it by path after the move.
79
+ const markerRelativePath = path.join('packages/consumer/node_modules/@scope/dep/marker.txt');
80
+ await fs.writeFile(path.join(workspaceRoot, 'packages/dep/marker.txt'), 'hello');
81
+
82
+ await PackageUtil.symlinkDependencies(consumerPkg, packageMap);
83
+
84
+ // Move the entire workspace to a new absolute path. If the symlink target
85
+ // were absolute (pointing at the old workspaceRoot), every resolution
86
+ // through it would now fail. With a relative target it continues to work.
87
+ const movedRoot = workspaceRoot + '-moved';
88
+ await fs.rename(workspaceRoot, movedRoot);
89
+ const prevRoot = workspaceRoot;
90
+ workspaceRoot = movedRoot; // redirect afterEach cleanup at the new location
91
+
92
+ const markerThroughSymlink = path.join(movedRoot, markerRelativePath);
93
+ const content = await fs.readFile(markerThroughSymlink, 'utf8');
94
+ expect(content).toBe('hello');
95
+
96
+ // Sanity check: confirm the old absolute path really is gone, so we know
97
+ // the move happened and we're not accidentally resolving through the
98
+ // original location via some leftover absolute link.
99
+ await expect(fs.stat(prevRoot)).rejects.toThrow();
100
+ });
101
+
102
+ test('replaces a pre-existing BROKEN symlink (prior run pointed at a now-missing path)', async () => {
103
+ // Regression test for the Fs.exists-vs-broken-symlink bug.
104
+ //
105
+ // `Fs.exists` is stat-backed: it follows symlinks and throws on a
106
+ // broken target, so broken symlinks read as "doesn't exist". If
107
+ // symlinkDependencies relied on Fs.exists alone to decide whether to
108
+ // pre-delete, a broken symlink from a prior run (e.g. a sandbox with
109
+ // a different mount point, a cross-machine checkout, a moved tree)
110
+ // would survive the pre-delete step and cause `ln -s` to fail with
111
+ // "File exists".
112
+ const depPkg = await writePackage('packages/dep', '@scope/dep');
113
+ const consumerPkg = await writePackage('packages/consumer', '@scope/consumer', {
114
+ '@scope/dep': '^1.0.0',
115
+ });
116
+ const packageMap: LocalPackageMap = {
117
+ '@scope/dep': depPkg,
118
+ '@scope/consumer': consumerPkg,
119
+ };
120
+
121
+ // Manually plant a broken symlink where the dep link would go,
122
+ // simulating the "ran symlink-workspace in a different environment
123
+ // earlier" state.
124
+ const symlinkDir = path.join(workspaceRoot, 'packages/consumer/node_modules/@scope');
125
+ await fs.mkdir(symlinkDir, { recursive: true });
126
+ const symlinkPath = path.join(symlinkDir, 'dep');
127
+ await fs.symlink('/nonexistent/absolute/path/that/cannot/resolve', symlinkPath);
128
+
129
+ // Confirm the broken state before we invoke symlinkDependencies.
130
+ await expect(fs.stat(symlinkPath)).rejects.toMatchObject({ code: 'ENOENT' });
131
+ // lstat succeeds because it doesn't follow the link.
132
+ const brokenStat = await fs.lstat(symlinkPath);
133
+ expect(brokenStat.isSymbolicLink()).toBe(true);
134
+
135
+ // This must NOT throw "File exists" — it should overwrite the broken link.
136
+ await PackageUtil.symlinkDependencies(consumerPkg, packageMap);
137
+
138
+ // Fresh symlink with a relative target.
139
+ const newTarget = await fs.readlink(symlinkPath);
140
+ expect(path.isAbsolute(newTarget)).toBe(false);
141
+ expect(newTarget).toBe(path.join('..', '..', '..', 'dep'));
142
+
143
+ // And it resolves — the dep directory exists.
144
+ const resolvedStat = await fs.stat(symlinkPath);
145
+ expect(resolvedStat.isDirectory()).toBe(true);
146
+ });
147
+
148
+ test('creates node_modules/.bin shims for deps that declare a `bin` field (object form)', async () => {
149
+ // Regression test for the missing-.bin-shim bug. `npm install`
150
+ // normally writes `node_modules/.bin/<name>` shims for every `bin` a
151
+ // dependency declares, and npm prepends `./node_modules/.bin` to PATH
152
+ // when running lifecycle scripts. If `symlink-workspace` doesn't
153
+ // create these shims itself, running `npm run watch` in a consumer
154
+ // package fails with `command not found` as soon as the shim from a
155
+ // prior `npm install` gets cleaned up (or the tree is freshly cloned).
156
+ const toolPkg = await writePackage('packages/tool', '@scope/tool');
157
+ // Populate a bin script in the tool's dist so the shim has something
158
+ // to resolve to.
159
+ const runBuildPath = path.join(workspaceRoot, 'packages/tool/dist/bin/run-build.js');
160
+ const runWatchPath = path.join(workspaceRoot, 'packages/tool/dist/bin/run-watch.js');
161
+ await fs.mkdir(path.dirname(runBuildPath), { recursive: true });
162
+ await fs.writeFile(runBuildPath, '#!/usr/bin/env node\nconsole.log("build");\n');
163
+ await fs.writeFile(runWatchPath, '#!/usr/bin/env node\nconsole.log("watch");\n');
164
+ // Re-write the tool's package.json with a `bin` entry.
165
+ toolPkg.packageJson.bin = {
166
+ 'tool-build': 'dist/bin/run-build.js',
167
+ 'tool-watch': 'dist/bin/run-watch.js',
168
+ };
169
+ await fs.writeFile(toolPkg.filePath, JSON.stringify(toolPkg.packageJson, null, 2));
170
+
171
+ const consumerPkg = await writePackage('packages/consumer', 'consumer', {
172
+ '@scope/tool': '^1.0.0',
173
+ });
174
+ const packageMap: LocalPackageMap = {
175
+ '@scope/tool': toolPkg,
176
+ consumer: consumerPkg,
177
+ };
178
+
179
+ await PackageUtil.symlinkDependencies(consumerPkg, packageMap);
180
+
181
+ const binDir = path.join(workspaceRoot, 'packages/consumer/node_modules/.bin');
182
+ const buildShim = path.join(binDir, 'tool-build');
183
+ const watchShim = path.join(binDir, 'tool-watch');
184
+
185
+ // Both shims exist as symlinks.
186
+ expect((await fs.lstat(buildShim)).isSymbolicLink()).toBe(true);
187
+ expect((await fs.lstat(watchShim)).isSymbolicLink()).toBe(true);
188
+
189
+ // Shim targets are relative.
190
+ const buildTarget = await fs.readlink(buildShim);
191
+ const watchTarget = await fs.readlink(watchShim);
192
+ expect(path.isAbsolute(buildTarget)).toBe(false);
193
+ expect(path.isAbsolute(watchTarget)).toBe(false);
194
+
195
+ // Shims resolve end-to-end — readFile through the shim returns the
196
+ // actual script contents, which means the whole chain
197
+ // (`.bin/name` → `../@scope/tool/dist/bin/...`) is intact.
198
+ const buildScript = await fs.readFile(buildShim, 'utf8');
199
+ const watchScript = await fs.readFile(watchShim, 'utf8');
200
+ expect(buildScript).toContain('console.log("build")');
201
+ expect(watchScript).toContain('console.log("watch")');
202
+ });
203
+
204
+ test('creates `.bin` shim for bin-as-string shorthand using the bare package name', async () => {
205
+ const toolPkg = await writePackage('packages/tool', '@scope/only-tool');
206
+ const onlyScriptPath = path.join(workspaceRoot, 'packages/tool/bin.js');
207
+ await fs.writeFile(onlyScriptPath, '#!/usr/bin/env node\nconsole.log("only");\n');
208
+ toolPkg.packageJson.bin = './bin.js';
209
+ await fs.writeFile(toolPkg.filePath, JSON.stringify(toolPkg.packageJson, null, 2));
210
+
211
+ const consumerPkg = await writePackage('packages/consumer', 'consumer', {
212
+ '@scope/only-tool': '^1.0.0',
213
+ });
214
+ const packageMap: LocalPackageMap = {
215
+ '@scope/only-tool': toolPkg,
216
+ consumer: consumerPkg,
217
+ };
218
+
219
+ await PackageUtil.symlinkDependencies(consumerPkg, packageMap);
220
+
221
+ // Bare name — scope stripped — just like npm.
222
+ const shim = path.join(workspaceRoot, 'packages/consumer/node_modules/.bin/only-tool');
223
+ expect((await fs.lstat(shim)).isSymbolicLink()).toBe(true);
224
+ const content = await fs.readFile(shim, 'utf8');
225
+ expect(content).toContain('console.log("only")');
226
+ });
227
+
228
+ test('handles scoped and unscoped dependencies uniformly', async () => {
229
+ const scopedDep = await writePackage('packages/scoped', '@scope/foo');
230
+ const unscopedDep = await writePackage('packages/unscoped', 'bar');
231
+ const consumerPkg = await writePackage('packages/consumer', 'consumer', {
232
+ '@scope/foo': '^1.0.0',
233
+ bar: '^1.0.0',
234
+ });
235
+ const packageMap: LocalPackageMap = {
236
+ '@scope/foo': scopedDep,
237
+ bar: unscopedDep,
238
+ consumer: consumerPkg,
239
+ };
240
+
241
+ await PackageUtil.symlinkDependencies(consumerPkg, packageMap);
242
+
243
+ const scopedLink = await fs.readlink(path.join(workspaceRoot, 'packages/consumer/node_modules/@scope/foo'));
244
+ const unscopedLink = await fs.readlink(path.join(workspaceRoot, 'packages/consumer/node_modules/bar'));
245
+
246
+ expect(path.isAbsolute(scopedLink)).toBe(false);
247
+ expect(path.isAbsolute(unscopedLink)).toBe(false);
248
+
249
+ // Scoped packages are nested one level deeper under node_modules/@scope,
250
+ // so the relative target has one extra `..`.
251
+ expect(scopedLink).toBe(path.join('..', '..', '..', 'scoped'));
252
+ expect(unscopedLink).toBe(path.join('..', '..', 'unscoped'));
253
+ });
254
+ });
@@ -13,7 +13,7 @@
13
13
  "resolveJsonModule": true,
14
14
  "baseUrl": "./",
15
15
  "paths": { "*": ["types/*"] },
16
- "types": ["node"]
16
+ "types": ["node", "jest"]
17
17
  },
18
18
  "include": ["index.ts"]
19
19
  }
@@ -336,7 +336,7 @@ ifeq ($(strip $(foreach prefix,$(NO_LOAD),\
336
336
  endif
337
337
 
338
338
  quiet_cmd_regen_makefile = ACTION Regenerating $@
339
- cmd_regen_makefile = cd $(srcdir); /Users/brentbahry/.nvm/versions/node/v24.11.1/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py -fmake --ignore-environment "-Dlibrary=shared_library" "-Dvisibility=default" "-Dnode_root_dir=/Users/brentbahry/Library/Caches/node-gyp/24.11.1" "-Dnode_gyp_dir=/Users/brentbahry/.nvm/versions/node/v24.11.1/lib/node_modules/npm/node_modules/node-gyp" "-Dnode_lib_file=/Users/brentbahry/Library/Caches/node-gyp/24.11.1/<(target_arch)/node.lib" "-Dmodule_root_dir=/Users/brentbahry/repos/farm/n3xa3/packages/proteinjs/packages/reflection/packages/reflection-build/test/examples/source-repository/b/node_modules/glob-watcher/node_modules/fsevents" "-Dnode_engine=v8" "--depth=." "-Goutput_dir=." "--generator-output=build" -I/Users/brentbahry/repos/farm/n3xa3/packages/proteinjs/packages/reflection/packages/reflection-build/test/examples/source-repository/b/node_modules/glob-watcher/node_modules/fsevents/build/config.gypi -I/Users/brentbahry/.nvm/versions/node/v24.11.1/lib/node_modules/npm/node_modules/node-gyp/addon.gypi -I/Users/brentbahry/Library/Caches/node-gyp/24.11.1/include/node/common.gypi "--toplevel-dir=." binding.gyp
339
+ cmd_regen_makefile = cd $(srcdir); /Users/brentbahry/.nvm/versions/node/v24.11.1/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py -fmake --ignore-environment "-Dlibrary=shared_library" "-Dvisibility=default" "-Dnode_root_dir=/Users/brentbahry/Library/Caches/node-gyp/24.11.1" "-Dnode_gyp_dir=/Users/brentbahry/.nvm/versions/node/v24.11.1/lib/node_modules/npm/node_modules/node-gyp" "-Dnode_lib_file=/Users/brentbahry/Library/Caches/node-gyp/24.11.1/<(target_arch)/node.lib" "-Dmodule_root_dir=/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/reflection/packages/reflection-build/test/examples/source-repository/b/node_modules/glob-watcher/node_modules/fsevents" "-Dnode_engine=v8" "--depth=." "-Goutput_dir=." "--generator-output=build" -I/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/reflection/packages/reflection-build/test/examples/source-repository/b/node_modules/glob-watcher/node_modules/fsevents/build/config.gypi -I/Users/brentbahry/.nvm/versions/node/v24.11.1/lib/node_modules/npm/node_modules/node-gyp/addon.gypi -I/Users/brentbahry/Library/Caches/node-gyp/24.11.1/include/node/common.gypi "--toplevel-dir=." binding.gyp
340
340
  Makefile: $(srcdir)/../../../../../../../../../../../../../../../../../.nvm/versions/node/v24.11.1/lib/node_modules/npm/node_modules/node-gyp/addon.gypi $(srcdir)/../../../../../../../../../../../../../../../../../Library/Caches/node-gyp/24.11.1/include/node/common.gypi $(srcdir)/binding.gyp $(srcdir)/build/config.gypi
341
341
  $(call do_cmd,regen_makefile)
342
342
 
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@proteinjs/reflection-build-test-b",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@proteinjs/reflection-build-test-b",
9
- "version": "0.0.19",
9
+ "version": "0.0.20",
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@dagrejs/graphlib": "2.1.4",
13
13
  "@proteinjs/reflection": "^1.1.11",
14
14
  "@proteinjs/util": "^1.6.0",
15
- "@proteinjs/util-node": "^1.7.3",
15
+ "@proteinjs/util-node": "^1.8.1",
16
16
  "globby": "11.0.1",
17
17
  "gulp": "4.0.2",
18
18
  "gulp-shell": "0.8.0",
@@ -1099,9 +1099,9 @@
1099
1099
  }
1100
1100
  },
1101
1101
  "node_modules/@proteinjs/util-node": {
1102
- "version": "1.7.3",
1103
- "resolved": "https://registry.npmjs.org/@proteinjs/util-node/-/util-node-1.7.3.tgz",
1104
- "integrity": "sha512-/OFYrPT3iblHVf5Y0GQCud8mGKgeP73/uI2qSd+rm3MVJm0yB8I1seZmRLKqZfMDM1eawnIsVFRRyX4mAH2BIg==",
1102
+ "version": "1.8.1",
1103
+ "resolved": "https://registry.npmjs.org/@proteinjs/util-node/-/util-node-1.8.1.tgz",
1104
+ "integrity": "sha512-F4BEBmroyTuRjQGYkJQhs2JoTg6VUIeEveMZbHzV5mG8jIyF2zbIF+qQCJ0cV2pdQg2PLhi61VlstTwJrEIKdQ==",
1105
1105
  "license": "ISC",
1106
1106
  "dependencies": {
1107
1107
  "@dagrejs/graphlib": "2.1.4",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proteinjs/reflection-build-test-b",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
4
4
  "private": true,
5
5
  "description": "Test package b",
6
6
  "repository": {
@@ -21,7 +21,7 @@
21
21
  "@dagrejs/graphlib": "2.1.4",
22
22
  "@proteinjs/reflection": "^1.1.11",
23
23
  "@proteinjs/util": "^1.6.0",
24
- "@proteinjs/util-node": "^1.7.3",
24
+ "@proteinjs/util-node": "^1.8.1",
25
25
  "globby": "11.0.1",
26
26
  "gulp": "4.0.2",
27
27
  "gulp-shell": "0.8.0",