vusion-api
Version:
Vusion Node.js API
1,038 lines (902 loc) • 38.8 kB
text/typescript
import * as path from 'path';
import * as fs from 'fs-extra';
import * as babel from '@babel/core';
import * as vfs from '../fs';
import * as vms from '../ms';
import * as compiler from 'vue-template-compiler';
import * as utils from '../utils';
export * from './nuims';
export async function addLayout(fullPath: string, nodePath: string, type: string) {
const vueFile = new vfs.VueFile(fullPath);
await vueFile.open();
vueFile.parseTemplate();
let tplPath = path.resolve(__dirname, `../../snippets/${type}.vue`);
let tpl = await fs.readFile(tplPath, 'utf8');
tpl = tpl.replace(/^<template>\s+/, '').replace(/\s+<\/template>$/, '') + '\n';
const rootEl = vueFile.templateHandler.ast;
const selectedEl = vueFile.templateHandler.findByNodePath(nodePath, rootEl) as compiler.ASTElement;
selectedEl.children.push(compiler.compile(tpl).ast);
await vueFile.save();
}
/**
* 添加页面时初始化布局
* @param fullPath Vue 文件路径
* @param type 布局类型
*/
export async function initLayout(fullPath: string, type: string) {
const vueFile = new vfs.VueFile(fullPath);
await vueFile.open();
let tplPath = path.resolve(__dirname, `../../snippets/${type}.vue`);
let tpl = await fs.readFile(tplPath, 'utf8');
tpl = tpl.replace(/^<template>\s*/, '').replace(/\s*<\/template>\s*$/, '') + '\n';
if (type.startsWith('grid-'))
tpl = `<u-grid-layout>${tpl}</u-grid-layout>\n`;
vueFile.template = tpl;
await vueFile.save();
}
/**
* 添加页面时初始化布局
* @param fullPath Vue 文件路径
* @param type 布局类型
*/
async function initViewLayout(fullPath: string, type: string) {
const vueFile = new vfs.VueFile(fullPath);
await vueFile.open();
vueFile.parseTemplate();
let tplPath = path.resolve(__dirname, `../../snippets/${type}.vue`);
let tpl = await fs.readFile(tplPath, 'utf8');
tpl = tpl.replace(/^<template>\s+/, '').replace(/\s+<\/template>$/, '') + '\n';
const rootEl = vueFile.templateHandler.ast;
rootEl.children.unshift(compiler.compile(tpl).ast);
await vueFile.save();
}
export async function addCode(fullPath: string, nodePath: string, tpl: string) {
const vueFile = new vfs.VueFile(fullPath);
await vueFile.open();
vueFile.parseTemplate();
tpl = tpl.replace(/^<template>\s+/, '').replace(/\s+<\/template>$/, '');
const rootEl = vueFile.templateHandler.ast;
const selectedEl = vueFile.templateHandler.findByNodePath(nodePath, rootEl) as compiler.ASTElement;
selectedEl.children.push(compiler.compile(tpl).ast);
await vueFile.save();
}
export async function openFile(fullPath: string) {
return fs.readFile(fullPath, 'utf8');
}
export async function saveFile(fullPath: string, content: string) {
return fs.writeFile(fullPath, content);
}
export async function ensureHotReload(fullPath: string) {
if (!fs.existsSync(fullPath))
return;
return fs.writeFile(fullPath, await fs.readFile(fullPath, 'utf8'))
}
// export async function mergeBlock(fullPath: string, type: string) {
// const vueFile = new vfs.VueFile(fullPath);
// await vueFile.open();
// vueFile.parseTemplate();
// let tplPath = path.resolve(__dirname, `../../snippets/${type}.vue`);
// let tpl = await fs.readFile(tplPath, 'utf8');
// tpl = tpl.replace(/^<template>\s+/, '').replace(/\s+<\/template>$/, '');
// const rootEl = vueFile.templateHandler.ast;
// rootEl.children.push(compiler.compile(tpl).ast);
// await vueFile.save();
// }
export interface ViewInfo {
fullPath: string,
viewType: vfs.ViewType,
// viewsPath?: string,
routePath?: string,
// vueFilePath?: string,
}
export interface AddParams {
name: string,
title: string,
ext?: string,
layout?: string,
crumb?: string,
first?: boolean,
}
function hasNewParams(params: AddParams) {
return params.hasOwnProperty('title') || params.hasOwnProperty('crumb') || params.hasOwnProperty('first');
}
async function initView(viewInfo: ViewInfo) {
const isDirectory = !(viewInfo.viewType === vfs.ViewType.vue || viewInfo.viewType === vfs.ViewType.md);
return new vfs.View(viewInfo.fullPath, viewInfo.viewType, isDirectory, viewInfo.routePath);
}
interface MetaData {
getMetaData(viewInfo: vfs.View, baseViewInfo?: ViewInfo | vfs.View): void,
saveMetaData(viewInfo: vfs.View, params: AddParams, baseViewInfo?: ViewInfo | vfs.View): void,
}
class EntryMetaData implements MetaData {
async getMetaData(viewInfo: vfs.View) {
const fullPath = viewInfo.fullPath;
// @TODO?
const index = fullPath.indexOf('src');
const pagesJSONPath = path.join(fullPath.slice(0, index), 'pages.json');
const data = {
title: '',
}
if (!fs.existsSync(pagesJSONPath))
throw new Error('Cannot find pagesJSONPath');
const pagesJSON = JSON.parse(await fs.readFile(pagesJSONPath, 'utf8'));
data.title = pagesJSON[viewInfo.baseName] && pagesJSON[viewInfo.baseName].title;
return data;
}
async saveMetaData(viewInfo: vfs.View, params: AddParams) {
const fullPath = viewInfo.fullPath;
const index = fullPath.indexOf('src');
const pagesJSONPath = path.join(fullPath.slice(0, index), 'pages.json');
if (!fs.existsSync(pagesJSONPath))
throw new Error('Cannot find pagesJSONPath');
const pagesJSON = JSON.parse(await fs.readFile(pagesJSONPath, 'utf8'));
if (pagesJSON[viewInfo.baseName])
Object.assign(pagesJSON[viewInfo.baseName], params);
return fs.writeFile(pagesJSONPath, JSON.stringify(pagesJSON, null, 4));
}
}
class PageMetaData implements MetaData {
async getMetaData(viewInfo: vfs.View, baseViewInfo: ViewInfo | vfs.View) {
if (!baseViewInfo)
return {};
const baseViewPath = baseViewInfo.fullPath;
const routePath = path.join(baseViewPath, 'routes.map.js');
const data = {
title: '',
first: false,
meta: {},
}
if (fs.existsSync(routePath)) {
let routeJSON = utils.JS.parse(await fs.readFile(routePath, 'utf8'));
let currentPath = viewInfo.routePath.replace(baseViewInfo.routePath, '').replace(/\/$/, '');
if (routeJSON[currentPath]) {
data.meta = routeJSON[currentPath].meta;
data.title = data.meta && routeJSON[currentPath].meta.title;
}
}
return data;
}
async saveMetaData(viewInfo: vfs.View, params: AddParams, baseViewInfo?: vfs.View) {
if (!baseViewInfo)
return {};
const baseViewPath = baseViewInfo.fullPath;
const routePath = path.join(baseViewPath, 'routes.map.js');
let routeJSON: { [name: string]: { meta?: { title: string, crumb?: string }, first?: boolean } } = {};
if (fs.existsSync(routePath))
routeJSON = utils.JS.parse(await fs.readFile(routePath, 'utf8'));
let currentPath = viewInfo.routePath.replace(baseViewInfo.routePath, '').replace(/\/$/, '');
if (!routeJSON[currentPath])
routeJSON[currentPath] = {};
routeJSON[currentPath].meta = Object.assign(routeJSON[currentPath].meta || {});
if (params.hasOwnProperty('title'))
routeJSON[currentPath].meta.title = params.title;
if (params.hasOwnProperty('crumb'))
routeJSON[currentPath].meta.crumb = params.crumb;
if (params.hasOwnProperty('first'))
routeJSON[currentPath].first = params.first;
return fs.writeFile(routePath, 'export default ' + utils.JS.stringify(routeJSON, null, 4));
}
}
async function getMetaData(viewInfo: vfs.View, baseViewInfo?: ViewInfo | vfs.View) {
let instance;
let meta = {};
if (viewInfo.viewType === 'entry') {
instance = new EntryMetaData();
meta = await instance.getMetaData(viewInfo);
} else if(viewInfo.viewType === 'branch' || viewInfo.viewType === 'vue') {
instance = new PageMetaData();
meta = await instance.getMetaData(viewInfo, baseViewInfo);
}
Object.assign(viewInfo, meta);
return viewInfo;
}
export async function saveMetaData(viewInfo: ViewInfo | vfs.View, params: AddParams, baseViewInfo?: vfs.View){
const view = viewInfo instanceof vfs.View ? viewInfo : await initView(viewInfo);
let instance;
if (view.viewType === 'entry') {
instance = new EntryMetaData();
} else if(view.viewType === 'branch' || view.viewType === 'vue') {
instance = new PageMetaData();
}
return instance.saveMetaData(view, params, baseViewInfo);
}
/**
* 获取页面列表
* @param viewInfo 父页面的信息
*/
export async function loadViews(viewInfo: ViewInfo | vfs.View, baseViewInfo?: ViewInfo | vfs.View) {
const view = viewInfo instanceof vfs.View ? viewInfo : await initView(viewInfo);
await view.open();
await Promise.all(view.children.map(async (child) => {
await child.preOpen();
return await getMetaData(child, baseViewInfo);
}));
return view.children;
}
export async function loadAllViews(viewInfo: ViewInfo | vfs.View) {
const view = viewInfo instanceof vfs.View ? viewInfo : await initView(viewInfo);
await view.open();
if (view.children) {
await Promise.all(view.children.map(async (child) => {
await child.open();
await getMetaData(child);
await loadAllViews(child);
}));
}
return view;
}
/**
* 获取页面内容
* @param viewInfo 父页面的信息
*/
export async function getViewContent(viewInfo: ViewInfo | vfs.View) {
const view = viewInfo instanceof vfs.View ? viewInfo : await initView(viewInfo);
await view.preOpen();
const vueFile = new vfs.VueFile(view.vueFilePath);
await vueFile.open();
return vueFile;
}
/**
* 保存页面内容
* @param viewInfo 父页面的信息
* @param content 页面代码内容
*/
export async function saveViewContent(viewInfo: ViewInfo | vfs.View, content: string) {
const view = viewInfo instanceof vfs.View ? viewInfo : await initView(viewInfo);
await view.preOpen();
return fs.writeFile(view.vueFilePath, content);
}
/**
* 保存 Vue 局部代码
* @param fullPath Vue 文件全路径
* @param type 内容类型
* @param content 代码内容
*/
export async function saveCode(fullPath: string, type: 'template' | 'script' | 'style', content: string) {
const vueFile = new vfs.VueFile(fullPath);
await vueFile.open();
if (type === 'template')
vueFile.template = content;
else if (type === 'script')
vueFile.script = content;
else if (type === 'style')
vueFile.style = content;
await vueFile.save();
}
export async function mergeCode(fullPath: string, content: string | vfs.VueFile, nodePath?: string) {
const vueFile = new vfs.VueFile(fullPath);
await vueFile.open();
vueFile.parseAll();
const blockVue = typeof content === 'string' ? vfs.VueFile.from(content) : content;
blockVue.parseAll();
vueFile.merge(blockVue, nodePath);
await vueFile.save();
}
export function findRouteObjectAndParentArray(objectExpression: babel.types.ObjectExpression, relativePath: string | Array<string>, createChildrenArrayIfNeeded: boolean = false, pos: number = 0): {
routeObject: babel.types.ObjectExpression,
parentArray: babel.types.ArrayExpression,
} {
const arr = Array.isArray(relativePath) ? relativePath : relativePath.split('/');
if (arr[pos] === 'views')
pos++;
if (pos === arr.length)
throw new Error('Route path error. Cannot find route: ' + arr.join('/'));
const ext = path.extname(arr[arr.length - 1]);
const nextName = arr[pos].replace(/\.[^.]*?$/, '');
let childrenProperty = objectExpression.properties.find((property) => property.type === 'ObjectProperty'
&& property.key.type === 'Identifier' && property.key.name === 'children') as babel.types.ObjectProperty;
if (!childrenProperty) {
if (createChildrenArrayIfNeeded) {
childrenProperty = babel.types.objectProperty(babel.types.identifier('children'), babel.types.arrayExpression([]));
objectExpression.properties.push(childrenProperty);
} else
return { routeObject: undefined, parentArray: undefined };
}
const arrayExpression = childrenProperty.value as babel.types.ArrayExpression;
const routeObject = arrayExpression.elements.find((element) => {
return ((element.type === 'ObjectExpression' && element.properties.some((property) => property.type === 'ObjectProperty'
&& property.key.type === 'Identifier' && property.key.name === 'path'
&& property.value.type === 'StringLiteral' && property.value.value === nextName))
|| (element.type === 'ObjectExpression' && element.properties.some((property) => property.type === 'ObjectProperty'
&& property.key.type === 'Identifier' && property.key.name === 'component'
&& property.value.type === 'ArrowFunctionExpression'
&& ((property.value.body as babel.types.CallExpression).arguments[0] as babel.types.StringLiteral).value === './' + arr.slice(0, pos + 1).join('/') + (arr[pos].endsWith(ext) ? '' : '/index' + ext)))
);
}) as babel.types.ObjectExpression;
if (pos === arr.length - 1) {
return { routeObject, parentArray: arrayExpression };
} else {
if (!routeObject)
return { routeObject: undefined, parentArray: undefined };
else
return findRouteObjectAndParentArray(routeObject, arr, createChildrenArrayIfNeeded, pos + 1);
}
}
export async function addLeafViewRoute(parent: vfs.View, baseView: vfs.View, params: AddParams) {
const routesPath = path.join(baseView.fullPath, 'routes.js');
const routesMapPath = path.join(baseView.fullPath, 'routes.map.js');
if (!fs.existsSync(routesPath) || fs.existsSync(routesMapPath))
return;
const jsFile = new vfs.JSFile(routesPath);
await jsFile.open();
const $js = jsFile.parse();
const relativePath = path.relative(baseView.fullPath, path.join(parent.fullPath, parent.viewsPath, params.name + params.ext)).replace(/\\/g, '/');
let changed = false;
const exportDefault = $js.export().default();
if (exportDefault.is('object')) {
const { routeObject, parentArray } = findRouteObjectAndParentArray(exportDefault.node as babel.types.ObjectExpression, relativePath, true);
if (parentArray && !routeObject) {
const tpl = babel.parse(`[{
path: '${params.name}',
component: () => import(/* webpackChunkName: '${baseView.baseName}' */ './${relativePath}'),
${params.title ? "meta: { title: '" + params.title + "' }," : ''}
}]`, {
filename: 'file.js',
plugins: [require('@babel/plugin-syntax-dynamic-import')]
}) as babel.types.File;
const element = ((tpl.program.body[0] as babel.types.ExpressionStatement).expression as babel.types.ArrayExpression).elements[0] as babel.types.ObjectExpression;
parentArray.elements.push(element);
changed = true;
}
}
if (changed)
await jsFile.save();
return;
}
export async function addLeafView(parent: vfs.View, params: AddParams): Promise<string>;
export async function addLeafView(parent: vfs.View, baseView: vfs.View, params: AddParams): Promise<string>;
export async function addLeafView(parentInfo: ViewInfo, baseViewInfo: ViewInfo, params: AddParams): Promise<string>;
export async function addLeafView(parentInfo: ViewInfo | vfs.View, baseViewInfo: ViewInfo | vfs.View | AddParams, params?: AddParams) {
let parent: vfs.View;
let baseView: vfs.View;
if (!params) {
parent = parentInfo as vfs.View;
params = baseViewInfo as AddParams;
baseView = parent;
while (baseView && baseView.viewType !== vfs.ViewType.entry)
baseView = baseView.parent;
if (!baseView)
return;
} else {
parent = parentInfo instanceof vfs.View ? parentInfo : await initView(parentInfo);
baseView = baseViewInfo instanceof vfs.View ? baseViewInfo : await initView(baseViewInfo as ViewInfo);
await parent.preOpen();
await baseView.preOpen();
}
params.ext = params.ext || '.vue';
// parent view 必然是个目录
const dest = path.join(parent.fullPath, parent.viewsPath, params.name + params.ext);
let tplPath;
if (params.ext === '.vue')
tplPath = path.resolve(__dirname, '../../templates/leaf-view.vue');
else if (params.ext === '.md')
tplPath = path.resolve(__dirname, '../../templates/leaf-view.md');
await fs.copy(tplPath, dest);
if (params.layout)
await initViewLayout(dest, params.layout);
if (baseView) {
await addLeafViewRoute(parent, baseView, params);
if (hasNewParams(params)) {
await saveMetaData({
fullPath: dest,
viewType: params.ext === '.vue' ? vfs.ViewType.vue : vfs.ViewType.md,
routePath: parent.routePath + params.name,
}, params, baseView);
} else {
await ensureHotReload(path.join(baseView.fullPath, 'routes.map.js'));
}
}
return dest;
}
export async function addBranchViewRoute(parent: vfs.View, baseView: vfs.View, params: AddParams) {
const routesPath = path.join(baseView.fullPath, 'routes.js');
const routesMapPath = path.join(baseView.fullPath, 'routes.map.js');
if (!fs.existsSync(routesPath) || fs.existsSync(routesMapPath))
return;
const jsFile = new vfs.JSFile(routesPath);
await jsFile.open();
const $js = jsFile.parse();
// 纯目录,不带 /index.vue 的
const relativePath = path.relative(baseView.fullPath, path.join(parent.fullPath, parent.viewsPath, params.name)).replace(/\\/g, '/');
let changed = false;
const exportDefault = $js.export().default();
if (exportDefault.is('object')) {
const { routeObject, parentArray } = findRouteObjectAndParentArray(exportDefault.node as babel.types.ObjectExpression, relativePath, true);
if (parentArray && !routeObject) {
const tpl = babel.parse(`[{
path: '${params.name}',
component: () => import(/* webpackChunkName: '${baseView.baseName}' */ './${relativePath + '/index' + params.ext}'),
${params.title ? "meta: { title: '" + params.title + "' }," : ''}
children: [],
}]`, {
filename: 'file.js',
plugins: [require('@babel/plugin-syntax-dynamic-import')]
}) as babel.types.File;
const element = ((tpl.program.body[0] as babel.types.ExpressionStatement).expression as babel.types.ArrayExpression).elements[0] as babel.types.ObjectExpression;
parentArray.elements.push(element);
changed = true;
}
}
if (changed)
await jsFile.save();
return;
}
export async function addBranchView(parent: vfs.View, params: AddParams): Promise<string>;
export async function addBranchView(parent: vfs.View, baseView: vfs.View, params: AddParams): Promise<string>;
export async function addBranchView(parentInfo: ViewInfo, baseViewInfo: ViewInfo, params: AddParams): Promise<string>;
export async function addBranchView(parentInfo: ViewInfo | vfs.View, baseViewInfo: ViewInfo | vfs.View | AddParams, params?: AddParams) {
let parent: vfs.View;
let baseView: vfs.View;
if (!params) {
parent = parentInfo as vfs.View;
params = baseViewInfo as AddParams;
baseView = parent;
while (baseView && baseView.viewType !== vfs.ViewType.entry)
baseView = baseView.parent;
if (!baseView)
return;
} else {
parent = parentInfo instanceof vfs.View ? parentInfo : await initView(parentInfo);
baseView = baseViewInfo instanceof vfs.View ? baseViewInfo : await initView(baseViewInfo as ViewInfo);
await parent.preOpen();
await baseView.preOpen();
}
params.ext = params.ext || '.vue';
// parent view 必然是个目录
const dir = path.join(parent.fullPath, parent.viewsPath, params.name);
let tplPath;
if (params.ext === '.vue')
tplPath = path.resolve(__dirname, '../../templates/branch-view');
else if (params.ext === '.md')
tplPath = path.resolve(__dirname, '../../templates/branch-view-md');
await fs.copy(tplPath, dir);
const dest = path.join(dir, 'index' + params.ext);
if (params.layout)
await initViewLayout(dest, params.layout);
if (baseView) {
await addBranchViewRoute(parent, baseView, params);
if (hasNewParams(params)) {
await saveMetaData({
fullPath: dest,
viewType: vfs.ViewType.branch,
routePath: parent.routePath + params.name + '/',
}, params, baseView);
} else {
await ensureHotReload(path.join(baseView.fullPath, 'routes.map.js'));
}
}
return dest;
}
export async function addBranchWrapper(parent: vfs.View, params: AddParams): Promise<string>;
export async function addBranchWrapper(parent: vfs.View, baseView: vfs.View, params: AddParams): Promise<string>;
export async function addBranchWrapper(parentInfo: ViewInfo, baseViewInfo: ViewInfo, params: AddParams): Promise<string>;
export async function addBranchWrapper(parentInfo: ViewInfo | vfs.View, baseViewInfo: ViewInfo | vfs.View | AddParams, params?: AddParams) {
let parent: vfs.View;
let baseView: vfs.View;
if (!params) {
parent = parentInfo as vfs.View;
params = baseViewInfo as AddParams;
baseView = parent;
while (baseView && baseView.viewType !== vfs.ViewType.entry)
baseView = baseView.parent;
if (!baseView)
return;
} else {
parent = parentInfo instanceof vfs.View ? parentInfo : await initView(parentInfo);
baseView = baseViewInfo instanceof vfs.View ? baseViewInfo : await initView(baseViewInfo as ViewInfo);
await parent.preOpen();
await baseView.preOpen();
}
params.ext = params.ext || '.vue';
// parent view 必然是个目录
const dir = path.join(parent.fullPath, parent.viewsPath, params.name);
const tplPath = path.resolve(__dirname, '../../templates/branch-view');
await fs.copy(tplPath, dir);
let dest = path.join(dir, 'index.vue');
await fs.remove(dest);
dest = path.dirname(dest);
const routesPath = path.join(baseView.fullPath, 'routes.js');
const routesMapPath = path.join(baseView.fullPath, 'routes.map.js');
if (!fs.existsSync(routesPath) || fs.existsSync(routesMapPath))
return dest;
const jsFile = new vfs.JSFile(routesPath);
await jsFile.open();
const $js = jsFile.parse();
let hasImportedLWrapper = false;
babel.traverse(jsFile.handler.ast, {
ImportDefaultSpecifier(nodeInfo) {
if (nodeInfo.node.local.name === 'LWrapper') {
hasImportedLWrapper = true;
nodeInfo.stop();
}
},
ImportSpecifier(nodeInfo) {
if (nodeInfo.node.local.name === 'LWrapper') {
hasImportedLWrapper = true;
nodeInfo.stop();
}
},
});
if (!hasImportedLWrapper) {
const importDeclaration = babel.template(`import { LWrapper } from 'cloud-ui.vusion'`)() as babel.types.ImportDeclaration;
jsFile.handler.ast.program.body.unshift(importDeclaration);
}
// 纯目录,不带 /index.vue 的
const relativePath = path.relative(baseView.fullPath, path.join(parent.fullPath, parent.viewsPath, params.name)).replace(/\\/g, '/');
let changed = false;
const exportDefault = $js.export().default();
if (exportDefault.is('object')) {
const { routeObject, parentArray } = findRouteObjectAndParentArray(exportDefault.node as babel.types.ObjectExpression, relativePath, true);
if (parentArray && !routeObject) {
const tpl = babel.parse(`[{
path: '${params.name}',
component: LWrapper,
${params.title ? "meta: { title: '" + params.title + "' }," : ''}
children: [],
}]`, {
filename: 'file.js',
plugins: [require('@babel/plugin-syntax-dynamic-import')]
}) as babel.types.File;
const element = ((tpl.program.body[0] as babel.types.ExpressionStatement).expression as babel.types.ArrayExpression).elements[0] as babel.types.ObjectExpression;
parentArray.elements.push(element);
changed = true;
}
}
if (changed)
await jsFile.save();
return dest;
}
/**
* @TODO remove page metaData
*/
export async function removeView(view: vfs.View): Promise<void>;
export async function removeView(view: vfs.View, baseView: vfs.View): Promise<void>;
export async function removeView(viewInfo: ViewInfo, baseViewInfo: ViewInfo): Promise<void>;
export async function removeView(viewInfo: ViewInfo | vfs.View, baseViewInfo?: ViewInfo | vfs.View) {
let view: vfs.View;
let baseView: vfs.View;
if (!baseViewInfo) {
view = viewInfo as vfs.View;
baseView = view;
while (baseView && baseView.viewType !== vfs.ViewType.entry)
baseView = baseView.parent;
if (!baseView)
return;
} else {
view = viewInfo instanceof vfs.View ? viewInfo : await initView(viewInfo);
baseView = baseViewInfo instanceof vfs.View ? baseViewInfo : await initView(baseViewInfo as ViewInfo);
await view.preOpen();
await baseView.preOpen();
}
if (baseView) {
const routesPath = path.join(baseView.fullPath, 'routes.js');
const routesMapPath = path.join(baseView.fullPath, 'routes.map.js');
if (fs.existsSync(routesPath) && !fs.existsSync(routesMapPath)) {
const jsFile = new vfs.JSFile(routesPath);
await jsFile.open();
const $js = jsFile.parse();
const relativePath = path.relative(baseView.fullPath, view.fullPath).replace(/\\/g, '/');
let changed = false;
const exportDefault = $js.export().default();
if (exportDefault.is('object')) {
const { routeObject, parentArray } = findRouteObjectAndParentArray(exportDefault.node as babel.types.ObjectExpression, relativePath, true);
if (routeObject) {
parentArray.elements.splice(parentArray.elements.indexOf(routeObject), 1);
// 判断是不是 LWrapper
const LWrapper = routeObject.properties.find((property) => property.type === 'ObjectProperty'
&& property.key.type === 'Identifier' && property.key.name === 'component'
&& property.value.type === 'Identifier' && property.value.name === 'LWrapper');
if (LWrapper) {
let wrapperCount = 0;
String(jsFile.content).replace(/LWrapper/, () => String(wrapperCount++));
if (wrapperCount === 2) {
babel.traverse(jsFile.handler.ast, {
ImportDefaultSpecifier(nodeInfo) {
if (nodeInfo.node.local.name === 'LWrapper') {
nodeInfo.remove();
nodeInfo.stop();
}
},
ImportSpecifier(nodeInfo) {
if (nodeInfo.node.local.name === 'LWrapper') {
nodeInfo.remove();
nodeInfo.stop();
}
},
});
}
}
changed = true;
}
}
if (changed)
await jsFile.save();
}
}
await fs.remove(view.fullPath);
if (baseView)
await ensureHotReload(path.join(baseView.fullPath, 'routes.map.js'));
}
export interface ParseTypes {
template?: boolean,
script?: boolean,
style?: boolean,
api?: boolean,
examples?: boolean,
}
/**
* 获取服务信息
*/
export async function loadServices(baseViewPath: string) {
const servicesPath = path.join(baseViewPath, 'services');
if (!fs.existsSync(servicesPath)) {
return [];
}
const directory = new vfs.Directory(servicesPath);
await directory.open();
const tasks = directory.children.filter((item) => item.isDirectory && item.fileName[0] !== '.')
.map(async (subdir) => {
const service = new vfs.Service(subdir.fullPath);
await service.open();
return service;
});
return Promise.all(tasks);
}
/**
* @deprecated
* @param fullPath
* @param newName
* @param name
*/
export async function addOrRenameService(fullPath: string, newName: string, name: string) {
if (!name) {
const dir = path.join(fullPath, 'services', newName);
let tplPath = path.resolve(__dirname, '../../templates/service');
await fs.copy(tplPath, dir);
return path.join(dir, 'api.json');
} else {
const oldPath = path.join(fullPath, 'services', name);
const newPath = path.join(fullPath, 'services', newName);
await fs.rename(oldPath, newPath);
return path.join(newPath, 'api.json');
}
}
export async function saveService(serviceInfo: vfs.Service) {
const service = new vfs.Service(serviceInfo.fullPath);
await service.open();
Object.assign(service, serviceInfo);
await service.save();
}
export async function removeService(fullPath: string) {
await fs.remove(fullPath);
await ensureHotReload(path.join(fullPath, '../index.js'));
}
/**
* 区块的复杂程度类型
*/
const enum BlockComplexity {
onlyTemplate, // Just add
hasScriptOrStyle, // Can merge
hasAssetsOrExtra, // Must external
}
/**
* 组件或区块信息
*/
interface BlockInfo {
name: string,
title: string,
tagName: string,
dependencies: { [name: string]: string },
vusionDependencies: { [name: string]: string },
registry: string,
uuid?: string,
}
/**
* 替换占位符内容
* @param fullPath 文件路径
* @param blockInfo 组件或区块信息
* @param content 要替换的内容
*/
async function replacePlaceholder(fullPath: string, blockInfo: BlockInfo, content: string | vfs.VueFile){
const vueFile = new vfs.VueFile(fullPath);
await vueFile.forceOpen();
vueFile.parseAll();
let progressArray: any[] = [];
vueFile.templateHandler.traverse((nodeInfo) => {
const node = nodeInfo.node as compiler.ASTElement;
if (node.tag === 'd-progress' && node.attrsMap.uuid === blockInfo.uuid){
progressArray.push(nodeInfo.route);
nodeInfo.remove();
}
});
const blockVue = typeof content === 'string' ? vfs.VueFile.from(content) : content;
blockVue.parseAll();
progressArray.forEach((route)=>{
vueFile.merge(blockVue, route);
});
await vueFile.save();
}
/**
* 在有其它代码或 Assets 的情况下,直接添加为外部区块
*/
async function external(fullPath: string, blockInfo: BlockInfo, blockVue: vfs.VueFile) {
if(!fs.existsSync(path.join(fullPath.replace(/\.vue$/, '.blocks'), blockInfo.tagName + '.vue'))){
await vms.addBlockExternally(blockVue, fullPath, blockInfo.tagName);
} else {
const vueFile = new vfs.VueFile(fullPath);
await vueFile.open();
/* 添加 import */
const relativePath = `./${vueFile.baseName}.blocks/${blockInfo.tagName}.vue`;
const { componentName } = utils.normalizeName(blockInfo.tagName);
const $js = vueFile.parseScript();
const components = $js.export().default().object().get('components');
if (!components || !components.get(componentName)) {
$js.import(componentName).from(relativePath);
$js.export().default().object()
.after(['el','name','parent','functional','delimiters','comments'])
.ensure('components', '{}')
.get('components')
.set(componentName, componentName);
await vueFile.save();
}
}
const content = `<template><${blockInfo.tagName}></${blockInfo.tagName}></template>`;
await replacePlaceholder(fullPath, blockInfo, content);
}
/**
* 添加区块
* @param fullPath 文件路径
* @param libraryPath 全局组件路径,components/index.js所在路径
* @param blockInfo 组件或区块信息
* @param tpl 组件代码字符串
* @param nodePath 节点路径
*/
export async function addBlock(fullPath: string, blockInfo: BlockInfo){
const options = {
source: {
type: 'file',
registry: blockInfo.registry,
name: blockInfo.name,
fileName: blockInfo.tagName + '.vue',
baseName: blockInfo.tagName,
},
target: fullPath,
name: blockInfo.tagName,
};
const blockPath = await vms.fetchBlock(options);
let blockVue: vfs.VueFile;
blockVue = new vfs.VueFile(blockPath.replace(/\.vue@.+$/, '.vue'));
blockVue.fullPath = blockPath;
await blockVue.open();
// 区块的复杂程度
let blockComplexity: BlockComplexity;
if (blockVue.hasAssets() || blockVue.hasExtra())
blockComplexity = BlockComplexity.hasAssetsOrExtra;
else if (blockVue.hasScript(true) || blockVue.hasStyle(true))
blockComplexity = BlockComplexity.hasScriptOrStyle;
else
blockComplexity = BlockComplexity.onlyTemplate;
if (blockComplexity === BlockComplexity.hasAssetsOrExtra) {
return await external(fullPath, blockInfo, blockVue);
}else{
return await replacePlaceholder(fullPath, blockInfo, blockVue);
}
}
/**
* 添加业务组件
* @param fullPath 文件路径
* @param libraryPath 全局组件路径,components/index.js所在路径
* @param blockInfo 组件或区块信息
* @param tpl 组件代码字符串
* @param nodePath 节点路径
*/
export async function addCustomComponent(fullPath: string, libraryPath: string, blockInfo: BlockInfo, content: string) {
const library = new vfs.Library(libraryPath, vfs.LibraryType.internal);
await library.open();
const indexFile = library.componentsIndexFile;
if(indexFile){
await indexFile.forceOpen();
const $js = indexFile.parse();
$js.export('*').from(blockInfo.name);
await indexFile.save();
}
await replacePlaceholder(fullPath, blockInfo, content);
}
export async function loadPackageJSON(rootPath: string) {
const pkgPath = path.resolve(rootPath, 'package.json');
if(!fs.existsSync(pkgPath))
return {};
return JSON.parse(await fs.readFile(pkgPath, 'utf8'));
}
export async function loadExternalLibrary(fullPath: string, parseTypes: ParseTypes = {}) {
const library = new vfs.Library(fullPath, vfs.LibraryType.external);
await library.open();
await Promise.all(library.components.map(async (vueFile) => {
await vueFile.open();
if (parseTypes.template)
vueFile.parseTemplate();
if (parseTypes.script)
vueFile.parseScript();
if (parseTypes.style)
vueFile.parseStyle();
if (parseTypes.api)
vueFile.parseAPI();
if (parseTypes.examples)
vueFile.parseExamples();
}));
return library;
}
/**
* 获取单个控件信息
* @param fullPath 控件路径
* @param parseTypes 需要获取的信息
*/
export async function loadComponentData(fullPath: string, parseTypes: ParseTypes = {}){
if(!fs.existsSync(fullPath))
return {};
const vueFile = new vfs.VueFile(fullPath);
await vueFile.open();
if (parseTypes.template)
vueFile.parseTemplate();
if (parseTypes.script)
vueFile.parseScript();
if (parseTypes.style)
vueFile.parseStyle();
if (parseTypes.api)
vueFile.parseAPI();
if (parseTypes.examples)
vueFile.parseExamples();
return vueFile;
}
/**
* 获取自定义组件信息,packages.json中有的组件,并且是以.vue结尾
* @param rootPath package.json所在的目录路径
* @param parseTypes 需要获取的信息
* @param baseName 组件信息,有该信息则获取该组件信息
*/
export async function loadCustomComponentsData(rootPath: string, parseTypes: ParseTypes = {}, baseName?: string){
const pkg = await loadPackageJSON(rootPath);
const tasks: Array<Promise<any>> = [];
Object.keys(pkg.dependencies || {}).filter((name) => {
if(baseName)
return name.includes(baseName + '.vue');
else
return name.endsWith('.vue');
}).forEach((name) => {
tasks.push(loadComponentData(`${rootPath}/node_modules/${name}`, parseTypes));
});
Object.keys(pkg.vusionDependencies || {}).filter((name) => {
if(baseName)
return name.includes(baseName + '.vue');
else
return name.endsWith('.vue');
}).forEach((name) => {
tasks.push(loadComponentData(`${rootPath}/vusion_packages/${name}`, parseTypes));
});
return Promise.all(tasks);
}
export async function addAuthCache(name: string, filePath: string) {
await fs.ensureFile(filePath);
let json: any = {};
try {
json = JSON.parse(await fs.readFile(filePath, 'utf8'));
} catch(e) {}
json[name] = true;
await fs.writeFile(filePath, JSON.stringify(json, null, 4));
}
export async function removeAuthCache(name: string, filePath: string) {
await fs.ensureFile(filePath);
let json: any = {};
try {
json = JSON.parse(await fs.readFile(filePath, 'utf8'));
} catch(e) {}
delete json[name];
await fs.writeFile(filePath, JSON.stringify(json, null, 4));
}
export async function loadAuthCache(filePath: string) {
try {
return JSON.parse(await fs.readFile(filePath, 'utf8'));
} catch(e) {
return {};
}
}