Skip to content

Commit

Permalink
feat: support asset path import from js + special treatment of /publi…
Browse files Browse the repository at this point in the history
…c dir
  • Loading branch information
yyx990803 committed May 8, 2020
1 parent e1dd37f commit 9061e44
Show file tree
Hide file tree
Showing 10 changed files with 117 additions and 27 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,11 @@ Both CSS and JSON imports also support Hot Module Replacement.
You can reference static assets in your `*.vue` templates, styles and plain `.css` files either using absolute public paths (based on project root) or relative paths (based on your file system). The latter is similar to the behavior you are used to if you have used `vue-cli` or webpack's `file-loader`.

There is no conventional `public` directory. All referenced assets, including those using absolute paths, will be copied to the dist folder with a hashed file name in the production build. Never-referenced assets will not be copied.
All referenced assets, including those using absolute paths, will be copied to the dist folder with a hashed file name in the production build. Never-referenced assets will not be copied. Similar to `vue-cli`, image assets smaller than 4kb will be base64 inlined.

Similar to `vue-cli`, image assets smaller than 4kb will be base64 inlined.
The exception is the `public` directory - assets placed in this directory will be copied to the dist directory as-is. It can be used to provide assets that are never referenced in your code - e.g. `robots.txt`.

All path references, including absolute paths and those starting with `/public`, should be based on your working directory structure. If you are deploying your project under a nested public path, simply specify `--base=/your/public/path/` and all asset paths will be rewritten accordingly. **You never need to think about the public path during development.**

### PostCSS

Expand Down
15 changes: 15 additions & 0 deletions playground/TestAssets.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
<p>
Fonts should be italic if font asset reference from CSS works.
</p>
<p class="asset-import">
Path for assets import from js: <code>{{ filepath }}</code>
</p>
<p>
Relative asset reference in template:
<img src="./testAssets.png" style="width: 30px;" />
Expand All @@ -19,6 +22,18 @@
</div>
</template>

<script>
import filepath from './testAssets.png'
export default {
data() {
return {
filepath
}
}
}
</script>

<style>
@font-face {
font-family: 'Inter';
Expand Down
54 changes: 38 additions & 16 deletions src/node/build/buildPluginAsset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ import mime from 'mime-types'
const debug = require('debug')('vite:build:asset')

interface AssetCacheEntry {
content: Buffer
fileName: string
content: Buffer | null
fileName: string | null
url: string
}

const assetResolveCache = new Map<string, AssetCacheEntry>()

export const resolveAsset = async (
id: string,
root: string,
publicBase: string,
assetsDir: string,
inlineLimit: number
Expand All @@ -27,27 +28,43 @@ export const resolveAsset = async (
return cached
}

const ext = path.extname(id)
const baseName = path.basename(id, ext)
const resolvedFileName = `${baseName}.${hash_sum(id)}${ext}`
let resolved: AssetCacheEntry | undefined

let url = slash(path.join(publicBase, assetsDir, resolvedFileName))
const content = await fs.readFile(id)
if (!id.endsWith(`.svg`) && content.length < inlineLimit) {
url = `data:${mime.lookup(id)};base64,${content.toString('base64')}`
const pathFromRoot = path.relative(root, id)
if (/^public(\/|\\)/.test(pathFromRoot)) {
// assets inside the public directory will be copied over verbatim
// so all we need to do is just append the baseDir
resolved = {
content: null,
fileName: null,
url: slash(path.join(publicBase, pathFromRoot))
}
}

const resolved = {
content,
fileName: resolvedFileName,
url
if (!resolved) {
const ext = path.extname(id)
const baseName = path.basename(id, ext)
const resolvedFileName = `${baseName}.${hash_sum(id)}${ext}`

let url = slash(path.join(publicBase, assetsDir, resolvedFileName))
const content = await fs.readFile(id)
if (!id.endsWith(`.svg`) && content.length < inlineLimit) {
url = `data:${mime.lookup(id)};base64,${content.toString('base64')}`
}

resolved = {
content,
fileName: resolvedFileName,
url
}
}

assetResolveCache.set(id, resolved)
return resolved
}

export const registerAssets = (
assets: Map<string, string>,
assets: Map<string, Buffer>,
bundle: OutputBundle
) => {
for (const [fileName, source] of assets) {
Expand All @@ -61,22 +78,27 @@ export const registerAssets = (
}

export const createBuildAssetPlugin = (
root: string,
publicBase: string,
assetsDir: string,
inlineLimit: number
): Plugin => {
const assets = new Map()
const assets = new Map<string, Buffer>()

return {
name: 'vite:asset',
async load(id) {
if (isStaticAsset(id)) {
const { fileName, content, url } = await resolveAsset(
id,
root,
publicBase,
assetsDir,
inlineLimit
)
assets.set(fileName, content)
if (fileName && content) {
assets.set(fileName, content)
}
debug(`${id} -> ${url.startsWith('data:') ? `base64 inlined` : url}`)
return `export default ${JSON.stringify(url)}`
}
Expand Down
7 changes: 5 additions & 2 deletions src/node/build/buildPluginCss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const createBuildCssPlugin = (
inlineLimit: number
): Plugin => {
const styles: Map<string, string> = new Map()
const assets = new Map()
const assets = new Map<string, Buffer>()

return {
name: 'vite:css',
Expand All @@ -36,11 +36,14 @@ export const createBuildCssPlugin = (
const file = path.join(fileDir, rawUrl)
const { fileName, content, url } = await resolveAsset(
file,
root,
publicBase,
assetsDir,
inlineLimit
)
assets.set(fileName, content)
if (fileName && content) {
assets.set(fileName, content)
}
debug(
`url(${rawUrl}) -> ${
url.startsWith('data:') ? `base64 inlined` : `url(${url})`
Expand Down
4 changes: 4 additions & 0 deletions src/node/build/buildPluginHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import MagicString from 'magic-string'
import { InternalResolver } from '../resolver'

export const createBuildHtmlPlugin = async (
root: string,
indexPath: string | null,
publicBasePath: string,
assetsDir: string,
Expand All @@ -31,6 +32,7 @@ export const createBuildHtmlPlugin = async (

const rawHtml = await fs.readFile(indexPath, 'utf-8')
let { html: processedHtml, js } = await compileHtml(
root,
rawHtml,
publicBasePath,
assetsDir,
Expand Down Expand Up @@ -111,6 +113,7 @@ const assetAttrsConfig: Record<string, string[]> = {
// compile index.html to a JS module, importing referenced assets
// and scripts
const compileHtml = async (
root: string,
html: string,
publicBasePath: string,
assetsDir: string,
Expand Down Expand Up @@ -176,6 +179,7 @@ const compileHtml = async (
const value = attr.value!
const { url } = await resolveAsset(
resolver.requestToFile(value.content),
root,
publicBasePath,
assetsDir,
inlineLimit
Expand Down
15 changes: 14 additions & 1 deletion src/node/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export async function build(options: BuildOptions = {}): Promise<BuildResult> {
const resolver = createResolver(root, resolvers)

const { htmlPlugin, renderIndex } = await createBuildHtmlPlugin(
root,
indexPath,
publicBasePath,
assetsDir,
Expand Down Expand Up @@ -220,7 +221,12 @@ export async function build(options: BuildOptions = {}): Promise<BuildResult> {
assetsInlineLimit
),
// vite:asset
createBuildAssetPlugin(publicBasePath, assetsDir, assetsInlineLimit),
createBuildAssetPlugin(
root,
publicBasePath,
assetsDir,
assetsInlineLimit
),
// minify with terser
// this is the default which has better compression, but slow
// the user can opt-in to use esbuild which is much faster but results
Expand Down Expand Up @@ -265,6 +271,7 @@ export async function build(options: BuildOptions = {}): Promise<BuildResult> {
await fs.remove(outDir)
await fs.ensureDir(outDir)

// write js chunks and assets
for (const chunk of output) {
if (chunk.type === 'chunk') {
// write chunk
Expand Down Expand Up @@ -300,6 +307,12 @@ export async function build(options: BuildOptions = {}): Promise<BuildResult> {
WriteType.HTML
)
}

// copy over /public if it exists
const publicDir = path.resolve(root, 'public')
if (await fs.pathExists(publicDir)) {
await fs.copy(publicDir, path.resolve(outDir, 'public'))
}
}

if (!silent) {
Expand Down
2 changes: 2 additions & 0 deletions src/node/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { hmrPlugin, HMRWatcher } from './serverPluginHmr'
import { serveStaticPlugin } from './serverPluginServeStatic'
import { jsonPlugin } from './serverPluginJson'
import { cssPlugin } from './serverPluginCss'
import { assetPathPlugin } from './serverPluginAssets'
import { esbuildPlugin } from './serverPluginEsbuild'

export { Resolver }
Expand Down Expand Up @@ -44,6 +45,7 @@ const internalPlugins: Plugin[] = [
esbuildPlugin,
jsonPlugin,
cssPlugin,
assetPathPlugin,
hmrPlugin,
serveStaticPlugin
]
Expand Down
13 changes: 13 additions & 0 deletions src/node/server/serverPluginAssets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Plugin } from '.'
import { isImportRequest, isStaticAsset } from '../utils'

export const assetPathPlugin: Plugin = ({ app }) => {
app.use(async (ctx, next) => {
if (isStaticAsset(ctx.path) && isImportRequest(ctx)) {
ctx.type = 'js'
ctx.body = `export default ${JSON.stringify(ctx.path)}`
return
}
return next()
})
}
18 changes: 12 additions & 6 deletions src/node/utils/pathUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,19 @@ export const isStaticAsset = (file: string) => {
return imageRE.test(file) || mediaRE.test(file) || fontsRE.test(file)
}

const timeStampRE = /(&|\?)t=\d+/
const jsSrcFileRE = /\.(vue|jsx?|tsx?)$/

/**
* Check if a request is an import from js (instead of fetch() or ajax requests)
* A request qualifies as long as it's not from page (no ext or .html).
* this is because non-js files can be transformed into js and import json
* as well.
* A request qualifies as long as it's from one of the supported js source file
* formats (vue,js,ts,jsx,tsx)
*/
export const isImportRequest = (ctx: Context) => {
const referer = cleanUrl(ctx.get('referer'))
return /\.\w+$/.test(referer) && !referer.endsWith('.html')
export const isImportRequest = (ctx: Context): boolean => {
if (!ctx.accepts('js')) {
return false
}
// strip HMR timestamps
const referer = ctx.get('referer').replace(timeStampRE, '')
return jsSrcFileRE.test(referer)
}
10 changes: 10 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ describe('vite', () => {
expect(has404).toBe(false)
})

test('asset import from js', async () => {
expect(await getText('.asset-import')).toMatch(
isBuild
? // hashed in production
/\/assets\/testAssets\.([\w\d]+)\.png$/
: // only resolved to absolute in dev
'/testAssets.png'
)
})

test('env variables', async () => {
expect(await getText('.dev')).toMatch(`__DEV__: ${!isBuild}`)
expect(await getText('.node_env')).toMatch(
Expand Down

0 comments on commit 9061e44

Please sign in to comment.