diff --git a/forge.config.ts b/forge.config.ts new file mode 100644 index 000000000..1ab9f7e90 --- /dev/null +++ b/forge.config.ts @@ -0,0 +1,52 @@ +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'; +import { FusesPlugin } from '@electron-forge/plugin-fuses'; +import { FuseV1Options, FuseVersion } from '@electron/fuses'; + +const config: ForgeConfig = { + packagerConfig: { + asar: true, + }, + 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', + }, + ], + }), + // Fuses are used to enable/disable various Electron functionality + // at package time, before code signing the application + new FusesPlugin({ + version: FuseVersion.V1, + [FuseV1Options.RunAsNode]: false, + [FuseV1Options.EnableCookieEncryption]: true, + [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, + [FuseV1Options.EnableNodeCliInspectArguments]: false, + [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, + [FuseV1Options.OnlyLoadAppFromAsar]: true, + }), + ], +}; + +export default config; diff --git a/forge.env.d.ts b/forge.env.d.ts new file mode 100644 index 000000000..8cbf19d3c --- /dev/null +++ b/forge.env.d.ts @@ -0,0 +1,31 @@ +export {}; // Make this a module + +declare global { + // This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Vite + // plugin that tells the Electron app where to look for the Vite-bundled app code (depending on + // whether you're running in development or production). + const MAIN_WINDOW_VITE_DEV_SERVER_URL: string; + const MAIN_WINDOW_VITE_NAME: string; + + namespace NodeJS { + interface Process { + // Used for hot reload after preload scripts. + viteDevServers: Record; + } + } + + type VitePluginConfig = ConstructorParameters[0]; + + interface VitePluginRuntimeKeys { + VITE_DEV_SERVER_URL: `${string}_VITE_DEV_SERVER_URL`; + VITE_NAME: `${string}_VITE_NAME`; + } +} + +declare module 'vite' { + interface ConfigEnv { + root: string; + forgeConfig: VitePluginConfig; + forgeConfigSelf: VitePluginConfig[K][number]; + } +} diff --git a/src/index.html b/index.html similarity index 100% rename from src/index.html rename to index.html diff --git a/package.json b/package.json index 0cd7fa9e9..33e2b35c1 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { - "name": "untitled-app", - "version": "0.24.10", "private": true, - "main":"src/main.ts", - "description":"Zoo Modeling App - CAD", - "license":"none", + "name": "zoo-modeling-app", + "productName": "Zoo Modeling App", + "description": "CAD", + "version": "0.24.10", + "main": ".vite/build/main.js", + "license": "none", "dependencies": { "@codemirror/autocomplete": "^6.17.0", "@codemirror/commands": "^6.6.0", @@ -55,7 +56,6 @@ "react-router-dom": "^6.23.1", "sketch-helpers": "^0.0.4", "three": "^0.166.1", - "typescript": "^5.4.5", "ua-parser-js": "^1.0.37", "uuid": "^9.0.1", "vscode-jsonrpc": "^8.2.1", @@ -65,7 +65,6 @@ "xstate": "^4.38.2" }, "scripts": { - "start": "electron-forge start", "start:prod": "vite preview --port=3000", "serve": "vite serve --port=3000", "build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build", @@ -93,8 +92,10 @@ "xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"", "make:dev": "make dev", "generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts", - "package": "electron-forge package", - "make": "electron-forge make" + "electron:start": "electron-forge start", + "electron:package": "electron-forge package", + "electron:make": "electron-forge make", + "electron:publish": "electron-forge publish" }, "prettier": { "trailingComma": "es5", @@ -116,7 +117,16 @@ }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", - "@babel/preset-env": "^7.25.0", + "@babel/preset-env": "^7.24.3", + "@electron-forge/cli": "^7.4.0", + "@electron-forge/maker-deb": "^7.4.0", + "@electron-forge/maker-rpm": "^7.4.0", + "@electron-forge/maker-squirrel": "^7.4.0", + "@electron-forge/maker-zip": "^7.4.0", + "@electron-forge/plugin-auto-unpack-natives": "^7.4.0", + "@electron-forge/plugin-fuses": "^7.4.0", + "@electron-forge/plugin-vite": "^7.4.0", + "@electron/fuses": "^1.8.0", "@iarna/toml": "^2.2.5", "@lezer/generator": "^1.7.1", "@playwright/test": "^1.45.1", @@ -148,9 +158,10 @@ "@xstate/cli": "^0.5.17", "autoprefixer": "^10.4.19", "electron": "^31.2.1", - "eslint": "^8.57.0", + "eslint": "^8.0.1", "eslint-config-react-app": "^7.0.1", "eslint-plugin-css-modules": "^2.12.0", + "eslint-plugin-import": "^2.25.0", "eslint-plugin-suggest-no-throw": "^1.0.0", "happy-dom": "^14.3.10", "http-server": "^14.1.1", @@ -163,7 +174,11 @@ "prettier": "^2.8.8", "setimmediate": "^1.0.5", "tailwindcss": "^3.4.1", - "vite": "^5.3.3", + "ts-node": "^10.0.0", + "typescript": "^4.5.4", + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.0.0", + "vite": "^5.0.12", "vite-plugin-eslint": "^1.8.1", "vite-plugin-package-version": "^1.1.0", "vite-tsconfig-paths": "^4.3.2", diff --git a/tsconfig.json b/tsconfig.json index 0313608ea..632842661 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "baseUrl": "src", + "outDir": "dist", "paths": { "@kittycad/codemirror-lsp-client": [ "../packages/codemirror-lsp-client/src/index.ts" @@ -20,11 +21,12 @@ "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, + "sourceMap": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, - "module": "ES2022", + "module": "commonjs", "moduleResolution": "node", "resolveJsonModule": true, "composite": true, diff --git a/vite.base.config.ts b/vite.base.config.ts new file mode 100644 index 000000000..25120d990 --- /dev/null +++ b/vite.base.config.ts @@ -0,0 +1,93 @@ +import { builtinModules } from 'node:module'; +import type { AddressInfo } from 'node:net'; +import type { ConfigEnv, Plugin, UserConfig } from 'vite'; +import pkg from './package.json'; + +export const builtins = ['electron', ...builtinModules.map((m) => [m, `node:${m}`]).flat()]; + +export const external = [...builtins, ...Object.keys('dependencies' in pkg ? (pkg.dependencies as Record) : {})]; + +export function getBuildConfig(env: ConfigEnv<'build'>): UserConfig { + const { root, mode, command } = env; + + return { + root, + mode, + build: { + // Prevent multiple builds from interfering with each other. + emptyOutDir: false, + // 🚧 Multiple builds may conflict. + outDir: '.vite/build', + watch: command === 'serve' ? {} : null, + minify: command === 'build', + }, + clearScreen: false, + }; +} + +export function getDefineKeys(names: string[]) { + const define: { [name: string]: VitePluginRuntimeKeys } = {}; + + return names.reduce((acc, name) => { + const NAME = name.toUpperCase(); + const keys: VitePluginRuntimeKeys = { + VITE_DEV_SERVER_URL: `${NAME}_VITE_DEV_SERVER_URL`, + VITE_NAME: `${NAME}_VITE_NAME`, + }; + + return { ...acc, [name]: keys }; + }, define); +} + +export function getBuildDefine(env: ConfigEnv<'build'>) { + const { command, forgeConfig } = env; + const names = forgeConfig.renderer.filter(({ name }) => name != null).map(({ name }) => name!); + const defineKeys = getDefineKeys(names); + const define = Object.entries(defineKeys).reduce((acc, [name, keys]) => { + const { VITE_DEV_SERVER_URL, VITE_NAME } = keys; + const def = { + [VITE_DEV_SERVER_URL]: command === 'serve' ? JSON.stringify(process.env[VITE_DEV_SERVER_URL]) : undefined, + [VITE_NAME]: JSON.stringify(name), + }; + return { ...acc, ...def }; + }, {} as Record); + + return define; +} + +export function pluginExposeRenderer(name: string): Plugin { + const { VITE_DEV_SERVER_URL } = getDefineKeys([name])[name]; + + return { + name: '@electron-forge/plugin-vite:expose-renderer', + configureServer(server) { + process.viteDevServers ??= {}; + // Expose server for preload scripts hot reload. + process.viteDevServers[name] = server; + + server.httpServer?.once('listening', () => { + const addressInfo = server.httpServer!.address() as AddressInfo; + // Expose env constant for main process use. + process.env[VITE_DEV_SERVER_URL] = `http://localhost:${addressInfo?.port}`; + }); + }, + }; +} + +export function pluginHotRestart(command: 'reload' | 'restart'): Plugin { + return { + name: '@electron-forge/plugin-vite:hot-restart', + closeBundle() { + if (command === 'reload') { + for (const server of Object.values(process.viteDevServers)) { + // Preload scripts hot reload. + server.ws.send({ type: 'full-reload' }); + } + } else { + // Main process hot restart. + // https://github.com/electron/forge/blob/v7.2.0/packages/api/core/src/api/start.ts#L216-L223 + process.stdin.emit('data', 'rs'); + } + }, + }; +} diff --git a/vite.main.config.ts b/vite.main.config.ts new file mode 100644 index 000000000..ccf9758d4 --- /dev/null +++ b/vite.main.config.ts @@ -0,0 +1,90 @@ +import type { ConfigEnv, UserConfig } from 'vite'; +import { defineConfig, mergeConfig } from 'vite'; +import { configDefaults } from 'vitest/config' +import viteTsconfigPaths from 'vite-tsconfig-paths' +import vitePluginEslint from 'vite-plugin-eslint' +import vitePluginPackageVersion from 'vite-plugin-package-version' +import { getBuildConfig, getBuildDefine, external, pluginHotRestart } from './vite.base.config'; +import viteJsPluginReact from '@vitejs/plugin-react' +// @ts-ignore: No types available +import { lezer } from '@lezer/generator/rollup' + +// https://vitejs.dev/config +export default defineConfig((env) => { + const forgeEnv = env as ConfigEnv<'build'>; + const { forgeConfigSelf } = forgeEnv; + const define = getBuildDefine(forgeEnv); + const config: UserConfig = { + server: { + open: true, + port: 3000, + watch: { + ignored: [ + '**/target/**', + '**/dist/**', + '**/build/**', + '**/test-results/**', + '**/playwright-report/**', + ], + }, + }, + test: { + globals: true, + pool: 'forks', + poolOptions: { + forks: { + maxForks: 2, + minForks: 1, + }, + }, + setupFiles: ['src/setupTests.ts', '@vitest/web-worker'], + environment: 'happy-dom', + coverage: { + provider: 'v8', + }, + exclude: [...configDefaults.exclude, '**/e2e/**/*'], + deps: { + optimizer: { + web: { + include: ['vitest-canvas-mock'], + }, + }, + }, + clearMocks: true, + restoreMocks: true, + mockReset: true, + reporters: process.env.GITHUB_ACTIONS + ? ['dot', 'github-actions'] + : ['verbose', 'hanging-process'], + testTimeout: 1000, + hookTimeout: 1000, + teardownTimeout: 1000, + }, + build: { + lib: { + entry: forgeConfigSelf.entry!, + fileName: () => '[name].js', + formats: ['cjs'], + }, + rollupOptions: { + external, + }, + }, + resolve: { + // Load the Node.js entry. + mainFields: ['module', 'jsnext:main', 'jsnext'], + alias: { + '@kittycad/codemirror-lsp-client': '/packages/codemirror-lsp-client/src', + }, + }, + plugins: [ + pluginHotRestart('restart'), viteJsPluginReact(), viteTsconfigPaths(), vitePluginEslint(), vitePluginPackageVersion(), lezer() + ], + worker: { + plugins: () => [viteTsconfigPaths()], + }, + define, + }; + + return mergeConfig(getBuildConfig(forgeEnv), config); +}); diff --git a/vite.preload.config.ts b/vite.preload.config.ts new file mode 100644 index 000000000..3cbadf6f5 --- /dev/null +++ b/vite.preload.config.ts @@ -0,0 +1,29 @@ +import type { ConfigEnv, UserConfig } from 'vite'; +import { defineConfig, mergeConfig } from 'vite'; +import { getBuildConfig, external, pluginHotRestart } from './vite.base.config'; + +// https://vitejs.dev/config +export default defineConfig((env) => { + const forgeEnv = env as ConfigEnv<'build'>; + const { forgeConfigSelf } = forgeEnv; + const config: UserConfig = { + build: { + rollupOptions: { + external, + // Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`. + input: forgeConfigSelf.entry!, + output: { + format: 'cjs', + // It should not be split chunks. + inlineDynamicImports: true, + entryFileNames: '[name].js', + chunkFileNames: '[name].js', + assetFileNames: '[name].[ext]', + }, + }, + }, + plugins: [pluginHotRestart('reload')], + }; + + return mergeConfig(getBuildConfig(forgeEnv), config); +}); diff --git a/vite.renderer.config.ts b/vite.renderer.config.ts new file mode 100644 index 000000000..e821a3bb6 --- /dev/null +++ b/vite.renderer.config.ts @@ -0,0 +1,24 @@ +import type { ConfigEnv, UserConfig } from 'vite'; +import { defineConfig } from 'vite'; +import { pluginExposeRenderer } from './vite.base.config'; + +// https://vitejs.dev/config +export default defineConfig((env) => { + const forgeEnv = env as ConfigEnv<'renderer'>; + const { root, mode, forgeConfigSelf } = forgeEnv; + const name = forgeConfigSelf.name ?? ''; + + return { + root, + mode, + base: './', + build: { + outDir: `.vite/renderer/${name}`, + }, + plugins: [pluginExposeRenderer(name)], + resolve: { + preserveSymlinks: true, + }, + clearScreen: false, + } as UserConfig; +});