Skip to content

Commit 84afaeb

Browse files
committed
Update: optimize es build as splitted file chunks
1 parent 8226e70 commit 84afaeb

File tree

23 files changed

+475
-108
lines changed

23 files changed

+475
-108
lines changed

build/configs/append-component-css.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { existsSync } from 'fs'
2+
import { extname, dirname, basename, relative, resolve } from 'path'
3+
import { createDistTransformPlugin } from './create-dist-transform'
4+
5+
const parsePath = (path: string) => {
6+
const ext = extname(path).replace('.', '')
7+
8+
return {
9+
ext,
10+
dir: dirname(path),
11+
name: basename(path, `.${ext}`),
12+
}
13+
}
14+
15+
/**
16+
* Checks if file is Vuestic component template source
17+
* If file is script source, but there is not template then add css to script.
18+
* Component usually have script which is stored in file with name like Va[ComponentName].vue_vue_type_script_lang
19+
*
20+
* @notice Component can have render function without template block. It also can have only template without script block.
21+
*/
22+
const isComponent = (filename: string) => {
23+
// [ComponentName].vue_vue_type_script_lang.mjs
24+
// [ComponentName].vue_vue_type_script_setup_true_lang.mjs
25+
const isScriptFile = /\w*.vue_vue_type_script\w*_lang.m?js$/.test(filename)
26+
if (isScriptFile) {
27+
return true
28+
}
29+
30+
// Va[ComponentName].mjs
31+
const isTemplateFile = /\w*\.m?js$/.test(filename)
32+
33+
// Va[ComponentName].vue_vue_type_script_lang.mjs
34+
const scriptFilePath = filename.replace(/\.(mjs|js)/, '.vue_vue_type_script_lang.mjs')
35+
const scriptSetupFilePath = filename.replace(/\.(mjs|js)/, '.vue_vue_type_script_setup_true_lang.mjs')
36+
37+
const haveScript = existsSync(scriptFilePath) || existsSync(scriptSetupFilePath)
38+
39+
if (isTemplateFile && !haveScript) {
40+
return true
41+
}
42+
43+
return false
44+
}
45+
46+
const extractVuesticComponentName = (filename: string) => {
47+
return filename.match(/(\w*)/)?.[1]
48+
}
49+
50+
const SOURCE_MAP_COMMENT_FRAGMENT = '//# sourceMappingURL='
51+
52+
const appendBeforeSourceMapComment = (content: string, append: string): string => {
53+
return content.replace(SOURCE_MAP_COMMENT_FRAGMENT, `${append}\n${SOURCE_MAP_COMMENT_FRAGMENT}`)
54+
}
55+
56+
export const appendComponentCss = createDistTransformPlugin({
57+
name: 'vuestic:append-component-css',
58+
59+
dir: (outDir) => `${outDir}/src/components`,
60+
61+
transform (componentContent, path) {
62+
if (!isComponent(path)) { return }
63+
64+
const { name, dir } = parsePath(path)
65+
66+
const componentName = extractVuesticComponentName(name)
67+
68+
if (!componentName) {
69+
throw new Error(`Can't extract component name from ${name}`)
70+
}
71+
72+
const distPath = resolve(this.outDir, '..', '..')
73+
const relativeDistPath = relative(dir, distPath)
74+
const relativeFilePath = relativeDistPath + '/' + componentName.replace(/-.*$/, '') + '.css'
75+
76+
// There are few cases how vite can store css files (depends on vite version, but we handle both for now):
77+
// CSS stored in dist folder (root)
78+
if (existsSync(resolve(dir, relativeFilePath))) {
79+
return appendBeforeSourceMapComment(componentContent, `\nimport '${relativeFilePath}';`)
80+
}
81+
82+
// CSS stored in component folder
83+
const cssFilePath = `${dir}/${componentName}.css`
84+
85+
if (existsSync(cssFilePath)) {
86+
return appendBeforeSourceMapComment(componentContent, `\nimport './${componentName}.css';`)
87+
}
88+
},
89+
})

build/configs/common.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* Inspired by VuesticUI build config
3+
* https://github.com/epicmaxco/vuestic-ui/blob/develop/packages/ui/build/common-config.ts
4+
*/
5+
import { readFileSync, lstatSync, readdirSync } from 'fs'
6+
import vue from '@vitejs/plugin-vue'
7+
import { UserConfig } from 'vite'
8+
import { resolve as resolver } from 'path'
9+
import { chunkSplitPlugin } from 'vite-plugin-chunk-split'
10+
import { removeSideEffectedChunks } from './remove-side-effect-chunks'
11+
import { fixVueGenericComponentFileNames } from './fix-generic-file-names'
12+
import { appendComponentCss } from './append-component-css'
13+
import { generateScopedName } from '../namespaced-classname.js';
14+
import svgLoader from 'vite-svg-loader';
15+
import { replaceCodePlugin } from 'vite-plugin-replace';
16+
import dts from 'vite-plugin-dts';
17+
18+
export const defineViteConfig = <T extends UserConfig>(p: T): UserConfig & T => p
19+
20+
import type { RollupOptions } from 'vite/node_modules/rollup';
21+
22+
const packageJSON = JSON.parse(readFileSync(resolver(process.cwd(), './package.json')).toString())
23+
const dependencies = [...Object.keys(packageJSON.dependencies), ...Object.keys(packageJSON.peerDependencies)]
24+
25+
export type BuildFormat = 'iife' | 'es' | 'cjs' | 'esm-node'
26+
27+
export const readDirRecursive = (path: string): string[] => {
28+
return readdirSync(path)
29+
.reduce<string[]>((acc, entry) => {
30+
const p = `${path}/${entry}`
31+
32+
if (lstatSync(p).isDirectory()) {
33+
return [...acc, ...readDirRecursive(p)]
34+
}
35+
36+
return [...acc, p]
37+
}, [])
38+
}
39+
40+
export const resolve = {
41+
alias: {
42+
'@icons': resolver(process.cwd(), 'node_modules/@shopify/polaris-icons/dist/svg'),
43+
'@polaris': resolver(process.cwd(), './polaris/polaris-react/src'),
44+
'@': resolver(process.cwd(), './src'),
45+
'~': resolver(process.cwd(), './node_modules'),
46+
},
47+
dedupe: ['vue'],
48+
}
49+
50+
const rollupOutputOptions = (ext: string): RollupOptions['output'] => ({
51+
entryFileNames: `[name].${ext}`,
52+
chunkFileNames: `[name].${ext}`,
53+
assetFileNames: '[name].[ext]',
54+
})
55+
56+
const rollupMjsBuildOptions: RollupOptions = {
57+
input: resolver(process.cwd(), 'src/polaris-vue.ts'),
58+
59+
output: {
60+
sourcemap: true,
61+
dir: 'dist/esm-node',
62+
format: 'esm',
63+
...rollupOutputOptions('mjs'),
64+
},
65+
}
66+
67+
const libBuildOptions = (format: 'iife' | 'es' | 'cjs') => ({
68+
entry: resolver(process.cwd(), 'src/polaris-vue.ts'),
69+
fileName: () => 'polaris-vue.js',
70+
formats: [format],
71+
72+
// only for iife/umd
73+
name: 'PolarisVue',
74+
})
75+
76+
export default function createViteConfig (format: BuildFormat) {
77+
const isEsm = ['es', 'esm-node'].includes(format)
78+
const isNode = format === 'esm-node'
79+
80+
const config = defineViteConfig({
81+
resolve,
82+
83+
css: {
84+
preprocessorOptions: {
85+
scss: {
86+
quietDeps: true, // Silent the deprecation warning
87+
},
88+
},
89+
modules: {
90+
generateScopedName,
91+
},
92+
},
93+
94+
build: {
95+
outDir: `dist/${isEsm ? format : ''}`,
96+
cssCodeSplit: false,
97+
sourcemap: true,
98+
minify: isEsm ? false : 'esbuild',
99+
lib: libBuildOptions(isNode ? 'es' : format),
100+
},
101+
102+
plugins: [
103+
vue({
104+
isProduction: true,
105+
exclude: [/\.md$/, /\.spec\.ts$/, /\.spec\.disabled$/],
106+
}),
107+
svgLoader(),
108+
replaceCodePlugin({
109+
replacements: [
110+
{
111+
from: '%POLARIS_VERSION%',
112+
to: packageJSON.polaris_version,
113+
},
114+
],
115+
}),
116+
dts({
117+
rollupTypes: true,
118+
outDir: 'dist/types',
119+
}),
120+
],
121+
})
122+
123+
// https://github.com/sanyuan0704/vite-plugin-chunk-split
124+
isEsm && config.plugins.push(chunkSplitPlugin({ strategy: 'unbundle' }))
125+
// isEsm && !isNode && config.plugins.push(appendComponentCss())
126+
isEsm && config.plugins.push(removeSideEffectedChunks())
127+
isEsm && config.plugins.push(fixVueGenericComponentFileNames)
128+
129+
if (isNode) {
130+
config.build.rollupOptions = {
131+
external: [...dependencies, 'vue'],
132+
...rollupMjsBuildOptions,
133+
};
134+
} else {
135+
config.build.rollupOptions = {
136+
external: [...dependencies, 'vue'],
137+
output: rollupOutputOptions('js'),
138+
}
139+
}
140+
141+
return config
142+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { Plugin } from 'vite'
2+
import { readdir, readFile, writeFile, lstat } from 'fs/promises'
3+
4+
type Nothing = null | undefined | void
5+
type TransformFnResult = string | Nothing
6+
type TransformFn = (this: { outDir: string }, content: string, path: string) => TransformFnResult | Promise<TransformFnResult>
7+
8+
export const createDistTransformPlugin = (options: {
9+
name: string,
10+
dir?: (outDir: string) => string,
11+
transform: TransformFn,
12+
}) => {
13+
let outDir = ''
14+
15+
const processFiles = async (dir: string) => {
16+
return (await readdir(dir))
17+
.map(async (entryName) => {
18+
const currentPath = `${dir}/${entryName}`
19+
20+
if ((await lstat(currentPath)).isDirectory()) {
21+
return processFiles(currentPath)
22+
}
23+
24+
const content = await readFile(currentPath, 'utf8')
25+
26+
const result = await options.transform.call({ outDir }, content, currentPath)
27+
28+
if (!result) { return }
29+
30+
await writeFile(currentPath, result)
31+
})
32+
}
33+
34+
return (): Plugin => ({
35+
name: 'vuestic:append-component-css',
36+
configResolved: (config) => {
37+
outDir = options.dir?.(config.build.outDir) || config.build.outDir
38+
},
39+
closeBundle: async () => {
40+
processFiles(outDir)
41+
},
42+
})
43+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Plugin } from 'vite';
2+
3+
const defineVitePlugin = <T extends Plugin>(p: T): Plugin & T => p;
4+
5+
/**
6+
* When vue compiles file, it encode generic into file name
7+
*
8+
* @example
9+
* AppProvider.vue_vue_type_script_generic_Item%20extends%20Record%3Cstring%2C%20any%3E_setup_true_lang
10+
*
11+
* This might be helpful for compiler, but it makes file names unreadable and some bundlers may not allow encoded characters in file names.
12+
* This plugin replaces encoded characters in file names and source maps with underscores.
13+
*/
14+
const GENERIC_NAME_REGEX = /.vue_vue_type_script_generic_.*_setup_true_lang/gm
15+
const URL_ENCODED_REGEX = /%([0-9]|[A-F]){2}/gm
16+
17+
const replaceEncodedCharacters = (match: string) => match
18+
.replace(URL_ENCODED_REGEX, '_') // Replace any encoded character with underscore
19+
.replace(/_{2,}/g, '_') // Replace multiple underscores with single underscore
20+
21+
export const fixVueGenericComponentFileNames = defineVitePlugin({
22+
name: 'polaris-vue:fix-vue-generic-component-file-names',
23+
24+
generateBundle (options, bundle) {
25+
Object.keys(bundle).forEach(fileName => {
26+
console.log(fileName);
27+
const file = bundle[fileName]
28+
29+
// Replace encoded characters in generic names in source maps
30+
if (file.type === 'asset' && file.fileName.endsWith('.map')) {
31+
if (typeof file.source === 'string') {
32+
file.source = file.source
33+
.replace(GENERIC_NAME_REGEX, replaceEncodedCharacters)
34+
}
35+
}
36+
37+
// Replace encoded characters in generic names in code
38+
if (file.type === 'chunk') {
39+
file.code = file.code
40+
.replace(GENERIC_NAME_REGEX, replaceEncodedCharacters)
41+
}
42+
43+
// Update file name if it has encoded characters
44+
if (GENERIC_NAME_REGEX.test(fileName)) {
45+
const newFileName = replaceEncodedCharacters(fileName)
46+
47+
bundle[newFileName] = {
48+
...bundle[fileName],
49+
fileName: newFileName,
50+
}
51+
52+
delete bundle[fileName]
53+
}
54+
})
55+
},
56+
})
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { createDistTransformPlugin } from './create-dist-transform';
2+
3+
/**
4+
* Vite think that some chunks contain side effects,
5+
* so it keep them in bundle and imports with `import "..."`.
6+
* It simply removes all imports from chunks that import side effected chunks
7+
* after build is done.
8+
*/
9+
export const removeSideEffectedChunks = createDistTransformPlugin({
10+
name: 'polaris-vue:remove-side-effected-chunks',
11+
12+
transform: (content) => {
13+
return content.replace(/import ".*";(\n)*/gm, '')
14+
},
15+
})

build/configs/vite.cjs.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { defineConfig } from 'vite'
2+
import createViteConfig from './common'
3+
4+
export default () => {
5+
return defineConfig({
6+
...createViteConfig('cjs'),
7+
})
8+
}

build/configs/vite.es.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { defineConfig } from 'vite'
2+
import createViteConfig from './common'
3+
4+
export default () => defineConfig({
5+
...createViteConfig('es'),
6+
})

docs/.vitepress/config.mts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,6 @@ export default defineConfig({
121121
// @ts-ignore
122122
'@polaris': fileURLToPath(new URL('../../polaris/polaris-react/src', import.meta.url)),
123123
// @ts-ignore
124-
'@tokens': fileURLToPath(new URL('../../polaris/polaris-tokens/src', import.meta.url)),
125-
// @ts-ignore
126124
'@': fileURLToPath(new URL('../../src', import.meta.url)),
127125
// @ts-ignore
128126
'~': fileURLToPath(new URL('../../node_modules', import.meta.url)),

0 commit comments

Comments
 (0)