UNPKG

vusion-api

Version:
396 lines (327 loc) 13.2 kB
/// <reference path="../../types/line-reader.d.ts" /> import * as fs from 'fs-extra'; import * as path from 'path'; import * as shell from 'shelljs'; import * as lineReader from 'line-reader'; import * as pluralize from 'pluralize'; import { kebab2Camel, Camel2kebab } from '../utils'; import FSEntry from './FSEntry'; import TemplateHandler from './TemplateHandler'; import ScriptHandler from './ScriptHandler'; import StyleHandler from './StyleHandler'; import traverse from '@babel/traverse'; const fetchPartialContent = (content: string, tag: string) => { const reg = new RegExp(`<${tag}.*?>([\\s\\S]+)<\\/${tag}>`); const m = content.match(reg); return m ? m[1].trim() + '\n' : ''; }; export enum VueFileExtendMode { style = 'style', script = 'script', template = 'template', all = 'all', }; export default class VueFile extends FSEntry { tagName: string; // 中划线名称 componentName: string; // 驼峰名称 alias: string; // 子组件 // 为`undefined`表示未打开过,为数组表示已经打开。 parent: VueFile; children: VueFile[]; isChild: boolean; // 单文件内容 // 为`undefined`表示未打开过 content: string; template: string; script: string; style: string; sample: string; templateHandler: TemplateHandler; // 为`undefined`表示还未解析 scriptHandler: ScriptHandler; // 为`undefined`表示还未解析 styleHandler: StyleHandler; // 为`undefined`表示还未解析 constructor(fullPath: string) { super(fullPath, undefined); this.isVue = true; this.tagName = VueFile.resolveTagName(fullPath); this.componentName = kebab2Camel(this.tagName); } /** * 提前检测 VueFile 文件类型,以及子组件等 * 需要异步,否则可能会比较慢 */ async preOpen() { if (!fs.existsSync(this.fullPath)) return; const stats = fs.statSync(this.fullPath); this.isDirectory = stats.isDirectory(); if (this.isDirectory) this.children = await this.loadDirectory(); this.alias = await this.readTitleInReadme(); } /** * 尝试读取 README.md 的标题行 * 在前 10 行中查找 */ async readTitleInReadme(): Promise<string> { const readmePath = path.join(this.fullPath, 'README.md'); if (!fs.existsSync(readmePath)) return; const titleRE = /^#\s+\w+\s*(.*?)$/; let count = 0; let title: string; return new Promise((resolve, reject) => { lineReader.eachLine(readmePath, { encoding: 'utf8' }, (line, last) => { line = line.trim(); const cap = titleRE.exec(line); if (cap) { title = cap[1]; return false; } else { count++; if (count > 10) return false; } }, (err) => { err? reject(err) : resolve(title); }); }); } async loadDirectory() { if (!fs.existsSync(this.fullPath)) throw new Error(`Cannot find: ${this.fullPath}`); const children: Array<VueFile> = []; const fileNames = await fs.readdir(this.fullPath); fileNames.forEach((name) => { if (!name.endsWith('.vue')) return; const fullPath = path.join(this.fullPath, name); let vueFile; if (this.isWatched) vueFile = VueFile.fetch(fullPath); else vueFile = new VueFile(fullPath); vueFile.parent = this; vueFile.isChild = true; children.push(vueFile); }); return children; } async forceOpen() { this.close(); await this.preOpen(); await this.load(); this.isOpen = true; } close() { this.isDirectory = undefined; this.alias = undefined; this.children = undefined; // 单文件内容 this.content = undefined; this.template = undefined; this.script = undefined; this.style = undefined; this.sample = undefined; this.templateHandler = undefined; this.scriptHandler = undefined; this.styleHandler = undefined; this.isOpen = false; } protected async load() { if (!fs.existsSync(this.fullPath)) throw new Error(`Cannot find: ${this.fullPath}!`); // const stats = fs.statSync(this.fullPath); // this.isDirectory = stats.isDirectory(); if (this.isDirectory) { if (fs.existsSync(path.join(this.fullPath, 'index.js'))) this.script = await fs.readFile(path.join(this.fullPath, 'index.js'), 'utf8'); else throw new Error(`Cannot find 'index.js' in multifile Vue!`); if (fs.existsSync(path.join(this.fullPath, 'index.html'))) this.template = await fs.readFile(path.join(this.fullPath, 'index.html'), 'utf8'); if (fs.existsSync(path.join(this.fullPath, 'module.css'))) this.style = await fs.readFile(path.join(this.fullPath, 'module.css'), 'utf8'); if (fs.existsSync(path.join(this.fullPath, 'sample.vue'))) { const sampleRaw = await fs.readFile(path.join(this.fullPath, 'sample.vue'), 'utf8'); const templateRE = /<template.*?>([\s\S]*?)<\/template>/i; const sample = sampleRaw.match(templateRE); this.sample = sample && sample[1].trim(); } } else { this.content = await fs.readFile(this.fullPath, 'utf8'); this.template = fetchPartialContent(this.content, 'template'); this.script = fetchPartialContent(this.content, 'script'); this.style = fetchPartialContent(this.content, 'style'); } return this; } async save() { this.isSaving = true; if (fs.statSync(this.fullPath).isDirectory() !== this.isDirectory) shell.rm('-rf', this.fullPath); let template = this.template; let script = this.script; let style = this.style; if (this.templateHandler) this.template = template = this.templateHandler.generate(); if (this.scriptHandler) this.script = script = this.scriptHandler.generate(); if (this.styleHandler) this.style = style = this.styleHandler.generate(); let result; if (this.isDirectory) { fs.ensureDirSync(this.fullPath); const promises = []; template && promises.push(fs.writeFile(path.resolve(this.fullPath, 'index.html'), template)); script && promises.push(fs.writeFile(path.resolve(this.fullPath, 'index.js'), script)); style && promises.push(fs.writeFile(path.resolve(this.fullPath, 'module.css'), style)); result = await Promise.all(promises); } else { const contents = []; template && contents.push(`<template>\n${template}</template>`); script && contents.push(`<script>\n${script}</script>`); style && contents.push(`<style module>\n${style}</style>`); result = await fs.writeFile(this.fullPath, contents.join('\n\n') + '\n'); } super.save(); return result; } parseTemplate() { if (this.templateHandler) return; this.templateHandler = new TemplateHandler(this.template); } parseScript() { if (this.scriptHandler) return; this.scriptHandler = new ScriptHandler(this.script); } parseStyle() { if (this.styleHandler) return; this.styleHandler = new StyleHandler(this.style); } checkTransform() { if (!this.isDirectory) return true; // @TODO else { const files = fs.readdirSync(this.fullPath); const normalBlocks = ['index.html', 'index.js', 'module.css']; const extraBlocks: Array<string> = []; files.forEach((file) => { if (!normalBlocks.includes(file)) extraBlocks.push(file); }); return extraBlocks.length ? extraBlocks : true; } } transform() { const isDirectory = this.isDirectory; this.parseScript(); this.parseStyle(); // this.parseTemplate(); function shortenPath(filePath: string) { if (filePath.startsWith('../')) { let newPath = filePath.replace(/^\.\.\//, ''); if (!newPath.startsWith('../')) newPath = './' + newPath; return newPath; } else return filePath; } function lengthenPath(filePath: string) { if (filePath.startsWith('.')) return path.join('../', filePath); else return filePath; } traverse(this.scriptHandler.ast, { ImportDeclaration(nodePath) { if (nodePath.node.source) nodePath.node.source.value = isDirectory ? shortenPath(nodePath.node.source.value) : lengthenPath(nodePath.node.source.value); }, ExportAllDeclaration(nodePath) { if (nodePath.node.source) nodePath.node.source.value = isDirectory ? shortenPath(nodePath.node.source.value) : lengthenPath(nodePath.node.source.value); }, ExportNamedDeclaration(nodePath) { if (nodePath.node.source) nodePath.node.source.value = isDirectory ? shortenPath(nodePath.node.source.value) : lengthenPath(nodePath.node.source.value); }, }); this.styleHandler.ast.walkAtRules((node) => { if (node.name !== 'import') return; const value = node.params.slice(1, -1); node.params = `'${isDirectory ? shortenPath(value) : lengthenPath(value)}'`; }); this.styleHandler.ast.walkDecls((node) => { const re = /url\((['"])(.+?)['"]\)/; const cap = re.exec(node.value); if (cap) { node.value = node.value.replace(re, (m, quote, url) => { url = isDirectory ? shortenPath(url) : lengthenPath(url); return `url(${quote}${url}${quote})`; }); } }); this.isDirectory = !this.isDirectory; } extend(mode: VueFileExtendMode, fullPath: string, fromPath: string) { const vueFile = new VueFile(fullPath); vueFile.isDirectory = true; // JS const tempComponentName = this.componentName.replace(/^[A-Z]/, 'O'); vueFile.script = fromPath.endsWith('.vue') ? `import ${this.componentName === vueFile.componentName ? tempComponentName : this.componentName} from '${fromPath}';` : `import { ${this.componentName}${this.componentName === vueFile.componentName ? ' as ' + tempComponentName : ''} } from '${fromPath}';`; vueFile.script += `\n export const ${vueFile.componentName} = { name: '${vueFile.tagName}', extends: ${this.componentName === vueFile.componentName ? tempComponentName : this.componentName}, }; export default ${vueFile.componentName}; `; if (mode === VueFileExtendMode.style || mode === VueFileExtendMode.all) vueFile.style = `@extend;\n`; if (mode === VueFileExtendMode.template || mode === VueFileExtendMode.all) vueFile.template = this.template; return vueFile; } private static _splitPath(fullPath: string) { const arr = fullPath.split(path.sep); let pos = arr.length - 1; // root Vue 的位置 while(arr[pos] && arr[pos].endsWith('.vue')) pos--; pos++; return { arr, pos }; } /** * 计算根组件所在的目录 * @param fullPath 完整路径 */ static resolveRootVueDir(fullPath: string) { const { arr, pos } = VueFile._splitPath(fullPath); return arr.slice(0, pos).join(path.sep); } static resolveTagName(fullPath: string) { const { arr, pos } = VueFile._splitPath(fullPath); const vueNames = arr.slice(pos); let result: Array<string> = []; vueNames.forEach((vueName) => { const baseName = path.basename(vueName, '.vue'); const arr = baseName.split('-'); if (arr[0].length === 1) // u-navbar result = arr; else if (pluralize(baseName) === result[result.length - 1]) // 如果是前一个的单数形式,u-actions -> action,u-checkboxes -> checkbox result[result.length - 1] = baseName; else result.push(baseName); }); return result.join('-'); } static fetch(fullPath: string) { return super.fetch(fullPath) as VueFile; } }