diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 4dc99b3..2b80979 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -11,7 +11,7 @@ concurrency: jobs: deploy: - if: "contains(github.event.head_commit.message, 'deploy')" + if: contains(github.event.head_commit.message, 'release') || contains(github.event.head_commit.message, 'deploy') runs-on: ubuntu-latest steps: - name: Checkout diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4d82651..364245b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -74,6 +74,10 @@ jobs: key: ${{ runner.os }}-playwright-bin-v1 path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} + - name: Install Playwright + # does not need to explicitly set chromium after https://github.com/microsoft/playwright/issues/14862 is solved + run: pnpx playwright install chromium + - name: Build run: pnpm run build diff --git a/README.md b/README.md index 2124498..f5380fd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ ![npm][npm-img] - **在vite的运行时或构建时编译指定目录下的typescript文件,供开发者独立使用** @@ -26,12 +25,13 @@ ## 功能 -- 运行时和构建时,把指定文件夹中的 `typescript` 文件编译为 `javascript`,供浏览器直接使用 - 输出带有 `hash` 的js文件,无需担心缓存 - 自定义编译选项,指定目标浏览器范围,无需担心兼容性 - 支持 `vite` 环境变量 - 支持 `HMR` - 支持不同的输出方式(内存模式和文件模式) +- 支持 `CSR` 和 `SSR` 应用 + ## 安装 @@ -41,16 +41,20 @@ pnpm add vite-plugin-public-typescript -D ## 配置项 -| 参数 | 类型 | 默认值 | 描述 | -| -------------- | -------------- | ------------------- | ---------------------------------------------- | -| inputDir | `string` | `public-typescript` | 存放需要编译的 `typescript` 的目录 | -| outputDir | `string` | `/` | 输出公共 javascript 的目录,相对于 `publicDir` | -| manifestName | `string` | `manifest` | `manifest` 的文件名 | -| hash | `boolean` | `true` | 编译后的 `js` 是否生成 `hash ` | -| esbuildOptions | `BuildOptions` | `{}` | esbuild 构建选项 | -| ssrBuild | `boolean` | `false` | 当前打包环境是否是 ssr | -| sideEffects | `boolean` | `false` | 若 `typescript` 文件中有导入第三方库,则开启 | -| destination | `string` | `memory` | 输出模式:内存模式 \| 文件模式 | +| 参数 | 类型 | 默认值 | 描述 | +| -------------- | -------------- | --------------------------------------------- | ---------------------------------------------- | +| inputDir | `string` | `public-typescript` | 存放需要编译的 `typescript` 的目录 | +| outputDir | `string` | `/` | 输出公共 javascript 的目录,相对于 `publicDir` | +| manifestName | `string` | `manifest` | `manifest` 的文件名 | +| hash | `boolean` | `true` | 编译后的 `js` 是否生成 `hash ` | +| esbuildOptions | `BuildOptions` | `{}` | esbuild 构建选项 | +| ssrBuild | `boolean` | `false` | 当前打包环境是否是 ssr | +| sideEffects | `boolean` | `true` | 是否编译三方库 | +| destination | `string` | `memory` | 输出模式:内存模式 \| 文件模式 | +| cacheDir | `string` | `node_modules/.vite-plugin-public-typescript` | 存放manifest缓存的目录 | +| base | `string` | vite config 中的 `base` | 资源 base url | + + ## 用法 @@ -58,7 +62,6 @@ pnpm add vite-plugin-public-typescript -D ```typescript import { defineConfig } from 'vite' import { publicTypescript, injectScripts } from 'vite-plugin-public-typescript' -import manifest from './public-typescript/manifest.json' export default defineConfig({ plugins: [ @@ -69,7 +72,7 @@ export default defineConfig({ outputDir: '/out', destination: 'memory', }), - injectScripts([ + injectScripts((manifest) => [ { attrs: { src: manifest.script, @@ -81,6 +84,14 @@ export default defineConfig({ }) ``` +### 获取manifest + +```typescript +import { manifest } from 'vite-plugin-public-typescript/client' + +console.log(manifest) +``` + ### SPA @@ -94,12 +105,11 @@ export default defineConfig({ import type { HtmlTagDescriptor } from 'vite' import { defineConfig } from 'vite' import { publicTypescript, injectScripts } from 'vite-plugin-public-typescript' -import manifest from './public-typescript/manifest.json' export default defineConfig({ plugins: [ publicTypescript(), - injectScripts([ + injectScripts((manifest) => [ { attrs: { src: manifest.spa, @@ -133,11 +143,16 @@ export default defineConfig({ #### server.js ```js -import manifest from './public-typescript/manifest.json' - -const html = template - // inject js - .replace('', ``) +import { injectScriptsToHtml } from 'vite-plugin-public-typescript' + +html = injectScriptsToHtml(html, (manifest) => [ + { + attrs: { + src: manifest.ssr, + }, + injectTo: 'head-prepend', + }, +]) ``` diff --git a/package.json b/package.json index 2a2b73d..d9965a6 100644 --- a/package.json +++ b/package.json @@ -22,14 +22,26 @@ "README.md", "dist" ], - "main": "./dist/index.cjs", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", + "main": "./dist/node/index.cjs", + "module": "./dist/node/index.js", + "types": "./dist/node/index.d.ts", "exports": { ".": { - "types": "./dist/index.d.ts", - "require": "./dist/index.cjs", - "import": "./dist/index.js" + "types": "./dist/node/index.d.ts", + "require": "./dist/node/index.cjs", + "import": "./dist/node/index.js" + }, + "./client": { + "types": "./dist/client/index.d.ts", + "require": "./dist/client/index.cjs", + "import": "./dist/client/index.js" + } + }, + "typesVersions": { + "*": { + "client": [ + "./dist/client/index.d.ts" + ] } }, "scripts": { @@ -57,14 +69,15 @@ "magic-string": "^0.30.5", "on-change": "^4.0.2", "parse5": "^7.1.2", + "picocolors": "^1.0.0", "tiny-glob": "^0.2.9", "watcher": "^2.3.0" }, "devDependencies": { "@commitlint/cli": "^18.0.0", - "@minko-fe/commitlint-config": "^2.0.2", - "@minko-fe/eslint-config": "^2.0.2", - "@minko-fe/tsconfig": "^2.0.2", + "@minko-fe/commitlint-config": "^2.0.4", + "@minko-fe/eslint-config": "^2.0.4", + "@minko-fe/tsconfig": "^2.0.4", "@types/debug": "^4.1.9", "@types/fs-extra": "^11.0.2", "@types/node": "^20.8.6", @@ -75,7 +88,7 @@ "npm-run-all": "^4.1.5", "simple-git-hooks": "^2.9.0", "taze": "^0.11.4", - "tsup": "^7.2.0", + "tsup": "^7.0.0", "typescript": "^5.2.2", "vite": "^4.4.11", "vitest": "^0.34.5", diff --git a/playground/spa-file-mode/public-typescript/manifest.json b/playground/spa-file-mode/public-typescript/manifest.json deleted file mode 100644 index fc4ce77..0000000 --- a/playground/spa-file-mode/public-typescript/manifest.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "define": "/vite-plugin-public-typescript/out/define.6fc3aeb7.js", - "env": "/vite-plugin-public-typescript/out/env.8bea83e0.js", - "hmr": "/vite-plugin-public-typescript/out/hmr.c01ee876.js" -} diff --git a/playground/spa-file-mode/public/out/define.0852dc5a.js b/playground/spa-file-mode/public/out/define.0852dc5a.js new file mode 100644 index 0000000..ac4b171 --- /dev/null +++ b/playground/spa-file-mode/public/out/define.0852dc5a.js @@ -0,0 +1 @@ +(()=>{var o={hello:"world"};window.VITE_DEFINE={};window.VITE_DEFINE["custom-define"]="custom define!";window.VITE_DEFINE["hello-world"]=o;})(); diff --git a/playground/spa-file-mode/public/out/env.e6648627.js b/playground/spa-file-mode/public/out/env.e6648627.js new file mode 100644 index 0000000..b694714 --- /dev/null +++ b/playground/spa-file-mode/public/out/env.e6648627.js @@ -0,0 +1 @@ +(()=>{var e={VITE_ENV_FROM_ENVFILE:"imfromdotenv",BASE_URL:"/vite-plugin-public-typescript/",MODE:"development",DEV:!0,PROD:!1};window.VITE_ENV=e;})(); diff --git a/playground/spa-file-mode/public/out/hmr.c01ee876.js b/playground/spa-file-mode/public/out/hmr.c01ee876.js new file mode 100644 index 0000000..cd5d92e --- /dev/null +++ b/playground/spa-file-mode/public/out/hmr.c01ee876.js @@ -0,0 +1 @@ +(()=>{console.log("hmr");window.hmr="hmr original text";})(); diff --git a/playground/spa-file-mode/src/App.tsx b/playground/spa-file-mode/src/App.tsx index bd2f4b8..3595d6e 100644 --- a/playground/spa-file-mode/src/App.tsx +++ b/playground/spa-file-mode/src/App.tsx @@ -1,12 +1,12 @@ import { type ReactNode, useEffect, useState } from 'react' -import manifest from '../public-typescript/manifest.json' +import { manifest } from 'vite-plugin-public-typescript/client' import './App.css' function formatManifst() { return Object.keys(manifest).map((key) => (
文件 {key}.ts 的 js uri 是:
-
{(manifest as Record)[key]}
+
{manifest[key]}
)) } diff --git a/playground/spa-file-mode/vite.config.ts b/playground/spa-file-mode/vite.config.ts index b63f747..209f1ed 100644 --- a/playground/spa-file-mode/vite.config.ts +++ b/playground/spa-file-mode/vite.config.ts @@ -1,7 +1,6 @@ import react from '@vitejs/plugin-react' import { defineConfig } from 'vite' import { injectScripts, publicTypescript } from 'vite-plugin-public-typescript' -import manifest from './public-typescript/manifest.json' // https://vitejs.dev/config/ export default defineConfig(() => ({ @@ -19,7 +18,7 @@ export default defineConfig(() => ({ outputDir: 'out', destination: 'file', }), - injectScripts([ + injectScripts((manifest) => [ { attrs: { src: manifest.hmr }, injectTo: 'body', diff --git a/playground/spa/__tests__/spa.spec.ts b/playground/spa/__tests__/spa.spec.ts index 052c88d..d64582f 100644 --- a/playground/spa/__tests__/spa.spec.ts +++ b/playground/spa/__tests__/spa.spec.ts @@ -16,6 +16,8 @@ import { beforeAll, describe, expect, test } from 'vitest' const hmrOriginText = 'hmr original text' +const manifestPath = 'node_modules/.vite-plugin-public-typescript/manifest.json' + describe('console', async () => { test('should console hmr string', async () => { await untilBrowserLogAfter(() => page.goto(viteTestUrl), 'hmr') @@ -47,7 +49,7 @@ describe('manifest', () => { let manifest: string beforeAll(() => { try { - manifest = readFile('public-typescript/manifest.json') + manifest = readFile(manifestPath) } catch {} }) @@ -62,7 +64,7 @@ describe('manifest', () => { }) test.runIf(isServe)('should not trigger vite server restart when manifest file changed', async () => { - editFile('public-typescript/manifest.json', (content) => content) + editFile(manifestPath, (content) => content) await withRetry(async () => { expect(serverLogs).not.toEqual(expect.arrayContaining([expect.stringMatching('server restarted')])) expect(serverLogs).not.toEqual(expect.arrayContaining([expect.stringMatching('error')])) @@ -75,7 +77,7 @@ describe.skipIf(isServe)('build', () => { let manifest: string beforeAll(() => { try { - manifest = readFile('public-typescript/manifest.json') + manifest = readFile(manifestPath) jsFiles = listFiles('dist/out') } catch {} }) diff --git a/playground/spa/public-typescript/manifest.json b/playground/spa/public-typescript/manifest.json deleted file mode 100644 index 07b496a..0000000 --- a/playground/spa/public-typescript/manifest.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "define": "/vite-plugin-public-typescript/out/define.0852dc5a.js", - "env": "/vite-plugin-public-typescript/out/env.e6648627.js", - "hmr": "/vite-plugin-public-typescript/out/hmr.c01ee876.js" -} diff --git a/playground/spa/src/App.tsx b/playground/spa/src/App.tsx index 24e42e6..be4a885 100644 --- a/playground/spa/src/App.tsx +++ b/playground/spa/src/App.tsx @@ -1,12 +1,12 @@ import { type ReactNode, useEffect, useState } from 'react' -import manifest from '../public-typescript/manifest.json' +import { manifest } from 'vite-plugin-public-typescript/client' import './App.css' function formatManifst() { return Object.keys(manifest).map((key) => (
文件 {key}.ts 的 js uri 是:
-
{(manifest as Record)[key]}
+
{manifest[key]}
)) } diff --git a/playground/spa/src/index.css b/playground/spa/src/index.css index 917888c..fa868ec 100644 --- a/playground/spa/src/index.css +++ b/playground/spa/src/index.css @@ -3,11 +3,9 @@ font-size: 16px; line-height: 24px; font-weight: 400; - color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; - font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; @@ -20,6 +18,7 @@ a { color: #646cff; text-decoration: inherit; } + a:hover { color: #535bf2; } @@ -48,9 +47,11 @@ button { cursor: pointer; transition: border-color 0.25s; } + button:hover { border-color: #646cff; } + button:focus, button:focus-visible { outline: 4px auto -webkit-focus-ring-color; @@ -61,9 +62,11 @@ button:focus-visible { color: #213547; background-color: #ffffff; } + a:hover { color: #747bff; } + button { background-color: #f9f9f9; } diff --git a/playground/spa/vite.config.ts b/playground/spa/vite.config.ts index e524c9d..22989f9 100644 --- a/playground/spa/vite.config.ts +++ b/playground/spa/vite.config.ts @@ -1,7 +1,6 @@ import react from '@vitejs/plugin-react' import { defineConfig } from 'vite' import { injectScripts, publicTypescript } from 'vite-plugin-public-typescript' -import manifest from './public-typescript/manifest.json' // https://vitejs.dev/config/ export default defineConfig(() => ({ @@ -18,8 +17,9 @@ export default defineConfig(() => ({ hash: true, outputDir: 'out', destination: 'memory', + cacheDir: 'node_modules/.vite-plugin-public-typescript', }), - injectScripts([ + injectScripts((manifest) => [ { attrs: { src: manifest.hmr }, injectTo: 'body', @@ -38,5 +38,5 @@ export default defineConfig(() => ({ }, ]), ], - clearScreen: true, + clearScreen: false, })) diff --git a/playground/ssr/public-typescript/manifest.json b/playground/ssr/public-typescript/manifest.json deleted file mode 100644 index 78897e5..0000000 --- a/playground/ssr/public-typescript/manifest.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "ssr": "/ssr.2507f2a9.js" -} diff --git a/playground/ssr/server.js b/playground/ssr/server.js index 2ff41f7..cadf414 100644 --- a/playground/ssr/server.js +++ b/playground/ssr/server.js @@ -1,16 +1,10 @@ import express from 'express' import fs from 'node:fs' -import { createRequire } from 'node:module' import path from 'node:path' import { fileURLToPath } from 'node:url' import { injectScriptsToHtml } from 'vite-plugin-public-typescript' -const require = createRequire(import.meta.url) - -const manifest = require('./public-typescript/manifest.json') - const __dirname = path.dirname(fileURLToPath(import.meta.url)) - const isTest = process.env.VITEST process.env.MY_CUSTOM_SECRET = 'API_KEY_qwertyuiop' @@ -82,7 +76,7 @@ export async function createServer(root = process.cwd(), isProd = process.env.NO let html = template.replace(``, appHtml) - html = injectScriptsToHtml(html, [ + html = injectScriptsToHtml(html, (manifest) => [ { attrs: { src: manifest.ssr, @@ -101,3 +95,9 @@ export async function createServer(root = process.cwd(), isProd = process.env.NO return { app, vite, hmrPort } } + +// const port = process.env.PORT || 5173 +// const { app } = await createServer() +// app.listen(port, () => { +// console.log(`http://localhost:${port}`) +// }) diff --git a/playground/ssr/src/App.tsx b/playground/ssr/src/App.tsx index d9a1983..6ccfc5b 100644 --- a/playground/ssr/src/App.tsx +++ b/playground/ssr/src/App.tsx @@ -1,7 +1,18 @@ +import { manifest } from 'vite-plugin-public-typescript/client' + export default function App() { + function formatManifst() { + return Object.keys(manifest).map((key) => ( +
+
文件 {key}.ts 的 js uri 是:
+
{manifest[key]}
+
+ )) + } return ( <>
this is app
+
{formatManifst()}
) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0550684..6271527 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: parse5: specifier: ^7.1.2 version: 7.1.2 + picocolors: + specifier: ^1.0.0 + version: 1.0.0 tiny-glob: specifier: ^0.2.9 version: 0.2.9 @@ -37,14 +40,14 @@ importers: specifier: ^18.0.0 version: 18.0.0 '@minko-fe/commitlint-config': - specifier: ^2.0.2 - version: 2.0.2 + specifier: ^2.0.4 + version: 2.0.4 '@minko-fe/eslint-config': - specifier: ^2.0.2 - version: 2.0.2(eslint@8.51.0) + specifier: ^2.0.4 + version: 2.0.4(eslint@8.51.0) '@minko-fe/tsconfig': - specifier: ^2.0.2 - version: 2.0.2(typescript@5.2.2) + specifier: ^2.0.4 + version: 2.0.4(typescript@5.2.2) '@types/debug': specifier: ^4.1.9 version: 4.1.9 @@ -1135,18 +1138,18 @@ packages: type-detect: 4.0.8 dev: true - /@minko-fe/commitlint-config@2.0.2: - resolution: {integrity: sha512-7q22WawonkD00+jJjhK36nuRAeystsTjd4Zj+kPLWys/g4mWlA4MlE05o0ucjklmDIwmx9FEhzwNmz2nI0j1QA==} + /@minko-fe/commitlint-config@2.0.4: + resolution: {integrity: sha512-ZtwKT0z6WR4c/1slE2bbM7w4sqKH6WDW8W548vkkmvlEB3MtQRIaXU6NKNgm4Rt3/0qO7/ZF+51nwwbRuvj/Tw==} dependencies: '@commitlint/config-conventional': 17.8.1 dev: true - /@minko-fe/eslint-config@2.0.2(eslint@8.51.0): - resolution: {integrity: sha512-GyBVygchSPWFXTXqLopjGWGbJjOnMWwowa17rnoN1mEy9FnJ/UNGiMzcVpvCjlv8PA/ryYB6R9d1mFyn1jwWDA==} + /@minko-fe/eslint-config@2.0.4(eslint@8.51.0): + resolution: {integrity: sha512-zTzYgGUKqm9tVVtvzneIDXZpumeZPtByEHOC+sZluPhR/9wientfn22RvI1C9Kw5t54ja0wbQfYlKH/omQxcRg==} peerDependencies: eslint: ^8.0.0 dependencies: - '@minko-fe/prettier-config': 2.0.2(prettier@3.0.3) + '@minko-fe/prettier-config': 2.0.4(prettier@3.0.3) '@typescript-eslint/eslint-plugin': 6.9.0(@typescript-eslint/parser@6.9.0)(eslint@8.51.0)(typescript@5.2.2) '@typescript-eslint/parser': 6.9.0(eslint@8.51.0)(typescript@5.2.2) eslint: 8.51.0 @@ -1154,6 +1157,7 @@ packages: eslint-config-standard: 17.1.0(eslint-plugin-import@2.29.0)(eslint-plugin-n@16.2.0)(eslint-plugin-promise@6.1.1)(eslint@8.51.0) eslint-define-config: 1.24.1 eslint-plugin-antfu: 1.0.0-beta.12(eslint@8.51.0)(typescript@5.2.2) + eslint-plugin-disable-autofix: 4.1.0(eslint@8.51.0) eslint-plugin-eslint-comments: 3.2.0(eslint@8.51.0) eslint-plugin-html: 7.1.0 eslint-plugin-i: 2.28.1(@typescript-eslint/parser@6.9.0)(eslint@8.51.0) @@ -1189,16 +1193,16 @@ packages: - vue-eslint-parser dev: true - /@minko-fe/prettier-config@2.0.2(prettier@3.0.3): - resolution: {integrity: sha512-Cwot3poiKrs1nTUBIBhohPgfW7R7F1Len3BOuBtdJupNd/MTIQRTvzaeV4ScGNBAYQt/LYphINU79IOihj6/Tw==} + /@minko-fe/prettier-config@2.0.4(prettier@3.0.3): + resolution: {integrity: sha512-TsfteRXsUD17EjIMa7lkHiwmuIV96cYorzhdmWKQIHBQff6s6+7RO9REyqGHCETWybnOKWbIHUVj5h9cJOWYJg==} peerDependencies: prettier: '>=2.0.0' dependencies: prettier: 3.0.3 dev: true - /@minko-fe/tsconfig@2.0.2(typescript@5.2.2): - resolution: {integrity: sha512-DPb+atqs7PyDhD3VbZSkiNZ2GI0wCGwdrUOdZMUOyoDyiQDIGVLdWBrJsxM2cnHyJVddoieeowXDYXexw2tYlA==} + /@minko-fe/tsconfig@2.0.4(typescript@5.2.2): + resolution: {integrity: sha512-WHXhBCA+hWMUVdVp2d1aeIRem8otq50Nji7CxAu3qZHjKveHfGO7XNjCKf1hZlASsNML99SD788qeQlj7Xzmew==} peerDependencies: typescript: '>=4.0.0' dependencies: @@ -1594,14 +1598,6 @@ packages: - supports-color dev: true - /@typescript-eslint/scope-manager@6.7.5: - resolution: {integrity: sha512-GAlk3eQIwWOJeb9F7MKQ6Jbah/vx1zETSDw8likab/eFcqkjSD7BI75SDAeC5N2L0MmConMoPvTsmkrg71+B1A==} - engines: {node: ^16.0.0 || >=18.0.0} - dependencies: - '@typescript-eslint/types': 6.7.5 - '@typescript-eslint/visitor-keys': 6.7.5 - dev: true - /@typescript-eslint/scope-manager@6.9.0: resolution: {integrity: sha512-1R8A9Mc39n4pCCz9o79qRO31HGNDvC7UhPhv26TovDsWPBDx+Sg3rOZdCELIA3ZmNoWAuxaMOT7aWtGRSYkQxw==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1630,37 +1626,11 @@ packages: - supports-color dev: true - /@typescript-eslint/types@6.7.5: - resolution: {integrity: sha512-WboQBlOXtdj1tDFPyIthpKrUb+kZf2VroLZhxKa/VlwLlLyqv/PwUNgL30BlTVZV1Wu4Asu2mMYPqarSO4L5ZQ==} - engines: {node: ^16.0.0 || >=18.0.0} - dev: true - /@typescript-eslint/types@6.9.0: resolution: {integrity: sha512-+KB0lbkpxBkBSiVCuQvduqMJy+I1FyDbdwSpM3IoBS7APl4Bu15lStPjgBIdykdRqQNYqYNMa8Kuidax6phaEw==} engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@6.7.5(typescript@5.2.2): - resolution: {integrity: sha512-NhJiJ4KdtwBIxrKl0BqG1Ur+uw7FiOnOThcYx9DpOGJ/Abc9z2xNzLeirCG02Ig3vkvrc2qFLmYSSsaITbKjlg==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/types': 6.7.5 - '@typescript-eslint/visitor-keys': 6.7.5 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.5.4 - ts-api-utils: 1.0.3(typescript@5.2.2) - typescript: 5.2.2 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/typescript-estree@6.9.0(typescript@5.2.2): resolution: {integrity: sha512-NJM2BnJFZBEAbCfBP00zONKXvMqihZCrmwCaik0UhLr0vAgb6oguXxLX1k00oQyD+vZZ+CJn3kocvv2yxm4awQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1682,25 +1652,6 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@6.7.5(eslint@8.51.0)(typescript@5.2.2): - resolution: {integrity: sha512-pfRRrH20thJbzPPlPc4j0UNGvH1PjPlhlCMq4Yx7EGjV7lvEeGX0U6MJYe8+SyFutWgSHsdbJ3BXzZccYggezA==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.51.0) - '@types/json-schema': 7.0.13 - '@types/semver': 7.5.2 - '@typescript-eslint/scope-manager': 6.7.5 - '@typescript-eslint/types': 6.7.5 - '@typescript-eslint/typescript-estree': 6.7.5(typescript@5.2.2) - eslint: 8.51.0 - semver: 7.5.4 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - /@typescript-eslint/utils@6.9.0(eslint@8.51.0)(typescript@5.2.2): resolution: {integrity: sha512-5Wf+Jsqya7WcCO8me504FBigeQKVLAMPmUzYgDbWchINNh1KJbxCgVya3EQ2MjvJMVeXl3pofRmprqX6mfQkjQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1720,14 +1671,6 @@ packages: - typescript dev: true - /@typescript-eslint/visitor-keys@6.7.5: - resolution: {integrity: sha512-3MaWdDZtLlsexZzDSdQWsFQ9l9nL8B80Z4fImSpyllFC/KLqWQRdEcB+gGGO+N3Q2uL40EsG66wZLsohPxNXvg==} - engines: {node: ^16.0.0 || >=18.0.0} - dependencies: - '@typescript-eslint/types': 6.7.5 - eslint-visitor-keys: 3.4.3 - dev: true - /@typescript-eslint/visitor-keys@6.9.0: resolution: {integrity: sha512-dGtAfqjV6RFOtIP8I0B4ZTBRrlTT8NHHlZZSchQx3qReaoDeXhYM++M4So2AgFK9ZB0emRPA6JI1HkafzA2Ibg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1935,6 +1878,11 @@ packages: picomatch: 2.3.1 dev: true + /app-root-path@3.1.0: + resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} + engines: {node: '>= 6.0.0'} + dev: true + /aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} dev: true @@ -3015,7 +2963,7 @@ packages: define-properties: 1.2.1 es-abstract: 1.22.2 es-set-tostringtag: 2.0.1 - function-bind: 1.1.1 + function-bind: 1.1.2 get-intrinsic: 1.2.1 globalthis: 1.0.3 has-property-descriptors: 1.0.0 @@ -3171,7 +3119,7 @@ packages: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} dependencies: debug: 3.2.7 - is-core-module: 2.13.0 + is-core-module: 2.13.1 resolve: 1.22.6 transitivePeerDependencies: - supports-color @@ -3211,13 +3159,24 @@ packages: peerDependencies: eslint: '*' dependencies: - '@typescript-eslint/utils': 6.7.5(eslint@8.51.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.9.0(eslint@8.51.0)(typescript@5.2.2) eslint: 8.51.0 transitivePeerDependencies: - supports-color - typescript dev: true + /eslint-plugin-disable-autofix@4.1.0(eslint@8.51.0): + resolution: {integrity: sha512-D/fUPM1hTygcZhxM5rc2oiMhud2qgF/sEy3QWMKgSr0A3nbpek6gKG8r8Zk25bBTvAqxjMzqxGDxFN9B/bvrSw==} + peerDependencies: + eslint: '>= 7' + dependencies: + app-root-path: 3.1.0 + eslint: 8.51.0 + eslint-rule-composer: 0.3.0 + lodash: 4.17.21 + dev: true + /eslint-plugin-es-x@7.2.0(eslint@8.51.0): resolution: {integrity: sha512-9dvv5CcvNjSJPqnS5uZkqb3xmbeqRLnvXKK7iI5+oK/yTusyc46zbBZKENGsOfojm/mKfszyZb+wNqNPAPeGXA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -3341,7 +3300,7 @@ packages: eslint-plugin-es-x: 7.2.0(eslint@8.51.0) get-tsconfig: 4.7.2 ignore: 5.2.4 - is-core-module: 2.13.0 + is-core-module: 2.13.1 minimatch: 3.1.2 resolve: 1.22.6 semver: 7.5.4 @@ -3365,7 +3324,7 @@ packages: vue-eslint-parser: optional: true dependencies: - '@typescript-eslint/utils': 6.7.5(eslint@8.51.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.9.0(eslint@8.51.0)(typescript@5.2.2) eslint: 8.51.0 minimatch: 9.0.3 natural-compare-lite: 1.4.0 @@ -5841,7 +5800,6 @@ packages: /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: true /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} @@ -6261,7 +6219,7 @@ packages: resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==} hasBin: true dependencies: - is-core-module: 2.13.0 + is-core-module: 2.13.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 dev: true diff --git a/src/client/index.ts b/src/client/index.ts new file mode 100644 index 0000000..5d12f9f --- /dev/null +++ b/src/client/index.ts @@ -0,0 +1 @@ +export * from './manifest' diff --git a/src/client/manifest/index.ts b/src/client/manifest/index.ts new file mode 100644 index 0000000..b2b3acd --- /dev/null +++ b/src/client/manifest/index.ts @@ -0,0 +1,5 @@ +import virtualManifest from 'virtual:public-typescript:manifest' + +const manifest = virtualManifest + +export { manifest } diff --git a/src/client/manifest/manifest.d.ts b/src/client/manifest/manifest.d.ts new file mode 100644 index 0000000..cff0ed9 --- /dev/null +++ b/src/client/manifest/manifest.d.ts @@ -0,0 +1,3 @@ +declare module 'virtual:public-typescript:manifest' { + export default manifest as Record +} diff --git a/src/global-config/GlobalConfigBuilder.ts b/src/node/global-config/GlobalConfigBuilder.ts similarity index 86% rename from src/global-config/GlobalConfigBuilder.ts rename to src/node/global-config/GlobalConfigBuilder.ts index 2d3f9ae..3e00447 100644 --- a/src/global-config/GlobalConfigBuilder.ts +++ b/src/node/global-config/GlobalConfigBuilder.ts @@ -1,6 +1,6 @@ import path from 'node:path' -import { type ResolvedConfig } from 'vite' -import { type VPPTPluginOptions } from '..' +import { type Logger, type ResolvedConfig } from 'vite' +import { type OptionsTypeWithDefault } from '../helper/utils' import { type CacheValue, type ManifestCache } from '../manifest-cache/ManifestCache' import { type BaseCacheProcessor } from '../processor/BaseCacheProcessor' @@ -9,7 +9,8 @@ export type UserConfig = { originFilesGlob: string[] viteConfig: ResolvedConfig cacheProcessor: BaseCacheProcessor -} & Required + logger: Logger +} & Required export type GlobalConfig = UserConfig & { absOutputDir: string @@ -27,6 +28,7 @@ export class GlobalConfigBuilder { const root = c.viteConfig.root || process.cwd() const absOutputDir = path.join(root, c.outputDir) const absInputDir = path.join(root, c.inputDir) + this._globalConfig = { ...c, absInputDir, diff --git a/src/global-config/index.ts b/src/node/global-config/index.ts similarity index 100% rename from src/global-config/index.ts rename to src/node/global-config/index.ts diff --git a/src/helper/build.ts b/src/node/helper/build.ts similarity index 95% rename from src/helper/build.ts rename to src/node/helper/build.ts index 641c926..cf83596 100644 --- a/src/helper/build.ts +++ b/src/node/helper/build.ts @@ -2,11 +2,9 @@ import createDebug from 'debug' import { type BuildResult, type Plugin, build as esbuild } from 'esbuild' import path from 'node:path' import { type ResolvedConfig } from 'vite' -import { type VPPTPluginOptions } from '..' -import { name } from '../../package.json' import { globalConfig } from '../global-config' import { type BaseCacheProcessor } from '../processor/BaseCacheProcessor' -import { getContentHash } from './utils' +import { type OptionsTypeWithDefault, getContentHash, pkgName } from './utils' const debug = createDebug('vite-plugin-public-typescript:build ===> ') @@ -57,7 +55,7 @@ function transformEnvToDefine(viteConfig: ResolvedConfig) { type IBuildOptions = { filePath: string viteConfig: ResolvedConfig -} & Required +} & OptionsTypeWithDefault export async function esbuildTypescript(buildOptions: IBuildOptions) { const { filePath, esbuildOptions, sideEffects, viteConfig } = buildOptions @@ -90,7 +88,7 @@ export async function esbuildTypescript(buildOptions: IBuildOptions) { debug('esbuild success:', filePath) } catch (error) { - console.error(`[${name}]`, error) + console.error(`[${pkgName}]`, error) return } diff --git a/src/helper/file-watcher.ts b/src/node/helper/file-watcher.ts similarity index 51% rename from src/helper/file-watcher.ts rename to src/node/helper/file-watcher.ts index 1dafecc..8f7e9e8 100644 --- a/src/helper/file-watcher.ts +++ b/src/node/helper/file-watcher.ts @@ -3,36 +3,41 @@ import path from 'node:path' import Watcher from 'watcher' import { globalConfig } from '../global-config' import { build } from './build' -import { _isPublicTypescript } from './utils' +import { _isPublicTypescript, type HmrFile } from './utils' const debug = createDebug('vite-plugin-public-typescript:file-watcher ===> ') -export async function handleUnlink(filePath: string, cb: () => void) { +export async function handleUnlink(filePath: string, cb?: () => void) { if (_isPublicTypescript(filePath)) { const fileName = path.parse(filePath).name debug('unlink:', fileName) await globalConfig.get().cacheProcessor.deleteOldJs({ originFileName: fileName }) - cb() + cb?.() } } -export async function handleFileAdded(filePath: string, cb: () => void) { +export async function handleFileAdded(filePath: string, cb?: () => void) { if (_isPublicTypescript(filePath)) { debug('file added:', filePath) await build({ filePath }, (...args) => globalConfig.get().cacheProcessor.onTsBuildEnd(...args)) - cb() + cb?.() } } -export async function handleFileRenamed(filePath: string, filePathNext: string, cb: () => void) { +export async function handleFileRenamed(filePath: string, filePathNext: string, cb?: () => void) { if (_isPublicTypescript(filePath)) { debug('file renamed:', filePath, '==>', filePathNext) - await handleUnlink(filePath, cb) - await handleFileAdded(filePathNext, cb) + await handleUnlink(filePath) + await handleFileAdded(filePathNext) + cb?.() } } -export function initWatcher(cb: () => void) { +async function handleFileChange(filePath: string, cb?: () => void) { + handleFileAdded(filePath, cb) +} + +export function initWatcher(cb: (file: HmrFile) => void) { try { const watcher = new Watcher(globalConfig.get().absInputDir, { debounce: 0, @@ -42,12 +47,16 @@ export function initWatcher(cb: () => void) { renameTimeout: 0, }) - watcher.on('unlink', (filePath) => handleUnlink(filePath, cb)) + watcher.on('unlink', (filePath: string) => handleUnlink(filePath, () => cb({ path: filePath, event: 'deleted' }))) - watcher.on('add', (filePath) => handleFileAdded(filePath, cb)) + watcher.on('add', (filePath: string) => handleFileAdded(filePath, () => cb({ path: filePath, event: 'added' }))) + + watcher.on('rename', async (f: string, fNext: string) => { + handleFileRenamed(f, fNext, () => cb({ path: fNext, event: `renamed` })) + }) - watcher.on('rename', async (f, fNext) => { - await handleFileRenamed(f, fNext, cb) + watcher.on('change', (filePath: string) => { + handleFileChange(filePath, () => cb({ path: filePath, event: 'changed' })) }) } catch (error) { console.error(error) diff --git a/src/helper/html.ts b/src/node/helper/html.ts similarity index 100% rename from src/helper/html.ts rename to src/node/helper/html.ts diff --git a/src/node/helper/server.ts b/src/node/helper/server.ts new file mode 100644 index 0000000..8570923 --- /dev/null +++ b/src/node/helper/server.ts @@ -0,0 +1,40 @@ +import colors from 'picocolors' +import { type ResolvedConfig, type WebSocketServer } from 'vite' +import { globalConfig } from '../global-config' +import { fileRelativeRootPath, isInTest } from './utils' + +export function addCodeHeader(code: string) { + return `// Generated via vite-plugin-public-typescript (This line print in serve mode only)\n${code}` +} + +export type HmrFile = { + path: string + event: string +} +export function reloadPage(ws: WebSocketServer, file: HmrFile) { + const { logger } = globalConfig.get() + + if (!isInTest()) { + logger.info( + colors.green(`page reload `) + colors.gray(`file ${file.event} `) + colors.dim(fileRelativeRootPath(file.path)), + { + clear: false, + timestamp: true, + }, + ) + } + + ws.send({ + path: '*', + type: 'full-reload', + }) +} + +export function disableManifestHmr(config: ResolvedConfig, manifestPath: string) { + if (config.command === 'serve') { + const index = config.configFileDependencies.indexOf(manifestPath) + if (index !== -1) { + config.configFileDependencies.splice(index, 1) + } + } +} diff --git a/src/helper/utils.ts b/src/node/helper/utils.ts similarity index 82% rename from src/helper/utils.ts rename to src/node/helper/utils.ts index e878747..f9b0d32 100644 --- a/src/helper/utils.ts +++ b/src/node/helper/utils.ts @@ -3,21 +3,37 @@ import fs from 'fs-extra' import { createHash } from 'node:crypto' import path from 'node:path' import glob from 'tiny-glob' -import { type ResolvedConfig, type WebSocketServer, normalizePath } from 'vite' +import { type ResolvedConfig, createLogger, normalizePath } from 'vite' import { type VPPTPluginOptions } from '..' +import { name as pkgName } from '../../../package.json' import { globalConfig } from '../global-config' import { manifestCache } from '../manifest-cache' import { initCacheProcessor } from '../processor/processor' +import { disableManifestHmr } from './server' const debug = createDebug('vite-plugin-public-typescript:util ===> ') -export function reloadPage(ws: WebSocketServer) { - ws.send({ - path: '*', - type: 'full-reload', +type PartialExclude = Omit & Partial> + +export type OptionsTypeWithDefault = PartialExclude, 'base'> + +export { pkgName } + +export function createInternalLogger(allowClearScreen?: boolean) { + return createLogger('info', { + allowClearScreen: !!allowClearScreen, + prefix: `[${pkgName}]`, }) } +export function fileRelativeRootPath(filePath: string) { + return normalizePath(`/${path.relative(globalConfig.get().viteConfig.root, filePath)}`) +} + +export function isInTest() { + return process.env.VITEST || process.env.CI +} + export function isPublicTypescript(args: { filePath: string; inputDir: string; root: string }) { const { filePath, root, inputDir } = args @@ -35,6 +51,10 @@ export function _isPublicTypescript(filePath: string) { }) } +export function isManifestFile(filePath: string) { + return filePath === manifestCache.manifestPath +} + export function isWindows() { return typeof process != 'undefined' && process.platform === 'win32' } @@ -141,7 +161,7 @@ export function extractHashFromFileName(filename: string, hash: VPPTPluginOption return '' } -export function validateOptions(options: Required) { +export function validateOptions(options: OptionsTypeWithDefault) { let { outputDir } = options // ensure outputDir is Dir if (!outputDir.startsWith('/')) { @@ -165,10 +185,6 @@ export function normalizeAssetsDirPath(dir: string) { return dir.replaceAll(/^\/|\/$/g, '') } -export function addCodeHeader(code: string) { - return `// gen via vite-plugin-public-typescript (show in serve mode only)\n${code}` -} - export function getInputDir(resolvedRoot: string, originInputDir: string, suffix = '') { return normalizePath(path.resolve(resolvedRoot, `${originInputDir}${suffix}`)) } @@ -198,24 +214,17 @@ export function removeOldJsFiles(oldFiles: string[]) { } } -export function disableManifestHmr(config: ResolvedConfig, manifestPath: string) { - if (config.command === 'serve') { - const index = config.configFileDependencies.indexOf(manifestPath) - if (index !== -1) { - config.configFileDependencies.splice(index, 1) - } - } -} - // NOTE: remmember call this before write compiled js file to disk export function removeBase(filePath: string, base: string): string { const devBase = base.at(-1) === '/' ? base : `${base}/` return filePath.startsWith(devBase) ? filePath.slice(devBase.length - 1) : filePath } -export async function setupGlobalConfig(viteConfig: ResolvedConfig, opts: Required) { +export async function setupGlobalConfig(viteConfig: ResolvedConfig, opts: OptionsTypeWithDefault) { const resolvedRoot = normalizePath(viteConfig.root ? path.resolve(viteConfig.root) : process.cwd()) + opts.base = opts.base ?? viteConfig.base + fs.ensureDirSync(getInputDir(resolvedRoot, opts.inputDir)) const originFilesGlob = await glob(getInputDir(resolvedRoot, opts.inputDir, `/*.ts`), { @@ -225,24 +234,28 @@ export async function setupGlobalConfig(viteConfig: ResolvedConfig, opts: Requir const cacheProcessor = initCacheProcessor(opts, manifestCache) + const logger = createInternalLogger(viteConfig.clearScreen) + globalConfig.init({ cacheProcessor, manifestCache, originFilesGlob, viteConfig, - ...opts, + logger, + ...(opts as Required), }) return globalConfig.get() } -export async function setupManifestCache(viteConfig: ResolvedConfig, opts: Required) { - manifestCache.setManifestPath(normalizePath(`${globalConfig.get().absInputDir}/${opts.manifestName}.json`)) +export async function setupManifestCache(viteConfig: ResolvedConfig, opts: OptionsTypeWithDefault) { + const cacheDir = path.resolve(viteConfig.root, opts.cacheDir) + manifestCache.setManifestPath(normalizePath(`${cacheDir}/${opts.manifestName}.json`)) // no need to set `_pathToDisk` manually anymore manifestCache.beforeSet = (value) => { if (value?.path) { - value._pathToDisk = removeBase(value.path, viteConfig.base) + value._pathToDisk = removeBase(value.path, opts.base!) } return value } diff --git a/src/node/helper/virtual.ts b/src/node/helper/virtual.ts new file mode 100644 index 0000000..5cbb3cb --- /dev/null +++ b/src/node/helper/virtual.ts @@ -0,0 +1,2 @@ +export const virtualModuleId = 'virtual:public-typescript:manifest' +export const resolvedVirtualModuleId = `\0${virtualModuleId}` diff --git a/src/index.ts b/src/node/index.ts similarity index 57% rename from src/index.ts rename to src/node/index.ts index 54c25b0..0e5ada6 100644 --- a/src/index.ts +++ b/src/node/index.ts @@ -1,27 +1,28 @@ import createDebug from 'debug' import { type BuildOptions } from 'esbuild' import fs from 'fs-extra' -import MagicString from 'magic-string' import path from 'node:path' -import { type PluginOption, type ResolvedConfig, send } from 'vite' +import { type PluginOption, type ResolvedConfig } from 'vite' import { globalConfig } from './global-config' -import { build, buildAllOnce, esbuildTypescript } from './helper/build' +import { buildAllOnce, esbuildTypescript } from './helper/build' import { initWatcher } from './helper/file-watcher' -import { getScriptInfo, nodeIsElement, traverseHtml } from './helper/html' +import { reloadPage } from './helper/server' import { _isPublicTypescript, - addCodeHeader, + type OptionsTypeWithDefault, eq, findAllOldJsFile, isEmptyObject, + isManifestFile, normalizeAssetsDirPath, - reloadPage, removeOldJsFiles, setupGlobalConfig, setupManifestCache, validateOptions, } from './helper/utils' import { manifestCache } from './manifest-cache' +import { pluginServer } from './plugins/server' +import { pluginVirtual } from './plugins/virtual' const debug = createDebug('vite-plugin-public-typescript:index ===> ') @@ -66,24 +67,38 @@ export interface VPPTPluginOptions { /** * @description treat `input` as sideEffect or not * @see https://esbuild.github.io/api/#tree-shaking-and-side-effects - * @default false + * @default true */ sideEffects?: boolean /** * @description build-out destination * @default 'memory' + * @version v1.5.0 introduced */ destination?: 'memory' | 'file' + /** + * @description manifest cache dir + * @default `node_modules/.vite-plugin-public-typescript` + * @version v2.0.0 introduced + */ + cacheDir?: string + /** + * @description base path for all files + * @default vite.config.ts `base` + * @version v2.0.0 introduced + */ + base?: string } -export const DEFAULT_OPTIONS: Required = { +export const DEFAULT_OPTIONS: OptionsTypeWithDefault = { destination: 'memory', esbuildOptions: {}, hash: true, inputDir: 'public-typescript', manifestName: 'manifest', outputDir: '/', - sideEffects: false, + sideEffects: true, + cacheDir: 'node_modules/.vite-plugin-public-typescript', } let previousOpts: VPPTPluginOptions @@ -112,13 +127,9 @@ export default function publicTypescript(options: VPPTPluginOptions = {}) { await setupManifestCache(viteConfig, opts) }, configureServer(server) { - if (process.env.VITEST || process.env.CI) { - return - } - const { ws } = server - initWatcher(() => reloadPage(ws)) + initWatcher((file) => reloadPage(ws, file)) }, async buildStart() { // skip server restart when options not changed @@ -166,7 +177,7 @@ export default function publicTypescript(options: VPPTPluginOptions = {}) { } } - buildAllOnce(originFilesGlob) + await buildAllOnce(originFilesGlob) }, generateBundle() { if (opts.destination === 'memory') { @@ -181,100 +192,17 @@ export default function publicTypescript(options: VPPTPluginOptions = {}) { } }, async handleHotUpdate(ctx) { - const { file, server } = ctx + const { file } = ctx - if (_isPublicTypescript(file)) { + if (_isPublicTypescript(file) || isManifestFile(file)) { debug('hmr:', file) - await build({ filePath: file }, (...args) => globalConfig.get().cacheProcessor.onTsBuildEnd(...args)) - - reloadPage(server.ws) - return [] } }, - transformIndexHtml: { - order: 'post', - async handler(html, { filename }) { - const s = new MagicString(html) - - await traverseHtml(html, filename, (node) => { - if (!nodeIsElement(node)) { - return - } - // script tags - if (node.nodeName === 'script') { - const { src, vppt } = getScriptInfo(node) - - if (vppt?.name && src?.value) { - const c = manifestCache.get() - let cacheItem = manifestCache.findCacheItemByPath(src.value) - - if (!cacheItem) { - const fileName = path.basename(src.value).split('.')[0] - cacheItem = c[fileName] - } - - if (cacheItem) { - const attrs = node.attrs - .reduce((acc, attr) => { - if (attr.name === src.name) { - acc += ` ${attr.name}="${cacheItem!.path}"` - return acc - } - acc += attr.value ? ` ${attr.name}="${attr.value}"` : ` ${attr.name}` - return acc - }, '') - .trim() - - s.update( - node.sourceCodeLocation!.startOffset, - node.sourceCodeLocation!.endOffset, - ``, - ) - } else { - s.remove(node.sourceCodeLocation!.startOffset, node.sourceCodeLocation!.endOffset) - } - } - } - }) - return s.toString() - }, - }, - }, - { - name: 'vite:public-typescript:server', - apply: 'serve', - enforce: 'post', - load(id) { - const cacheItem = manifestCache.findCacheItemByPath(id) - if (cacheItem) { - return { - code: '', - map: null, - } - } - }, - async configureServer(server) { - server.middlewares.use((req, res, next) => { - try { - if (req?.url?.startsWith('/') && req?.url?.endsWith('.js')) { - const cacheItem = manifestCache.findCacheItemByPath(req.url) - if (cacheItem) { - return send(req, res, addCodeHeader(cacheItem._code || ''), 'js', { - cacheControl: 'max-age=31536000,immutable', - headers: server.config.server.headers, - map: null, - }) - } - } - } catch (error) { - return next(error) - } - next() - }) - }, }, + pluginServer(), + pluginVirtual(), ] return plugins diff --git a/src/manifest-cache/ManifestCache.ts b/src/node/manifest-cache/ManifestCache.ts similarity index 100% rename from src/manifest-cache/ManifestCache.ts rename to src/node/manifest-cache/ManifestCache.ts diff --git a/src/manifest-cache/index.ts b/src/node/manifest-cache/index.ts similarity index 100% rename from src/manifest-cache/index.ts rename to src/node/manifest-cache/index.ts diff --git a/src/plugins/inject-script.ts b/src/node/plugins/inject-script.ts similarity index 73% rename from src/plugins/inject-script.ts rename to src/node/plugins/inject-script.ts index 29a04a6..c4022d9 100644 --- a/src/plugins/inject-script.ts +++ b/src/node/plugins/inject-script.ts @@ -1,10 +1,12 @@ import { type HtmlTagDescriptor, type PluginOption } from 'vite' import { VPPT_DATA_ATTR, injectTagsToHtml } from '../helper/html' +import { manifestCache } from '../manifest-cache' -export type Scripts = Omit[] +export type Scripts = (manifest: Record) => Omit[] export function generateScriptTags(scripts: Scripts) { - const tags: HtmlTagDescriptor[] = scripts.map((s) => ({ + const _scripts = scripts(manifestCache.getManifestJson()) + const tags: HtmlTagDescriptor[] = _scripts.map((s) => ({ ...s, attrs: { crossorigin: true, @@ -23,6 +25,7 @@ export function injectScriptsToHtml(html: string, scripts: Scripts) { export function injectScripts(scripts: Scripts) { const plugin: PluginOption = { name: 'vite:public-typescript:inject-script', + enforce: 'post', transformIndexHtml: { async handler(html) { return { diff --git a/src/node/plugins/server.ts b/src/node/plugins/server.ts new file mode 100644 index 0000000..20c53dd --- /dev/null +++ b/src/node/plugins/server.ts @@ -0,0 +1,92 @@ +import MagicString from 'magic-string' +import path from 'node:path' +import { type PluginOption, send } from 'vite' +import { getScriptInfo, nodeIsElement, traverseHtml } from '../helper/html' +import { addCodeHeader } from '../helper/server' +import { manifestCache } from '../manifest-cache' + +export function pluginServer() { + const plugin: PluginOption = { + name: 'vite:public-typescript:server', + apply: 'serve', + enforce: 'post', + load(id) { + const cacheItem = manifestCache.findCacheItemByPath(id) + if (cacheItem) { + return { + code: '', + map: null, + } + } + }, + async configureServer(server) { + server.middlewares.use((req, res, next) => { + try { + if (req?.url?.startsWith('/') && req?.url?.endsWith('.js')) { + const cacheItem = manifestCache.findCacheItemByPath(req.url) + if (cacheItem) { + return send(req, res, addCodeHeader(cacheItem._code || ''), 'js', { + cacheControl: 'max-age=31536000,immutable', + headers: server.config.server.headers, + map: null, + }) + } + } + } catch (error) { + return next(error) + } + next() + }) + }, + transformIndexHtml: { + order: 'post', + async handler(html, { filename }) { + const s = new MagicString(html) + + await traverseHtml(html, filename, (node) => { + if (!nodeIsElement(node)) { + return + } + // script tags + if (node.nodeName === 'script') { + const { src, vppt } = getScriptInfo(node) + + if (vppt?.name && src?.value) { + const c = manifestCache.get() + let cacheItem = manifestCache.findCacheItemByPath(src.value) + + if (!cacheItem) { + const fileName = path.basename(src.value).split('.')[0] + cacheItem = c[fileName] + } + + if (cacheItem) { + const attrs = node.attrs + .reduce((acc, attr) => { + if (attr.name === src.name) { + acc += ` ${attr.name}="${cacheItem!.path}"` + return acc + } + acc += attr.value ? ` ${attr.name}="${attr.value}"` : ` ${attr.name}` + return acc + }, '') + .trim() + + s.update( + node.sourceCodeLocation!.startOffset, + node.sourceCodeLocation!.endOffset, + ``, + ) + } else { + s.remove(node.sourceCodeLocation!.startOffset, node.sourceCodeLocation!.endOffset) + } + } + } + }) + return s.toString() + }, + }, + } + + return plugin +} diff --git a/src/node/plugins/virtual.ts b/src/node/plugins/virtual.ts new file mode 100644 index 0000000..89739d8 --- /dev/null +++ b/src/node/plugins/virtual.ts @@ -0,0 +1,27 @@ +import { type PluginOption } from 'vite' +import { resolvedVirtualModuleId, virtualModuleId } from '../helper/virtual' +import { manifestCache } from '../manifest-cache' + +export function pluginVirtual() { + const plugin: PluginOption = { + name: 'vite:public-typescript:virtual', + enforce: 'post', + config: () => ({ + optimizeDeps: { + exclude: [virtualModuleId], + }, + }), + async resolveId(id: string) { + if (id === virtualModuleId) { + return resolvedVirtualModuleId + } + }, + async load(id) { + if (id === resolvedVirtualModuleId) { + return `export default ${JSON.stringify(manifestCache.getManifestJson())}` + } + }, + } + + return plugin +} diff --git a/src/processor/BaseCacheProcessor.ts b/src/node/processor/BaseCacheProcessor.ts similarity index 100% rename from src/processor/BaseCacheProcessor.ts rename to src/node/processor/BaseCacheProcessor.ts diff --git a/src/processor/FileCacheProcessor.ts b/src/node/processor/FileCacheProcessor.ts similarity index 100% rename from src/processor/FileCacheProcessor.ts rename to src/node/processor/FileCacheProcessor.ts diff --git a/src/processor/ManifestCacheProcessor.ts b/src/node/processor/ManifestCacheProcessor.ts similarity index 95% rename from src/processor/ManifestCacheProcessor.ts rename to src/node/processor/ManifestCacheProcessor.ts index 6e01d5d..b91c206 100644 --- a/src/processor/ManifestCacheProcessor.ts +++ b/src/node/processor/ManifestCacheProcessor.ts @@ -37,10 +37,7 @@ export abstract class ManifestCacheProcessor extends BaseCacheProcessor, manifestCache: ManifestCache) { +export function initCacheProcessor(options: OptionsTypeWithDefault, manifestCache: ManifestCache) { const { destination } = options const processorContainer: Record = { diff --git a/tests/fixtures/demo/vite.config.ts b/tests/fixtures/demo/vite.config.ts index 0ca361c..55ba058 100644 --- a/tests/fixtures/demo/vite.config.ts +++ b/tests/fixtures/demo/vite.config.ts @@ -1,5 +1,5 @@ import { defineConfig } from 'vite' -import { publicTypescript } from '../../../src' +import { publicTypescript } from '../../../src/node' // https://vitejs.dev/config/ export default defineConfig(() => ({ diff --git a/tests/utils.test.ts b/tests/utils.test.ts index ed7361b..444bcf8 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -1,6 +1,6 @@ import path from 'node:path' import { describe, expect, test } from 'vitest' -import { globalConfig } from '../src/global-config' +import { globalConfig } from '../src/node/global-config' import { eq, extractHashFromFileName, @@ -9,9 +9,9 @@ import { linebreak, setEol, validateOptions, -} from '../src/helper/utils' +} from '../src/node/helper/utils' -describe.skip('unit test', () => { +describe('unit test', () => { test('should return true when filePath is a public typescript file', () => { const filePath = 'src/foo/bar.ts' const root = 'src' @@ -31,14 +31,14 @@ describe.skip('unit test', () => { const tsFile = 'hello.ts' const otherFile = 'hello.js' const res1 = isPublicTypescript({ - filePath: path.resolve(root, `publicTypescript/${tsFile}`), - inputDir: 'publicTypescript', + filePath: path.resolve(root, `public-typescript/${tsFile}`), + inputDir: 'public-typescript', root, }) const res2 = isPublicTypescript({ - filePath: path.resolve(root, `publicTypescript/${otherFile}`), - inputDir: 'publicTypescript', + filePath: path.resolve(root, `public-typescript/${otherFile}`), + inputDir: 'public-typescript', root, }) @@ -91,6 +91,7 @@ describe.skip('unit test', () => { outputDir: '/', sideEffects: false, ssrBuild: false, + cacheDir: 'node_modules/.vite-plugin-public-typescript', } as const expect(() => validateOptions(opts)).not.toThrowError() diff --git a/tests/vitestSetup.ts b/tests/vitestSetup.ts index b6a0154..dc6fb24 100644 --- a/tests/vitestSetup.ts +++ b/tests/vitestSetup.ts @@ -1,10 +1,10 @@ import path from 'node:path' import { type InlineConfig, type ResolvedConfig, resolveConfig } from 'vite' import { beforeEach } from 'vitest' -import { DEFAULT_OPTIONS } from '../src' -import { type GlobalConfig } from '../src/global-config/GlobalConfigBuilder' -import { setupGlobalConfig } from '../src/helper/utils' -import { type CacheValueEx } from '../src/manifest-cache' +import { DEFAULT_OPTIONS } from '../src/node' +import { type GlobalConfig } from '../src/node/global-config/GlobalConfigBuilder' +import { setupGlobalConfig } from '../src/node/helper/utils' +import { type CacheValueEx } from '../src/node/manifest-cache' const config: InlineConfig = { configFile: path.resolve(__dirname, './fixtures/demo/vite.config.ts'), diff --git a/tsup.config.ts b/tsup.config.ts index f25b73d..7187559 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -3,28 +3,43 @@ import { type Options, defineConfig } from 'tsup' const commonConfig = (option: Options): Options => { return { clean: false, - minify: false, - platform: 'node', sourcemap: !!option.watch, define: { 'import.meta.vitest': 'undefined', }, tsconfig: option.watch ? './tsconfig.dev.json' : './tsconfig.json', - target: 'node16', + dts: true, + minify: false, + external: [/^virtual:.*/], } } export const tsup = defineConfig((option) => [ { - entry: ['src/index.ts'], - dts: true, + entry: { + 'node/index': './src/node/index.ts', + }, format: ['esm'], + target: 'node16', + platform: 'node', ...commonConfig(option), }, { - entry: ['src/index.ts'], + entry: { + 'node/index': './src/node/index.ts', + }, noExternal: ['on-change', 'watcher'], format: ['cjs'], + target: 'node16', + platform: 'node', + ...commonConfig(option), + }, + { + entry: { + 'client/index': './src/client/index.ts', + }, + format: ['esm', 'cjs'], + platform: 'neutral', ...commonConfig(option), }, ]) diff --git a/vitest.config.e2e.ts b/vitest.config.e2e.ts index f99e0bc..bf32d9c 100644 --- a/vitest.config.e2e.ts +++ b/vitest.config.e2e.ts @@ -11,7 +11,7 @@ export default defineConfig({ }, }, test: { - include: ['./playground/**/*.spec.[tj]s'], + include: ['./playground/spa/**/*.spec.[tj]s'], reporters: 'dot', coverage: { provider: undefined,