Skip to content
Open
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
51 changes: 50 additions & 1 deletion src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,34 @@ export function normalizePath(pathToNormalize: string) {
return pathToNormalize.replace(/\\/g, '/');
}

/**
* Resolves a path and canonicalizes it via `realpath`, so that two paths referring to the same
* location compare as equal even when one of them goes through a symlink (e.g. `/var` vs
* `/private/var` on macOS). Falls back to the resolved path if it does not exist.
*/
function canonicalizePath(pathToCanonicalize: string): string {
const resolvedPath = path.resolve(pathToCanonicalize);
try {
return fs.realpathSync(resolvedPath);
} catch {
return resolvedPath;
}
}

/**
* Whether `childPath` is equal to or contained within `parentPath`. Both paths must already be
* canonicalized. Uses `path.relative` so that, on Windows, the comparison is case-insensitive.
*/
function isPathInside(childPath: string, parentPath: string): boolean {
const relativePath = path.relative(parentPath, childPath);
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
}

function findOutDirContaining(fullPath: string, ignoredOutDirs: string[]): string | undefined {
const canonicalFullPath = canonicalizePath(fullPath);
return ignoredOutDirs.find((outDir) => isPathInside(canonicalFullPath, canonicalizePath(outDir)));
}

/**
* Validates that the application directory contains a package.json file, and that there exists an
* appropriate main entry point file, per the rules of the "main" field in package.json.
Expand All @@ -107,13 +135,28 @@ export function normalizePath(pathToNormalize: string) {
*
* @param appDir - the directory specified by the user
* @param bundledAppDir - the directory where the appDir is copied to in the bundled Electron app
* @param ignoredOutDirs - resolved out directories that are excluded from the bundled app
*/
export async function validateElectronApp(appDir: string, bundledAppDir: string) {
export async function validateElectronApp(
appDir: string,
bundledAppDir: string,
ignoredOutDirs: string[] = [],
) {
debug('Validating bundled Electron app');
debug('Checking for a package.json file');

const bundledPackageJSONPath = path.join(bundledAppDir, 'package.json');
if (!fs.existsSync(bundledPackageJSONPath)) {
const canonicalAppDir = canonicalizePath(appDir);
if (
ignoredOutDirs.some(
(outDir) => path.relative(canonicalizePath(outDir), canonicalAppDir) === '',
)
) {
throw new Error(
`The out directory (${path.resolve(appDir)}) is the same as your app directory. The out directory is automatically excluded from packaging, so nothing would be packaged; choose an out directory outside of your app directory`,
);
}
const originalPackageJSONPath = path.join(appDir, 'package.json');
throw new Error(
`Application manifest was not found. Make sure "${originalPackageJSONPath}" exists and does not get ignored by your ignore option`,
Expand All @@ -126,6 +169,12 @@ export async function validateElectronApp(appDir: string, bundledAppDir: string)
const mainScript = path.resolve(bundledAppDir, mainScriptBasename);
if (!fs.existsSync(mainScript)) {
const originalMainScript = path.join(appDir, mainScriptBasename);
const outDir = findOutDirContaining(path.resolve(appDir, mainScriptBasename), ignoredOutDirs);
if (outDir) {
throw new Error(
`The out directory (${outDir}) is inside your app directory and contains your app's main entry point (${originalMainScript}). The out directory is automatically excluded from packaging; choose an out directory outside of your app directory`,
);
}
throw new Error(
`The main entry point to your app was not found. Make sure "${originalMainScript}" exists and does not get ignored by your ignore option`,
);
Expand Down
8 changes: 6 additions & 2 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
validateElectronApp,
warning,
} from './common.js';
import { userPathFilter } from './copy-filter.js';
import { generateIgnoredOutDirs, userPathFilter } from './copy-filter.js';
import { runHooks } from './hooks.js';
import crypto from 'node:crypto';
import type { ProcessedOptionsWithSinglePlatformArch } from './types.js';
Expand Down Expand Up @@ -176,7 +176,11 @@ export class App {

async buildApp() {
await this.copyTemplate();
await validateElectronApp(this.opts.dir, this.originalResourcesAppDir);
await validateElectronApp(
this.opts.dir,
this.originalResourcesAppDir,
generateIgnoredOutDirs(this.opts),
);
await this.asarApp();
}

Expand Down
20 changes: 20 additions & 0 deletions test/common.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,26 @@ describe('validateElectronApp', () => {
`The main entry point to your app was not found. Make sure "${path.join('original-dir', 'main.js')}" exists and does not get ignored by your ignore option`,
);
});

it('fails on an Electron app whose app directory is the out directory', async () => {
const fixture = path.join(__dirname, 'fixtures', 'validate-failure-without-package-json');

await expect(
validateElectronApp('original-dir', fixture, [path.resolve('original-dir')]),
).rejects.toThrow(
`The out directory (${path.resolve('original-dir')}) is the same as your app directory. The out directory is automatically excluded from packaging, so nothing would be packaged; choose an out directory outside of your app directory`,
);
});

it('fails on an Electron app whose main entry point is inside the out directory', async () => {
const fixture = path.join(__dirname, 'fixtures', 'validate-failure-main-in-out-dir');

await expect(
validateElectronApp('original-dir', fixture, [path.resolve('original-dir', 'dist')]),
).rejects.toThrow(
`The out directory (${path.resolve('original-dir', 'dist')}) is inside your app directory and contains your app's main entry point (${path.join('original-dir', 'dist', 'main.js')}). The out directory is automatically excluded from packaging; choose an out directory outside of your app directory`,
);
});
});

describe('createAsarOpts', () => {
Expand Down
3 changes: 3 additions & 0 deletions test/fixtures/validate-failure-main-in-out-dir/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"main": "dist/main.js"
}
43 changes: 43 additions & 0 deletions test/packager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,49 @@ describe('packager', () => {
expect(fs.existsSync(path.join(resourcesPath, 'app', path.basename(opts.out)))).toBe(false);
});

it('should fail with a descriptive error if the out dir is the app dir', async ({ baseOpts }) => {
const opts = {
...baseOpts,
out: baseOpts.dir,
};

await expect(packager(opts)).rejects.toThrowError(
`The out directory (${path.resolve(opts.dir)}) is the same as your app directory. The out directory is automatically excluded from packaging, so nothing would be packaged; choose an out directory outside of your app directory`,
);
});

it('should fail with a descriptive error if the out dir contains the main entry point', async ({
baseOpts,
}) => {
const fixture = path.join(__dirname, 'fixtures', 'basic');
const appDir = path.join(baseOpts.out, 'app');
await fs.promises.cp(fixture, appDir, {
dereference: true,
filter: (file) => path.basename(file) !== 'node_modules',
recursive: true,
});
const packageJSON = JSON.parse(
await fs.promises.readFile(path.join(appDir, 'package.json'), 'utf8'),
);
packageJSON.main = 'dist/main.js';
await fs.promises.writeFile(
path.join(appDir, 'package.json'),
JSON.stringify(packageJSON, null, 2),
);
await fs.promises.mkdir(path.join(appDir, 'dist'), { recursive: true });
await fs.promises.rename(path.join(appDir, 'main.js'), path.join(appDir, 'dist', 'main.js'));

const opts = {
...baseOpts,
dir: appDir,
out: path.join(appDir, 'dist'),
};

await expect(packager(opts)).rejects.toThrowError(
`The out directory (${path.join(appDir, 'dist')}) is inside your app directory and contains your app's main entry point (${path.join(appDir, 'dist', 'main.js')}). The out directory is automatically excluded from packaging; choose an out directory outside of your app directory`,
);
});

describe('hooks', () => {
it.for([
{
Expand Down
Loading