Skip to content

Commit

Permalink
feat: support yarn workspaces (#250)
Browse files Browse the repository at this point in the history
Co-authored-by: Mark Lee <[email protected]>
  • Loading branch information
Sign312 and malept authored Jan 29, 2020
1 parent 94cb04d commit 367e081
Show file tree
Hide file tree
Showing 11 changed files with 263 additions and 48 deletions.
5 changes: 4 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
45 changes: 19 additions & 26 deletions src/electron-locator.ts
Original file line number Diff line number Diff line change
@@ -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
}
});
}

return null
}

export function locateElectronModule(): string | null {
const siblingModules: string[] | null = locateSiblingModules();
if (siblingModules.length > 0) {
return siblingModules[0];
}
export async function locateElectronModule(projectRootPath?: string): Promise<string | null> {
for (const moduleName of electronModuleNames) {
const electronPath = await searchForModule(process.cwd(), moduleName, projectRootPath)[0];

const requiredModules = locateModulesByRequire();
if (requiredModules && requiredModules.length > 0) {
return requiredModules[0];
if (electronPath && await fs.pathExists(path.join(electronPath, 'package.json'))) {
return electronPath;
}
}

return null;
return locateModuleByRequire();
}
40 changes: 27 additions & 13 deletions src/rebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -27,6 +28,7 @@ export interface RebuildOptions {
useCache?: boolean;
cachePath?: string;
prebuildTagPrefix?: string;
projectRootPath?: string;
}

export type HashTree = { [path: string]: string | HashTree };
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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();
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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);
}
Expand Down
86 changes: 86 additions & 0 deletions src/search-module.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 8 additions & 8 deletions test/electron-locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
};

Expand All @@ -31,24 +31,24 @@ 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();

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'));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
16 changes: 16 additions & 0 deletions test/fixture/workspace-test/child-workspace/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
6 changes: 6 additions & 0 deletions test/fixture/workspace-test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"private": true,
"workspaces": [
"child-workspace"
]
}
46 changes: 46 additions & 0 deletions test/rebuild-yarnworkspace.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Loading

0 comments on commit 367e081

Please sign in to comment.