UNPKG

parcel-transformer-vue2

Version:
494 lines (479 loc) 13.5 kB
// @flow strict-local import type { TransformerResult } from '@parcel/types'; import { Transformer } from '@parcel/plugin'; import nullthrows from 'nullthrows'; import { hashObject } from '@parcel/utils'; import ThrowableDiagnostic, { type Diagnostic, escapeMarkdown, md, } from '@parcel/diagnostic'; import SourceMap from '@parcel/source-map'; import semver from 'semver'; import { basename, extname, relative, dirname } from 'path'; // $FlowFixMe import * as compiler from './compiler'; // $FlowFixMe import consolidate from 'consolidate'; const MODULE_BY_NAME_RE = /\.module\./; // TODO: Use language-specific config files during preprocessing export default (new Transformer({ async loadConfig({ config }) { let conf = await config.getConfig( ['.vuerc', '.vuerc.json', '.vuerc.js', 'vue.config.js'], { packageKey: 'vue' }, ); let contents = {}; if (conf) { config.invalidateOnStartup(); contents = conf.contents; if (typeof contents !== 'object') { // TODO: codeframe throw new ThrowableDiagnostic({ diagnostic: { message: 'Vue config should be an object.', origin: '@parcel/transformer-vue', }, }); } } return { customBlocks: contents.customBlocks || {}, filePath: conf && conf.filePath, }; }, canReuseAST({ ast }) { return ast.type === 'vue' && semver.satisfies(ast.version, '^2.6.14'); }, async parse({ asset }) { // TODO: This parses the vue component multiple times. Fix? let code = await asset.getCode(); let parsed = compiler.parse(code, { sourceMap: true, filename: asset.filePath, }); if (parsed.errors.length) { throw new ThrowableDiagnostic({ diagnostic: parsed.errors.map(err => { return createDiagnostic(err, asset.filePath); }), }); } return { type: 'vue', version: '2.6.14', program: parsed.descriptor, }; }, async transform({ asset, options, resolve, config }) { let id = hashObject({ filePath: asset.filePath, }).slice(-6); let scopeId = 'data-v-' + id; let hmrId = id + '-hmr'; let basePath = basename(asset.filePath); let { template, script, styles, customBlocks } = nullthrows( await asset.getAST(), ).program; if (asset.pipeline != null) { return processPipeline({ asset, template, script, styles, customBlocks, config, basePath, options, resolve, id, hmrId, }); } return [ { type: 'js', uniqueKey: asset.id + '-glue', content: ` let script; let initialize = () => { script = ${script != null ? `require('script:./${basePath}'); if (script.__esModule) script = script.default` : '{}' }; ${template != null ? `script.render = require('template:./${basePath}').render; script.staticRenderFns = require('template:./${basePath}').staticRenderFns; script._scopeId = "${scopeId}"; ` : '' } ${styles.length !== 0 ? `script.__cssModules = require('style:./${basePath}').default;` : '' } ${customBlocks != null ? `require('custom:./${basePath}').default(script);` : '' } script.__scopeId = '${scopeId}'; script.__file = ${JSON.stringify( options.mode === 'production' ? basePath : asset.filePath, )}; }; initialize(); ${options.hmrOptions ? `if (module.hot) { script.__hmrId = '${hmrId}'; if(!window.__VUE_HMR_RUNTIME__) { const api = require('vue-hot-reload-api') const Vue = require('vue') api.install(Vue) window.__VUE_HMR_RUNTIME__ = api; } window.__VUE_HMR_RUNTIME__.createRecord('${hmrId}', script); module.hot.accept(() => { setTimeout(() => { initialize(); // window.location.reload(); window.__VUE_HMR_RUNTIME__.reload('${hmrId}', script); }, 0); }); }` : '' } export default script;`, }, ]; }, }): Transformer); function createDiagnostic(err, filePath) { if (typeof err === 'string') { return { message: err, origin: '@parcel/transformer-vue', filePath, }; } // TODO: codeframe let diagnostic: Diagnostic = { message: escapeMarkdown(err.message), origin: '@parcel/transformer-vue', name: err.name, stack: err.stack, }; if (err.loc) { diagnostic.codeFrames = [ { codeHighlights: [ { start: { line: err.loc.start.line + err.loc.start.offset, column: err.loc.start.column, }, end: { line: err.loc.end.line + err.loc.end.offset, column: err.loc.end.column, }, }, ], }, ]; } return diagnostic; } async function processPipeline({ asset, template, script, styles, customBlocks, config, basePath, options, resolve, id, hmrId, }) { switch (asset.pipeline) { case 'template': { let isFunctional = template.functional; if (template.src) { template.content = ( await options.inputFS.readFile( await resolve(asset.filePath, template.src), ) ).toString(); template.lang = extname(template.src).slice(1); } let content = template.content; if (template.lang && !['htm', 'html'].includes(template.lang)) { let preprocessor = consolidate[template.lang]; if (!preprocessor) { // TODO: codeframe throw new ThrowableDiagnostic({ diagnostic: { message: md`Unknown template language: "${template.lang}"`, origin: '@parcel/transformer-vue', }, }); } content = await preprocessor.render(content, {}); } let templateComp = compiler.compileTemplate({ filename: asset.filePath, source: content, inMap: template.src ? undefined : template.map, scoped: styles.some(style => style.scoped), isFunctional, id, }); if (templateComp.errors.length) { throw new ThrowableDiagnostic({ diagnostic: templateComp.errors.map(err => { return createDiagnostic(err, asset.filePath); }), }); } let templateAsset: TransformerResult = { type: 'js', uniqueKey: asset.id + '-template', ...(!template.src && asset.env.sourceMap && { map: createMap(templateComp.map, options.projectRoot), }), content: templateComp.code + '\nexports.render = render;\nexports.staticRenderFns = staticRenderFns;\n' + ` ${options.hmrOptions ? `if (module.hot) { module.hot.accept(() => { // window.location.reload(); if(!window.__VUE_HMR_RUNTIME__) { const api = require('vue-hot-reload-api') const Vue = require('vue') api.install(Vue) window.__VUE_HMR_RUNTIME__ = api; } window.__VUE_HMR_RUNTIME__.rerender('${hmrId}', exports); }) }` : '' }`, }; return [templateAsset]; } case 'script': { if (script.src) { script.content = ( await options.inputFS.readFile( await resolve(asset.filePath, script.src), ) ).toString(); script.lang = extname(script.src).slice(1); } let type; switch (script.lang || 'js') { case 'javascript': case 'js': type = 'js'; break; case 'jsx': type = 'jsx'; break; case 'typescript': case 'ts': type = 'ts'; break; case 'tsx': type = 'tsx'; break; case 'coffeescript': case 'coffee': type = 'coffee'; break; default: // TODO: codeframe throw new ThrowableDiagnostic({ diagnostic: { message: md`Unknown script language: "${script.lang}"`, origin: '@parcel/transformer-vue', }, }); } let scriptAsset = { type, uniqueKey: asset.id + '-script', content: script.content, ...(!script.src && asset.env.sourceMap && { map: createMap(script.map, options.projectRoot), }), }; return [scriptAsset]; } case 'style': { let cssModules = {}; let assets = await Promise.all( styles.map(async (style, i) => { if (style.src) { style.content = ( await options.inputFS.readFile( await resolve(asset.filePath, style.src), ) ).toString(); if (!style.module) { style.module = MODULE_BY_NAME_RE.test(style.src); } style.lang = extname(style.src).slice(1); } switch (style.lang) { case 'less': case 'stylus': case 'styl': case 'scss': case 'sass': case 'css': case undefined: break; default: // TODO: codeframe throw new ThrowableDiagnostic({ diagnostic: { message: md`Unknown style language: "${style.lang}"`, origin: '@parcel/transformer-vue', }, }); } let styleComp = await compiler.compileStyle({ filename: asset.filePath, source: style.content, modules: style.module, preprocessLang: style.lang || 'css', scoped: style.scoped ? true : false, map: style.src ? undefined : style.map, id: 'data-v-' + id, }); if (styleComp.errors.length) { throw new ThrowableDiagnostic({ diagnostic: styleComp.errors.map(err => { return createDiagnostic(err, asset.filePath); }), }); } let styleAsset = { type: 'css', content: styleComp.code, sideEffects: true, ...(!style.src && asset.env.sourceMap && { map: createMap(style.map, options.projectRoot), }), uniqueKey: asset.id + '-style' + i, }; if (styleComp.modules) { if (typeof style.module === 'boolean') style.module = '$style'; cssModules[style.module] = { ...cssModules[style.module], ...styleComp.modules, }; } return styleAsset; }), ); if (Object.keys(cssModules).length !== 0) { assets.push({ type: 'js', uniqueKey: asset.id + '-cssModules', content: ` import {render} from 'template:./${basePath}'; let cssModules = ${JSON.stringify(cssModules)}; ${options.hmrOptions ? `if (module.hot) { module.hot.accept(() => { // window.location.reload(); if(!window.__VUE_HMR_RUNTIME__) { const api = require('vue-hot-reload-api') const Vue = require('vue') api.install(Vue) window.__VUE_HMR_RUNTIME__ = api; } window.__VUE_HMR_RUNTIME__.rerender('${hmrId}', render); }); };` : '' } export default cssModules;`, }); } return assets; } case 'custom': { let toCall = []; // To satisfy flow if (!config) return []; let types = new Set(); for (let block of customBlocks) { let { type, src, content, attrs } = block; if (!config.customBlocks[type]) { // TODO: codeframe throw new ThrowableDiagnostic({ diagnostic: { message: md`No preprocessor found for block type ${type}`, origin: '@parcel/transformer-vue', }, }); } if (src) { content = ( await options.inputFS.readFile(await resolve(asset.filePath, src)) ).toString(); } toCall.push([type, content, attrs]); types.add(type); } return [ { type: 'js', uniqueKey: asset.id + '-custom', content: ` let NOOP = () => {}; ${( await Promise.all( [...types].map( async type => `import p${type} from './${relative( dirname(asset.filePath), await resolve(nullthrows(config.filePath), config.customBlocks[type]), )}'; if (typeof p${type} !== 'function') { p${type} = NOOP; }`, ), ) ).join('\n')} export default script => { ${toCall .map( ([type, content, attrs]) => ` p${type}(script, ${JSON.stringify(content)}, ${JSON.stringify( attrs, )});`, ) .join('\n')} }`, }, ]; } default: { return []; } } } function createMap(rawMap, projectRoot: string) { let newMap = new SourceMap(projectRoot); if(rawMap) newMap.addVLQMap(rawMap); return newMap; }