fuse-box
Version:
Fuse-Box a bundler that does it right
302 lines (248 loc) • 10.9 kB
text/typescript
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);
};