|
|
@ -1,5 +1,6 @@ |
|
|
|
/* @flow */ |
|
|
|
|
|
|
|
const path = require('path') |
|
|
|
const serialize = require('serialize-javascript') |
|
|
|
|
|
|
|
import TemplateStream from './template-stream' |
|
|
@ -8,10 +9,14 @@ import { createMapper } from './create-async-file-mapper' |
|
|
|
import type { ParsedTemplate } from './parse-template' |
|
|
|
import type { AsyncFileMapper } from './create-async-file-mapper' |
|
|
|
|
|
|
|
const JS_RE = /\.js($|\?)/ |
|
|
|
export const isJS = (file: string): boolean => JS_RE.test(file) |
|
|
|
|
|
|
|
type TemplateRendererOptions = { |
|
|
|
template: string; |
|
|
|
serverManifest?: ServerManifest; |
|
|
|
clientManifest?: ClientManifest; |
|
|
|
shouldPreload?: (file: string, type: string) => boolean; |
|
|
|
}; |
|
|
|
|
|
|
|
export type ServerManifest = { |
|
|
@ -34,6 +39,7 @@ export type ClientManifest = { |
|
|
|
}; |
|
|
|
|
|
|
|
export default class TemplateRenderer { |
|
|
|
options: TemplateRendererOptions; |
|
|
|
template: ParsedTemplate; |
|
|
|
publicPath: string; |
|
|
|
serverManifest: ServerManifest; |
|
|
@ -43,6 +49,7 @@ export default class TemplateRenderer { |
|
|
|
mapFiles: AsyncFileMapper; |
|
|
|
|
|
|
|
constructor (options: TemplateRendererOptions) { |
|
|
|
this.options = options |
|
|
|
this.template = parseTemplate(options.template) |
|
|
|
|
|
|
|
// extra functionality with client manifest
|
|
|
@ -80,7 +87,29 @@ export default class TemplateRenderer { |
|
|
|
const usedAsyncFiles = this.getUsedAsyncFiles(context) |
|
|
|
if (this.preloadFiles || usedAsyncFiles) { |
|
|
|
return (this.preloadFiles || []).concat(usedAsyncFiles || []).map(file => { |
|
|
|
return `<link rel="preload" href="${this.publicPath}/${file}" as="script">` |
|
|
|
let extra = '' |
|
|
|
const withoutQuery = file.replace(/\?.*/, '') |
|
|
|
const ext = path.extname(withoutQuery).slice(1) |
|
|
|
const type = getPreloadType(ext) |
|
|
|
const shouldPreload = this.options.shouldPreload |
|
|
|
// by default, we only preload scripts and fonts
|
|
|
|
if (!shouldPreload && type !== 'script' && type !== 'font') { |
|
|
|
return '' |
|
|
|
} |
|
|
|
// user wants to explicitly control what to preload
|
|
|
|
if (shouldPreload && !shouldPreload(withoutQuery, type)) { |
|
|
|
return '' |
|
|
|
} |
|
|
|
if (type === 'font') { |
|
|
|
extra = ` type="font/${ext}" crossorigin` |
|
|
|
} |
|
|
|
return `<link rel="preload" href="${ |
|
|
|
this.publicPath}/${file |
|
|
|
}"${ |
|
|
|
type !== '' ? ` as="${type}"` : '' |
|
|
|
}${ |
|
|
|
extra |
|
|
|
}>` |
|
|
|
}).join('') |
|
|
|
} else { |
|
|
|
return '' |
|
|
@ -118,7 +147,7 @@ export default class TemplateRenderer { |
|
|
|
const initial = this.clientManifest.initial |
|
|
|
const async = this.getUsedAsyncFiles(context) |
|
|
|
const needed = [initial[0]].concat(async || [], initial.slice(1)) |
|
|
|
return needed.map(file => { |
|
|
|
return needed.filter(isJS).map(file => { |
|
|
|
return `<script src="${this.publicPath}/${file}"></script>` |
|
|
|
}).join('') |
|
|
|
} else { |
|
|
@ -136,7 +165,7 @@ export default class TemplateRenderer { |
|
|
|
if (noCssHash) { |
|
|
|
mapped = mapped.map(file => { |
|
|
|
return noCssHash[file] |
|
|
|
? file.replace(/\.js$/, '.no-css.js') |
|
|
|
? file.replace(JS_RE, '.no-css.js') |
|
|
|
: file |
|
|
|
}) |
|
|
|
} |
|
|
@ -152,3 +181,18 @@ export default class TemplateRenderer { |
|
|
|
return new TemplateStream(this, context || {}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function getPreloadType (ext: string): string { |
|
|
|
if (ext === 'js') { |
|
|
|
return 'script' |
|
|
|
} else if (ext === 'css') { |
|
|
|
return 'style' |
|
|
|
} else if (/jpe?g|png|svg|gif|webp|ico/.test(ext)) { |
|
|
|
return 'image' |
|
|
|
} else if (/woff2?|ttf|otf|eot/.test(ext)) { |
|
|
|
return 'font' |
|
|
|
} else { |
|
|
|
// not exhausting all possbilities here, but above covers common cases
|
|
|
|
return '' |
|
|
|
} |
|
|
|
} |
|
|
|