feat: support yarn workspaces (#250)
Co-authored-by: Mark Lee <malept@users.noreply.github.com>
This commit is contained in:
Родитель
94cb04d9a4
Коммит
367e08129a
|
@ -7,6 +7,7 @@ import ora from 'ora';
|
|||
import * as argParser from 'yargs';
|
||||
|
||||
import { rebuild, ModuleType } from './rebuild';
|
||||
import { getProjectRootPath } from './search-module';
|
||||
import { locateElectronModule } from './electron-locator';
|
||||
|
||||
const yargs = argParser
|
||||
|
@ -68,7 +69,8 @@ process.on('unhandledRejection', handler);
|
|||
|
||||
|
||||
(async (): Promise<void> => {
|
||||
const electronModulePath = argv.e ? path.resolve(process.cwd(), (argv.e as string)) : locateElectronModule();
|
||||
const projectRootPath = await getProjectRootPath(process.cwd());
|
||||
const electronModulePath = argv.e ? path.resolve(process.cwd(), (argv.e as string)) : await locateElectronModule(projectRootPath);
|
||||
let electronModuleVersion = argv.v as string;
|
||||
|
||||
if (!electronModuleVersion) {
|
||||
|
@ -128,6 +130,7 @@ process.on('unhandledRejection', handler);
|
|||
mode: argv.p ? 'parallel' : (argv.s ? 'sequential' : undefined),
|
||||
debug: argv.b as boolean,
|
||||
prebuildTagPrefix: (argv.prebuildTagPrefix as string) || 'v',
|
||||
projectRootPath,
|
||||
});
|
||||
|
||||
const lifecycle = rebuilder.lifecycle;
|
||||
|
|
|
@ -1,38 +1,31 @@
|
|||
import * as fs from 'fs';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { searchForModule } from './search-module';
|
||||
|
||||
const electronModuleNames = ['electron', 'electron-prebuilt', 'electron-prebuilt-compile'];
|
||||
const relativeNodeModulesDir = path.resolve(__dirname, '..', '..');
|
||||
|
||||
function locateModules(pathMapper: (moduleName: string) => string | null): string[] {
|
||||
const possibleModulePaths = electronModuleNames.map(pathMapper);
|
||||
return possibleModulePaths.filter((modulePath) => modulePath && fs.existsSync(path.join(modulePath, 'package.json'))) as string[];
|
||||
}
|
||||
|
||||
function locateSiblingModules(): string[] {
|
||||
return locateModules((moduleName) => path.join(relativeNodeModulesDir, moduleName));
|
||||
}
|
||||
|
||||
function locateModulesByRequire(): string[] | null {
|
||||
return locateModules((moduleName) => {
|
||||
async function locateModuleByRequire(): Promise<string | null> {
|
||||
for (const moduleName of electronModuleNames) {
|
||||
try {
|
||||
return path.resolve(require.resolve(path.join(moduleName, 'package.json')), '..');
|
||||
} catch (error) {
|
||||
return null;
|
||||
const modulePath = path.resolve(require.resolve(path.join(moduleName, 'package.json')), '..');
|
||||
if (await fs.pathExists(path.join(modulePath, 'package.json'))) {
|
||||
return modulePath;
|
||||
}
|
||||
} catch (_error) { // eslint-disable-line no-empty
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function locateElectronModule(): string | null {
|
||||
const siblingModules: string[] | null = locateSiblingModules();
|
||||
if (siblingModules.length > 0) {
|
||||
return siblingModules[0];
|
||||
}
|
||||
|
||||
const requiredModules = locateModulesByRequire();
|
||||
if (requiredModules && requiredModules.length > 0) {
|
||||
return requiredModules[0];
|
||||
return null
|
||||
}
|
||||
|
||||
export async function locateElectronModule(projectRootPath?: string): Promise<string | null> {
|
||||
for (const moduleName of electronModuleNames) {
|
||||
const electronPath = await searchForModule(process.cwd(), moduleName, projectRootPath)[0];
|
||||
|
||||
if (electronPath && await fs.pathExists(path.join(electronPath, 'package.json'))) {
|
||||
return electronPath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return locateModuleByRequire();
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import * as os from 'os';
|
|||
import * as path from 'path';
|
||||
import { readPackageJson } from './read-package-json';
|
||||
import { lookupModuleState, cacheModuleState } from './cache';
|
||||
import { searchForModule, searchForNodeModules } from './search-module';
|
||||
|
||||
export type ModuleType = 'prod' | 'dev' | 'optional';
|
||||
export type RebuildMode = 'sequential' | 'parallel';
|
||||
|
@ -27,6 +28,7 @@ export interface RebuildOptions {
|
|||
useCache?: boolean;
|
||||
cachePath?: string;
|
||||
prebuildTagPrefix?: string;
|
||||
projectRootPath?: string;
|
||||
}
|
||||
|
||||
export type HashTree = { [path: string]: string | HashTree };
|
||||
|
@ -84,6 +86,7 @@ class Rebuilder {
|
|||
public useCache: boolean;
|
||||
public cachePath: string;
|
||||
public prebuildTagPrefix: string;
|
||||
public projectRootPath?: string;
|
||||
|
||||
constructor(options: RebuilderOptions) {
|
||||
this.lifecycle = options.lifecycle;
|
||||
|
@ -105,6 +108,7 @@ class Rebuilder {
|
|||
console.warn('[WARNING]: Electron Rebuild has force enabled and cache enabled, force take precedence and the cache will not be used.');
|
||||
this.useCache = false;
|
||||
}
|
||||
this.projectRootPath = options.projectRootPath;
|
||||
|
||||
if (typeof this.electronVersion === 'number') {
|
||||
if (`${this.electronVersion}`.split('.').length === 1) {
|
||||
|
@ -118,7 +122,7 @@ class Rebuilder {
|
|||
}
|
||||
|
||||
this.ABI = nodeAbi.getAbi(this.electronVersion, 'electron');
|
||||
this.prodDeps = this.extraModules.reduce((acc, x) => acc.add(x), new Set<string>());
|
||||
this.prodDeps = this.extraModules.reduce((acc: Set<string>, x: string) => acc.add(x), new Set<string>());
|
||||
this.rebuilds = [];
|
||||
this.realModulePaths = new Set();
|
||||
this.realNodeModulesPaths = new Set();
|
||||
|
@ -158,14 +162,28 @@ class Rebuilder {
|
|||
|
||||
for (const key of depKeys) {
|
||||
this.prodDeps[key] = true;
|
||||
markWaiters.push(this.markChildrenAsProdDeps(path.resolve(this.buildPath, 'node_modules', key)));
|
||||
const modulePaths: string[] = await searchForModule(
|
||||
this.buildPath,
|
||||
key,
|
||||
this.projectRootPath
|
||||
);
|
||||
for (const modulePath of modulePaths) {
|
||||
markWaiters.push(this.markChildrenAsProdDeps(modulePath));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(markWaiters);
|
||||
|
||||
d('identified prod deps:', this.prodDeps);
|
||||
|
||||
await this.rebuildAllModulesIn(path.resolve(this.buildPath, 'node_modules'));
|
||||
const nodeModulesPaths = await searchForNodeModules(
|
||||
this.buildPath,
|
||||
this.projectRootPath
|
||||
);
|
||||
for (const nodeModulesPath of nodeModulesPaths) {
|
||||
await this.rebuildAllModulesIn(nodeModulesPath);
|
||||
}
|
||||
|
||||
this.rebuilds.push(() => this.rebuildModuleAt(this.buildPath));
|
||||
|
||||
if (this.mode !== 'sequential') {
|
||||
|
@ -448,17 +466,13 @@ class Rebuilder {
|
|||
}
|
||||
|
||||
async findModule(moduleName: string, fromDir: string, foundFn: ((p: string) => Promise<void>)): Promise<void[]> {
|
||||
let targetDir = fromDir;
|
||||
const foundFns = [];
|
||||
|
||||
while (targetDir !== path.dirname(this.buildPath)) {
|
||||
const testPath = path.resolve(targetDir, 'node_modules', moduleName);
|
||||
if (await fs.pathExists(testPath)) {
|
||||
foundFns.push(foundFn(testPath));
|
||||
}
|
||||
|
||||
targetDir = path.dirname(targetDir);
|
||||
}
|
||||
const testPaths = await searchForModule(
|
||||
fromDir,
|
||||
moduleName,
|
||||
this.projectRootPath
|
||||
);
|
||||
const foundFns = testPaths.map(testPath => foundFn(testPath));
|
||||
|
||||
return Promise.all(foundFns);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
async function shouldContinueSearch(traversedPath: string, rootPath?: string, stopAtPackageJSON?: boolean): Promise<boolean> {
|
||||
if (rootPath) {
|
||||
return Promise.resolve(traversedPath !== path.dirname(rootPath));
|
||||
} else if (stopAtPackageJSON) {
|
||||
return fs.pathExists(path.join(traversedPath, 'package.json'));
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
type PathGeneratorFunction = (traversedPath: string) => string;
|
||||
|
||||
async function traverseAncestorDirectories(
|
||||
cwd: string,
|
||||
pathGenerator: PathGeneratorFunction,
|
||||
rootPath?: string,
|
||||
maxItems?: number,
|
||||
stopAtPackageJSON?: boolean
|
||||
): Promise<string[]> {
|
||||
const paths: string[] = [];
|
||||
let traversedPath = path.resolve(cwd);
|
||||
|
||||
while (await shouldContinueSearch(traversedPath, rootPath, stopAtPackageJSON)) {
|
||||
const generatedPath = pathGenerator(traversedPath);
|
||||
if (await fs.pathExists(generatedPath)) {
|
||||
paths.push(generatedPath);
|
||||
}
|
||||
|
||||
const parentPath = path.dirname(traversedPath);
|
||||
if (parentPath === traversedPath || (maxItems && paths.length >= maxItems)) {
|
||||
break;
|
||||
}
|
||||
traversedPath = parentPath;
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all instances of a given module in node_modules subdirectories while traversing up
|
||||
* ancestor directories.
|
||||
*
|
||||
* @param cwd the initial directory to traverse
|
||||
* @param moduleName the Node module name (should work for scoped modules as well)
|
||||
* @param rootPath the project's root path. If provided, the traversal will stop at this path.
|
||||
*/
|
||||
export async function searchForModule(
|
||||
cwd: string,
|
||||
moduleName: string,
|
||||
rootPath?: string
|
||||
): Promise<string[]> {
|
||||
const pathGenerator: PathGeneratorFunction = (traversedPath) => path.join(traversedPath, 'node_modules', moduleName);
|
||||
return traverseAncestorDirectories(cwd, pathGenerator, rootPath, undefined, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all instances of node_modules subdirectories while traversing up ancestor directories.
|
||||
*
|
||||
* @param cwd the initial directory to traverse
|
||||
* @param rootPath the project's root path. If provided, the traversal will stop at this path.
|
||||
*/
|
||||
export async function searchForNodeModules(cwd: string, rootPath?: string): Promise<string[]> {
|
||||
const pathGenerator: PathGeneratorFunction = (traversedPath) => path.join(traversedPath, 'node_modules');
|
||||
return traverseAncestorDirectories(cwd, pathGenerator, rootPath, undefined, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the root directory of a given project, by looking for a directory with an
|
||||
* NPM or yarn lockfile.
|
||||
*
|
||||
* @param cwd the initial directory to traverse
|
||||
*/
|
||||
export async function getProjectRootPath(cwd: string): Promise<string> {
|
||||
for (const lockFilename of ['yarn.lock', 'package-lock.json']) {
|
||||
const pathGenerator: PathGeneratorFunction = (traversedPath) => path.join(traversedPath, lockFilename);
|
||||
const lockPaths = await traverseAncestorDirectories(cwd, pathGenerator, undefined, 1)
|
||||
if (lockPaths.length > 0) {
|
||||
return path.dirname(lockPaths[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return cwd;
|
||||
}
|
|
@ -16,11 +16,11 @@ const install: ((s: string) => Promise<void>) = packageCommand.bind(null, 'insta
|
|||
const uninstall: ((s: string) => Promise<void>) = packageCommand.bind(null, 'uninstall');
|
||||
|
||||
const testElectronCanBeFound = (): void => {
|
||||
it('should return a valid path', () => {
|
||||
const electronPath = locateElectronModule();
|
||||
it('should return a valid path', async () => {
|
||||
const electronPath = await locateElectronModule();
|
||||
expect(electronPath).to.be.a('string');
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
expect(fs.existsSync(electronPath!)).to.be.equal(true);
|
||||
expect(await fs.pathExists(electronPath!)).to.be.equal(true);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -31,10 +31,10 @@ describe('locateElectronModule', function() {
|
|||
|
||||
it('should return null when electron is not installed', async () => {
|
||||
await fs.remove(path.resolve(__dirname, '..', 'node_modules', 'electron'));
|
||||
expect(locateElectronModule()).to.be.equal(null);
|
||||
expect(await locateElectronModule()).to.be.equal(null);
|
||||
});
|
||||
|
||||
describe('with electron-prebuilt installed', () => {
|
||||
describe('with electron-prebuilt installed', async () => {
|
||||
before(() => install('electron-prebuilt'));
|
||||
|
||||
testElectronCanBeFound();
|
||||
|
@ -42,13 +42,13 @@ describe('locateElectronModule', function() {
|
|||
after(() => uninstall('electron-prebuilt'));
|
||||
});
|
||||
|
||||
describe('with electron installed', () => {
|
||||
before(() => install('electron'));
|
||||
describe('with electron installed', async () => {
|
||||
before(() => install('electron@^5.0.13'));
|
||||
|
||||
testElectronCanBeFound();
|
||||
|
||||
after(() => uninstall('electron'));
|
||||
});
|
||||
|
||||
after(() => install('electron'));
|
||||
after(() => install('electron@^5.0.13'));
|
||||
});
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "workspace-app",
|
||||
"productName": "Workspace App",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "src/index.js",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"ffi-napi": "2.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"ref-napi": "1.4.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"child-workspace"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { spawnPromise } from 'spawn-rx';
|
||||
|
||||
import { expectNativeModuleToBeRebuilt, expectNativeModuleToNotBeRebuilt } from './helpers/rebuild';
|
||||
import { rebuild } from '../src/rebuild';
|
||||
import { getProjectRootPath } from '../src/search-module';
|
||||
|
||||
describe('rebuild for yarn workspace', function() {
|
||||
this.timeout(2 * 60 * 1000);
|
||||
const testModulePath = path.resolve(os.tmpdir(), 'electron-rebuild-test');
|
||||
|
||||
describe('core behavior', () => {
|
||||
before(async () => {
|
||||
await fs.remove(testModulePath);
|
||||
await fs.copy(path.resolve(__dirname, 'fixture/workspace-test'), testModulePath);
|
||||
|
||||
await spawnPromise('yarn', [], {
|
||||
cwd: testModulePath,
|
||||
stdio: 'ignore'
|
||||
});
|
||||
|
||||
const projectRootPath = await getProjectRootPath(path.join(testModulePath, 'workspace-test', 'child-workspace'));
|
||||
|
||||
await rebuild({
|
||||
buildPath: path.resolve(testModulePath, 'child-workspace'),
|
||||
electronVersion: '5.0.13',
|
||||
arch: process.arch,
|
||||
projectRootPath
|
||||
});
|
||||
});
|
||||
|
||||
it('should have rebuilt top level prod dependencies', async () => {
|
||||
await expectNativeModuleToBeRebuilt(testModulePath, 'ref-napi');
|
||||
});
|
||||
|
||||
it('should not have rebuilt top level devDependencies', async () => {
|
||||
await expectNativeModuleToNotBeRebuilt(testModulePath, 'ffi-napi');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await fs.remove(testModulePath);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
import { expect } from 'chai';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { getProjectRootPath } from '../src/search-module';
|
||||
|
||||
let baseDir: string;
|
||||
|
||||
async function createTempDir(): Promise<void> {
|
||||
baseDir = await fs.mkdtemp(path.join(os.tmpdir(), 'electron-rebuild-test-'));
|
||||
}
|
||||
|
||||
async function removeTempDir(): Promise<void> {
|
||||
await fs.remove(baseDir);
|
||||
}
|
||||
|
||||
describe('search-module', () => {
|
||||
describe('getProjectRootPath', () => {
|
||||
describe('multi-level workspace', () => {
|
||||
for (const lockFile of ['yarn.lock', 'package-lock.json']) {
|
||||
describe(lockFile, () => {
|
||||
before(async () => {
|
||||
await createTempDir();
|
||||
await fs.copy(path.resolve(__dirname, 'fixture', 'multi-level-workspace'), baseDir);
|
||||
await fs.ensureFile(path.join(baseDir, lockFile));
|
||||
});
|
||||
|
||||
it('finds the folder with the lockfile', async () => {
|
||||
const packageDir = path.join(baseDir, 'packages', 'bar');
|
||||
expect(await getProjectRootPath(packageDir)).to.equal(baseDir);
|
||||
});
|
||||
|
||||
after(removeTempDir);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('no workspace', () => {
|
||||
before(createTempDir);
|
||||
|
||||
it('returns the input directory if a lockfile cannot be found', async () => {
|
||||
expect(await getProjectRootPath(baseDir)).to.equal(baseDir);
|
||||
});
|
||||
|
||||
after(removeTempDir);
|
||||
});
|
||||
});
|
||||
});
|
Загрузка…
Ссылка в новой задаче