Skip to content
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,7 @@
<!doctype html>
<html>
<head>
<title>test</title>
</head>
<body></body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// eslint-disable-next-line no-undef
export const rendererName = MAIN_WINDOW_VITE_NAME;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const hello = 'world';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
globalThis.preloadMarker = 'from-preload';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
globalThis.secondaryMarker = 'from-secondary';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/* eslint-disable */
export default {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/* eslint-disable */
export default {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/* eslint-disable */
export default {};
223 changes: 223 additions & 0 deletions packages/plugin/vite/spec/subprocess-worker.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';

import { afterEach, describe, expect, it } from 'vitest';

import type { VitePluginConfig } from '../src/Config';

const projectDir = path.join(
import.meta.dirname,
'fixtures',
'subprocess-build',
);
const workerPath = path.resolve(
import.meta.dirname,
'..',
'dist',
'subprocess-worker.js',
);

function runWorker(
kind: 'build' | 'renderer',
index: number,
config: Pick<VitePluginConfig, 'build' | 'renderer'>,
) {
return new Promise<{ code: number | null; stderr: string }>(
(resolve, reject) => {
const child = spawn(process.execPath, [workerPath], {
cwd: projectDir,
env: {
...process.env,
FORGE_VITE_PROJECT_DIR: projectDir,
FORGE_VITE_KIND: kind,
FORGE_VITE_INDEX: String(index),
FORGE_VITE_CONFIG: JSON.stringify(config),
},
stdio: ['ignore', 'ignore', 'pipe'],
});
let stderr = '';
child.stderr.setEncoding('utf8');
child.stderr.on('data', (c) => (stderr += c));
child.on('error', reject);
child.on('close', (code) => resolve({ code, stderr }));
},
);
}

describe('subprocess-worker', () => {
const viteOutDir = path.join(projectDir, '.vite');

afterEach(() => {
fs.rmSync(viteOutDir, { recursive: true, force: true });
});

it('builds a main target and writes output', async () => {
const config: Pick<VitePluginConfig, 'build' | 'renderer'> = {
build: [
{
entry: 'src/main.js',
config: path.join(projectDir, 'vite.main.config.mjs'),
target: 'main',
},
],
renderer: [
{
name: 'main_window',
config: path.join(projectDir, 'vite.renderer.config.mjs'),
},
],
};

const { code, stderr } = await runWorker('build', 0, config);
expect(code, stderr).toBe(0);

const outFile = path.join(viteOutDir, 'build', 'main.js');
expect(fs.existsSync(outFile)).toBe(true);
// getBuildDefine should have injected the renderer name define.
const contents = fs.readFileSync(outFile, 'utf8');
expect(contents).toContain('world');
});

it('builds a renderer target and writes output', async () => {
const config: Pick<VitePluginConfig, 'build' | 'renderer'> = {
build: [],
renderer: [
{
name: 'main_window',
config: path.join(projectDir, 'vite.renderer.config.mjs'),
},
],
};

const { code, stderr } = await runWorker('renderer', 0, config);
expect(code, stderr).toBe(0);

const outHtml = path.join(
viteOutDir,
'renderer',
'main_window',
'index.html',
);
expect(fs.existsSync(outHtml)).toBe(true);
});

it('injects renderer name defines into main targets', async () => {
// This validates that the worker receives the FULL renderer list, not just
// the single build spec. getBuildDefine() reads forgeConfig.renderer to
// generate ${NAME}_VITE_NAME defines — if the worker only got a
// single-spec config, this define would be missing and the build would
// fail (undefined reference) or produce wrong output.
const config: Pick<VitePluginConfig, 'build' | 'renderer'> = {
build: [
{
entry: 'src/main-with-define.js',
config: path.join(projectDir, 'vite.main.config.mjs'),
target: 'main',
},
],
renderer: [
{
name: 'main_window',
config: path.join(projectDir, 'vite.renderer.config.mjs'),
},
],
};

const { code, stderr } = await runWorker('build', 0, config);
expect(code, stderr).toBe(0);

const outFile = path.join(viteOutDir, 'build', 'main-with-define.js');
const contents = fs.readFileSync(outFile, 'utf8');
// MAIN_WINDOW_VITE_NAME should be statically replaced with "main_window"
expect(contents).toContain('"main_window"');
expect(contents).not.toContain('MAIN_WINDOW_VITE_NAME');
});

it('builds a preload target', async () => {
const config: Pick<VitePluginConfig, 'build' | 'renderer'> = {
build: [
{
entry: 'src/preload.js',
config: path.join(projectDir, 'vite.preload.config.mjs'),
target: 'preload',
},
],
renderer: [],
};

const { code, stderr } = await runWorker('build', 0, config);
expect(code, stderr).toBe(0);

const outFile = path.join(viteOutDir, 'build', 'preload.js');
expect(fs.existsSync(outFile)).toBe(true);
const contents = fs.readFileSync(outFile, 'utf8');
expect(contents).toContain('from-preload');
});

it('builds the correct target when given a non-zero index', async () => {
const config: Pick<VitePluginConfig, 'build' | 'renderer'> = {
build: [
{
entry: 'src/main.js',
config: path.join(projectDir, 'vite.main.config.mjs'),
target: 'main',
},
{
entry: 'src/secondary.js',
config: path.join(projectDir, 'vite.main.config.mjs'),
target: 'main',
},
],
renderer: [],
};

const { code, stderr } = await runWorker('build', 1, config);
expect(code, stderr).toBe(0);

// Only secondary should be built, not main.
const secondaryOut = path.join(viteOutDir, 'build', 'secondary.js');
const mainOut = path.join(viteOutDir, 'build', 'main.js');
expect(fs.existsSync(secondaryOut)).toBe(true);
expect(fs.existsSync(mainOut)).toBe(false);
const contents = fs.readFileSync(secondaryOut, 'utf8');
expect(contents).toContain('from-secondary');
});

it('exits nonzero and surfaces error when build fails', async () => {
const config: Pick<VitePluginConfig, 'build' | 'renderer'> = {
build: [
{
entry: 'src/does-not-exist.js',
config: path.join(projectDir, 'vite.main.config.mjs'),
target: 'main',
},
],
renderer: [],
};

const { code, stderr } = await runWorker('build', 0, config);
expect(code).not.toBe(0);
expect(stderr).toMatch(/does-not-exist/);
});

it('exits nonzero when required env vars are missing', async () => {
const { code, stderr } = await new Promise<{
code: number | null;
stderr: string;
}>((resolve, reject) => {
const child = spawn(process.execPath, [workerPath], {
env: { ...process.env, FORGE_VITE_PROJECT_DIR: projectDir },
stdio: ['ignore', 'ignore', 'pipe'],
});
let stderr = '';
child.stderr.setEncoding('utf8');
child.stderr.on('data', (c) => (stderr += c));
child.on('error', reject);
child.on('close', (code) => resolve({ code, stderr }));
});

expect(code).toBe(1);
expect(stderr).toContain('missing');
});
});
118 changes: 118 additions & 0 deletions packages/plugin/vite/src/VitePlugin.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { spawn } from 'node:child_process';
import path from 'node:path';

import { namedHookWithTaskFn, PluginBase } from '@electron-forge/plugin-base';
Expand All @@ -16,9 +17,63 @@ import type {
ResolvedForgeConfig,
} from '@electron-forge/shared-types';
import type { AddressInfo } from 'node:net';
import type { LibraryOptions } from 'vite';

const d = debug('electron-forge:plugin:vite');

const subprocessWorkerPath = path.resolve(
import.meta.dirname,
'subprocess-worker.js',
);

function spawnViteBuild(
pluginConfig: Pick<VitePluginConfig, 'build' | 'renderer'>,
kind: 'build' | 'renderer',
index: number,
projectDir: string,
) {
return new Promise<void>((resolve, reject) => {
const child = spawn(process.execPath, [subprocessWorkerPath], {
cwd: projectDir,
env: {
...process.env,
FORGE_VITE_PROJECT_DIR: projectDir,
FORGE_VITE_KIND: kind,
FORGE_VITE_INDEX: String(index),
FORGE_VITE_CONFIG: JSON.stringify(pluginConfig),
},
stdio: ['ignore', 'ignore', 'pipe'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also pipe out stdout back to the parent process in case any logs/warnings are emitted from the Vite build?

});

let stderr = '';
child.stderr.setEncoding('utf8');
child.stderr.on('data', (chunk) => {
stderr += chunk;
});

child.on('error', reject);
child.on('close', (code) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we handle signal as well here as the second param?

if (code === 0) {
resolve();
} else {
reject(
new Error(
`Vite build subprocess exited with code ${code}${
stderr ? `:\n${stderr}` : ''
}`,
),
);
}
});
});
}

function entryToDisplay(entry: LibraryOptions['entry']): string {
if (typeof entry === 'string') return entry;
if (Array.isArray(entry)) return entry.join(' ');
return Object.keys(entry).join(' ');
}

export default class VitePlugin extends PluginBase<VitePluginConfig> {
private static alreadyStarted = false;

Expand Down Expand Up @@ -205,8 +260,47 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}.`);
});
};

/**
* Serializable snapshot of the plugin config to pass to subprocess workers.
* We only include build[] and renderer[] — the worker needs the full renderer
* list for defines even when building a single main target.
*/
private get serializableConfig(): Pick<
VitePluginConfig,
'build' | 'renderer'
> {
return {
build: this.config.build,
renderer: this.config.renderer,
};
}

// Main process, Preload scripts and Worker process, etc.
build = async (task?: ForgeListrTask<null>): Promise<Listr | void> => {
if (this.isProd) {
const targets = this.config.build
.map((spec, index) => ({ spec, index }))
.filter(({ spec }) => spec.config);
return task?.newListr(
targets.map(({ spec, index }) => ({
title: `Building ${chalk.green(entryToDisplay(spec.entry))}`,
task: async (_ctx, subtask) => {
await spawnViteBuild(
this.serializableConfig,
'build',
index,
this.projectDir,
);
subtask.title = `Built target ${chalk.dim(entryToDisplay(spec.entry))}`;
},
})),
{
concurrent: this.config.concurrent ?? true,
exitOnError: true,
},
);
}

const configs = await this.configGenerator.getBuildConfigs();
/**
* Checks if the result of the Vite build is a Rollup watcher.
Expand Down Expand Up @@ -336,6 +430,30 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}.`);

// Renderer process
buildRenderer = async (task?: ForgeListrTask<null>) => {
if (this.isProd) {
const targets = this.config.renderer
.map((spec, index) => ({ spec, index }))
.filter(({ spec }) => spec.config);
return task?.newListr(
targets.map(({ spec, index }) => ({
title: `Building ${chalk.green(spec.name)}`,
task: async (_ctx, subtask) => {
await spawnViteBuild(
this.serializableConfig,
'renderer',
index,
this.projectDir,
);
subtask.title = `Built target ${chalk.dim(spec.name)}`;
},
})),
{
concurrent: this.config.concurrent ?? true,
exitOnError: true,
},
);
}

const rendererConfigs = await this.configGenerator.getRendererConfig();
return task?.newListr(
rendererConfigs.map((userConfig) => ({
Expand Down
Loading
Loading