@pnpm/workspace.injected-deps-syncer 1100.0.38 → 1100.0.40
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.
- package/CHANGELOG.md +42 -0
- package/lib/DirPatcher.d.ts +13 -0
- package/lib/DirPatcher.js +138 -7
- package/lib/index.d.ts +35 -0
- package/lib/index.js +134 -10
- package/package.json +15 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# @pnpm/workspace.injected-deps-syncer
|
|
2
2
|
|
|
3
|
+
## 1100.0.40
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- `pnpm install` no longer fails for an injected workspace dependency whose package publishes from a `publishConfig.directory` that its own `prepare` script builds. The injected copy now picks up that directory once `prepare` finishes building it. `pnpm install --frozen-lockfile` no longer reports the dependency as outdated while the directory has not been built yet. [pnpm/pnpm#7811](https://github.com/pnpm/pnpm/issues/7811)
|
|
8
|
+
|
|
9
|
+
- With `sharedWorkspaceLockfile: false`, an injected workspace package that has lifecycle scripts is now hard linked into the projects that depend on it. Before, pnpm left a plain copy, so later edits to the package did not reach those projects [#9828](https://github.com/pnpm/pnpm/issues/9828).
|
|
10
|
+
|
|
11
|
+
- Scripts listed in `syncInjectedDepsAfterScripts` now update injected dependencies while they run. A watcher on the injected package, such as a dev server, sees each change before the script exits [pnpm/pnpm#4410](https://github.com/pnpm/pnpm/issues/4410).
|
|
12
|
+
|
|
13
|
+
- `pnpm install` now works in StackBlitz WebContainers on projects without a lockfile. It used to fail there with `ENOENT ... pnpm-lock.yaml`, because pnpm did not recognize the errors that WebContainers return from asynchronous file system calls [#15649](https://github.com/pnpm/pnpm/issues/15649).
|
|
14
|
+
|
|
15
|
+
- Updated dependencies:
|
|
16
|
+
- @pnpm/bins.linker@1100.0.34
|
|
17
|
+
- @pnpm/bins.remover@1100.0.26
|
|
18
|
+
- @pnpm/error@1100.2.1
|
|
19
|
+
- @pnpm/fetching.directory-fetcher@1100.0.36
|
|
20
|
+
- @pnpm/fs.graceful-fs@1100.2.4
|
|
21
|
+
- @pnpm/installing.modules-yaml@1101.0.5
|
|
22
|
+
- @pnpm/pkg-manifest.reader@1100.0.21
|
|
23
|
+
- @pnpm/workspace.projects-reader@1101.1.1
|
|
24
|
+
|
|
25
|
+
## 1100.0.39
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- `syncInjectedDepsAfterScripts` now copies files into injected dependencies when `node_modules` is on another filesystem than the package sources. The sync previously failed with a cross-device link error and made the script run exit with an error [pnpm/pnpm#14703](https://github.com/pnpm/pnpm/issues/14703).
|
|
30
|
+
|
|
31
|
+
- Fixed injected workspace dependency synchronization failing with `EPERM` on Windows when removing nested directories.
|
|
32
|
+
|
|
33
|
+
- Updated dependencies:
|
|
34
|
+
- @pnpm/bins.linker@1100.0.33
|
|
35
|
+
- @pnpm/bins.remover@1100.0.25
|
|
36
|
+
- @pnpm/bins.resolver@1100.0.17
|
|
37
|
+
- @pnpm/error@1100.2.0
|
|
38
|
+
- @pnpm/fetching.directory-fetcher@1100.0.35
|
|
39
|
+
- @pnpm/fs.graceful-fs@1100.2.3
|
|
40
|
+
- @pnpm/installing.modules-yaml@1101.0.4
|
|
41
|
+
- @pnpm/pkg-manifest.reader@1100.0.20
|
|
42
|
+
- @pnpm/types@1102.1.1
|
|
43
|
+
- @pnpm/workspace.projects-reader@1101.1.0
|
|
44
|
+
|
|
3
45
|
## 1100.0.38
|
|
4
46
|
|
|
5
47
|
### Patch Changes
|
package/lib/DirPatcher.d.ts
CHANGED
|
@@ -62,6 +62,19 @@ export interface ExtendFilesMapOptions {
|
|
|
62
62
|
* into an inodes map, which is a map from relative path of every file and directory to their inode type.
|
|
63
63
|
*/
|
|
64
64
|
export declare function extendFilesMap({ filesMap, filesStats }: ExtendFilesMapOptions): Promise<InodeMap>;
|
|
65
|
+
/** A source directory's files, read once per poll and published to each of its injected copies. */
|
|
66
|
+
export interface PublishSource {
|
|
67
|
+
dir: string;
|
|
68
|
+
map: InodeMap;
|
|
69
|
+
}
|
|
70
|
+
export declare function readPublishSource(dir: string): Promise<PublishSource>;
|
|
71
|
+
/**
|
|
72
|
+
* Copy changed files into `targetDir` as independent files, so a watcher on
|
|
73
|
+
* the injected directory sees the write. A hardlink edited in place is
|
|
74
|
+
* republished when its mtime is at least `editedSinceMs`. A copy whose size
|
|
75
|
+
* and mtime already match the source is left alone.
|
|
76
|
+
*/
|
|
77
|
+
export declare function publishEditsForWatchers(source: PublishSource, targetDir: string, editedSinceMs: number): Promise<void>;
|
|
65
78
|
export declare class DirPatcher {
|
|
66
79
|
private readonly sourceDir;
|
|
67
80
|
private readonly targetDir;
|
package/lib/DirPatcher.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import
|
|
3
|
+
import { pipeline } from 'node:stream/promises';
|
|
4
|
+
import { isError } from '@pnpm/error';
|
|
4
5
|
import { fetchFromDir } from '@pnpm/fetching.directory-fetcher';
|
|
6
|
+
import { renameFileWithRetry, renameFileWithRetryAsync } from '@pnpm/fs.graceful-fs';
|
|
7
|
+
import { pathTemp } from 'path-temp';
|
|
5
8
|
export const DIR = Symbol('Path is a directory');
|
|
6
9
|
// length comparison should place every directory before the files it contains because
|
|
7
10
|
// a directory path is always shorter than any file path it contains
|
|
@@ -39,12 +42,41 @@ export async function applyPatch(optimizedDirPatch, sourceDir, targetDir) {
|
|
|
39
42
|
}
|
|
40
43
|
else if (typeof value === 'string') {
|
|
41
44
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
42
|
-
await retryOverBlockingInode(targetPath, async () =>
|
|
45
|
+
await retryOverBlockingInode(targetPath, async () => linkOrCopy(sourcePath, targetPath));
|
|
43
46
|
}
|
|
44
47
|
else {
|
|
45
48
|
const _ = value; // static type guard
|
|
46
49
|
}
|
|
47
50
|
}
|
|
51
|
+
async function linkOrCopy(sourcePath, targetPath) {
|
|
52
|
+
try {
|
|
53
|
+
await fs.promises.link(sourcePath, targetPath);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (isError(error) && 'code' in error && error.code === 'EXDEV') {
|
|
57
|
+
await copyIntoPlace(sourcePath, targetPath);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Copy through a temp sibling, so that a reader of the target never sees a
|
|
65
|
+
* partial copy. The rename replaces a symlink at the target without following
|
|
66
|
+
* it.
|
|
67
|
+
*/
|
|
68
|
+
async function copyIntoPlace(sourcePath, targetPath) {
|
|
69
|
+
// A random name, since concurrent syncs share the thread that writes it.
|
|
70
|
+
const tempPath = pathTemp(path.dirname(targetPath));
|
|
71
|
+
try {
|
|
72
|
+
await fs.promises.copyFile(sourcePath, tempPath, fs.constants.COPYFILE_EXCL);
|
|
73
|
+
renameFileWithRetry(tempPath, targetPath);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
await fs.promises.rm(tempPath, { force: true });
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
48
80
|
/**
|
|
49
81
|
* The target may hold an inode that {@link extendFilesMap} skips — a FIFO, a
|
|
50
82
|
* socket, a device. The diff cannot see it, so it is never scheduled for
|
|
@@ -56,7 +88,7 @@ export async function applyPatch(optimizedDirPatch, sourceDir, targetDir) {
|
|
|
56
88
|
await add();
|
|
57
89
|
}
|
|
58
90
|
catch (error) {
|
|
59
|
-
if (!
|
|
91
|
+
if (!isError(error) || !('code' in error) || (error.code !== 'EEXIST')) {
|
|
60
92
|
throw error;
|
|
61
93
|
}
|
|
62
94
|
await removeRecursive(targetPath);
|
|
@@ -68,7 +100,7 @@ export async function applyPatch(optimizedDirPatch, sourceDir, targetDir) {
|
|
|
68
100
|
await fs.promises.rm(targetPath, { recursive: true, force: true });
|
|
69
101
|
}
|
|
70
102
|
catch (error) {
|
|
71
|
-
if (!
|
|
103
|
+
if (!isError(error) || !('code' in error) || (error.code !== 'ENOENT')) {
|
|
72
104
|
throw error;
|
|
73
105
|
}
|
|
74
106
|
}
|
|
@@ -93,9 +125,9 @@ export async function applyPatch(optimizedDirPatch, sourceDir, targetDir) {
|
|
|
93
125
|
// files a sibling had already linked. A path the target holds as a file and
|
|
94
126
|
// the source as a directory lands in `modified` rather than `added`, so both
|
|
95
127
|
// arrays feed the directory pass.
|
|
96
|
-
|
|
97
|
-
await removeRecursive(path.join(targetDir, item.path));
|
|
98
|
-
}
|
|
128
|
+
for (const item of optimizedDirPatch.removed) {
|
|
129
|
+
await removeRecursive(path.join(targetDir, item.path)); // eslint-disable-line no-await-in-loop
|
|
130
|
+
}
|
|
99
131
|
for (const item of newDirs) {
|
|
100
132
|
await applyChange(item); // eslint-disable-line no-await-in-loop
|
|
101
133
|
}
|
|
@@ -130,6 +162,105 @@ export async function extendFilesMap({ filesMap, filesStats }) {
|
|
|
130
162
|
return result;
|
|
131
163
|
}
|
|
132
164
|
const fileId = (stats) => `${stats.dev}:${stats.ino}`;
|
|
165
|
+
const WATCH_MTIME_TOLERANCE_MS = 1;
|
|
166
|
+
const WATCH_FETCH_OPTIONS = { resolveSymlinks: false };
|
|
167
|
+
export async function readPublishSource(dir) {
|
|
168
|
+
return { dir, map: await extendFilesMap(await fetchFromDir(dir, WATCH_FETCH_OPTIONS)) };
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Copy changed files into `targetDir` as independent files, so a watcher on
|
|
172
|
+
* the injected directory sees the write. A hardlink edited in place is
|
|
173
|
+
* republished when its mtime is at least `editedSinceMs`. A copy whose size
|
|
174
|
+
* and mtime already match the source is left alone.
|
|
175
|
+
*/
|
|
176
|
+
export async function publishEditsForWatchers(source, targetDir, editedSinceMs) {
|
|
177
|
+
const { dir: sourceDir, map: sourceMap } = source;
|
|
178
|
+
const targetMap = await extendFilesMap(await fetchFromDir(targetDir, WATCH_FETCH_OPTIONS));
|
|
179
|
+
const removed = Object.keys(targetMap)
|
|
180
|
+
.filter(relPath => !(relPath in sourceMap) && relPath !== '.')
|
|
181
|
+
.sort(comparePaths)
|
|
182
|
+
.reverse();
|
|
183
|
+
for (const relPath of removed) {
|
|
184
|
+
await removePath(path.join(targetDir, relPath)); // eslint-disable-line no-await-in-loop
|
|
185
|
+
}
|
|
186
|
+
const sourcePaths = Object.keys(sourceMap).sort(comparePaths);
|
|
187
|
+
for (const relPath of sourcePaths) {
|
|
188
|
+
if (sourceMap[relPath] !== DIR || relPath === '.')
|
|
189
|
+
continue;
|
|
190
|
+
const targetPath = path.join(targetDir, relPath);
|
|
191
|
+
if (targetMap[relPath] != null && targetMap[relPath] !== DIR) {
|
|
192
|
+
await removePath(targetPath); // eslint-disable-line no-await-in-loop
|
|
193
|
+
}
|
|
194
|
+
await fs.promises.mkdir(targetPath, { recursive: true }); // eslint-disable-line no-await-in-loop
|
|
195
|
+
}
|
|
196
|
+
for (const relPath of sourcePaths) {
|
|
197
|
+
const sourceValue = sourceMap[relPath];
|
|
198
|
+
if (typeof sourceValue !== 'string')
|
|
199
|
+
continue;
|
|
200
|
+
const sourcePath = path.join(sourceDir, relPath);
|
|
201
|
+
const targetPath = path.join(targetDir, relPath);
|
|
202
|
+
const sourceStat = await fs.promises.stat(sourcePath); // eslint-disable-line no-await-in-loop
|
|
203
|
+
const targetValue = targetMap[relPath];
|
|
204
|
+
const targetStat = typeof targetValue === 'string'
|
|
205
|
+
? await statFile(targetPath) // eslint-disable-line no-await-in-loop
|
|
206
|
+
: null;
|
|
207
|
+
if (!shouldPublish({ sourceStat, targetStat, sourceId: sourceValue, targetValue, editedSinceMs }))
|
|
208
|
+
continue;
|
|
209
|
+
await copyForWatchers(sourcePath, targetPath, sourceStat); // eslint-disable-line no-await-in-loop
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function shouldPublish({ sourceStat, targetStat, sourceId, targetValue, editedSinceMs }) {
|
|
213
|
+
if (targetStat == null || typeof targetValue !== 'string')
|
|
214
|
+
return true;
|
|
215
|
+
if (targetValue === sourceId)
|
|
216
|
+
return sourceStat.mtimeMs >= editedSinceMs;
|
|
217
|
+
return sourceStat.size !== targetStat.size ||
|
|
218
|
+
Math.abs(sourceStat.mtimeMs - targetStat.mtimeMs) > WATCH_MTIME_TOLERANCE_MS;
|
|
219
|
+
}
|
|
220
|
+
async function statFile(filePath) {
|
|
221
|
+
try {
|
|
222
|
+
return await fs.promises.stat(filePath);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (isEnoent(error))
|
|
226
|
+
return null;
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function isEnoent(error) {
|
|
231
|
+
return isError(error) && 'code' in error && error.code === 'ENOENT';
|
|
232
|
+
}
|
|
233
|
+
async function removePath(targetPath) {
|
|
234
|
+
await fs.promises.rm(targetPath, { recursive: true, force: true });
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Read and write the bytes. `fs.copyFile` may reflink on macOS, and a
|
|
238
|
+
* reflink does not notify watchers the way a new file in the injected
|
|
239
|
+
* directory does.
|
|
240
|
+
*/
|
|
241
|
+
async function copyForWatchers(sourcePath, targetPath, sourceStat) {
|
|
242
|
+
const existing = await fs.promises.lstat(targetPath).catch((error) => {
|
|
243
|
+
if (isEnoent(error))
|
|
244
|
+
return null;
|
|
245
|
+
throw error;
|
|
246
|
+
});
|
|
247
|
+
if (existing?.isDirectory() === true) {
|
|
248
|
+
await removePath(targetPath);
|
|
249
|
+
}
|
|
250
|
+
await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
|
|
251
|
+
const tempPath = pathTemp(path.dirname(targetPath));
|
|
252
|
+
try {
|
|
253
|
+
await pipeline(fs.createReadStream(sourcePath), fs.createWriteStream(tempPath, { flags: 'wx' }));
|
|
254
|
+
await fs.promises.chmod(tempPath, sourceStat.mode & 0o7777);
|
|
255
|
+
await renameFileWithRetryAsync(tempPath, targetPath);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
await fs.promises.rm(tempPath, { force: true });
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
// Best effort: a target left with a different mtime is only published again on the next poll.
|
|
262
|
+
await fs.promises.utimes(targetPath, new Date(), sourceStat.mtime).catch(() => { });
|
|
263
|
+
}
|
|
133
264
|
export class DirPatcher {
|
|
134
265
|
sourceDir;
|
|
135
266
|
targetDir;
|
package/lib/index.d.ts
CHANGED
|
@@ -11,4 +11,39 @@ export interface SyncInjectedDepsOptions {
|
|
|
11
11
|
*/
|
|
12
12
|
manifestBeforeScripts?: DependencyManifest;
|
|
13
13
|
}
|
|
14
|
+
export interface InjectedEditWatch {
|
|
15
|
+
stop: () => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The source directory and injected copies a running script should publish.
|
|
19
|
+
* `undefined` when the package has no name, no workspace, or no injected copies,
|
|
20
|
+
* or when the modules manifest cannot be read. The sync after the script
|
|
21
|
+
* reports that error, so it does not stop the script from starting.
|
|
22
|
+
*/
|
|
23
|
+
export declare function injectedEditDirs(opts: SyncInjectedDepsOptions): Promise<{
|
|
24
|
+
sourceDir: string;
|
|
25
|
+
targetDirs: string[];
|
|
26
|
+
} | undefined>;
|
|
27
|
+
/**
|
|
28
|
+
* Publish injected copies about every 200ms until `stop`. `editedSinceMs` stays
|
|
29
|
+
* two seconds behind the start of the watch, which covers one-second mtime
|
|
30
|
+
* resolution. Stop waits for an in-flight publish so the end-of-script hardlink
|
|
31
|
+
* sync does not run beside it.
|
|
32
|
+
*/
|
|
33
|
+
export declare function watchInjectedEdits(sourceDir: string, targetDirs: string[]): InjectedEditWatch;
|
|
14
34
|
export declare function syncInjectedDeps(opts: SyncInjectedDepsOptions): Promise<void>;
|
|
35
|
+
export interface SyncInjectedDepsOfModulesDirOptions {
|
|
36
|
+
/** The directory whose lockfile lists the injected dependencies. */
|
|
37
|
+
lockfileDir: string;
|
|
38
|
+
/** The modules directory that holds the `.modules.yaml` of `lockfileDir`. */
|
|
39
|
+
modulesDir: string;
|
|
40
|
+
/** The sources whose copies are synced. Copies of other sources keep their contents. */
|
|
41
|
+
sourceDirs: ReadonlySet<string>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Brings the injected copies listed in one modules directory back in step
|
|
45
|
+
* with their sources. A project with its own lockfile injects copies of
|
|
46
|
+
* workspace projects whose lifecycle scripts run in their own install, so
|
|
47
|
+
* nothing else syncs those copies after the scripts run.
|
|
48
|
+
*/
|
|
49
|
+
export declare function syncInjectedDepsOfModulesDir(opts: SyncInjectedDepsOfModulesDirOptions): Promise<void>;
|
package/lib/index.js
CHANGED
|
@@ -1,15 +1,94 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import { linkBins, linkBinsOfPackages } from '@pnpm/bins.linker';
|
|
3
4
|
import { removeBin } from '@pnpm/bins.remover';
|
|
4
5
|
import { getBinsFromPackageManifest } from '@pnpm/bins.resolver';
|
|
5
|
-
import { PnpmError } from '@pnpm/error';
|
|
6
|
+
import { isError, PnpmError } from '@pnpm/error';
|
|
6
7
|
import { readModulesManifest } from '@pnpm/installing.modules-yaml';
|
|
7
8
|
import { logger as createLogger } from '@pnpm/logger';
|
|
8
9
|
import { safeReadPackageJsonFromDir } from '@pnpm/pkg-manifest.reader';
|
|
9
10
|
import { findWorkspaceProjectsNoCheck } from '@pnpm/workspace.projects-reader';
|
|
10
11
|
import normalizePath from 'normalize-path';
|
|
11
|
-
import { DirPatcher } from './DirPatcher.js';
|
|
12
|
+
import { DirPatcher, publishEditsForWatchers, readPublishSource } from './DirPatcher.js';
|
|
12
13
|
const logger = createLogger('skip-sync-injected-deps');
|
|
14
|
+
const watchLogger = createLogger('sync-injected-deps-watch');
|
|
15
|
+
/**
|
|
16
|
+
* The source directory and injected copies a running script should publish.
|
|
17
|
+
* `undefined` when the package has no name, no workspace, or no injected copies,
|
|
18
|
+
* or when the modules manifest cannot be read. The sync after the script
|
|
19
|
+
* reports that error, so it does not stop the script from starting.
|
|
20
|
+
*/
|
|
21
|
+
export async function injectedEditDirs(opts) {
|
|
22
|
+
if (!opts.pkgName || opts.workspaceDir == null)
|
|
23
|
+
return undefined;
|
|
24
|
+
let located;
|
|
25
|
+
try {
|
|
26
|
+
located = await readInjectedTargets(opts.workspaceDir, opts.pkgRootDir);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
watchLogger.debug({
|
|
30
|
+
err,
|
|
31
|
+
message: `Not publishing injected dependencies of ${opts.pkgRootDir} while its script is running`,
|
|
32
|
+
});
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
if (located?.resolvedTargetDirs == null)
|
|
36
|
+
return undefined;
|
|
37
|
+
return { sourceDir: located.pkgRootDir, targetDirs: located.resolvedTargetDirs };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Publish injected copies about every 200ms until `stop`. `editedSinceMs` stays
|
|
41
|
+
* two seconds behind the start of the watch, which covers one-second mtime
|
|
42
|
+
* resolution. Stop waits for an in-flight publish so the end-of-script hardlink
|
|
43
|
+
* sync does not run beside it.
|
|
44
|
+
*/
|
|
45
|
+
export function watchInjectedEdits(sourceDir, targetDirs) {
|
|
46
|
+
const editedSinceMs = Date.now() - 2000;
|
|
47
|
+
let stopped = false;
|
|
48
|
+
let timer;
|
|
49
|
+
let inFlight;
|
|
50
|
+
const publish = () => {
|
|
51
|
+
if (stopped || inFlight != null)
|
|
52
|
+
return;
|
|
53
|
+
inFlight = publishToTargets(sourceDir, targetDirs, editedSinceMs).then(() => {
|
|
54
|
+
inFlight = undefined;
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
publish();
|
|
58
|
+
timer = setInterval(publish, 200);
|
|
59
|
+
return {
|
|
60
|
+
stop: async () => {
|
|
61
|
+
stopped = true;
|
|
62
|
+
if (timer != null)
|
|
63
|
+
clearInterval(timer);
|
|
64
|
+
await inFlight;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function publishToTargets(sourceDir, targetDirs, editedSinceMs) {
|
|
69
|
+
let source;
|
|
70
|
+
try {
|
|
71
|
+
source = await readPublishSource(sourceDir);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
watchLogger.debug({
|
|
75
|
+
err,
|
|
76
|
+
message: `Failed to read injected dependency ${sourceDir} while its script is running`,
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
await Promise.all(targetDirs.map(async (targetDir) => {
|
|
81
|
+
try {
|
|
82
|
+
await publishEditsForWatchers(source, targetDir, editedSinceMs);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
watchLogger.debug({
|
|
86
|
+
err,
|
|
87
|
+
message: `Failed to publish injected dependency ${targetDir} while its script is running`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
13
92
|
export async function syncInjectedDeps(opts) {
|
|
14
93
|
if (!opts.pkgName) {
|
|
15
94
|
logger.debug({
|
|
@@ -22,10 +101,8 @@ export async function syncInjectedDeps(opts) {
|
|
|
22
101
|
if (!opts.workspaceDir) {
|
|
23
102
|
throw new PnpmError('NO_WORKSPACE_DIR', 'Cannot update injected dependencies without workspace dir');
|
|
24
103
|
}
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const modules = await readModulesManifest(modulesDir);
|
|
28
|
-
if (!modules?.injectedDeps) {
|
|
104
|
+
const located = await readInjectedTargets(opts.workspaceDir, opts.pkgRootDir);
|
|
105
|
+
if (located == null) {
|
|
29
106
|
logger.debug({
|
|
30
107
|
reason: 'no-injected-deps',
|
|
31
108
|
message: 'Skipping sync of injected dependencies because none were detected',
|
|
@@ -33,9 +110,8 @@ export async function syncInjectedDeps(opts) {
|
|
|
33
110
|
});
|
|
34
111
|
return;
|
|
35
112
|
}
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
if (!targetDirs || targetDirs.length === 0) {
|
|
113
|
+
const { modules, pkgRootDir, resolvedTargetDirs } = located;
|
|
114
|
+
if (resolvedTargetDirs == null) {
|
|
39
115
|
logger.debug({
|
|
40
116
|
reason: 'no-injected-deps',
|
|
41
117
|
message: `There are no injected dependencies from ${opts.pkgRootDir}`,
|
|
@@ -43,7 +119,6 @@ export async function syncInjectedDeps(opts) {
|
|
|
43
119
|
});
|
|
44
120
|
return;
|
|
45
121
|
}
|
|
46
|
-
const resolvedTargetDirs = targetDirs.map(targetDir => path.resolve(opts.workspaceDir, targetDir));
|
|
47
122
|
const patchers = await DirPatcher.fromMultipleTargets(pkgRootDir, resolvedTargetDirs);
|
|
48
123
|
await Promise.all(patchers.map(patcher => patcher.apply()));
|
|
49
124
|
await syncBinLinks({
|
|
@@ -59,6 +134,55 @@ export async function syncInjectedDeps(opts) {
|
|
|
59
134
|
workspaceDir: opts.workspaceDir,
|
|
60
135
|
});
|
|
61
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Brings the injected copies listed in one modules directory back in step
|
|
139
|
+
* with their sources. A project with its own lockfile injects copies of
|
|
140
|
+
* workspace projects whose lifecycle scripts run in their own install, so
|
|
141
|
+
* nothing else syncs those copies after the scripts run.
|
|
142
|
+
*/
|
|
143
|
+
export async function syncInjectedDepsOfModulesDir(opts) {
|
|
144
|
+
const modules = await readModulesManifest(opts.modulesDir);
|
|
145
|
+
if (!modules?.injectedDeps)
|
|
146
|
+
return;
|
|
147
|
+
await Promise.all(Object.entries(modules.injectedDeps).map(async ([sourceId, targetDirs]) => {
|
|
148
|
+
const sourceDir = path.resolve(opts.lockfileDir, sourceId);
|
|
149
|
+
if (!opts.sourceDirs.has(sourceDir) || targetDirs.length === 0)
|
|
150
|
+
return;
|
|
151
|
+
// A publish directory the build did not produce leaves the copies alone
|
|
152
|
+
// rather than emptying them.
|
|
153
|
+
if (!await dirExists(sourceDir))
|
|
154
|
+
return;
|
|
155
|
+
const resolvedTargetDirs = targetDirs.map((targetDir) => path.resolve(opts.lockfileDir, targetDir));
|
|
156
|
+
const patchers = await DirPatcher.fromMultipleTargets(sourceDir, resolvedTargetDirs);
|
|
157
|
+
await Promise.all(patchers.map(patcher => patcher.apply()));
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
async function dirExists(dir) {
|
|
161
|
+
try {
|
|
162
|
+
await fs.promises.stat(dir);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
if (isError(err) && 'code' in err && err.code === 'ENOENT')
|
|
167
|
+
return false;
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function readInjectedTargets(workspaceDir, pkgRootDirInput) {
|
|
172
|
+
const pkgRootDir = path.resolve(workspaceDir, pkgRootDirInput);
|
|
173
|
+
const modules = await readModulesManifest(path.resolve(workspaceDir, 'node_modules'));
|
|
174
|
+
if (modules?.injectedDeps == null)
|
|
175
|
+
return undefined;
|
|
176
|
+
const injectedDepKey = normalizePath(path.relative(workspaceDir, pkgRootDir), true);
|
|
177
|
+
const targetDirs = modules.injectedDeps[injectedDepKey];
|
|
178
|
+
return {
|
|
179
|
+
modules,
|
|
180
|
+
pkgRootDir,
|
|
181
|
+
resolvedTargetDirs: targetDirs == null || targetDirs.length === 0
|
|
182
|
+
? undefined
|
|
183
|
+
: targetDirs.map(targetDir => path.resolve(workspaceDir, targetDir)),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
62
186
|
/** The commands a package declares, or none when it declares no bins. */
|
|
63
187
|
async function readBinNames(pkgDir) {
|
|
64
188
|
const manifest = await safeReadPackageJsonFromDir(pkgDir);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/workspace.injected-deps-syncer",
|
|
3
|
-
"version": "1100.0.
|
|
3
|
+
"version": "1100.0.40",
|
|
4
4
|
"description": "Update all injected replica of a workspace package",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -27,18 +27,20 @@
|
|
|
27
27
|
"!*.map"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@pnpm/bins.linker": "1100.0.
|
|
31
|
-
"@pnpm/bins.remover": "1100.0.
|
|
32
|
-
"@pnpm/bins.resolver": "1100.0.
|
|
33
|
-
"@pnpm/error": "1100.1
|
|
34
|
-
"@pnpm/fetching.directory-fetcher": "1100.0.
|
|
35
|
-
"@pnpm/
|
|
36
|
-
"@pnpm/
|
|
30
|
+
"@pnpm/bins.linker": "1100.0.34",
|
|
31
|
+
"@pnpm/bins.remover": "1100.0.26",
|
|
32
|
+
"@pnpm/bins.resolver": "1100.0.17",
|
|
33
|
+
"@pnpm/error": "1100.2.1",
|
|
34
|
+
"@pnpm/fetching.directory-fetcher": "1100.0.36",
|
|
35
|
+
"@pnpm/fs.graceful-fs": "1100.2.4",
|
|
36
|
+
"@pnpm/installing.modules-yaml": "1101.0.5",
|
|
37
|
+
"@pnpm/pkg-manifest.reader": "1100.0.21",
|
|
37
38
|
"@pnpm/text.ordinal-comparator": "1100.0.0",
|
|
38
|
-
"@pnpm/types": "1102.1.
|
|
39
|
-
"@pnpm/workspace.projects-reader": "1101.
|
|
39
|
+
"@pnpm/types": "1102.1.1",
|
|
40
|
+
"@pnpm/workspace.projects-reader": "1101.1.1",
|
|
40
41
|
"@types/normalize-path": "^3.0.2",
|
|
41
|
-
"normalize-path": "^3.0.0"
|
|
42
|
+
"normalize-path": "^3.0.0",
|
|
43
|
+
"path-temp": "^3.0.0"
|
|
42
44
|
},
|
|
43
45
|
"peerDependencies": {
|
|
44
46
|
"@pnpm/logger": "^1100.0.0"
|
|
@@ -46,8 +48,8 @@
|
|
|
46
48
|
"devDependencies": {
|
|
47
49
|
"@jest/globals": "30.4.1",
|
|
48
50
|
"@pnpm/logger": "1100.0.0",
|
|
49
|
-
"@pnpm/prepare": "1100.0.
|
|
50
|
-
"@pnpm/workspace.injected-deps-syncer": "1100.0.
|
|
51
|
+
"@pnpm/prepare": "1100.0.33",
|
|
52
|
+
"@pnpm/workspace.injected-deps-syncer": "1100.0.40",
|
|
51
53
|
"@types/is-windows": "^1.0.2",
|
|
52
54
|
"is-windows": "^1.0.2"
|
|
53
55
|
},
|