├── examples
├── src
│ ├── locale
│ │ ├── en.json
│ │ └── zh-cn.json
│ ├── publicPath.ts
│ ├── typing.d.ts
│ ├── views
│ │ ├── Basic.vue
│ │ ├── Home.vue
│ │ └── About.vue
│ ├── assets
│ │ ├── logo.png
│ │ ├── logo2.png
│ │ └── styles
│ │ │ └── base.css
│ ├── env.d.ts
│ ├── main.ts
│ ├── components
│ │ └── HelloWorld.vue
│ └── App.vue
├── .gitignore
├── .vscode
│ └── extensions.json
├── public
│ └── favicon.ico
├── tsconfig.json
├── index.html
├── package.json
├── vite.config.ts
└── README.md
├── .npmrc
├── tsconfig.build.json
├── .gitignore
├── vitest.config.ts
├── src
├── types.ts
├── core
│ ├── utils.ts
│ ├── ast.ts
│ └── transform.ts
└── index.ts
├── tsconfig.json
├── tests
├── basic.test.ts
├── configWithoutDefine.test.ts
└── transform.test.ts
├── .github
└── FUNDING.yml
├── LICENSE
├── package.json
├── CHANGELOG.zh-CN.md
├── README.zh-CN.md
├── README.md
├── CHANGELOG.md
└── pnpm-lock.yaml
/examples/src/locale/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "hello": "hello"
3 | }
--------------------------------------------------------------------------------
/examples/src/locale/zh-cn.json:
--------------------------------------------------------------------------------
1 | {
2 | "hello": "你好"
3 | }
--------------------------------------------------------------------------------
/examples/src/publicPath.ts:
--------------------------------------------------------------------------------
1 | window.__dynamic_base__ = 'http://localhost:4173'
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | ignore-workspace-root-check=true
2 | shamefully-hoist=true
3 | git-checks=false
--------------------------------------------------------------------------------
/examples/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
6 |
--------------------------------------------------------------------------------
/examples/src/typing.d.ts:
--------------------------------------------------------------------------------
1 | declare interface Window {
2 | __dynamic_base__:string
3 | }
--------------------------------------------------------------------------------
/examples/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["johnsoncodehk.volar"]
3 | }
4 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["./src"]
4 | }
--------------------------------------------------------------------------------
/examples/src/views/Basic.vue:
--------------------------------------------------------------------------------
1 |
2 | Basic
3 |
4 |
--------------------------------------------------------------------------------
/examples/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 | Home
3 |
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
6 | # tests
7 | coverage
8 | __snapshots__/
--------------------------------------------------------------------------------
/examples/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenxch/vite-plugin-dynamic-base/HEAD/examples/public/favicon.ico
--------------------------------------------------------------------------------
/examples/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenxch/vite-plugin-dynamic-base/HEAD/examples/src/assets/logo.png
--------------------------------------------------------------------------------
/examples/src/assets/logo2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenxch/vite-plugin-dynamic-base/HEAD/examples/src/assets/logo2.png
--------------------------------------------------------------------------------
/examples/src/assets/styles/base.css:
--------------------------------------------------------------------------------
1 | .base {
2 | width: 100px;
3 | height: 100px;
4 | background-image: url("../logo.png");
5 | }
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config'
2 |
3 | export default defineConfig({
4 | test: {
5 | include: ['tests/**/*.test.ts'],
6 | globals: true,
7 | },
8 | })
--------------------------------------------------------------------------------
/examples/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
10 |
11 |
12 |
13 |
14 |
33 |
--------------------------------------------------------------------------------
/examples/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 | // import { dynamicBase } from 'vite-plugin-dynamic-base'
4 | import { dynamicBase } from '../dist/index'
5 | import legacy from '@vitejs/plugin-legacy'
6 | import { VitePWA } from 'vite-plugin-pwa'
7 |
8 | // https://vitejs.dev/config/
9 | export default defineConfig({
10 | // base: 'a/b',
11 | base: process.env.NODE_ENV === "production" ? "/__dynamic_base__/" : "/",
12 | plugins: [
13 | legacy({
14 | targets: ['ie >= 11'],
15 | additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
16 | modernPolyfills: true,
17 | }),
18 | vue(),
19 | VitePWA({}),
20 | dynamicBase({ transformIndexHtml: true }),
21 | ],
22 | build: {
23 | // assetsDir: 'assets/a/b'
24 | }
25 | })
26 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: https://cdn.staticaly.com/gh/chenxch/pic-image@master/20221026/1666791805873.30bhic398gu0.webp
14 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Vue 3 + Typescript + Vite
2 |
3 | This template should help get you started developing with Vue 3 and Typescript in Vite. The template uses Vue 3 `
8 |
9 |
10 | 13 | Recommended IDE setup: 14 | VSCode 15 | + 16 | Volar 17 |
18 | 19 |See README.md for more information.
22 | 23 | Vite Docs 24 | 25 | | 26 | Vue 3 Docs 27 |
28 | 29 | 30 |
31 | Edit
32 | components/HelloWorld.vue to test hot module replacement.
33 |
14 |
15 |
18 |
19 |
20 |
21 |
23 | English | 简体中文 24 |
25 | 26 | - 🦾 解析所有资源文件动态路径(多cdn切换), 类似 Webpack 的 `__webpack_public_path__`. 27 | 28 | ## 安装 29 | 30 | ```bash 31 | npm i vite-plugin-dynamic-base -D 32 | ``` 33 | 34 | ## 变更日志 35 | 36 | [变更日志](./CHANGELOG.zh-CN.md) 37 | 38 | ## 编译模式 39 | 40 | - [x] es 41 | - [x] system 42 | 43 | ## 兼容插件 44 | 45 | - [x] [@vitejs/plugin-legacy](https://www.npmjs.com/package/@vitejs/plugin-legacy) 46 | - [x] [vite-plugin-pwa](https://www.npmjs.com/package/vite-plugin-pwa) 47 | 48 | 49 | ## 使用 50 | 51 | ```ts 52 | // vite.config.ts 53 | import { dynamicBase } from 'vite-plugin-dynamic-base' 54 | 55 | export default defineConfig({ 56 | // base: "/", 57 | base: process.env.NODE_ENV === "production" ? "/__dynamic_base__/" : "/", 58 | plugins: [ 59 | dynamicBase({ /* options */ }), 60 | ], 61 | }) 62 | ``` 63 | 64 | ## 配置 65 | 66 | 以下显示配置的默认值 67 | 68 | ```ts 69 | dynamicBase({ 70 | // dynamic public path var string, default window.__dynamic_base__ 71 | publicPath: 'window.__dynamic_base__', 72 | // dynamic load resources on index.html, default false. maybe change default true 73 | transformIndexHtml: false 74 | // provide conversion configuration parameters. by 1.1.0 75 | // transformIndexHtmlConfig: { insertBodyAfter: false } 76 | }) 77 | ``` 78 | 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vite-plugin-dynamic-base 2 | 3 | 4 | 5 | 6 |22 | English | 简体中文 23 |
24 | 25 | - 🦾 Resolve all resource files dynamic publicPath, like Webpack's `__webpack_public_path__`. 26 | 27 | 28 | ## Installation 29 | 30 | ```bash 31 | npm i vite-plugin-dynamic-base -D 32 | ``` 33 | 34 | 35 | ## Changelog 36 | 37 | [Changelogs](./CHANGELOG.md) 38 | 39 | 40 | ## Build Mode 41 | 42 | - [x] es 43 | - [x] system 44 | 45 | ## Compatible plugins 46 | 47 | - [x] [@vitejs/plugin-legacy](https://www.npmjs.com/package/@vitejs/plugin-legacy) 48 | - [x] [vite-plugin-pwa](https://www.npmjs.com/package/vite-plugin-pwa) 49 | 50 | ## Usage 51 | 52 | ```ts 53 | // vite.config.ts 54 | import { dynamicBase } from 'vite-plugin-dynamic-base' 55 | 56 | export default defineConfig({ 57 | // base: "/", 58 | base: process.env.NODE_ENV === "production" ? "/__dynamic_base__/" : "/", 59 | plugins: [ 60 | dynamicBase({ /* options */ }), 61 | ], 62 | }) 63 | ``` 64 | 65 | ## Configuration 66 | 67 | The following show the default values of the configuration 68 | 69 | ```ts 70 | dynamicBase({ 71 | // dynamic public path var string, default window.__dynamic_base__ 72 | publicPath: 'window.__dynamic_base__', 73 | // dynamic load resources on index.html, default false. maybe change default true 74 | transformIndexHtml: false 75 | // provide conversion configuration parameters. by 1.1.0 76 | // transformIndexHtmlConfig: { insertBodyAfter: false } 77 | }) 78 | ``` 79 | 80 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vite' 2 | import type { Options, TransformOptions } from './types' 3 | import { transformChunk, transformAsset, transformLegacyHtml, transformHtml } from './core/transform' 4 | 5 | export function dynamicBase(options?: Options): Plugin { 6 | const defaultOptions: Options = { 7 | publicPath: 'window.__dynamic_base__', 8 | transformIndexHtml: false, // maybe default true 9 | transformIndexHtmlConfig: {} 10 | } 11 | 12 | const { publicPath, transformIndexHtml, transformIndexHtmlConfig } = { ...defaultOptions, ...(options || {}) } 13 | 14 | // const preloadHelperId = 'vite/preload-helper' 15 | let assetsDir = 'assets' 16 | let base = '/' 17 | let legacy = false 18 | let baseOptions: TransformOptions = { assetsDir, base, legacy, publicPath: ` ${publicPath}`, transformIndexHtml } 19 | 20 | return { 21 | name: 'vite-plugin-dynamic-base', 22 | enforce: 'post', 23 | apply: 'build', 24 | configResolved(resolvedConfig) { 25 | assetsDir = resolvedConfig.build.assetsDir 26 | base = resolvedConfig.base 27 | legacy = !!resolvedConfig?.define?.['import.meta.env.LEGACY'] 28 | if (!base || base === '/') { 29 | throw new Error( 30 | 'Please replace `config.base` in build with unique markup text, (e.g. /__dynamic_base__/)\n' + 31 | 'Recommended changes:\n' + 32 | ` - base: ${JSON.stringify(base)},\n` + 33 | ` + base: process.env.NODE_ENV === "production" ? "/__dynamic_base__/" : "/",\n` + 34 | ' (in your vite.config.ts/js file)' 35 | ) 36 | } 37 | Object.assign(baseOptions, { assetsDir, base, legacy }) 38 | }, 39 | async generateBundle({ format }, bundle) { 40 | if (format !== 'es' && format !== 'system') { 41 | return 42 | } 43 | await Promise.all( 44 | Object.entries(bundle).map(async ([, chunk]) => { 45 | if (chunk.type === 'chunk' && chunk.code.indexOf(base) > -1) { 46 | chunk.code = await transformChunk(chunk.code, baseOptions); 47 | } else if (chunk.type === 'asset' && typeof chunk.source === 'string') { 48 | if (!chunk.fileName.endsWith('.html')) { 49 | chunk.source = transformAsset(chunk.source, baseOptions) 50 | } else if (transformIndexHtml) { 51 | chunk.source = transformHtml(chunk.source, baseOptions, transformIndexHtmlConfig) 52 | if(legacy){ 53 | chunk.source = transformLegacyHtml(chunk.source, baseOptions) 54 | } 55 | } 56 | } 57 | }) 58 | ) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/core/ast.ts: -------------------------------------------------------------------------------- 1 | import {Expression, ModuleItem, parse, StringLiteral, TemplateElement, TemplateLiteral} from "@swc/core"; 2 | import Visitor from "@swc/core/Visitor"; 3 | 4 | /** 5 | * Traverses an AST (or parts of it) to collect all StringLiterals and TemplateElements that contain 6 | * needle in their value. 7 | */ 8 | export class StringCollector extends Visitor { 9 | public matchingStrings: (StringLiteral|TemplateElement)[] = []; 10 | private readonly needle: string; 11 | 12 | constructor(needle: string) { 13 | super(); 14 | this.needle = needle; 15 | } 16 | 17 | visitStringLiteral(n: StringLiteral): StringLiteral { 18 | if (n.value.indexOf(this.needle) !== -1) { 19 | this.matchingStrings.push(n); 20 | } 21 | 22 | return super.visitStringLiteral(n); 23 | } 24 | 25 | visitTemplateLiteral(n: TemplateLiteral): Expression { 26 | for(const q of n.quasis) { 27 | if (q.raw.indexOf(this.needle) !== -1) { 28 | this.matchingStrings.push(q); 29 | } 30 | } 31 | 32 | return super.visitTemplateLiteral(n); 33 | } 34 | } 35 | 36 | /** 37 | * Represents a string as bytes, so it can be sliced via 38 | * byte-positions. 39 | */ 40 | export class StringAsBytes { 41 | private string: Uint8Array; 42 | private decoder: TextDecoder; 43 | 44 | constructor(string: string) { 45 | this.decoder = new TextDecoder(); 46 | this.string = (new TextEncoder()).encode(string); 47 | } 48 | 49 | /** 50 | * Returns a slice of the string by providing byte indices. 51 | * @param from - Byte index to slice from 52 | * @param to - Optional byte index to slice to 53 | */ 54 | public slice(from: number, to?: number): string { 55 | return this.decoder.decode( 56 | new DataView(this.string.buffer, from, to !== undefined ? to - from : undefined) 57 | ); 58 | } 59 | } 60 | 61 | /** 62 | * Parses js code into a AST. 63 | * @param code 64 | */ 65 | export async function parseCode(code: string): Promise<[number, ModuleItem[]]> { 66 | const module = await parse(code, { target: 'esnext', syntax: 'ecmascript' }); 67 | return [module.span.start, module.body]; 68 | } 69 | 70 | /** 71 | * Returns an array of StringLiterals and TemplateElements from an AST that contain needle in their value. 72 | * @param needle 73 | * @param ast 74 | */ 75 | export function collectMatchingStrings(needle: string, ast: ModuleItem[]): (StringLiteral|TemplateElement)[] { 76 | const visitor = new StringCollector(needle); 77 | visitor.visitModuleItems(ast); 78 | 79 | return visitor.matchingStrings; 80 | } 81 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ### 1.2.0 4 | 5 | _2025-06-05_ 6 | 7 | #### Feat 8 | 9 | - transform HTML to support custom publicPath. 10 | 11 | ### 1.1.2 12 | 13 | _2025-06-04_ 14 | 15 | #### Fix 16 | 17 | - update asset tag selector to include all relevant tags in transformHtml function. Change from head to global. 18 | 19 | ### 1.1.0 20 | 21 | _2024-05-15_ 22 | 23 | #### Chore 24 | 25 | - support defer load script. 26 | 27 | 28 | ### 1.0.3 29 | 30 | _2024-04-29_ 31 | 32 | #### Chore 33 | 34 | - add peerDependencies. 35 | 36 | ### 1.0.2 37 | 38 | _2024-02-02_ 39 | 40 | #### Fix 41 | 42 | - Fix failing transformations due to wrong string / template order. 43 | 44 | ### 1.0.1 45 | 46 | _2024-02-02_ 47 | 48 | #### Feat 49 | 50 | - Add support for replacement in template literals. ([#28](https://github.com/chenxch/vite-plugin-dynamic-base/pull/28) by [@joarfish](https://github.com/joarfish)) 51 | 52 | 53 | ### 1.0.0 54 | 55 | _2023-06-07_ 56 | 57 | #### Feat 58 | 59 | - Using SWC for token transformation ([#23](https://github.com/chenxch/vite-plugin-dynamic-base/pull/23) by [@joarfish](https://github.com/joarfish)) 60 | 61 | 62 | ### 0.4.9 63 | 64 | _2023-04-11_ 65 | #### Fix 66 | 67 | - fix html template src parse ([#21](https://github.com/chenxch/vite-plugin-dynamic-base/issues/21)) 68 | 69 | ### 0.4.8 70 | 71 | _2023-01-15_ 72 | #### Fix 73 | 74 | - fix legacy assets path ([#19](https://github.com/chenxch/vite-plugin-dynamic-base/issues/19) by [@jgsrty](https://github.com/jgsrty)) 75 | 76 | ### 0.4.5 77 | 78 | _2022-09-07_ 79 | #### Fix 80 | 81 | - support aysnc load components.([#14](https://github.com/chenxch/vite-plugin-dynamic-base/issues/14)) 82 | ### 0.4.4 83 | 84 | _2022-06-24_ 85 | #### Feat 86 | 87 | - support legacy modernPolyfills.([#9](https://github.com/chenxch/vite-plugin-dynamic-base/issues/9)) 88 | 89 | ### 0.4.3 90 | 91 | _2022-06-21_ 92 | #### Bug fixes 93 | 94 | - template strings does not work.([#8](https://github.com/chenxch/vite-plugin-dynamic-base/issues/8)) 95 | 96 | ### 0.4.1 97 | 98 | _2022-05-09_ 99 | #### Bug fixes 100 | 101 | - Legacy is invalid in browsers such as IE11. 102 | 103 | 104 | ### 0.4.0 105 | 106 | _2022-05-01_ 107 | 108 | #### Features 109 | 110 | - compatible `vite-plugin-pwa` 111 | - base mark 112 | 113 | #### Bug fixes 114 | 115 | - Multi-level cdn reference resource path fix 116 | 117 | #### Refactors 118 | 119 | - Replace the matching scheme and use the base attribute as a marker bit 120 | - Code structure adjustment, introduction of asynchronous processing 121 | 122 | 123 | ### 0.3.0 124 | 125 | _2022-04-23_ 126 | 127 | #### Features 128 | 129 | - setup simple unit tests with `vitest`. (#5 by @zhoujinfu) 130 | 131 | #### Bug fixes 132 | 133 | - import.env.LEGACY cause undefined errors with vite config `define`. (#5 by @zhoujinfu) -------------------------------------------------------------------------------- /src/core/transform.ts: -------------------------------------------------------------------------------- 1 | import type {TransformOptions, TransformIndexHtmlConfig} from '../types' 2 | import {parse} from 'node-html-parser' 3 | import {replace, replaceImport, replaceInStringLiteral, replaceInTemplateElement, replaceSrc} from './utils' 4 | import {StringAsBytes, collectMatchingStrings, parseCode} from "./ast"; 5 | 6 | export async function transformChunk(codeStr: string, options: TransformOptions): Promise