@@ -19,7 +19,26 @@ import { pnpmSyncGetJsonVersion } from './utilities';
1919/**
2020 * @beta
2121 */
22- export interface IPnpmSyncPrepareOptions {
22+ export interface IPnpmSyncUpdateFileBaseOptions {
23+ /**
24+ * A lockfileId that can be used to recognize the `pnpm-lock.yaml`
25+ */
26+ lockfileId ?: string ;
27+
28+ /**
29+ * A callback for reporting events during the operation.
30+ *
31+ * @remarks
32+ * `LogMessageKind.ERROR` events do NOT cause the promise to reject,
33+ * so they must be handled appropriately.
34+ */
35+ logMessageCallback : ( options : ILogMessageCallbackOptions ) => void ;
36+ }
37+
38+ /**
39+ * @beta
40+ */
41+ export interface IPnpmSyncPrepareOptions extends IPnpmSyncUpdateFileBaseOptions {
2342 /**
2443 * The path to the `pnpm-lock.yaml` file
2544 */
@@ -30,11 +49,6 @@ export interface IPnpmSyncPrepareOptions {
3049 */
3150 dotPnpmFolder : string ;
3251
33- /**
34- * A lockfileId that can be used to recognize the `pnpm-lock.yaml`
35- */
36- lockfileId ?: string ;
37-
3852 /**
3953 * Environment-provided API to avoid an NPM dependency.
4054 * The "pnpm-sync" NPM package provides a reference implementation.
@@ -49,15 +63,121 @@ export interface IPnpmSyncPrepareOptions {
4963 lockfilePath : string ,
5064 options : { ignoreIncompatible : boolean }
5165 ) => Promise < ILockfile | undefined > ;
66+ }
5267
68+ /**
69+ * @beta
70+ */
71+ export interface IPnpmSyncUpdateFileOptions extends IPnpmSyncUpdateFileBaseOptions {
5372 /**
54- * A callback for reporting events during the operation.
73+ * The folder path of the project whose build outputs will get synced into the node_modules folder
74+ * of dependent projects.
75+ * @remarks
76+ * It be an absolute file path, such as `/path/to/my-library`, which will have a sync file
77+ * `/path/to/my-library/node_modules/.pnpm-sync.json`
78+ */
79+ sourceProjectFolder : string ;
80+
81+ /**
82+ * A list of destination folders that `pnpmSyncCopyAsync()` will copy into.
5583 *
5684 * @remarks
57- * `LogMessageKind.ERROR` events do NOT cause the promise to reject,
58- * so they must be handled appropriately.
85+ * Each string should be an absolute file path to a folder, whose name will generally
86+ * be the same as the `sourceProjectFolder`'s directory name.
87+ *
88+ * For example:
89+ * `/path/to/my-workspace/node_modules/.pnpm/my-library@1.2.3/node_modules/my-library`
90+ *
91+ * When your tool needs to update this set of files, providing the `IPnpmSyncUpdateFileBaseOptions.lockfileId`
92+ * will enable `pnpmSyncUpdateFileAsync()` to clear the old target folders from `.pnpm-sync.json`
93+ * before adding the new ones.
5994 */
60- logMessageCallback : ( options : ILogMessageCallbackOptions ) => void ;
95+ targetFolders : Array < string > ;
96+ }
97+
98+ /**
99+ * @beta
100+ */
101+ export async function pnpmSyncUpdateFileAsync ( options : IPnpmSyncUpdateFileOptions ) : Promise < void > {
102+ const { lockfileId, logMessageCallback, sourceProjectFolder, targetFolders } = options ;
103+
104+ const pnpmSyncJsonFolder = `${ sourceProjectFolder } /node_modules` ;
105+ const pnpmSyncJsonPath = `${ pnpmSyncJsonFolder } /.pnpm-sync.json` ;
106+ const expectedPnpmSyncJsonVersion : string = pnpmSyncGetJsonVersion ( ) ;
107+
108+ let pnpmSyncJsonFile : IPnpmSyncJson = {
109+ version : expectedPnpmSyncJsonVersion ,
110+ postbuildInjectedCopy : {
111+ sourceFolder : '..' , // path from pnpmSyncJsonFolder to sourceProjectFolder
112+ targetFolders : [ ]
113+ }
114+ } ;
115+
116+ // if .pnpm-sync.json already exists, read it first
117+ if ( fs . existsSync ( pnpmSyncJsonPath ) ) {
118+ let existingPnpmSyncJsonFile : IPnpmSyncJson | undefined ;
119+ try {
120+ existingPnpmSyncJsonFile = JSON . parse ( fs . readFileSync ( pnpmSyncJsonPath ) . toString ( ) ) ;
121+ } catch ( e ) {
122+ // no-catch
123+ // Regenerate .pnpm-sync.json when failed to load the current one
124+ }
125+
126+ if ( existingPnpmSyncJsonFile ) {
127+ const actualPnpmSyncJsonVersion : string = existingPnpmSyncJsonFile . version ;
128+ if ( actualPnpmSyncJsonVersion === expectedPnpmSyncJsonVersion ) {
129+ // If a lockfileId is provided
130+ // then all entries with this lockfileId should be deleted
131+ // they will be regenerated later
132+ if ( lockfileId ) {
133+ const filteredTargetFolders = existingPnpmSyncJsonFile . postbuildInjectedCopy . targetFolders . filter (
134+ ( targetFolder ) => targetFolder ?. lockfileId !== lockfileId
135+ ) ;
136+ existingPnpmSyncJsonFile . postbuildInjectedCopy . targetFolders = filteredTargetFolders ;
137+ }
138+ pnpmSyncJsonFile = existingPnpmSyncJsonFile ;
139+ } else {
140+ logMessageCallback ( {
141+ message : `The .pnpm-sync.json file in ${ pnpmSyncJsonFolder } has an incompatible version; pnpm-sync will regenerate it.` ,
142+ messageKind : LogMessageKind . VERBOSE ,
143+ details : {
144+ messageIdentifier : LogMessageIdentifier . PREPARE_REPLACING_FILE ,
145+ pnpmSyncJsonPath,
146+ sourceProjectFolder,
147+ actualVersion : actualPnpmSyncJsonVersion ,
148+ expectedVersion : expectedPnpmSyncJsonVersion
149+ }
150+ } ) ;
151+ }
152+ }
153+ }
154+
155+ const existingTargetFolderSet : Set < string > = new Set ( ) ;
156+
157+ for ( const targetFolder of pnpmSyncJsonFile . postbuildInjectedCopy . targetFolders ) {
158+ existingTargetFolderSet . add ( targetFolder . folderPath ) ;
159+ }
160+
161+ for ( const targetFolder of targetFolders ) {
162+ let relativePath : string = path . relative ( pnpmSyncJsonFolder , targetFolder ) ;
163+
164+ // the final path in .pnpm-sync.json will always in posix style
165+ relativePath = relativePath . split ( path . sep ) . join ( path . posix . sep ) ;
166+
167+ if ( ! existingTargetFolderSet . has ( relativePath ) ) {
168+ const targetFolderItem : ITargetFolder = {
169+ folderPath : relativePath
170+ } ;
171+
172+ if ( lockfileId ) {
173+ targetFolderItem . lockfileId = lockfileId ;
174+ }
175+
176+ pnpmSyncJsonFile . postbuildInjectedCopy . targetFolders . push ( targetFolderItem ) ;
177+ }
178+ }
179+
180+ await fs . promises . writeFile ( pnpmSyncJsonPath , JSON . stringify ( pnpmSyncJsonFile , null , 2 ) ) ;
61181}
62182
63183/**
@@ -150,7 +270,7 @@ export async function pnpmSyncPrepareAsync(options: IPnpmSyncPrepareOptions): Pr
150270 const pnpmLockFolder = path . dirname ( lockfilePath ) ;
151271
152272 // generate a map, where key is the absolute path of the injectedDependency, value is all available paths in .pnpm folder
153- const injectedDependencyToFilePathSet : Map < string , Set < string > > = new Map ( ) ;
273+ const injectedDependencyToFilePathSet : Map < string , Array < string > > = new Map ( ) ;
154274 for ( const [ injectedDependency , injectedDependencyVersionSet ] of injectedDependencyToVersion ) {
155275 for ( const injectedDependencyVersion of injectedDependencyVersionSet ) {
156276 // this logic is heavily depends on pnpm-lock formate
@@ -159,7 +279,7 @@ export async function pnpmSyncPrepareAsync(options: IPnpmSyncPrepareOptions): Pr
159279 let injectedDependencyPath = injectedDependencyVersion . split ( '(' ) [ 0 ] . slice ( 'file:' . length ) ;
160280 injectedDependencyPath = path . resolve ( pnpmLockFolder , injectedDependencyPath ) ;
161281 if ( ! injectedDependencyToFilePathSet . has ( injectedDependencyPath ) ) {
162- injectedDependencyToFilePathSet . set ( injectedDependencyPath , new Set ( ) ) ;
282+ injectedDependencyToFilePathSet . set ( injectedDependencyPath , [ ] ) ;
163283 }
164284
165285 const fullPackagePath = path . join (
@@ -169,18 +289,18 @@ export async function pnpmSyncPrepareAsync(options: IPnpmSyncPrepareOptions): Pr
169289 injectedDependency
170290 ) ;
171291
172- injectedDependencyToFilePathSet . get ( injectedDependencyPath ) ?. add ( fullPackagePath ) ;
292+ injectedDependencyToFilePathSet . get ( injectedDependencyPath ) ?. push ( fullPackagePath ) ;
173293 }
174294 }
175295
176296 // now, we have everything we need to generate the the .pnpm-sync.json
177297 // console.log('injectedDependencyToFilePathSet =>', injectedDependencyToFilePathSet);
178- for ( const [ projectFolder , targetFolderSet ] of injectedDependencyToFilePathSet ) {
179- if ( targetFolderSet . size === 0 ) {
298+ for ( const [ sourceProjectFolder , targetFolders ] of injectedDependencyToFilePathSet ) {
299+ if ( targetFolders . length === 0 ) {
180300 continue ;
181301 }
182302
183- const pnpmSyncJsonFolder = `${ projectFolder } /node_modules` ;
303+ const pnpmSyncJsonFolder = `${ sourceProjectFolder } /node_modules` ;
184304 const pnpmSyncJsonPath = `${ pnpmSyncJsonFolder } /.pnpm-sync.json` ;
185305
186306 logMessageCallback ( {
@@ -189,7 +309,7 @@ export async function pnpmSyncPrepareAsync(options: IPnpmSyncPrepareOptions): Pr
189309 details : {
190310 messageIdentifier : LogMessageIdentifier . PREPARE_WRITING_FILE ,
191311 pnpmSyncJsonPath,
192- projectFolder
312+ sourceProjectFolder
193313 }
194314 } ) ;
195315
@@ -202,81 +322,12 @@ export async function pnpmSyncPrepareAsync(options: IPnpmSyncPrepareOptions): Pr
202322 await ensureFolderAsync ( pnpmSyncJsonFolder ) ;
203323 }
204324
205- const expectedPnpmSyncJsonVersion : string = pnpmSyncGetJsonVersion ( ) ;
206-
207- let pnpmSyncJsonFile : IPnpmSyncJson = {
208- version : expectedPnpmSyncJsonVersion ,
209- postbuildInjectedCopy : {
210- sourceFolder : '..' , // path from pnpmSyncJsonFolder to projectFolder
211- targetFolders : [ ]
212- }
213- } ;
214-
215- // if .pnpm-sync.json already exists, read it first
216- if ( fs . existsSync ( pnpmSyncJsonPath ) ) {
217- let existingPnpmSyncJsonFile : IPnpmSyncJson | undefined ;
218- try {
219- existingPnpmSyncJsonFile = JSON . parse ( fs . readFileSync ( pnpmSyncJsonPath ) . toString ( ) ) ;
220- } catch ( e ) {
221- // no-catch
222- // Regenerate .pnpm-sync.json when failed to load the current one
223- }
224-
225- if ( existingPnpmSyncJsonFile ) {
226- const actualPnpmSyncJsonVersion : string = existingPnpmSyncJsonFile . version ;
227- if ( actualPnpmSyncJsonVersion === expectedPnpmSyncJsonVersion ) {
228- // If a lockfileId is provided
229- // then all entries with this lockfileId should be deleted
230- // they will be regenerated later
231- if ( lockfileId ) {
232- const filteredTargetFolders = existingPnpmSyncJsonFile . postbuildInjectedCopy . targetFolders . filter (
233- ( targetFolder ) => targetFolder ?. lockfileId !== lockfileId
234- ) ;
235- existingPnpmSyncJsonFile . postbuildInjectedCopy . targetFolders = filteredTargetFolders ;
236- }
237- pnpmSyncJsonFile = existingPnpmSyncJsonFile ;
238- } else {
239- logMessageCallback ( {
240- message : `The .pnpm-sync.json file in ${ pnpmSyncJsonFolder } has an incompatible version; pnpm-sync will regenerate it.` ,
241- messageKind : LogMessageKind . VERBOSE ,
242- details : {
243- messageIdentifier : LogMessageIdentifier . PREPARE_REPLACING_FILE ,
244- pnpmSyncJsonPath,
245- projectFolder,
246- actualVersion : actualPnpmSyncJsonVersion ,
247- expectedVersion : expectedPnpmSyncJsonVersion
248- }
249- } ) ;
250- }
251- }
252- }
253-
254- const existingTargetFolderSet : Set < string > = new Set ( ) ;
255-
256- for ( const targetFolder of pnpmSyncJsonFile . postbuildInjectedCopy . targetFolders ) {
257- existingTargetFolderSet . add ( targetFolder . folderPath ) ;
258- }
259-
260- for ( const targetFolder of targetFolderSet ) {
261- let relativePath : string = path . relative ( pnpmSyncJsonFolder , targetFolder ) ;
262-
263- // the final path in .pnpm-sync.json will always in posix style
264- relativePath = relativePath . split ( path . sep ) . join ( path . posix . sep ) ;
265-
266- if ( ! existingTargetFolderSet . has ( relativePath ) ) {
267- const targetFolderItem : ITargetFolder = {
268- folderPath : relativePath
269- } ;
270-
271- if ( lockfileId ) {
272- targetFolderItem . lockfileId = lockfileId ;
273- }
274-
275- pnpmSyncJsonFile . postbuildInjectedCopy . targetFolders . push ( targetFolderItem ) ;
276- }
277- }
278-
279- await fs . promises . writeFile ( pnpmSyncJsonPath , JSON . stringify ( pnpmSyncJsonFile , null , 2 ) ) ;
325+ await pnpmSyncUpdateFileAsync ( {
326+ sourceProjectFolder,
327+ targetFolders,
328+ logMessageCallback,
329+ lockfileId
330+ } ) ;
280331 }
281332
282333 const endTime = process . hrtime . bigint ( ) ;
0 commit comments