diff --git a/__test__/basic.spec.ts b/__test__/basic.spec.ts index d23d1b9..feea24f 100644 --- a/__test__/basic.spec.ts +++ b/__test__/basic.spec.ts @@ -28,8 +28,16 @@ vi.mock('fs-extra', async () => { function initCache() { const cache = new ManifestCache() - cache.setCache({ key: 'name', value: 'value' }) - cache.setCache({ key: 'typescript', value: 'powerful' }) + cache.set({ + x: { + path: 'x.js', + _code: 'console.log("x")', + }, + y: { + path: 'y.js', + _code: 'console.log("y")', + }, + }) return cache } @@ -70,17 +78,17 @@ afterEach((ctx) => { describe('manifestCache', () => { test('should set cache and get cache right', ({ cache }) => { - expect(cache.getCache('name')).toBe('value') + expect(cache.getByKey('x')).toStrictEqual({ path: 'x.js', _code: 'console.log("x")' }) }) test('should remove cache', ({ cache }) => { - cache.removeCache('name') + cache.remove('x') - expect(cache.getCache('name')).toBeFalsy() + expect(cache.getByKey('x')).toBeFalsy() }) test('should get all', ({ cache }) => { - const v = cache.getAll() + const v = cache.get() expect(Object.keys(v).length === 2).toBe(true) }) @@ -102,7 +110,7 @@ describe('manifestCache', () => { { const content = fs.readFileSync(manifestPath, 'utf-8') const c = initCache() - expect(eq(JSON.parse(content), c.getAll())).toBe(true) + expect(eq(JSON.parse(content), c.extractPath(c.get()))).toBe(true) } }) }) diff --git a/__test__/utils.spec.ts b/__test__/utils.spec.ts index ac511b0..4e4ae96 100644 --- a/__test__/utils.spec.ts +++ b/__test__/utils.spec.ts @@ -9,7 +9,7 @@ import { setEol, validateOptions, } from '../src/helper/utils' -import { getGlobalConfig, setGlobalConfig } from '../src/helper/GlobalConfigBuilder' +import { globalConfigBuilder } from '../src/helper/GlobalConfigBuilder' describe('vite-plugin-public-typescript', () => { it('should return true when filePath is a public typescript file', () => { @@ -70,14 +70,8 @@ describe('vite-plugin-public-typescript', () => { expect(a).toBe(b) }) - test('should getGlobalConfig throw error', () => { - expect(() => getGlobalConfig()).toThrowError('init') - }) - test('should get globalConfig', () => { - // @ts-expect-error - setGlobalConfig({ config: { publicDir: 'public' }, inputDir: 'publicTypescript' }) - expect(() => getGlobalConfig()).not.toThrowError() + expect(() => globalConfigBuilder.get()).not.toThrowError() }) test('should extract hash', () => { @@ -96,22 +90,9 @@ describe('vite-plugin-public-typescript', () => { ssrBuild: false, esbuildOptions: {}, sideEffects: false, - } + destination: 'file', + } as const expect(() => validateOptions(opts)).not.toThrowError() }) - - test('should validate options throw error', () => { - const opts = { - inputDir: '/publicTypescript/foo', - outputDir: 'js', - manifestName: 'manifest', - hash: true, - ssrBuild: false, - esbuildOptions: {}, - sideEffects: false, - } - - expect(() => validateOptions(opts)).toThrowError('dir') - }) }) diff --git a/package.json b/package.json index 5f59abe..29b5935 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "esbuild": "^0.17.12", "fs-extra": "^11.1.1", "on-change": "^4.0.2", + "sirv": "^2.0.3", "tiny-glob": "^0.2.9", "watcher": "^2.2.2" }, diff --git a/playground/spa/index.html b/playground/spa/index.html index 74e6de5..2b6686b 100644 --- a/playground/spa/index.html +++ b/playground/spa/index.html @@ -11,6 +11,7 @@
+ diff --git a/playground/spa/package.json b/playground/spa/package.json index 83b6c25..9f68017 100644 --- a/playground/spa/package.json +++ b/playground/spa/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "vite", - "debug": "cross-env DEBUG='*===>*' vite", + "debug": "cross-env DEBUG='MemoryCacheProcessor*,index*,ManifestCache*' vite", "build": "vite build", "preview": "vite preview" }, diff --git a/playground/spa/publicTypescript/haha.ts b/playground/spa/public-typescript/haha.ts similarity index 100% rename from playground/spa/publicTypescript/haha.ts rename to playground/spa/public-typescript/haha.ts diff --git a/playground/spa/publicTypescript/index.ts b/playground/spa/public-typescript/index.ts similarity index 100% rename from playground/spa/publicTypescript/index.ts rename to playground/spa/public-typescript/index.ts diff --git a/playground/spa/public-typescript/manifest.json b/playground/spa/public-typescript/manifest.json new file mode 100644 index 0000000..ad45b2a --- /dev/null +++ b/playground/spa/public-typescript/manifest.json @@ -0,0 +1,5 @@ +{ + "haha": "/js/haha.bdaaba63.js", + "index": "/js/index.cccf1b56.js", + "test": "/js/test.40879d01.js" +} diff --git a/playground/spa/public-typescript/test.ts b/playground/spa/public-typescript/test.ts new file mode 100644 index 0000000..7d71fc8 --- /dev/null +++ b/playground/spa/public-typescript/test.ts @@ -0,0 +1 @@ +console.log('this is a') diff --git a/playground/spa/publicTypescript/manifest.json b/playground/spa/publicTypescript/manifest.json deleted file mode 100644 index 0967ef4..0000000 --- a/playground/spa/publicTypescript/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/playground/spa/publicTypescript/test.ts b/playground/spa/publicTypescript/test.ts deleted file mode 100644 index c793f10..0000000 --- a/playground/spa/publicTypescript/test.ts +++ /dev/null @@ -1 +0,0 @@ -console.log('this is test') diff --git a/playground/spa/src/App.tsx b/playground/spa/src/App.tsx index 7af4974..17a455c 100644 --- a/playground/spa/src/App.tsx +++ b/playground/spa/src/App.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import manifest from '../publicTypescript/manifest.json' +import manifest from '../public-typescript/manifest.json' import reactLogo from './assets/react.svg' import './App.css' diff --git a/playground/spa/temp/1.js b/playground/spa/temp/1.js new file mode 100644 index 0000000..4560806 --- /dev/null +++ b/playground/spa/temp/1.js @@ -0,0 +1 @@ +console.log('temp') diff --git a/playground/spa/tsconfig.json b/playground/spa/tsconfig.json index d1b332c..b816984 100644 --- a/playground/spa/tsconfig.json +++ b/playground/spa/tsconfig.json @@ -18,7 +18,7 @@ "baseUrl": ".", "types": ["vite/client"] }, - "include": ["src", "publicTypescript"], + "include": ["src", "public-typescript"], "references": [{ "path": "./tsconfig.node.json" }], "exclude": ["**/*.js"] } diff --git a/playground/spa/tsconfig.node.json b/playground/spa/tsconfig.node.json index 80b6f7b..3146625 100644 --- a/playground/spa/tsconfig.node.json +++ b/playground/spa/tsconfig.node.json @@ -7,5 +7,5 @@ "resolveJsonModule": true, "baseUrl": "." }, - "include": ["vite.config.ts", "./publicTypescript/manifest.json"] + "include": ["vite.config.ts", "public-typescript/manifest.json"] } diff --git a/playground/spa/vite.config.ts b/playground/spa/vite.config.ts index c9774fd..d7f8efd 100644 --- a/playground/spa/vite.config.ts +++ b/playground/spa/vite.config.ts @@ -1,18 +1,11 @@ -import path from 'path' import type { HtmlTagDescriptor } from 'vite' import { defineConfig } from 'vite' import { publicTypescript } from 'vite-plugin-public-typescript' import react from '@vitejs/plugin-react' -import glob from 'tiny-glob' -import manifest from './publicTypescript/manifest.json' +import manifest from './public-typescript/manifest.json' // https://vitejs.dev/config/ export default defineConfig({ - build: { - rollupOptions: { - external: ['virtual:my-module'], - }, - }, define: { haha: JSON.stringify('custom define!'), app: JSON.stringify({ hello: 'world' }), @@ -20,26 +13,25 @@ export default defineConfig({ plugins: [ react(), publicTypescript({ - inputDir: 'publicTypescript', + inputDir: 'public-typescript', manifestName: 'manifest', hash: true, - outputDir: '/', - buildDestination: 'memory', + outputDir: '/js', + destination: 'memory', }), { name: 'add-script', async transformIndexHtml(html) { - const scripts = await glob('./public/*.js') - const tags: HtmlTagDescriptor[] = scripts.map((s) => { - return { + const tags: HtmlTagDescriptor[] = [ + { tag: 'script', attrs: { - src: manifest[path.parse(s).name.split('.')[0]], + src: manifest.test, }, - injectTo: 'head-prepend', - } - }) + injectTo: 'body', + }, + ] return { html, diff --git a/playground/ssr/public-typescript/manifest.json b/playground/ssr/public-typescript/manifest.json new file mode 100644 index 0000000..78897e5 --- /dev/null +++ b/playground/ssr/public-typescript/manifest.json @@ -0,0 +1,3 @@ +{ + "ssr": "/ssr.2507f2a9.js" +} diff --git a/playground/ssr/publicTypescript/ssr.ts b/playground/ssr/public-typescript/ssr.ts similarity index 100% rename from playground/ssr/publicTypescript/ssr.ts rename to playground/ssr/public-typescript/ssr.ts diff --git a/playground/ssr/public/ssr.4c5dba79.js b/playground/ssr/public/ssr.4c5dba79.js deleted file mode 100644 index 2feb6e8..0000000 --- a/playground/ssr/public/ssr.4c5dba79.js +++ /dev/null @@ -1 +0,0 @@ -(()=>{console.log("this is ssr");})(); diff --git a/playground/ssr/publicTypescript/custom-manifest.json b/playground/ssr/publicTypescript/custom-manifest.json deleted file mode 100644 index 5cbe07d..0000000 --- a/playground/ssr/publicTypescript/custom-manifest.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "ssr": "/ssr.4c5dba79.js" -} diff --git a/playground/ssr/server.js b/playground/ssr/server.js index ccdfb1e..291f6fc 100644 --- a/playground/ssr/server.js +++ b/playground/ssr/server.js @@ -1,6 +1,6 @@ import fs from 'fs/promises' import express from 'express' -import manifest from './publicTypescript/custom-manifest.json' assert { type: 'json' } +import manifest from './public-typescript/manifest.json' assert { type: 'json' } // Constants const isProduction = process.env.NODE_ENV === 'production' diff --git a/playground/ssr/vite.config.ts b/playground/ssr/vite.config.ts index 6e56d04..611b408 100644 --- a/playground/ssr/vite.config.ts +++ b/playground/ssr/vite.config.ts @@ -3,5 +3,5 @@ import { publicTypescript } from 'vite-plugin-public-typescript' // https://vitejs.dev/config/ export default defineConfig({ - plugins: [publicTypescript()], + plugins: [publicTypescript({ destination: 'memory' })], }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e5b1c7..ca28b54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: on-change: specifier: ^4.0.2 version: 4.0.2 + sirv: + specifier: ^2.0.3 + version: 2.0.3 tiny-glob: specifier: ^0.2.9 version: 0.2.9 @@ -796,7 +799,7 @@ packages: /@types/body-parser@1.19.2: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: - '@types/connect': 3.4.35 + '@types/connect': 3.4.36 '@types/node': 18.15.5 dev: true @@ -810,8 +813,8 @@ packages: resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==} dev: true - /@types/connect@3.4.35: - resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} + /@types/connect@3.4.36: + resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} dependencies: '@types/node': 18.15.5 dev: true @@ -3841,6 +3844,15 @@ packages: totalist: 3.0.0 dev: false + /sirv@2.0.3: + resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.21 + mrmime: 1.0.1 + totalist: 3.0.0 + dev: false + /sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: true diff --git a/src/helper/AbsCacheProcessor.ts b/src/helper/AbsCacheProcessor.ts index 951c1a5..04ce1a1 100644 --- a/src/helper/AbsCacheProcessor.ts +++ b/src/helper/AbsCacheProcessor.ts @@ -1,3 +1,6 @@ +import { normalizePath } from 'vite' +import type { TGlobalConfig } from './GlobalConfigBuilder' + export interface IDeleteFile { fileName: string jsFileName?: string @@ -13,4 +16,28 @@ export interface IAddFile { export abstract class AbsCacheProcessor { abstract deleteOldJs(args: IDeleteFile): Promise abstract addNewJs(args: IAddFile): Promise + setCache(args: IAddFile, globalConfig: TGlobalConfig) { + const { contentHash, code = '', fileName } = args + const { cache, outputDir } = globalConfig + + function getOutputPath(p: string, hash?: string) { + hash = hash ? `.${hash}` : '' + return normalizePath(`${p}/${fileName}${hash}.js`) + } + + let outPath = getOutputPath(outputDir) + if (contentHash) { + outPath = getOutputPath(outputDir, contentHash) + } + + cache.set({ + [fileName]: { + path: outPath, + _code: code, + _hash: contentHash, + }, + }) + + return outPath + } } diff --git a/src/helper/FileCacheProcessor.ts b/src/helper/FileCacheProcessor.ts index 5e04b4b..524484e 100644 --- a/src/helper/FileCacheProcessor.ts +++ b/src/helper/FileCacheProcessor.ts @@ -5,8 +5,8 @@ import { normalizePath } from 'vite' import createDebug from 'debug' import { assert } from './assert' import { globalConfigBuilder } from './GlobalConfigBuilder' -import type { IAddFile, IDeleteFile } from './AbsCacheProcessor' import { AbsCacheProcessor } from './AbsCacheProcessor' +import type { IAddFile, IDeleteFile } from './AbsCacheProcessor' import { writeFile } from './utils' const debug = createDebug('FileCacheProcessor ===> ') @@ -24,6 +24,7 @@ export class FileCacheProcessor extends AbsCacheProcessor { let oldFiles: string[] = [] try { fs.ensureDirSync(path.join(publicDir, outputDir)) + oldFiles = await glob(normalizePath(path.join(publicDir, `${outputDir}/${fileName}.?(*.)js`))) } catch (e) { console.error(e) @@ -33,6 +34,8 @@ export class FileCacheProcessor extends AbsCacheProcessor { assert(Array.isArray(oldFiles)) + debug('cache:', cache.get()) + if (oldFiles.length) { for (const f of oldFiles) { if (path.parse(f).name === jsFileName) { @@ -40,8 +43,9 @@ export class FileCacheProcessor extends AbsCacheProcessor { continue } // skip repeat js file if (fs.existsSync(f)) { - if (cache.getCache(fileName) || force) { - cache.removeCache(fileName) + debug('deleteOldJsFile - file exists:', f, fileName) + if (cache.getByKey(fileName) || force) { + cache.remove(fileName) debug('deleteOldJsFile - cache removed:', fileName) fs.remove(f) debug('deleteOldJsFile -file removed:', f) @@ -49,35 +53,23 @@ export class FileCacheProcessor extends AbsCacheProcessor { } } } else if (force) { - cache.removeCache(fileName) + cache.remove(fileName) debug('cache force removed:', fileName) } } async addNewJs(args: IAddFile): Promise { - const { contentHash, code = '', fileName } = args + const { code = '' } = args const { - cache, - outputDir, config: { publicDir }, } = globalConfigBuilder.get() - let outPath = normalizePath(`${outputDir}/${fileName}.js`) - if (contentHash) { - outPath = normalizePath(`${outputDir}/${fileName}.${contentHash}.js`) - } + const outPath = this.setCache(args, globalConfigBuilder.get()) const fp = normalizePath(path.join(publicDir, outPath)) + await fs.ensureDir(path.dirname(fp)) writeFile(fp, code) - - cache.setCache({ - [fileName]: { - path: outPath, - }, - }) - - debug('addJsFile cache seted:', fileName, outPath) } } diff --git a/src/helper/GlobalConfigBuilder.ts b/src/helper/GlobalConfigBuilder.ts index 44b2e5b..534fd64 100644 --- a/src/helper/GlobalConfigBuilder.ts +++ b/src/helper/GlobalConfigBuilder.ts @@ -9,11 +9,10 @@ type UserConfig = cache: ManifestCache filesGlob: string[] config: ResolvedConfig - } & { cacheProcessor: AbsCacheProcessor } & Required -type TGlobalConfig = UserConfig & { +export type TGlobalConfig = UserConfig & { absOutputDir: string absInputDir: string } @@ -27,8 +26,8 @@ class GlobalConfigBuilder { init(c: UserConfig) { const root = c.config.root || process.cwd() - const absOutputDir = path.resolve(root, c.config.publicDir) - const absInputDir = path.resolve(root, c.inputDir) + const absOutputDir = path.join(root, c.outputDir) + const absInputDir = path.join(root, c.inputDir) this.globalConfig = { ...c, absOutputDir, diff --git a/src/helper/ManifestCache.ts b/src/helper/ManifestCache.ts index 947864a..e78f93e 100644 --- a/src/helper/ManifestCache.ts +++ b/src/helper/ManifestCache.ts @@ -12,12 +12,8 @@ export interface IManifestConstructor { onChange?: (path: string, value: ValueType, previousValue: ValueType, applyData: ApplyData) => void } -type TCacheValue = { - path: string - _code?: string -} & Partial<{ [_key: string]: string }> - /** + * @example * { * fileName: { * path: '/some-path', @@ -26,6 +22,12 @@ type TCacheValue = { * } * } */ +type TCacheValue = { + path: string + _code?: string + _hash?: string +} & Partial<{ [_key: string]: string }> + type TDefaultCache = { [fileName in string]: TCacheValue } @@ -47,11 +49,11 @@ export class ManifestCache { } } - setCache(c: T, opts?: { disableWatch?: boolean }) { + set(c: T, opts?: { disableWatch?: boolean }) { const keys = Object.keys(c) keys.forEach((k) => { - const cacheV = this.getCache(k) + const cacheV = this.getByKey(k) if (cacheV !== c[k]) { if (opts?.disableWatch) { ;(onChange.target(this.cache) as TDefaultCache)[k] = c[k] @@ -64,18 +66,18 @@ export class ManifestCache { return this } - getCache(k: keyof T) { + getByKey(k: keyof T) { return this.cache[k] } - removeCache(k: keyof T) { + remove(k: keyof T) { if (this.cache[k]) { delete this.cache[k] } return this } - getAll() { + get() { return Object.assign({}, this.cache) } @@ -96,7 +98,7 @@ export class ManifestCache { return this.manifestPath } - private extractPath(c: T) { + extractPath(c: T) { const cache = Object.assign({}, c) const pathOnlyCache: Record = {} for (const key in cache) { @@ -107,16 +109,19 @@ export class ManifestCache { async writeManifestJSON() { const targetPath = this.getManifestPath() - const cacheObj = this.extractPath(this.getAll()) - const orderdCache = Object.assign({}, cacheObj) await fs.ensureDir(path.dirname(targetPath)) + const cacheObj = this.extractPath(this.get()) + const orderdCache = Object.assign({}, cacheObj) + const parsedCache = this.readManifestFromFile() if (!isEmptyObject(parsedCache) && eq(parsedCache, orderdCache)) { return } + + debug('write manifest json:', JSON.stringify(orderdCache || {}, null, 2)) writeFile(targetPath, JSON.stringify(orderdCache || {}, null, 2)) } } diff --git a/src/helper/MemoryCacheProcessor.ts b/src/helper/MemoryCacheProcessor.ts index 968591d..d714140 100644 --- a/src/helper/MemoryCacheProcessor.ts +++ b/src/helper/MemoryCacheProcessor.ts @@ -1,37 +1,16 @@ -// import createDebug from 'debug' -import type { IAddFile, IDeleteFile } from './AbsCacheProcessor' import { AbsCacheProcessor } from './AbsCacheProcessor' +import type { IAddFile, IDeleteFile } from './AbsCacheProcessor' import { globalConfigBuilder } from './GlobalConfigBuilder' -import { VIRTUAL } from './virtual' - -// const debug = createDebug('MemoryCacheProcessor ===> ') export class MemoryCacheProcessor extends AbsCacheProcessor { async deleteOldJs(args: IDeleteFile): Promise { const { fileName } = args const { cache } = globalConfigBuilder.get() - cache.removeCache(fileName) + cache.remove(fileName) } async addNewJs(args: IAddFile): Promise { - const { contentHash, code = '', fileName } = args - const { cache } = globalConfigBuilder.get() - - // const getContentHash = () => { - // if (contentHash) { - // return `:${contentHash}` - // } - // return '' - // } - - cache.setCache({ - [fileName]: { - // path: 虚拟前缀-文件名 - path: `${VIRTUAL}:[${fileName}]`, - _code: code, - contentHash, - }, - }) + this.setCache(args, globalConfigBuilder.get()) } } diff --git a/src/helper/build.ts b/src/helper/build.ts index e0bc2af..d9ac892 100644 --- a/src/helper/build.ts +++ b/src/helper/build.ts @@ -111,8 +111,6 @@ export async function build(options: { filePath: string }) { const code = await esbuildTypescript({ filePath, ...globalConfig }) - debug('cacheManifest:', globalConfig.cache.getAll()) - if (globalConfig.hash) { contentHash = getContentHash(code, globalConfig.hash) fileNameWithHash = `${fileName}.${contentHash}` @@ -121,4 +119,6 @@ export async function build(options: { filePath: string }) { await globalConfig.cacheProcessor.deleteOldJs({ fileName, jsFileName: fileNameWithHash }) await globalConfig.cacheProcessor.addNewJs({ code, fileName, contentHash }) + + debug('cacheManifest:', globalConfig.cache.get()) } diff --git a/src/helper/processor.ts b/src/helper/processor.ts index b72d986..ac433c2 100644 --- a/src/helper/processor.ts +++ b/src/helper/processor.ts @@ -3,13 +3,13 @@ import type { AbsCacheProcessor } from './AbsCacheProcessor' import { FileCacheProcessor } from './FileCacheProcessor' import { MemoryCacheProcessor } from './MemoryCacheProcessor' -export function initCacheProcessor(destination: VPPTPluginOptions['buildDestination']): AbsCacheProcessor { +export function initCacheProcessor(destination: VPPTPluginOptions['destination']): AbsCacheProcessor { switch (destination) { case 'file': return new FileCacheProcessor() case 'memory': return new MemoryCacheProcessor() default: - return new FileCacheProcessor() + return new MemoryCacheProcessor() } } diff --git a/src/helper/utils.ts b/src/helper/utils.ts index be81b13..d35ea0b 100644 --- a/src/helper/utils.ts +++ b/src/helper/utils.ts @@ -133,25 +133,33 @@ export function extractHashFromFileName(filename: string, hash: VPPTPluginOption } export function validateOptions(options: Required) { - const { outputDir } = options + let { outputDir } = options // ensure outputDir is Dir - if (!/^\/([a-zA-Z0-9]+\/)*[a-zA-Z0-9]*$/.test(outputDir)) { - throw new Error(`outputDir must be a directory, but got ${outputDir}`) + if (!outputDir.startsWith('/')) { + outputDir = `/${outputDir}` } else { if (outputDir.length > 1 && outputDir.endsWith('/')) { // remove last slash options.outputDir = outputDir.replace(/\/$/, '') } } + options.outputDir = outputDir // ensure inputDir is Dir const { inputDir } = options - if (!/^([a-zA-Z0-9]+\/)*[a-zA-Z0-9]*$/.test(inputDir)) { - throw new Error(`inputDir must be a directory, but got ${inputDir}`) - } else { - if (inputDir.endsWith('/')) { - // remove last slash - options.inputDir = inputDir.replace(/\/$/, '') - } + if (inputDir.endsWith('/')) { + // remove last slash + options.inputDir = inputDir.replace(/\/$/, '') + } +} + +// normalize dir path +export function normalizeDirPath(dir: string) { + if (dir.startsWith('/')) { + dir = dir.slice(1) + } + if (dir.endsWith('/')) { + dir = dir.slice(0, -1) } + return dir } diff --git a/src/helper/virtual.ts b/src/helper/virtual.ts deleted file mode 100644 index 154c2c6..0000000 --- a/src/helper/virtual.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const VIRTUAL = 'virtual:public-typescript' - -export const RESOLVED_VIRTUAL_PREFIX = `\0${VIRTUAL}` diff --git a/src/index.ts b/src/index.ts index dfbd4e9..9fcf4a0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,20 @@ import path from 'path' import type { PluginOption, ResolvedConfig } from 'vite' -import { normalizePath } from 'vite' +import { normalizePath, send } from 'vite' import glob from 'tiny-glob' import type { BuildOptions } from 'esbuild' import Watcher from 'watcher' import fs from 'fs-extra' import createDebug from 'debug' -import { TS_EXT, _isPublicTypescript, eq, isEmptyObject, reloadPage, validateOptions } from './helper/utils' +import { + TS_EXT, + _isPublicTypescript, + eq, + isEmptyObject, + normalizeDirPath, + reloadPage, + validateOptions, +} from './helper/utils' import { build, esbuildTypescript } from './helper/build' import { assert } from './helper/assert' import { globalConfigBuilder } from './helper/GlobalConfigBuilder' @@ -16,20 +24,21 @@ import { ManifestCache } from './helper/ManifestCache' const debug = createDebug('index ===> ') export interface VPPTPluginOptions { - /** - * @description vite ssrBuild - * @see https://vitejs.dev/config/#conditional-config - * @default false - */ - ssrBuild?: boolean | undefined /** * @description input public typescript dir - * @default 'publicTypescript' + * @default 'public-typescript' */ inputDir?: string /** - * @description output public javascript dir, relative to `publicDir` - * @note outputDir should start with '/' + * @description output public javascript dir after build + * @note relative with vite.config.ts `publicDir` + * @example + * ```ts + * // vite.config.ts + * export default defineConfig({ + * publicDir: 'some-public-dir', // outputDir will be '/some-public-dir' + * }) + * ``` * @default '/' */ outputDir?: string @@ -58,27 +67,30 @@ export interface VPPTPluginOptions { * @default false */ sideEffects?: boolean - /** - * @description build output type - * @default 'file' + * @description vite ssrBuild + * @see https://vitejs.dev/config/#conditional-config + * @default false */ - buildDestination?: 'file' | 'memory' + ssrBuild?: boolean | undefined + destination?: 'file' | 'memory' } export const DEFAULT_OPTIONS: Required = { - inputDir: 'publicTypescript', + inputDir: 'public-typescript', outputDir: '/', manifestName: 'manifest', hash: true, ssrBuild: false, esbuildOptions: {}, sideEffects: false, - buildDestination: 'file', + destination: 'memory', } let previousOpts: VPPTPluginOptions +const cache = new ManifestCache({ watchMode: true }) + export function publicTypescript(options: VPPTPluginOptions = {}) { const opts = { ...DEFAULT_OPTIONS, @@ -87,8 +99,6 @@ export function publicTypescript(options: VPPTPluginOptions = {}) { validateOptions(opts) - const cache = new ManifestCache({ watchMode: true }) - debug('options:', opts) let config: ResolvedConfig @@ -96,21 +106,24 @@ export function publicTypescript(options: VPPTPluginOptions = {}) { const plugins: PluginOption = [ { name: 'vite:public-typescript', + async configResolved(c) { config = c + const resolvedRoot = normalizePath(config.root ? path.resolve(config.root) : process.cwd()) + function getInputDir(suffix = '') { - return normalizePath(path.resolve(config.root, `${opts.inputDir}${suffix}`)) + return normalizePath(path.resolve(resolvedRoot, `${opts.inputDir}${suffix}`)) } fs.ensureDirSync(getInputDir()) const filesGlob = await glob(getInputDir(`/*${TS_EXT}`), { - cwd: config.root, + cwd: resolvedRoot, absolute: true, }) - const cacheProcessor = initCacheProcessor(opts.buildDestination) + const cacheProcessor = initCacheProcessor(opts.destination) globalConfigBuilder.init({ cache, @@ -209,24 +222,40 @@ export function publicTypescript(options: VPPTPluginOptions = {}) { debug('buildStart - filesGlob:', filesGlob) debug('buildStart - fileNames:', fileNames) - if (!isEmptyObject(parsedCacheJson)) { - const keys = Object.keys(parsedCacheJson) - keys.forEach((key) => { - if (fileNames.includes(key)) { - cache.setCache({ [key]: parsedCacheJson[key] }, { disableWatch: true }) - } else { - cache.setCache({ [key]: parsedCacheJson[key] }) - globalConfigBuilder.get().cacheProcessor.deleteOldJs({ fileName: key, force: true }) - } - }) + if (opts.destination === 'file') { + if (!isEmptyObject(parsedCacheJson)) { + const keys = Object.keys(parsedCacheJson) + keys.forEach((key) => { + if (fileNames.includes(key)) { + cache.set({ [key]: parsedCacheJson[key] }, { disableWatch: true }) + } else { + cache.set({ [key]: parsedCacheJson[key] }) + globalConfigBuilder.get().cacheProcessor.deleteOldJs({ fileName: key, force: true }) + } + }) + } } - debug('buildStart - cache:', cache.getAll()) - filesGlob.forEach((f) => { build({ filePath: f }) }) }, + generateBundle() { + if (opts.ssrBuild || config.build.ssr) { + return + } + + if (opts.destination === 'memory') { + const c = cache.get() + Object.keys(c).forEach((key) => { + this.emitFile({ + type: 'asset', + fileName: normalizeDirPath(`${c[key].path}`), + source: c[key]._code, + }) + }) + } + }, async handleHotUpdate(ctx) { if (_isPublicTypescript(ctx.file)) { debug('hmr:', ctx.file) @@ -236,6 +265,35 @@ export function publicTypescript(options: VPPTPluginOptions = {}) { } }, }, + { + name: 'vite:public-typescript-server', + enforce: 'post', + apply: 'serve', + configureServer(server) { + function addHeader(code: string) { + return `// gen via vite-plugin-public-typescript (only show in serve mode); + ${code}` + } + server.middlewares.use((req, res, next) => { + try { + if (req?.url?.endsWith('.js')) { + const c = cache.get() + const fileName = path.basename(req.url).split('.')[0] + if (fileName && c[fileName]) { + return send(req, res, addHeader(c[fileName]._code || ''), 'js', { + cacheControl: 'no-cache', + headers: server.config.server.headers, + map: null, + }) + } + } + } catch (e) { + return next(e) + } + next() + }) + }, + }, ] // Return as `any` to avoid Plugin type mismatches when there are multiple Vite versions installed