Skip to content

Commit

Permalink
feat: new manifest format
Browse files Browse the repository at this point in the history
BREAKING CHANGE: the build manifest format has changed. See
https://vitejs.dev/guide/backend-integration.html for more details.
  • Loading branch information
yyx990803 committed Jan 27, 2021
1 parent ce71d71 commit 51bc1ec
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 48 deletions.
36 changes: 33 additions & 3 deletions docs/guide/backend-integration.md
Expand Up @@ -8,6 +8,7 @@ If you want to serve the HTML using a traditional backend (e.g. Rails, Laravel)
// vite.config.js
export default {
build: {
// generate manifest.json in outDir
manifest: true,
rollupOptions: {
// overwrite default .html entry
Expand All @@ -34,10 +35,39 @@ If you want to serve the HTML using a traditional backend (e.g. Rails, Laravel)

Also make sure the server is configured to serve static assets in the Vite working directory, otherwise assets such as images won't be loaded properly.

3. For production: after running `vite build`, a `manifest.json` file will be generated alongside other asset files. You can use this file to render links with hashed filenames (note: the syntax here is for explanation only, substitute with your server templating language):
3. For production: after running `vite build`, a `manifest.json` file will be generated alongside other asset files. An example manifest file looks like this:

```json
{
"main.js": {
"file": "assets/main.4889e940.js",
"src": "main.js",
"isEntry": true,
"dynamicImports": ["views/foo.js"],
"css": "assets/main.b82dbe22.css",
"assets": ["assets/asset.0ab0f9cd.png"]
},
"views/foo.js": {
"file": "assets/foo.869aea0d.js",
"src": "views/foo.js",
"isDynamicEntry": true,
"imports": ["_shared.83069a53.js"]
},
"_shared.83069a53.js": {
"file": "assets/shared.83069a53.js"
}
}
```

- The manifest has a `Record<name, chunk>` structure
- For entry or dynamic entry chunks, the key is the relative src path from project root.
- For non entry chunks, the key is the base name of the generated file prefixed with `_`.
- Chunks will contain information on its static and dynamic imports (both are keys that maps to the corresponding chunk in the manifest), and also its corresponding CSS and asset files (if any).

You can use this file to render links or preload directives with hashed filenames (note: the syntax here is for explanation only, substitute with your server templating language):

```html
<!-- if production -->
<link rel="stylesheet" href="/assets/{{ manifest['index.css'].file }}" />
<script type="module" src="/assets/{{ manifest['index.js'].file }}"></script>
<link rel="stylesheet" href="/assets/{{ manifest['main.js'].css }}" />
<script type="module" src="/assets/{{ manifest['main.js'].file }}"></script>
```
8 changes: 6 additions & 2 deletions packages/vite/src/node/build.ts
Expand Up @@ -138,8 +138,12 @@ export interface BuildOptions {
*
* ```json
* {
* "main.js": { "file": "main.68fe3fad.js" },
* "style.css": { "file": "style.e6b63442.css" }
* "main.js": {
* "file": "main.68fe3fad.js",
* "css": "main.e6b63442.css",
* "imports": [...],
* "dynamicImports": [...]
* }
* }
* ```
* @default false
Expand Down
16 changes: 12 additions & 4 deletions packages/vite/src/node/plugins/asset.ts
Expand Up @@ -6,7 +6,7 @@ import { Plugin } from '../plugin'
import { ResolvedConfig } from '../config'
import { cleanUrl } from '../utils'
import { FS_PREFIX } from '../constants'
import { PluginContext } from 'rollup'
import { PluginContext, RenderedChunk } from 'rollup'
import MagicString from 'magic-string'

export const assetUrlRE = /__VITE_ASSET__([a-z\d]{8})__(?:(.*?)__)?/g
Expand All @@ -15,6 +15,8 @@ export const assetUrlRE = /__VITE_ASSET__([a-z\d]{8})__(?:(.*?)__)?/g
// a different regex
const assetUrlQuotedRE = /"__VITE_ASSET__([a-z\d]{8})__(?:(.*?)__)?"/g

export const chunkToEmittedAssetsMap = new WeakMap<RenderedChunk, Set<string>>()

/**
* Also supports loading plain strings with import text from './foo.txt?raw'
*/
Expand Down Expand Up @@ -52,14 +54,20 @@ export function assetPlugin(config: ResolvedConfig): Plugin {
return `export default ${JSON.stringify(url)}`
},

renderChunk(code) {
renderChunk(code, chunk) {
let emitted = chunkToEmittedAssetsMap.get(chunk)
let match
let s
while ((match = assetUrlQuotedRE.exec(code))) {
s = s || (s = new MagicString(code))
const [full, fileHandle, postfix = ''] = match
const outputFilepath =
config.base + this.getFileName(fileHandle) + postfix
const file = this.getFileName(fileHandle)
if (!emitted) {
emitted = new Set()
chunkToEmittedAssetsMap.set(chunk, emitted)
}
emitted.add(file)
const outputFilepath = config.base + file + postfix
s.overwrite(
match.index,
match.index + full.length,
Expand Down
102 changes: 63 additions & 39 deletions packages/vite/src/node/plugins/manifest.ts
@@ -1,12 +1,18 @@
import path from 'path'
import { OutputChunk } from 'rollup'
import { ResolvedConfig } from '..'
import { Plugin } from '../plugin'
import { chunkToEmittedCssFileMap } from './css'
import { chunkToEmittedAssetsMap } from './asset'
import { normalizePath } from '../utils'

type Manifest = Record<string, ManifestEntry>
type Manifest = Record<string, ManifestChunk>

interface ManifestEntry {
interface ManifestChunk {
src?: string
file: string
facadeModuleId?: string
css?: string
assets?: string[]
isEntry?: boolean
isDynamicEntry?: boolean
imports?: string[]
Expand All @@ -21,45 +27,63 @@ export function manifestPlugin(config: ResolvedConfig): Plugin {
return {
name: 'vite:manifest',
generateBundle({ format }, bundle) {
function getChunkName(chunk: OutputChunk) {
if (chunk.facadeModuleId) {
let name = normalizePath(
path.relative(config.root, chunk.facadeModuleId)
)
if (format === 'system' && !chunk.name.includes('-legacy')) {
const ext = path.extname(name)
name = name.slice(0, -ext.length) + `-legacy` + ext
}
return name
} else {
return `_` + path.basename(chunk.fileName)
}
}

function createChunk(chunk: OutputChunk): ManifestChunk {
const manifestChunk: ManifestChunk = {
file: chunk.fileName
}

if (chunk.facadeModuleId) {
manifestChunk.src = getChunkName(chunk)
}
if (chunk.isEntry) {
manifestChunk.isEntry = true
}
if (chunk.isDynamicEntry) {
manifestChunk.isDynamicEntry = true
}

if (chunk.imports.length) {
manifestChunk.imports = chunk.imports.map((file) =>
getChunkName(bundle[file] as OutputChunk)
)
}

if (chunk.dynamicImports.length) {
manifestChunk.dynamicImports = chunk.dynamicImports.map((file) =>
getChunkName(bundle[file] as OutputChunk)
)
}

const cssFile = chunkToEmittedCssFileMap.get(chunk)
if (cssFile) {
manifestChunk.css = cssFile
}

const assets = chunkToEmittedAssetsMap.get(chunk)
if (assets) [(manifestChunk.assets = [...assets])]

return manifestChunk
}

for (const file in bundle) {
const chunk = bundle[file]
if (chunk.type === 'chunk') {
if (chunk.isEntry || chunk.isDynamicEntry) {
let name =
format === 'system' && !chunk.name.includes('-legacy')
? chunk.name + '-legacy'
: chunk.name
let dedupeIndex = 0
while (name + '.js' in manifest) {
name = `${name}-${++dedupeIndex}`
}
const entry: ManifestEntry = {
isEntry: chunk.isEntry,
isDynamicEntry: chunk.isDynamicEntry,
file: chunk.fileName,
imports: chunk.imports,
dynamicImports: chunk.dynamicImports
}

if (
chunk.facadeModuleId &&
chunk.facadeModuleId.startsWith(config.root)
) {
entry.facadeModuleId = chunk.facadeModuleId.slice(
config.root.length + 1
)
}

manifest[name + '.js'] = entry
}
} else if (chunk.name) {
const ext = path.extname(chunk.name) || ''
let name = chunk.name.slice(0, -ext.length)
let dedupeIndex = 0
while (name + ext in manifest) {
name = `${name}-${++dedupeIndex}`
}
manifest[name + ext] = { file: chunk.fileName }
manifest[getChunkName(chunk)] = createChunk(chunk)
}
}

Expand Down

0 comments on commit 51bc1ec

Please sign in to comment.