Skip to content

Commit

Permalink
feat(template): add vite-typescript template (#3178)
Browse files Browse the repository at this point in the history
Co-authored-by: Black-Hole <[email protected]>
  • Loading branch information
caoxiemeihao and BlackHole1 authored Aug 17, 2023
1 parent dee1970 commit f7018a9
Show file tree
Hide file tree
Showing 16 changed files with 445 additions and 3 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
"lint:fix": "prettier --write .",
"link:prepare": "lerna exec -- node ../../../tools/silent.js yarn link --silent --no-bin-links --link-folder ../../../.links",
"link:remove": "lerna exec -- node ../../../tools/silent.js yarn unlink --silent --no-bin-links --link-folder ../../../.links",
"test": "xvfb-maybe cross-env LINK_FORGE_DEPENDENCIES_ON_INIT=1 TS_NODE_PROJECT='./tsconfig.test.json' TS_NODE_FILES=1 mocha './tools/test-globber.ts'",
"test:fast": "xvfb-maybe cross-env LINK_FORGE_DEPENDENCIES_ON_INIT=1 TS_NODE_PROJECT='./tsconfig.test.json' TEST_FAST_ONLY=1 TS_NODE_FILES=1 mocha './tools/test-globber.ts'",
"test:slow": "xvfb-maybe cross-env LINK_FORGE_DEPENDENCIES_ON_INIT=1 TS_NODE_PROJECT='./tsconfig.test.json' TEST_SLOW_ONLY=1 TS_NODE_FILES=1 mocha './tools/test-globber.ts'",
"test": "xvfb-maybe cross-env NODE_ENV=test LINK_FORGE_DEPENDENCIES_ON_INIT=1 TS_NODE_PROJECT='./tsconfig.test.json' TS_NODE_FILES=1 mocha './tools/test-globber.ts'",
"test:fast": "xvfb-maybe cross-env NODE_ENV=test LINK_FORGE_DEPENDENCIES_ON_INIT=1 TS_NODE_PROJECT='./tsconfig.test.json' TEST_FAST_ONLY=1 TS_NODE_FILES=1 mocha './tools/test-globber.ts'",
"test:slow": "xvfb-maybe cross-env NODE_ENV=test LINK_FORGE_DEPENDENCIES_ON_INIT=1 TS_NODE_PROJECT='./tsconfig.test.json' TEST_SLOW_ONLY=1 TS_NODE_FILES=1 mocha './tools/test-globber.ts'",
"postinstall": "rimraf node_modules/.bin/*.ps1 && ts-node ./tools/gen-tsconfigs.ts && ts-node ./tools/gen-ts-glue.ts",
"prepare": "husky install",
"preversion": "yarn build"
Expand Down
1 change: 1 addition & 0 deletions packages/template/vite-typescript/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tmpl
36 changes: 36 additions & 0 deletions packages/template/vite-typescript/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@electron-forge/template-vite-typescript",
"version": "6.3.0",
"description": "Vite-TypeScript template for Electron Forge, gets you started with Vite really quickly",
"repository": {
"type": "git",
"url": "https://github.com/electron/forge",
"directory": "packages/template/vite-typescript"
},
"author": "caoxiemeihao",
"license": "MIT",
"main": "dist/ViteTypeScriptTemplate.js",
"typings": "dist/ViteTypeScriptTemplate.d.ts",
"scripts": {
"test": "mocha --config ../../../.mocharc.js test/**/*_spec_slow.ts"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"dependencies": {
"@electron-forge/shared-types": "6.3.0",
"@electron-forge/template-base": "6.3.0",
"fs-extra": "^10.0.0"
},
"devDependencies": {
"@electron-forge/core-utils": "6.3.0",
"@electron-forge/test-utils": "6.3.0",
"chai": "^4.3.3",
"fast-glob": "^3.2.7"
},
"files": [
"dist",
"src",
"tmpl"
]
}
72 changes: 72 additions & 0 deletions packages/template/vite-typescript/src/ViteTypeScriptTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import path from 'path';

import { ForgeListrTaskDefinition, InitTemplateOptions } from '@electron-forge/shared-types';
import { BaseTemplate } from '@electron-forge/template-base';
import fs from 'fs-extra';

class ViteTypeScriptTemplate extends BaseTemplate {
public templateDir = path.resolve(__dirname, '..', 'tmpl');

public async initializeTemplate(directory: string, options: InitTemplateOptions): Promise<ForgeListrTaskDefinition[]> {
const superTasks = await super.initializeTemplate(directory, options);
return [
...superTasks,
{
title: 'Setting up Forge configuration',
task: async () => {
await this.copyTemplateFile(directory, 'forge.config.ts');
await fs.remove(path.resolve(directory, 'forge.config.js'));
},
},
{
title: 'Preparing TypeScript files and configuration',
task: async () => {
const filePath = (fileName: string) => path.join(directory, 'src', fileName);

// Copy Vite files
await this.copyTemplateFile(directory, 'vite.main.config.ts');
await this.copyTemplateFile(directory, 'vite.renderer.config.ts');
await this.copyTemplateFile(directory, 'vite.preload.config.ts');

// Copy tsconfig with a small set of presets
await this.copyTemplateFile(directory, 'tsconfig.json');

// Copy eslint config with recommended settings
await this.copyTemplateFile(directory, '.eslintrc.json');

// Remove index.js and replace with main.ts
await fs.remove(filePath('index.js'));
await this.copyTemplateFile(path.join(directory, 'src'), 'main.ts');

await this.copyTemplateFile(path.join(directory, 'src'), 'renderer.ts');
await this.copyTemplateFile(path.join(directory, 'src'), 'types.d.ts');

// Remove preload.js and replace with preload.ts
await fs.remove(filePath('preload.js'));
await this.copyTemplateFile(path.join(directory, 'src'), 'preload.ts');

// TODO: Compatible with any path entry.
// Vite uses index.html under the root path as the entry point.
await fs.move(filePath('index.html'), path.join(directory, 'index.html'));
await this.updateFileByLine(path.join(directory, 'index.html'), (line) => {
if (line.includes('link rel="stylesheet"')) return '';
if (line.includes('</body>')) return ' <script type="module" src="/src/renderer.ts"></script>\n </body>';
return line;
});

// update package.json
const packageJSONPath = path.resolve(directory, 'package.json');
const packageJSON = await fs.readJson(packageJSONPath);
packageJSON.main = '.vite/build/main.js';
// Configure scripts for TS template
packageJSON.scripts.lint = 'eslint --ext .ts,.tsx .';
await fs.writeJson(packageJSONPath, packageJSON, {
spaces: 2,
});
},
},
];
}
}

export default new ViteTypeScriptTemplate();
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import cp from 'child_process';
import path from 'path';

import { yarnOrNpmSpawn } from '@electron-forge/core-utils';
import * as testUtils from '@electron-forge/test-utils';
import { expect } from 'chai';
import glob from 'fast-glob';
import fs from 'fs-extra';

import { api } from '../../../api/core';
import { initLink } from '../../../api/core/src/api/init-scripts/init-link';

describe('ViteTypeScriptTemplate', () => {
let dir: string;

before(async () => {
await yarnOrNpmSpawn(['link:prepare']);
dir = await testUtils.ensureTestDirIsNonexistent();
});

after(async () => {
await yarnOrNpmSpawn(['link:remove']);
await killWindowsEsbuildExe();
await fs.remove(dir);
});

describe('template files are copied to project', () => {
it('should succeed in initializing the typescript template', async () => {
await api.init({
dir,
template: path.resolve(__dirname, '..', 'src', 'ViteTypeScriptTemplate'),
interactive: false,
});
});

const expectedFiles = [
'tsconfig.json',
'.eslintrc.json',
'forge.config.ts',
'vite.main.config.ts',
'vite.renderer.config.ts',
'vite.preload.config.ts',
path.join('src', 'main.ts'),
path.join('src', 'renderer.ts'),
path.join('src', 'preload.ts'),
path.join('src', 'types.d.ts'),
];
for (const filename of expectedFiles) {
it(`${filename} should exist`, async () => {
await testUtils.expectProjectPathExists(dir, filename, 'file');
});
}

it('should ensure js source files from base template are removed', async () => {
const jsFiles = await glob(path.join(dir, 'src', '**', '*.js'));
expect(jsFiles.length).to.equal(0, `The following unexpected js files were found in the src/ folder: ${JSON.stringify(jsFiles)}`);
});
});

describe('lint', () => {
it('should initially pass the linting process', async () => {
delete process.env.TS_NODE_PROJECT;
await testUtils.expectLintToPass(dir);
});
});

describe('package', () => {
let cwd: string;

before(async () => {
delete process.env.TS_NODE_PROJECT;
// Vite resolves plugins via cwd
cwd = process.cwd();
process.chdir(dir);
// We need the version of vite to match exactly during development due to a quirk in
// typescript type-resolution. In prod no one has to worry about things like this
const pj = await fs.readJson(path.resolve(dir, 'package.json'));
pj.resolutions = {
// eslint-disable-next-line @typescript-eslint/no-var-requires
vite: `${require('../../../../node_modules/vite/package.json').version}`,
};
await fs.writeJson(path.resolve(dir, 'package.json'), pj);
await yarnOrNpmSpawn(['install'], {
cwd: dir,
});

// Installing deps removes symlinks that were added at the start of this
// spec via `api.init`. So we should re-link local forge dependencies
// again.
await initLink(dir);
});

after(() => {
process.chdir(cwd);
});

it('should pass', async () => {
await api.package({
dir,
interactive: false,
});
});
});
});

/**
* TODO: resolve `esbuild` can not exit normally on the Windows platform.
* @deprecated
*/
async function killWindowsEsbuildExe() {
if (process.platform !== 'win32') {
return Promise.resolve();
}

return new Promise<void>((resolve, reject) => {
cp.exec('tasklist', (error, stdout) => {
if (error) {
reject(error);
return;
}

const esbuild = stdout
.toString()
.split('\n')
.map((line) => line.split(/\s+/))
.find((line) => line.includes('esbuild.exe'));

if (!esbuild) {
resolve();
return;
}

// ['esbuild.exe', '4564', 'Console', '1', '14,400', 'K', '']
const [, pid] = esbuild;
const result = process.kill(+pid, 'SIGINT');

if (result) {
resolve();
} else {
reject(new Error('kill esbuild process failed'));
}
});
});
}
16 changes: 16 additions & 0 deletions packages/template/vite-typescript/tmpl/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/electron",
"plugin:import/typescript"
],
"parser": "@typescript-eslint/parser"
}
37 changes: 37 additions & 0 deletions packages/template/vite-typescript/tmpl/forge.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { ForgeConfig } from '@electron-forge/shared-types';
import { MakerSquirrel } from '@electron-forge/maker-squirrel';
import { MakerZIP } from '@electron-forge/maker-zip';
import { MakerDeb } from '@electron-forge/maker-deb';
import { MakerRpm } from '@electron-forge/maker-rpm';
import { VitePlugin } from '@electron-forge/plugin-vite';

const config: ForgeConfig = {
packagerConfig: {},
rebuildConfig: {},
makers: [new MakerSquirrel({}), new MakerZIP({}, ['darwin']), new MakerRpm({}), new MakerDeb({})],
plugins: [
new VitePlugin({
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
// If you are familiar with Vite configuration, it will look really familiar.
build: [
{
// `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
entry: 'src/main.ts',
config: 'vite.main.config.ts',
},
{
entry: 'src/preload.ts',
config: 'vite.preload.config.ts',
},
],
renderer: [
{
name: 'main_window',
config: 'vite.renderer.config.ts',
},
],
}),
],
};

export default config;
53 changes: 53 additions & 0 deletions packages/template/vite-typescript/tmpl/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { app, BrowserWindow } from 'electron';
import path from 'path';

// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) {
app.quit();
}

const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});

// and load the index.html of the app.
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
} else {
mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));
}

// Open the DevTools.
mainWindow.webContents.openDevTools();
};

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);

// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});

app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.
11 changes: 11 additions & 0 deletions packages/template/vite-typescript/tmpl/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"devDependencies": {
"@electron-forge/plugin-vite": "ELECTRON_FORGE/VERSION",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
"eslint-plugin-import": "^2.25.0",
"ts-node": "^10.0.0",
"typescript": "~4.5.4"
}
}
2 changes: 2 additions & 0 deletions packages/template/vite-typescript/tmpl/preload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// See the Electron documentation for details on how to use preload scripts:
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
Loading

0 comments on commit f7018a9

Please sign in to comment.