UNPKG

fuse-box

Version:

Fuse-Box a bundler that does it right

302 lines (248 loc) • 10.9 kB
import { File } from "../../core/File"; import { WorkFlowContext, Plugin } from "../../core/WorkflowContext"; import { CSSPluginClass } from "../stylesheet/CSSplugin"; import { Concat, hashString, ensurePublicExtension } from "../../Utils"; import { VueBlockFile } from './VueBlockFile'; import { VueTemplateFile } from './VueTemplateFile'; import { VueStyleFile } from './VueStyleFile'; import { VueScriptFile } from './VueScriptFile'; import * as path from "path"; import * as fs from "fs"; import { each } from "realm-utils"; export interface IVueComponentPluginOptions { script?: Plugin[], template?: Plugin[], style?: Plugin[] } export class VueComponentClass implements Plugin { public dependencies: ["process", "fusebox-hot-reload"]; public test: RegExp = /\.vue$/ public options: IVueComponentPluginOptions; public hasProcessedVueFile = false; constructor(options: IVueComponentPluginOptions) { this.options = Object.assign({}, { script: [], template: [], style: [] }, options); this.options.script = Array.isArray(this.options.script) ? this.options.script : [this.options.script]; this.options.template = Array.isArray(this.options.template) ? this.options.template : [this.options.template]; this.options.style = Array.isArray(this.options.style) ? this.options.style : [this.options.style]; } public init(context: WorkFlowContext) { context.allowExtension(".vue"); } private getDefaultExtension(block: any) { switch (block.type) { case 'template': return 'html'; case 'script': return 'js'; case 'style': return 'css'; } } private createVirtualFile(file: File, block: any, scopeId: string, pluginChain: Plugin[]): VueBlockFile { let extension = block.lang || this.getDefaultExtension(block); let src = `./${block.type}.${extension}`; if (block.src) { let srcExtension = path.extname(block.src) || ''; if (srcExtension.indexOf('.') > -1) { srcExtension = srcExtension.substr(1); extension = srcExtension; src = block.src; } else { extension = (block.lang) ? `${block.lang}` : '' || this.getDefaultExtension(block); src = `${block.src}.${extension}`; } } file.context.allowExtension(`.${extension}`); const fileInfo = file.collection.pm.resolve(src, file.info.absDir); switch (block.type) { case 'script': return new VueScriptFile(file, fileInfo, block, scopeId, pluginChain); case 'template': return new VueTemplateFile(file, fileInfo, block, scopeId, pluginChain); case 'style': return new VueStyleFile(file, fileInfo, block, scopeId, pluginChain); } } private addToCacheObject(cacheItem: any, path: string, contents: string, sourceMap: any, file: VueBlockFile) { cacheItem[path] = { contents, sourceMap }; cacheItem.override = (file.hasExtensionOverride) ? file.info.absPath : ''; } private isFileInCacheData (block: any, override: any, path: string) { return (block[path] || (override && override.indexOf(path) > -1)); } public bundleEnd(context: WorkFlowContext) { if (context.useCache && this.hasProcessedVueFile) { context.source.addContent(` var process = FuseBox.import('process'); if (process.env.NODE_ENV !== "production") { var api = FuseBox.import('vue-hot-reload-api'); var Vue = FuseBox.import('vue'); api.install(Vue); FuseBox.addPlugin({ hmrUpdate: function (data) { var componentWildcardPath = '~/' + data.path.substr(0, data.path.lastIndexOf('/') + 1) + '*.vue'; var isComponentStyling = (data.type === "css" && !!FuseBox.import(componentWildcardPath)); if (data.type === "js" && /.vue$/.test(data.path) || isComponentStyling) { var fusePath = '~/' + data.path; FuseBox.flush(); FuseBox.flush(function (file) { return file === data.path; }); FuseBox.dynamic(data.path, data.content); if (!isComponentStyling) { var component = FuseBox.import(fusePath).default; api.reload(component._vueModuleId||component.options._vueModuleId, component); } return true; } } }); } `); } } public async transform(file: File) { this.hasProcessedVueFile = true; const vueCompiler = require("vue-template-compiler"); const bundle = file.context.bundle let cacheValid = false; if (file.context.useCache && file.loadFromCache()) { const data = file.cacheData; cacheValid = true; if (bundle && bundle.lastChangedFile) { const lastChangedFusePath = ensurePublicExtension(bundle.lastChangedFile); if (this.isFileInCacheData(data.template, data.template.override, lastChangedFusePath) || this.isFileInCacheData(data.script, data.script.override, lastChangedFusePath) || this.isFileInCacheData(data.styles, data.styles.override, lastChangedFusePath)) { cacheValid = false; } } } if (!cacheValid) { file.isLoaded = false; file.cached = false; file.analysis.skipAnalysis = false; } const concat = new Concat(true, "", "\n"); file.loadContents(); const cache = { template : {}, script : {}, styles: {} }; const component = vueCompiler.parseComponent(fs.readFileSync(file.info.absPath).toString()); const hasScopedStyles = component.styles && !!component.styles.find((style) => style.scoped); const moduleId = `data-v-${hashString(file.info.absPath)}`; const scopeId = hasScopedStyles ? moduleId : null; concat.add(null, `var _options = { _vueModuleId: '${moduleId}'}`); if (hasScopedStyles) { concat.add(null, `Object.assign(_options, {_scopeId: '${scopeId}'})`); } if (component.template) { const templateFile = this.createVirtualFile(file, component.template, scopeId, this.options.template); templateFile.setPluginChain(component.template, this.options.template); if (cacheValid) { const templateCacheData = file.cacheData.template[templateFile.info.fuseBoxPath]; this.addToCacheObject(cache.template, templateFile.info.fuseBoxPath, templateCacheData.contents, templateCacheData.sourceMap, templateFile); } else { await templateFile.process(); this.addToCacheObject(cache.template, templateFile.info.fuseBoxPath, templateFile.contents, templateFile.sourceMap, templateFile); concat.add(null, templateFile.contents); } } if (component.script) { const scriptFile = this.createVirtualFile(file, component.script, scopeId, this.options.script); scriptFile.setPluginChain(component.script, this.options.script); if (cacheValid) { const scriptCacheData = file.cacheData.script[scriptFile.info.fuseBoxPath]; scriptFile.isLoaded = true; scriptFile.contents = scriptCacheData.contents; scriptFile.sourceMap = scriptCacheData.sourceMap; this.addToCacheObject(cache.script, scriptFile.info.fuseBoxPath, scriptCacheData.contents, scriptCacheData.sourceMap, scriptFile); } else { await scriptFile.process(); this.addToCacheObject(cache.script, scriptFile.info.fuseBoxPath, scriptFile.contents, scriptFile.sourceMap, scriptFile); concat.add(null, scriptFile.contents, scriptFile.sourceMap); concat.add(null, `Object.assign(exports.default.options||exports.default, _options)`); } } else { if (!cacheValid) { concat.add(null, "exports.default = {}"); concat.add(null, `Object.assign(exports.default.options||exports.default, _options)`); } } if (component.styles && component.styles.length > 0) { file.addStringDependency("fuse-box-css"); const styleFiles = await each(component.styles, (styleBlock) => { const styleFile = this.createVirtualFile(file, styleBlock, scopeId, this.options.style) as VueStyleFile; styleFile.setPluginChain(styleBlock, this.options.style); if (cacheValid) { const CSSPlugin = this.options.style.find((plugin) => plugin instanceof CSSPluginClass); styleFile.isLoaded = true; styleFile.contents = file.cacheData.styles[styleFile.info.fuseBoxPath].contents; styleFile.sourceMap = file.cacheData.styles[styleFile.info.fuseBoxPath].sourceMap; cache.styles[styleFile.info.fuseBoxPath] = { contents: styleFile.contents, sourceMap: styleFile.sourceMap }; styleFile.fixSourceMapName(); return (CSSPlugin.transform(styleFile) || Promise.resolve()).then(() => styleFile); } else { return styleFile.process().then(() => styleFile).then(() => { this.addToCacheObject(cache.styles, styleFile.info.fuseBoxPath, styleFile.contents, styleFile.sourceMap, styleFile); if (styleFile.cssDependencies) { styleFile.cssDependencies.forEach((path) => { cache.styles[path] = 1; }) } return styleFile; }); } }); await each(styleFiles, (styleFile) => { if (styleFile.alternativeContent) { concat.add(null, styleFile.alternativeContent); } else { // TODO: Do we need this anymore? Everything seems to work without? concat.add(null, `require('fuse-box-css')('${styleFile.info.fuseBoxPath}', ${JSON.stringify(styleFile.contents)})`, styleFile.sourceMap); } }); } if (file.context.useCache) { concat.add(null, ` var process = FuseBox.import('process'); if (process.env.NODE_ENV !== "production") { var api = require('vue-hot-reload-api'); process.env.vueHMR = process.env.vueHMR || {}; if (!process.env.vueHMR['${moduleId}']) { process.env.vueHMR['${moduleId}'] = true; api.createRecord('${moduleId}', module.exports.default); } } `); file.addStringDependency("vue-hot-reload-api"); } file.addStringDependency('vue'); if (!cacheValid) { file.contents = concat.content.toString(); file.sourceMap = concat.sourceMap.toString(); file.analysis.parseUsingAcorn(); file.analysis.analyze(); } if (file.context.useCache && !cacheValid) { file.setCacheData(cache); file.context.cache.writeStaticCache(file, file.sourceMap); file.context.emitJavascriptHotReload(file); } } } export const VueComponentPlugin = (options: any = {}) => { return new VueComponentClass(options); };