Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
44 changes: 44 additions & 0 deletions packages/plugin/vite/spec/ViteConfig.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,26 @@ describe('ViteConfigGenerator', () => {
});
});

it('getBuildConfigs:main with type module', async () => {
const forgeConfig: VitePluginConfig = {
build: [
{
entry: 'src/main.js',
config: path.join(configRoot, 'vite.main.config.mjs'),
target: 'main',
},
],
renderer: [],
type: 'module',
};
const generator = new ViteConfigGenerator(forgeConfig, configRoot, true);
const buildConfig = (await generator.getBuildConfigs())[0];

expect(buildConfig.build?.lib && buildConfig.build.lib.formats).toEqual([
'es',
]);
});

it('getBuildConfigs:preload', async () => {
const forgeConfig: VitePluginConfig = {
build: [
Expand Down Expand Up @@ -94,6 +114,30 @@ describe('ViteConfigGenerator', () => {
).toEqual(['@electron-forge/plugin-vite:hot-restart']);
});

it('getBuildConfigs:preload with type module', async () => {
const forgeConfig: VitePluginConfig = {
build: [
{
entry: 'src/preload.js',
config: path.join(configRoot, 'vite.preload.config.mjs'),
target: 'preload',
},
],
renderer: [],
type: 'module',
};
const generator = new ViteConfigGenerator(forgeConfig, configRoot, true);
const buildConfig = (await generator.getBuildConfigs())[0];

expect(buildConfig.build?.rollupOptions?.output).toEqual({
format: 'es',
inlineDynamicImports: true,
entryFileNames: '[name].mjs',
chunkFileNames: '[name].mjs',
assetFileNames: '[name].[ext]',
});
});

it('getRendererConfig:renderer', async () => {
const forgeConfig = {
build: [],
Expand Down
60 changes: 55 additions & 5 deletions packages/plugin/vite/spec/VitePlugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,21 @@ describe('VitePlugin', async () => {
plugin.packageAfterCopy({} as ResolvedForgeConfig, packagedPath),
).rejects.toThrow(/entry point/);
});

afterAll(async () => {
await fs.promises.rm(viteTestDir, { recursive: true });
});
});

describe('resolveForgeConfig', () => {
const packageJSONPath = path.join(viteTestDir, 'package.json');
let plugin: VitePlugin;

beforeAll(() => {
beforeAll(async () => {
plugin = new VitePlugin(baseConfig);
plugin.setDirectories(viteTestDir);
// Write a default package.json for tests that don't care about its contents
await fs.promises.writeFile(
packageJSONPath,
JSON.stringify({ main: '.vite/build/main.js' }),
'utf-8',
);
});

it('sets packagerConfig and packagerConfig.ignore if it does not exist', async () => {
Expand All @@ -107,6 +111,48 @@ describe('VitePlugin', async () => {
expect(config.packagerConfig.ignore).toBeTypeOf('function');
});

it('should fail if plugin type is "module" but package.json has no "type": "module" and main is not .mjs', async () => {
const esmPlugin = new VitePlugin({ ...baseConfig, type: 'module' });
esmPlugin.setDirectories(viteTestDir);

await fs.promises.writeFile(
packageJSONPath,
JSON.stringify({ main: '.vite/build/main.js' }),
'utf-8',
);
await expect(
esmPlugin.resolveForgeConfig({} as ResolvedForgeConfig),
).rejects.toThrow(/type: "module"/);
});

it('should succeed if plugin type is "module" and package.json has "type": "module"', async () => {
const esmPlugin = new VitePlugin({ ...baseConfig, type: 'module' });
esmPlugin.setDirectories(viteTestDir);

await fs.promises.writeFile(
packageJSONPath,
JSON.stringify({ main: '.vite/build/main.js', type: 'module' }),
'utf-8',
);
await expect(
esmPlugin.resolveForgeConfig({} as ResolvedForgeConfig),
).resolves.toBeDefined();
});

it('should succeed if plugin type is "module" and main entry uses .mjs extension', async () => {
const esmPlugin = new VitePlugin({ ...baseConfig, type: 'module' });
esmPlugin.setDirectories(viteTestDir);

await fs.promises.writeFile(
packageJSONPath,
JSON.stringify({ main: '.vite/build/main.mjs' }),
'utf-8',
);
await expect(
esmPlugin.resolveForgeConfig({} as ResolvedForgeConfig),
).resolves.toBeDefined();
});

describe('packagerConfig.ignore', () => {
it('does not overwrite an existing ignore value', async () => {
const config = await plugin.resolveForgeConfig({
Expand Down Expand Up @@ -207,4 +253,8 @@ describe('VitePlugin', async () => {
});
});
});

afterAll(async () => {
await fs.promises.rm(viteTestDir, { recursive: true });
});
});
12 changes: 12 additions & 0 deletions packages/plugin/vite/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,16 @@ export interface VitePluginConfig {
* @defaultValue `true`
*/
concurrent?: boolean | number;

/**
* The module type to use for the main process and preload script builds.
*
* - `'commonjs'` outputs CJS bundles (the default, matching Electron's traditional module system).
* - `'module'` outputs ES module bundles. When using this option, make sure your `package.json`
* has `"type": "module"` and that your Electron version supports ESM (Electron >= 28).
*
* @defaultValue `'commonjs'`
* @see https://www.electronjs.org/docs/latest/tutorial/esm
*/
type?: 'commonjs' | 'module';
Comment thread
erikian marked this conversation as resolved.
Outdated
}
18 changes: 16 additions & 2 deletions packages/plugin/vite/src/VitePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const subprocessWorkerPath = path.resolve(
);

function spawnViteBuild(
pluginConfig: Pick<VitePluginConfig, 'build' | 'renderer'>,
pluginConfig: Pick<VitePluginConfig, 'build' | 'renderer' | 'type'>,
kind: 'build' | 'renderer',
index: number,
projectDir: string,
Expand Down Expand Up @@ -222,6 +222,19 @@ export default class VitePlugin extends PluginBase<VitePluginConfig> {
resolveForgeConfig = async (
forgeConfig: ResolvedForgeConfig,
): Promise<ResolvedForgeConfig> => {
if (this.config.type === 'module') {
const pj = await fs.readJson(
path.resolve(this.projectDir, 'package.json'),
);
if (pj.type !== 'module' && !pj.main?.endsWith('.mjs')) {
Comment thread
erikian marked this conversation as resolved.
Outdated
throw new Error(
`The Vite plugin is configured with type: "module", but your package.json does not have "type": "module" ` +
`and the main entry point does not use an .mjs extension. Electron requires one of these for ESM support in the main process. ` +
`See https://www.electronjs.org/docs/latest/tutorial/esm for more details.`,
);
}
}

forgeConfig.packagerConfig ??= {};

if (forgeConfig.packagerConfig.ignore) {
Expand Down Expand Up @@ -275,11 +288,12 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}.`);
*/
private get serializableConfig(): Pick<
VitePluginConfig,
'build' | 'renderer'
'build' | 'renderer' | 'type'
> {
return {
build: this.config.build,
renderer: this.config.renderer,
type: this.config.type,
};
}

Expand Down
5 changes: 3 additions & 2 deletions packages/plugin/vite/src/config/vite.main.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export function getConfig(
forgeEnv: ConfigEnv<'build'>,
userConfig: UserConfig = {},
): UserConfig {
const { forgeConfigSelf } = forgeEnv;
const { forgeConfigSelf, forgeConfig } = forgeEnv;
const isEsm = forgeConfig.type === 'module';
const define = getBuildDefine(forgeEnv);
const config: UserConfig = {
build: {
Expand All @@ -34,7 +35,7 @@ export function getConfig(
config.build!.lib = {
entry: forgeConfigSelf.entry,
fileName: () => '[name].js',
formats: ['cjs'],
formats: [isEsm ? 'es' : 'cjs'],
};
}

Expand Down
9 changes: 5 additions & 4 deletions packages/plugin/vite/src/config/vite.preload.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export function getConfig(
forgeEnv: ConfigEnv<'build'>,
userConfig: UserConfig = {},
): UserConfig {
const { forgeConfigSelf } = forgeEnv;
const { forgeConfigSelf, forgeConfig } = forgeEnv;
const isEsm = forgeConfig.type === 'module';
const config: UserConfig = {
build: {
copyPublicDir: false,
Expand All @@ -19,11 +20,11 @@ export function getConfig(
// Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`.
input: forgeConfigSelf.entry,
output: {
format: 'cjs',
format: isEsm ? 'es' : 'cjs',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

note: ESM preloads will only work in unsandboxed renderers according to the docs.

Since the Vite config doesn't have access to the parameters passed to the BrowserWindow constructor to allow us to pick the right format, we could account for that by tweaking the JSDoc in packages/plugin/vite/src/Config.ts to make this caveat clear, and maybe even adding some troubleshooting code like the following directly to the createWindow function in the Vite templates' main files to preemptively provide support for this issue:

// ESM preloads only work if your renderer is unsandboxed, which is disabled
// by default for security reasons. If your preload file fails to load and
// your renderer is sandboxed (i.e. the `webPreferences.sandbox` option in your
// `BrowserWindow` constructor is `true` or isn't set), please set
// `config.build.rollupOptions.output.format` to `commonjs` in your
// `vite.preload.config.ts` file.
mainWindow.webContents.on('preload-error', (event, preloadPath, error) => {
  if(preloadPath.endsWith('.mjs') &&

    // optional - these might be unnecessary or even wrong, but syntax errors
    // thrown when using `import` or top-level `await` in a non-ESM context
    // both contain the word "module" and they're bound to be the most common
    // ones in this scenario 🤷🏼
    // would be fine to omit these conditions, though, even if it means
    // showing this message for unrelated errors thrown in the preload.
    error.stack?.startsWith('SyntaxError') &&
    error.message.includes('module')
  ) {
    console.error(`Fail to load ${preloadPath}. Make sure you're using the \`commonjs\` output format in \`vite.preload.config.ts\` if your renderer is sandboxed.`)
  }
})

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

// It should not be split chunks.
inlineDynamicImports: true,
entryFileNames: '[name].js',
chunkFileNames: '[name].js',
entryFileNames: isEsm ? '[name].mjs' : '[name].js',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we make the other .cjs?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, makes sense to me.

chunkFileNames: isEsm ? '[name].mjs' : '[name].js',
assetFileNames: '[name].[ext]',
},
},
Expand Down
12 changes: 6 additions & 6 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10828,8 +10828,8 @@ __metadata:
linkType: hard

"got@npm:^11.7.0, got@npm:^11.8.5":
version: 11.8.5
resolution: "got@npm:11.8.5"
version: 11.8.6
resolution: "got@npm:11.8.6"
dependencies:
"@sindresorhus/is": "npm:^4.0.0"
"@szmarczak/http-timer": "npm:^4.0.5"
Expand All @@ -10842,13 +10842,13 @@ __metadata:
lowercase-keys: "npm:^2.0.0"
p-cancelable: "npm:^2.0.0"
responselike: "npm:^2.0.0"
checksum: 10c0/14d160a21d085b0fca1c794ae17411d6abe05491a1db37b97e8218bf434d086eea335cadc022964f1896a60ac036db6af0debad94d3747f85503bc7d21bf0fa0
checksum: 10c0/754dd44877e5cf6183f1e989ff01c648d9a4719e357457bd4c78943911168881f1cfb7b2cb15d885e2105b3ad313adb8f017a67265dd7ade771afdb261ee8cb1
languageName: node
linkType: hard

"got@npm:^14.4.5":
version: 14.6.4
resolution: "got@npm:14.6.4"
version: 14.6.6
resolution: "got@npm:14.6.6"
dependencies:
"@sindresorhus/is": "npm:^7.0.1"
byte-counter: "npm:^0.1.0"
Expand All @@ -10862,7 +10862,7 @@ __metadata:
p-cancelable: "npm:^4.0.1"
responselike: "npm:^4.0.2"
type-fest: "npm:^4.26.1"
checksum: 10c0/ee8980feb842db876cffa42fa27da6d90cc1a9cfe2a38942f4b319cbb37c000e34919a7e5dea017a0fa53b0535c02d00426abbbf528d6a4e89c6eb07b2bde977
checksum: 10c0/dab4dbd35deac5634450cd745187ba68cfb9fd8d9236bec4861b633c7dc54f6383fde04cf504b16148625c307a229ff8cccf35d6622824ab13243c9d0af0fcc1
languageName: node
linkType: hard

Expand Down