From 7560f15a244e3f6ae87e3d4f6963c3000e4576c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8D=89=E9=9E=8B=E6=B2=A1=E5=8F=B7?= <308487730@qq.com> Date: Mon, 4 Sep 2023 11:48:03 +0800 Subject: [PATCH] fix(plugin-vite): build size to lage --- packages/plugin/vite/package.json | 4 +- packages/plugin/vite/src/VitePlugin.ts | 68 +++++++++- packages/plugin/vite/src/util/package.ts | 115 +++++++++++++++++ packages/plugin/vite/src/util/packageJson.ts | 45 +++++++ packages/plugin/vite/test/VitePlugin_spec.ts | 125 +++++++++++++++++++ 5 files changed, 350 insertions(+), 7 deletions(-) create mode 100644 packages/plugin/vite/src/util/package.ts create mode 100644 packages/plugin/vite/src/util/packageJson.ts create mode 100644 packages/plugin/vite/test/VitePlugin_spec.ts diff --git a/packages/plugin/vite/package.json b/packages/plugin/vite/package.json index f979706d45..b24c9cbb38 100644 --- a/packages/plugin/vite/package.json +++ b/packages/plugin/vite/package.json @@ -12,13 +12,12 @@ "main": "dist/VitePlugin.js", "typings": "dist/VitePlugin.d.ts", "scripts": { - "test": "xvfb-maybe mocha --config ../../../.mocharc.js test/**/*_spec.ts" + "test": "xvfb-maybe mocha --config ../../../.mocharc.js test/**/*_spec.ts test/*_spec.ts" }, "devDependencies": { "@malept/cross-spawn-promise": "^2.0.0", "@types/node": "^18.0.3", "chai": "^4.3.3", - "fs-extra": "^10.0.0", "mocha": "^9.0.1", "which": "^2.0.2", "xvfb-maybe": "^0.2.1" @@ -33,6 +32,7 @@ "@electron-forge/web-multi-logger": "7.2.0", "chalk": "^4.0.0", "debug": "^4.3.1", + "fs-extra": "^10.0.0", "vite": "^4.1.1" }, "publishConfig": { diff --git a/packages/plugin/vite/src/VitePlugin.ts b/packages/plugin/vite/src/VitePlugin.ts index 0625915855..261db1499b 100644 --- a/packages/plugin/vite/src/VitePlugin.ts +++ b/packages/plugin/vite/src/VitePlugin.ts @@ -1,17 +1,22 @@ -import fs from 'node:fs/promises'; import { AddressInfo } from 'node:net'; import path from 'node:path'; import { namedHookWithTaskFn, PluginBase } from '@electron-forge/plugin-base'; -import { ForgeMultiHookMap, StartResult } from '@electron-forge/shared-types'; +import { ForgeMultiHookMap, ResolvedForgeConfig, StartResult } from '@electron-forge/shared-types'; +import chalk from 'chalk'; import debug from 'debug'; +import fs from 'fs-extra'; // eslint-disable-next-line node/no-extraneous-import import { RollupWatcher } from 'rollup'; import { default as vite } from 'vite'; import { VitePluginConfig } from './Config'; +import { getFlatDependencies } from './util/package'; import ViteConfigGenerator from './ViteConfig'; +// Convenient for user customization. +export { resolveDependencies } from './util/package'; + const d = debug('electron-forge:plugin:vite'); export default class VitePlugin extends PluginBase { @@ -41,7 +46,7 @@ export default class VitePlugin extends PluginBase { process.on('SIGINT' as NodeJS.Signals, (_signal) => this.exitHandler({ exit: true })); }; - private setDirectories(dir: string): void { + public setDirectories(dir: string): void { this.projectDir = dir; this.baseDir = path.join(dir, '.vite'); } @@ -55,7 +60,7 @@ export default class VitePlugin extends PluginBase { prePackage: [ namedHookWithTaskFn<'prePackage'>(async () => { this.isProd = true; - await fs.rm(this.baseDir, { recursive: true, force: true }); + await fs.remove(this.baseDir); await Promise.all([this.build(), this.buildRenderer()]); }, 'Building vite bundles'), @@ -67,14 +72,67 @@ export default class VitePlugin extends PluginBase { this.exitHandler({ cleanup: true, exit: true }); }); }, + resolveForgeConfig: this.resolveForgeConfig, + packageAfterCopy: this.packageAfterCopy, }; }; + resolveForgeConfig = async (forgeConfig: ResolvedForgeConfig): Promise => { + forgeConfig.packagerConfig ??= {}; + + if (forgeConfig.packagerConfig.ignore) { + if (typeof forgeConfig.packagerConfig.ignore !== 'function') { + console.error( + chalk.red(`You have set packagerConfig.ignore, the Electron Forge Vite plugin normally sets this automatically. + +Your packaged app may be larger than expected if you dont ignore everything other than the '.vite' folder`) + ); + } + return forgeConfig; + } + + const flatDependencies = await getFlatDependencies(this.projectDir); + + forgeConfig.packagerConfig.ignore = (file: string) => { + if (!file) return false; + + const isViteBuiltFile = /^[/\\]\.vite($|[/\\]).*$/.test(file); + if (isViteBuiltFile) return true; + + const isAppRuntimeDeps = flatDependencies.find((dep) => file.startsWith(dep.src)); + if (isAppRuntimeDeps) return true; + + return false; + }; + return forgeConfig; + }; + + packageAfterCopy = async (_forgeConfig: ResolvedForgeConfig, buildPath: string): Promise => { + const pj = await fs.readJson(path.resolve(this.projectDir, 'package.json')); + + if (!/^(.\/)?.vite\//.test(pj.main)) { + throw new Error(`Electron Forge is configured to use the Vite plugin. The plugin expects the +"main" entry point in "package.json" to be ".vite/*" (where the plugin outputs +the generated files). Instead, it is ${JSON.stringify(pj.main)}`); + } + + if (pj.config) { + delete pj.config.forge; + } + + await fs.writeJson(path.resolve(buildPath, 'package.json'), pj, { + spaces: 2, + }); + + // TODO: exact node_modules files includes + await fs.copy(path.resolve(this.projectDir, 'node_modules'), path.resolve(buildPath, 'node_modules')); + }; + startLogic = async (): Promise => { if (VitePlugin.alreadyStarted) return false; VitePlugin.alreadyStarted = true; - await fs.rm(this.baseDir, { recursive: true, force: true }); + await fs.remove(this.baseDir); return { tasks: [ diff --git a/packages/plugin/vite/src/util/package.ts b/packages/plugin/vite/src/util/package.ts new file mode 100644 index 0000000000..c1322d23fb --- /dev/null +++ b/packages/plugin/vite/src/util/package.ts @@ -0,0 +1,115 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import type { PackageJsonManifest } from './packageJson'; + +export interface Dependency { + name: string; + path: SourceAndDestination; + dependencies: Dependency[]; +} + +export interface SourceAndDestination { + src: string; + dest: string; +} + +const VOLUME_RE = /^[A-Z]:/i; +export async function lookupNodeModulesPaths(root: string, paths: string[] = []): Promise { + if (!root) return paths; + // Linux or Windows absolute path + if (!(root.startsWith('/') || VOLUME_RE.test(root))) return paths; + + const p = path.join(root, 'node_modules'); + + if (fs.existsSync(p) && (await fs.promises.stat(p)).isDirectory()) { + paths = paths.concat(p); + } + root = path.join(root, '..'); + + return root === '/' || /^[A-Z]:$/i.test(root) // root path + ? paths + : await lookupNodeModulesPaths(root, paths); +} + +export async function readPackageJson(root = process.cwd()): Promise { + const packageJsonPath = path.join(root, 'package.json'); + try { + const packageJsonStr = await fs.promises.readFile(packageJsonPath, 'utf8'); + try { + return JSON.parse(packageJsonStr); + } catch (error) { + console.error(`parse 'package.json': ${packageJsonPath}`); + throw error; + } + } catch (error) { + console.error(`'package.json' not found: ${packageJsonPath}`); + throw error; + } +} + +export async function resolveDependencies(root: string) { + const rootDependencies = Object.keys((await readPackageJson(root)).dependencies || {}); + const resolve = async (prePath: string, dependencies: string[], collected: Map = new Map()) => + await Promise.all( + dependencies.map(async (name) => { + let curPath = prePath, + depPath = null, + packageJson = null; + while (!packageJson && root.length <= curPath.length) { + const allNodeModules = await lookupNodeModulesPaths(curPath); + + for (const nodeModules of allNodeModules) { + depPath = path.join(nodeModules, name); + if (fs.existsSync(depPath)) break; + } + + if (depPath) { + try { + packageJson = await readPackageJson(depPath); + } catch (err) { + // lookup node_modules + curPath = path.join(curPath, '..'); + if (curPath.length < root.length) { + console.error(`not found 'node_modules' in root path: ${root}`); + throw err; + } + } + } + } + + if (!depPath || !packageJson) { + throw new Error(`find dependencies error in: ${curPath}`); + } + + const result: Dependency = { + name, + path: { + src: depPath, + dest: path.relative(root, depPath), + }, + dependencies: [], + }; + const shouldResolveDeps = !collected.has(depPath); + collected.set(depPath, result); + if (shouldResolveDeps) { + result.dependencies = await resolve(depPath, Object.keys(packageJson.dependencies || {}), collected); + } + return result; + }) + ); + return resolve(root, rootDependencies); +} + +export async function getFlatDependencies(root = process.cwd()) { + const dpesTree = await resolveDependencies(root); + const collected: SourceAndDestination[] = []; + + const flatten = (dep: Dependency) => { + collected.push(dep.path); + dep.dependencies.forEach(flatten); + }; + dpesTree.forEach(flatten); + + return collected; +} diff --git a/packages/plugin/vite/src/util/packageJson.ts b/packages/plugin/vite/src/util/packageJson.ts new file mode 100644 index 0000000000..2c903c354e --- /dev/null +++ b/packages/plugin/vite/src/util/packageJson.ts @@ -0,0 +1,45 @@ +export interface Person { + name: string; + url?: string; + email?: string; +} + +export interface PackageJsonManifest { + // mandatory (npm) + name: string; + version: string; + engines: { [name: string]: string }; + + // optional (npm) + author?: string | Person; + displayName?: string; + description?: string; + keywords?: string[]; + categories?: string[]; + homepage?: string; + bugs?: string | { url?: string; email?: string }; + license?: string; + contributors?: string | Person[]; + main?: string; + browser?: string; + repository?: string | { type?: string; url?: string }; + scripts?: { [name: string]: string }; + dependencies?: { [name: string]: string }; + devDependencies?: { [name: string]: string }; + private?: boolean; + pricing?: string; + + // not supported (npm) + // files?: string[]; + // bin + // man + // directories + // config + // peerDependencies + // bundledDependencies + // optionalDependencies + // os?: string[]; + // cpu?: string[]; + // preferGlobal + // publishConfig +} diff --git a/packages/plugin/vite/test/VitePlugin_spec.ts b/packages/plugin/vite/test/VitePlugin_spec.ts new file mode 100644 index 0000000000..c1e8efc71d --- /dev/null +++ b/packages/plugin/vite/test/VitePlugin_spec.ts @@ -0,0 +1,125 @@ +import * as os from 'os'; +import * as path from 'path'; + +import { ResolvedForgeConfig } from '@electron-forge/shared-types'; +import { expect } from 'chai'; +import { IgnoreFunction } from 'electron-packager'; +import * as fs from 'fs-extra'; + +import { VitePluginConfig } from '../src/Config'; +import { VitePlugin } from '../src/VitePlugin'; + +describe('VitePlugin', () => { + const baseConfig: VitePluginConfig = { + build: [], + renderer: [], + }; + + const viteTestDir = path.resolve(os.tmpdir(), 'electron-forge-plugin-vite-test'); + + describe('packageAfterCopy', () => { + const packageJSONPath = path.join(viteTestDir, 'package.json'); + const packagedPath = path.join(viteTestDir, 'packaged'); + const packagedPackageJSONPath = path.join(packagedPath, 'package.json'); + let plugin: VitePlugin; + + before(async () => { + await fs.ensureDir(packagedPath); + plugin = new VitePlugin(baseConfig); + plugin.setDirectories(viteTestDir); + }); + + it('should remove config.forge from package.json', async () => { + const packageJSON = { main: './.vite/build/main.js', config: { forge: 'config.js' } }; + await fs.writeJson(packageJSONPath, packageJSON); + await plugin.packageAfterCopy({} as ResolvedForgeConfig, packagedPath); + expect(await fs.pathExists(packagedPackageJSONPath)).to.equal(true); + expect((await fs.readJson(packagedPackageJSONPath)).config).to.not.have.property('forge'); + }); + + it('should succeed if there is no config.forge', async () => { + const packageJSON = { main: '.vite/build/main.js' }; + await fs.writeJson(packageJSONPath, packageJSON); + await plugin.packageAfterCopy({} as ResolvedForgeConfig, packagedPath); + expect(await fs.pathExists(packagedPackageJSONPath)).to.equal(true); + expect(await fs.readJson(packagedPackageJSONPath)).to.not.have.property('config'); + }); + + it('should fail if there is no main key in package.json', async () => { + const packageJSON = {}; + await fs.writeJson(packageJSONPath, packageJSON); + await expect(plugin.packageAfterCopy({} as ResolvedForgeConfig, packagedPath)).to.eventually.be.rejectedWith(/entry point/); + }); + + it('should fail if main in package.json does not starts with .vite/', async () => { + const packageJSON = { main: 'src/main.js' }; + await fs.writeJson(packageJSONPath, packageJSON); + await expect(plugin.packageAfterCopy({} as ResolvedForgeConfig, packagedPath)).to.eventually.be.rejectedWith(/entry point/); + }); + + after(async () => { + await fs.remove(viteTestDir); + }); + }); + + describe('resolveForgeConfig', () => { + let plugin: VitePlugin; + + before(() => { + plugin = new VitePlugin(baseConfig); + }); + + it('sets packagerConfig and packagerConfig.ignore if it does not exist', async () => { + const config = await plugin.resolveForgeConfig({} as ResolvedForgeConfig); + expect(config.packagerConfig).to.not.equal(undefined); + expect(config.packagerConfig.ignore).to.be.a('function'); + }); + + describe('packagerConfig.ignore', () => { + it('does not overwrite an existing ignore value', async () => { + const config = await plugin.resolveForgeConfig({ + packagerConfig: { + ignore: /test/, + }, + } as ResolvedForgeConfig); + + expect(config.packagerConfig.ignore).to.deep.equal(/test/); + }); + + it('ignores everything but files in .vite', async () => { + const config = await plugin.resolveForgeConfig({} as ResolvedForgeConfig); + const ignore = config.packagerConfig.ignore as IgnoreFunction; + + expect(ignore('')).to.equal(false); + expect(ignore('/abc')).to.equal(true); + expect(ignore('/.vite')).to.equal(false); + expect(ignore('/.vite/foo')).to.equal(false); + }); + + it('ignores source map files by default', async () => { + const viteConfig = { ...baseConfig }; + plugin = new VitePlugin(viteConfig); + const config = await plugin.resolveForgeConfig({} as ResolvedForgeConfig); + const ignore = config.packagerConfig.ignore as IgnoreFunction; + + expect(ignore(path.join('/.vite', 'build', 'main.js'))).to.equal(false); + // TODO: check sourcemap files + // expect(ignore(path.join('/.vite', 'build', 'main.js.map'))).to.equal(true); + expect(ignore(path.join('/.vite', 'renderer', 'main_window', 'assets', 'index.js'))).to.equal(false); + // expect(ignore(path.join('/.vite', 'renderer', 'main_window', 'assets', 'index.js.map'))).to.equal(true); + }); + + it('includes source map files when specified by config', async () => { + const viteConfig = { ...baseConfig, packageSourceMaps: true }; + plugin = new VitePlugin(viteConfig); + const config = await plugin.resolveForgeConfig({} as ResolvedForgeConfig); + const ignore = config.packagerConfig.ignore as IgnoreFunction; + + expect(ignore(path.join('/.vite', 'build', 'main.js'))).to.equal(false); + expect(ignore(path.join('/.vite', 'build', 'main.js.map'))).to.equal(false); + expect(ignore(path.join('/.vite', 'renderer', 'main_window', 'assets', 'index.js'))).to.equal(false); + expect(ignore(path.join('/.vite', 'renderer', 'main_window', 'assets', 'index.js.map'))).to.equal(false); + }); + }); + }); +});