Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[package-extractor] Allow for a custom link creation script path to be used for the create-links.js file #4918

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/package-extractor",
"comment": "Add the ability to change where the \"create-links.js\" file and the associated metadata file are generated when running in the \"script\" linkCreation mode",
"type": "minor"
}
],
"packageName": "@rushstack/package-extractor"
}
1 change: 1 addition & 0 deletions common/reviews/api/package-extractor.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface IExtractorOptions {
includeDevDependencies?: boolean;
includeNpmIgnoreFiles?: boolean;
linkCreation?: 'default' | 'script' | 'none';
linkCreationScriptPath?: string;
mainProjectName: string;
overwriteExisting: boolean;
pnpmInstallFolder?: string;
Expand Down
66 changes: 45 additions & 21 deletions libraries/package-extractor/src/PackageExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
FileSystem,
Import,
JsonFile,
AlreadyExistsBehavior,
type IPackageJson
} from '@rushstack/node-core-library';
import { Colorize, type ITerminal } from '@rushstack/terminal';
Expand All @@ -36,6 +35,9 @@ declare module 'npm-packlist' {
}
}

export const TARGET_ROOT_SCRIPT_RELATIVE_PATH_TEMPLATE_STRING: '{TARGET_ROOT_SCRIPT_RELATIVE_PATH}' =
'{TARGET_ROOT_SCRIPT_RELATIVE_PATH}';

/**
* Part of the extractor-matadata.json file format. Represents an extracted project.
*
Expand Down Expand Up @@ -188,7 +190,7 @@ export interface IExtractorOptions {

/**
* Whether to skip copying files to the extraction target directory, and only create an extraction
* archive. This is only supported when linkCreation is 'script' or 'none'.
* archive. This is only supported when {@link IExtractorOptions.linkCreation} is 'script' or 'none'.
*/
createArchiveOnly?: boolean;

Expand Down Expand Up @@ -224,6 +226,12 @@ export interface IExtractorOptions {
*/
linkCreation?: 'default' | 'script' | 'none';

/**
* The path to the generated link creation script. This is only used when {@link IExtractorOptions.linkCreation}
* is 'script'.
*/
linkCreationScriptPath?: string;

/**
* An additional folder containing files which will be copied into the root of the extraction.
*/
Expand Down Expand Up @@ -363,7 +371,9 @@ export class PackageExtractor {
sourceRootFolder,
targetRootFolder,
folderToCopy: additionalFolderToCopy,
linkCreation
linkCreation,
linkCreationScriptPath,
createArchiveOnly
} = options;
const { projectConfigurationsByName, foldersToCopy, symlinkAnalyzer } = state;

Expand Down Expand Up @@ -395,7 +405,7 @@ export class PackageExtractor {
await this._collectFoldersAsync(projectFolder, options, state);
}

if (!options.createArchiveOnly) {
if (!createArchiveOnly) {
terminal.writeLine(`Copying folders to target folder "${targetRootFolder}"`);
}
await Async.forEachAsync(
Expand All @@ -415,25 +425,31 @@ export class PackageExtractor {
const additionalFolderExtractorOptions: IExtractorOptions = {
...options,
sourceRootFolder: additionalFolderPath,
targetRootFolder: options.targetRootFolder
targetRootFolder
};
await this._extractFolderAsync(additionalFolderPath, additionalFolderExtractorOptions, state);
}

switch (linkCreation) {
case 'script': {
const sourceFilePath: string = path.join(scriptsFolderPath, createLinksScriptFilename);
if (!options.createArchiveOnly) {
terminal.writeLine(`Creating ${createLinksScriptFilename}`);
await FileSystem.copyFileAsync({
sourcePath: sourceFilePath,
destinationPath: path.join(targetRootFolder, createLinksScriptFilename),
alreadyExistsBehavior: AlreadyExistsBehavior.Error
D4N14L marked this conversation as resolved.
Show resolved Hide resolved
terminal.writeLine(`Creating ${createLinksScriptFilename}`);
const createLinksSourceFilePath: string = `${scriptsFolderPath}/${createLinksScriptFilename}`;
const createLinksTargetFilePath: string = linkCreationScriptPath
? path.resolve(targetRootFolder, linkCreationScriptPath)
: `${targetRootFolder}/${createLinksScriptFilename}`;
let createLinksScriptContent: string = await FileSystem.readFileAsync(createLinksSourceFilePath);
createLinksScriptContent = createLinksScriptContent.replace(
TARGET_ROOT_SCRIPT_RELATIVE_PATH_TEMPLATE_STRING,
Path.convertToSlashes(path.relative(path.dirname(createLinksTargetFilePath), targetRootFolder))
);
if (!createArchiveOnly) {
await FileSystem.writeFileAsync(createLinksTargetFilePath, createLinksScriptContent, {
ensureFolderExists: true
});
}
await state.archiver?.addToArchiveAsync({
filePath: sourceFilePath,
archivePath: createLinksScriptFilename
fileData: createLinksScriptContent,
archivePath: path.relative(targetRootFolder, createLinksTargetFilePath)
});
break;
}
Expand Down Expand Up @@ -951,11 +967,18 @@ export class PackageExtractor {
options: IExtractorOptions,
state: IExtractorState
): Promise<void> {
const { mainProjectName, targetRootFolder } = options;
const { mainProjectName, targetRootFolder, linkCreation, linkCreationScriptPath } = options;
const { projectConfigurationsByPath } = state;

const extractorMetadataFileName: string = 'extractor-metadata.json';
const extractorMetadataFilePath: string = path.join(targetRootFolder, extractorMetadataFileName);
const extractorMetadataFolderPath: string =
linkCreation === 'script' && linkCreationScriptPath
? path.dirname(path.resolve(targetRootFolder, linkCreationScriptPath))
: targetRootFolder;
const extractorMetadataFilePath: string = path.join(
extractorMetadataFolderPath,
extractorMetadataFileName
);
const extractorMetadataJson: IExtractorMetadataJson = {
mainProjectName,
projects: [],
Expand Down Expand Up @@ -983,12 +1006,13 @@ export class PackageExtractor {

const extractorMetadataFileContent: string = JSON.stringify(extractorMetadataJson, undefined, 0);
if (!options.createArchiveOnly) {
await FileSystem.writeFileAsync(extractorMetadataFilePath, extractorMetadataFileContent);
await FileSystem.writeFileAsync(extractorMetadataFilePath, extractorMetadataFileContent, {
ensureFolderExists: true
});
}
await state.archiver?.addToArchiveAsync({
fileData: extractorMetadataFileContent,
archivePath: extractorMetadataFileName
});

const archivePath: string = path.relative(targetRootFolder, extractorMetadataFilePath);
await state.archiver?.addToArchiveAsync({ archivePath, fileData: extractorMetadataFileContent });
}

private async _makeBinLinksAsync(options: IExtractorOptions, state: IExtractorState): Promise<void> {
Expand Down
23 changes: 14 additions & 9 deletions libraries/package-extractor/src/scripts/create-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@

import * as fs from 'fs';
import * as path from 'path';
import type { IExtractorMetadataJson } from '../PackageExtractor';
import type { IFileSystemCreateLinkOptions } from '@rushstack/node-core-library';
import type {
TARGET_ROOT_SCRIPT_RELATIVE_PATH_TEMPLATE_STRING as TargetRootScriptRelativePathTemplateString,
IExtractorMetadataJson
} from '../PackageExtractor';

const TARGET_ROOT_SCRIPT_RELATIVE_PATH: typeof TargetRootScriptRelativePathTemplateString =
'{TARGET_ROOT_SCRIPT_RELATIVE_PATH}';
const TARGET_ROOT_FOLDER: string = path.resolve(__dirname, TARGET_ROOT_SCRIPT_RELATIVE_PATH);

// API borrowed from @rushstack/node-core-library, since this script avoids using any
// NPM dependencies.
Expand Down Expand Up @@ -105,9 +112,7 @@ function main(): boolean {
return false;
}

const targetRootFolder: string = __dirname;
const extractorMetadataPath: string = path.join(targetRootFolder, 'extractor-metadata.json');

const extractorMetadataPath: string = `${__dirname}/extractor-metadata.json`;
if (!fs.existsSync(extractorMetadataPath)) {
throw new Error('Input file not found: ' + extractorMetadataPath);
}
Expand All @@ -116,12 +121,12 @@ function main(): boolean {
const extractorMetadataObject: IExtractorMetadataJson = JSON.parse(extractorMetadataJson);

if (args[0] === 'create') {
console.log(`\nCreating links for extraction at path "${targetRootFolder}"`);
removeLinks(targetRootFolder, extractorMetadataObject);
createLinks(targetRootFolder, extractorMetadataObject);
console.log(`\nCreating links for extraction at path "${TARGET_ROOT_FOLDER}"`);
removeLinks(TARGET_ROOT_FOLDER, extractorMetadataObject);
createLinks(TARGET_ROOT_FOLDER, extractorMetadataObject);
} else {
console.log(`\nRemoving links for extraction at path "${targetRootFolder}"`);
removeLinks(targetRootFolder, extractorMetadataObject);
console.log(`\nRemoving links for extraction at path "${TARGET_ROOT_FOLDER}"`);
removeLinks(TARGET_ROOT_FOLDER, extractorMetadataObject);
}

console.log('The operation completed successfully.');
Expand Down
135 changes: 134 additions & 1 deletion libraries/package-extractor/src/test/PackageExtractor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
// See LICENSE in the project root for license information.

import path from 'path';
import type { ChildProcess } from 'child_process';

import { FileSystem } from '@rushstack/node-core-library';
import { Executable, FileSystem } from '@rushstack/node-core-library';
import { Terminal, StringBufferTerminalProvider } from '@rushstack/terminal';
import { PackageExtractor, type IExtractorProjectConfiguration } from '../PackageExtractor';

Expand Down Expand Up @@ -464,4 +465,136 @@ describe(PackageExtractor.name, () => {
await expect(FileSystem.existsAsync(path.join(targetFolder, 'package.json'))).resolves.toBe(true);
await expect(FileSystem.existsAsync(path.join(targetFolder, 'src', 'index.js'))).resolves.toBe(true);
});

it('should extract project with script linkCreation', async () => {
const targetFolder: string = path.join(extractorTargetFolder, 'extractor-output-10');

await expect(
packageExtractor.extractAsync({
mainProjectName: project1PackageName,
sourceRootFolder: repoRoot,
targetRootFolder: targetFolder,
overwriteExisting: true,
projectConfigurations: getDefaultProjectConfigurations(),
terminal,
includeNpmIgnoreFiles: true,
linkCreation: 'script',
includeDevDependencies: true
})
).resolves.not.toThrow();

// Validate project 1 files
await expect(
FileSystem.existsAsync(path.join(targetFolder, project1RelativePath, 'src', 'index.js'))
).resolves.toBe(true);

// Validate project 2 is not linked through node_modules
const project1NodeModulesPath: string = path.join(targetFolder, project1RelativePath, 'node_modules');
await expect(
FileSystem.existsAsync(path.join(project1NodeModulesPath, project2PackageName, 'src', 'index.js'))
).resolves.toEqual(false);

// Validate project 3 is not linked through node_modules
await expect(
FileSystem.existsAsync(path.join(project1NodeModulesPath, project3PackageName, 'src', 'index.js'))
).resolves.toEqual(false);

// Run the linkCreation script
const createLinksProcess: ChildProcess = Executable.spawn(process.argv0, [
path.join(targetFolder, 'create-links.js'),
'create'
]);
await expect(
Executable.waitForExitAsync(createLinksProcess, { throwOnNonZeroExitCode: true })
).resolves.not.toThrow();

// Validate project 2 is linked through node_modules
await expect(
FileSystem.getRealPathAsync(path.join(project1NodeModulesPath, project2PackageName, 'src', 'index.js'))
).resolves.toEqual(path.join(targetFolder, project2RelativePath, 'src', 'index.js'));

// Validate project 3 is linked through node_modules
await expect(
FileSystem.getRealPathAsync(path.join(project1NodeModulesPath, project3PackageName, 'src', 'index.js'))
).resolves.toEqual(path.join(targetFolder, project3RelativePath, 'src', 'index.js'));
await expect(
FileSystem.getRealPathAsync(
path.join(
project1NodeModulesPath,
project2PackageName,
'node_modules',
project3PackageName,
'src',
'index.js'
)
)
).resolves.toEqual(path.join(targetFolder, project3RelativePath, 'src', 'index.js'));
});

it('should extract project with script linkCreation and custom linkCreationScriptPath', async () => {
const targetFolder: string = path.join(extractorTargetFolder, 'extractor-output-11');
const linkCreationScriptPath: string = path.join(targetFolder, 'foo', 'bar', 'baz.js');

await expect(
packageExtractor.extractAsync({
mainProjectName: project1PackageName,
sourceRootFolder: repoRoot,
targetRootFolder: targetFolder,
overwriteExisting: true,
projectConfigurations: getDefaultProjectConfigurations(),
terminal,
includeNpmIgnoreFiles: true,
linkCreation: 'script',
linkCreationScriptPath,
includeDevDependencies: true
})
).resolves.not.toThrow();

// Validate project 1 files
await expect(
FileSystem.existsAsync(path.join(targetFolder, project1RelativePath, 'src', 'index.js'))
).resolves.toBe(true);

// Validate project 2 is not linked through node_modules
const project1NodeModulesPath: string = path.join(targetFolder, project1RelativePath, 'node_modules');
await expect(
FileSystem.existsAsync(path.join(project1NodeModulesPath, project2PackageName, 'src', 'index.js'))
).resolves.toEqual(false);

// Validate project 3 is not linked through node_modules
await expect(
FileSystem.existsAsync(path.join(project1NodeModulesPath, project3PackageName, 'src', 'index.js'))
).resolves.toEqual(false);

// Run the linkCreation script
const createLinksProcess: ChildProcess = Executable.spawn(process.argv0, [
linkCreationScriptPath,
'create'
]);
await expect(
Executable.waitForExitAsync(createLinksProcess, { throwOnNonZeroExitCode: true })
).resolves.not.toThrow();

// Validate project 2 is linked through node_modules
await expect(
FileSystem.getRealPathAsync(path.join(project1NodeModulesPath, project2PackageName, 'src', 'index.js'))
).resolves.toEqual(path.join(targetFolder, project2RelativePath, 'src', 'index.js'));

// Validate project 3 is linked through node_modules
await expect(
FileSystem.getRealPathAsync(path.join(project1NodeModulesPath, project3PackageName, 'src', 'index.js'))
).resolves.toEqual(path.join(targetFolder, project3RelativePath, 'src', 'index.js'));
await expect(
FileSystem.getRealPathAsync(
path.join(
project1NodeModulesPath,
project2PackageName,
'node_modules',
project3PackageName,
'src',
'index.js'
)
)
).resolves.toEqual(path.join(targetFolder, project3RelativePath, 'src', 'index.js'));
});
});
Loading