vusion-api
Version:
Vusion Node.js API
404 lines (352 loc) • 13.9 kB
text/typescript
import * as path from 'path';
import * as babel from '@babel/core';
import * as compiler from 'vue-template-compiler';
import * as fs from 'fs-extra';
import * as os from 'os';
import * as vfs from '../fs';
import * as utils from '../utils';
import * as compressing from 'compressing';
import * as rc from '../rc';
import axios, { AxiosInstance } from 'axios';
let platformAxios: AxiosInstance;
const getPlatformAxios = (): Promise<AxiosInstance> => {
return new Promise((res, rej) => {
if (platformAxios)
return res(platformAxios);
const config = rc.configurator.load();
platformAxios = axios.create({
baseURL: config.platform + '/internal',
headers: {
'access_token': config.access_token,
}
});
res(platformAxios);
});
}
export function getCacheDir(subPath: string = '') {
const cacheDir = path.join(os.homedir(), '.vusion', subPath);
if (!fs.existsSync(cacheDir))
fs.ensureDirSync(cacheDir);
return cacheDir;
}
export function getRunControl() {
const rcPath = path.join(os.homedir(), '.vusion');
return rcPath;
}
export interface MaterialSource {
type: string,
registry: string,
name: string, // source.name, npm name, repo name
path: string,
version?: string,
commit?: string,
fileName: string,
baseName: string,
};
export interface MaterialOptions {
/**
* file: ./templates/moduleA
* file: /Users/alice/templates/moduleA
* npm: s-basic-form
* npm: s-basic-form.vue
* npm: s-basic-form.vue@0.3.2
* disable: npm: s-basic-form.vue@0.3.2:some/directory
* npm: @cloud-ui/s-basic-form.vue
* npm: @cloud-ui/s-basic-form.vue:some/directory
* cnpm: cnpm:@cloud-ui/s-basic-form.vue
* nnpm: nnpm:@cloud-ui/s-basic-form.vue
* github: github:user/repo
* disable: gitlab: gitlab:user/repo#master:some/directory
*/
source: string | MaterialSource,
target: string,
name: string,
title?: string,
};
export interface ProcessedMaterialOptions {
/**
* file: ./templates/moduleA
* file: /Users/alice/templates/moduleA
* npm: s-basic-form
* npm: s-basic-form.vue
* npm: s-basic-form.vue@0.3.2
* disable: npm: s-basic-form.vue@0.3.2:some/directory
* npm: @cloud-ui/s-basic-form.vue
* npm: @cloud-ui/s-basic-form.vue:some/directory
* cnpm: cnpm:@cloud-ui/s-basic-form.vue
* nnpm: nnpm:@cloud-ui/s-basic-form.vue
* github: github:user/repo
* disable: gitlab: gitlab:user/repo#master:some/directory
*/
source: MaterialSource,
target: string,
name: string,
title?: string,
};
export function processOptions(options: MaterialOptions): ProcessedMaterialOptions {
const result: ProcessedMaterialOptions = {
source: {
type: 'file',
registry: '',
name: '',
path: '',
version: '',
commit: '',
fileName: '',
baseName: '',
},
target: options.target,
name: options.name,
title: options.title,
};
let source = options.source;
if (typeof source !== 'string') {
result.source = source;
// const fileName = result.source.fileName = path.basename(result.source.name);
// result.source.baseName = path.basename(fileName, path.extname(fileName));
return result;
}
if (source[0] === '.' || source[0] === '~' || source[0] === '/') {
result.source.type = 'file';
result.source.path = source;
const fileName = result.source.fileName = path.basename(source);
result.source.baseName = path.basename(fileName, path.extname(fileName));
} else {
const repoRE = /^\w+:/;
const cap = repoRE.exec(source);
if (cap) {
result.source.type = cap[0].slice(0, -1);
source = source.slice(cap[0].length);
} else
result.source.type = 'npm';
const arr = source.split(':');
result.source.path = arr[1];
let name = arr[0];
if (name.includes('#')) {
const arr2 = name.split('#');
result.source.name = arr2[0];
result.source.commit = arr2[1];
} else if (name.includes('@')) {
const arr2 = name.split('@');
result.source.name = arr2[0];
result.source.version = arr2[1];
} else {
result.source.name = name;
}
const fileName = result.source.fileName = path.basename(result.source.name);
result.source.baseName = path.basename(fileName, path.extname(fileName));
}
return result;
}
/**
* 从业务模板中添加模块
*/
export async function addModule(options: MaterialOptions) {
const opts = processOptions(options);
const moduleCacheDir = getCacheDir('modules');
await fs.emptyDir(moduleCacheDir);
if (opts.source.type === 'file') {
const temp = path.resolve(moduleCacheDir, opts.source.fileName + '-' + new Date().toJSON().replace(/[-:TZ]/g, '').slice(0, -4));
const dest = path.resolve(opts.target, opts.name);
await fs.copy(path.resolve(opts.source.path), temp);
await vfs.batchReplace(vfs.listAllFiles(moduleCacheDir, {
type: 'file',
}), [
[/sample/g, opts.name],
[/Sample/g, utils.kebab2Camel(opts.name)],
[/样本/g, opts.title],
]);
await fs.move(temp, dest);
// 修改 modules.order 配置
const modulesOrderPath = path.resolve(opts.target, 'modules.order.js');
if (fs.existsSync(modulesOrderPath)) {
const jsFile = new vfs.JSFile(modulesOrderPath);
await jsFile.open();
jsFile.parse();
let changed = false;
babel.traverse(jsFile.handler.ast, {
ExportDefaultDeclaration(nodePath) {
const declaration = nodePath.node.declaration;
if (declaration && declaration.type === 'ArrayExpression') {
const element = Object.assign(
babel.types.stringLiteral(opts.name),
{ raw: `'${opts.name}'` },
);
declaration.elements.push(element);
changed = true;
}
}
});
if (changed)
await jsFile.save();
}
}
}
export async function removeModule(options: MaterialOptions) {
const dest = path.resolve(options.target, options.name);
await fs.remove(dest);
// 修改 modules.order 配置
const modulesOrderPath = path.resolve(options.target, 'modules.order.js');
if (fs.existsSync(modulesOrderPath)) {
const jsFile = new vfs.JSFile(modulesOrderPath);
await jsFile.open();
jsFile.parse();
let changed = false;
babel.traverse(jsFile.handler.ast, {
ExportDefaultDeclaration(nodePath) {
const declaration = nodePath.node.declaration;
if (declaration && declaration.type === 'ArrayExpression') {
for (let i = 0; i < declaration.elements.length; i++) {
const element = declaration.elements[i];
if (element.type === 'StringLiteral' && element.value === options.name) {
declaration.elements.splice(i, 1);
changed = true;
break;
}
}
}
}
});
await jsFile.save();
}
}
export async function getBlock(packageName: string) {
const pfAxios = await getPlatformAxios();
return pfAxios.get('block/info', {
params: {
name: packageName,
},
}).then((res) => res.data.result);
}
export async function getBlocks() {
const pfAxios = await getPlatformAxios();
return pfAxios.get('block/list')
.then((res) => res.data.result.rows);
}
export async function publishBlock(params: object) {
const pfAxios = await getPlatformAxios();
return pfAxios.post('block/publish', params)
.then((res) => res.data);
}
export async function publishComponent(params: object) {
const pfAxios = await getPlatformAxios();
return pfAxios.post('component/publish', params)
.then((res) => res.data);
}
export async function publishTemplate(params: object) {
const pfAxios = await getPlatformAxios();
return pfAxios.post('template/publish', params)
.then((res) => res.data);
}
export async function createBlock(dir: string, name?: string, title?: string) {
const normalized = utils.normalizeName(name);
const dest = vfs.handleSame(dir, normalized.baseName);
await fs.copy(path.resolve(__dirname, '../../templates/s-block.vue'), dest);
await vfs.batchReplace(vfs.listAllFiles(dest, {
type: 'file',
}), [
[/s-block/g, normalized.baseName],
[/SBlock/g, normalized.componentName],
[/区块/g, title || '区块'],
]);
return dest;
}
export async function createBlockInLibrary(dir: string, name?: string, title?: string) {
const normalized = utils.normalizeName(name);
const dest = vfs.handleSame(dir, normalized.baseName);
await fs.copy(path.resolve(__dirname, '../../templates/s-library-block.vue'), dest);
await vfs.batchReplace(vfs.listAllFiles(dest, {
type: 'file',
}), [
[/s-block/g, normalized.baseName],
[/SBlock/g, normalized.componentName],
[/区块/g, title || '区块'],
]);
return dest;
}
export async function addBlock(options: MaterialOptions) {
const opts = processOptions(options);
// if (opts.source.type === 'npm')
const blockCacheDir = getCacheDir('blocks');
const tempPath = await downloadPackage(opts.source.registry, opts.source.name, blockCacheDir);
// if (fs.statSync(opts.target).isFile())
const vueFile = new vfs.VueFile(opts.target);
await vueFile.open();
if (!vueFile.isDirectory) {
vueFile.transform();
await vueFile.save();
}
const localBlocksPath = path.join(vueFile.fullPath, 'blocks');
const dest = path.join(localBlocksPath, opts.name + '.vue');
await fs.ensureDir(localBlocksPath);
await fs.move(tempPath, dest);
vueFile.parseScript();
vueFile.parseTemplate();
const relativePath = './blocks/' + opts.name + '.vue';
const { componentName } = utils.normalizeName(opts.name);
const body = vueFile.scriptHandler.ast.program.body;
for (let i = 0; i < body.length; i++) {
const node = body[i];
if (node.type !== 'ImportDeclaration') {
const importDeclaration = babel.template(`import ${componentName} from '${relativePath}'`)() as babel.types.ImportDeclaration;
body.splice(i, 0, importDeclaration);
break;
}
}
babel.traverse(vueFile.scriptHandler.ast, {
ExportDefaultDeclaration(nodePath) {
const declaration = nodePath.node.declaration;
if (declaration && declaration.type === 'ObjectExpression') {
let pos = 0;
const propertiesBefore = [
'el',
'name',
'parent',
'functional',
'delimiters',
'comments',
]
let componentsProperty = declaration.properties.find((property, index) => {
if (property.type === 'ObjectProperty' && propertiesBefore.includes(property.key.name))
pos = index;
return property.type === 'ObjectProperty' && property.key.name === 'components';
}) as babel.types.ObjectProperty;
const blockProperty = babel.types.objectProperty(babel.types.identifier(componentName), babel.types.identifier(componentName));
if (!componentsProperty) {
componentsProperty = babel.types.objectProperty(babel.types.identifier('components'), babel.types.objectExpression([]));
declaration.properties.splice(pos, 0, componentsProperty);
}
(componentsProperty.value as babel.types.ObjectExpression).properties.push(blockProperty);
}
},
});
const rootEl = vueFile.templateHandler.ast;
rootEl.children.unshift(compiler.compile(`<${opts.name}></${opts.name}>`).ast);
await vueFile.save();
}
/**
*
* @param registry For example: https://registry.npm.taobao.org
* @param packageName For example: lodash
* @param saveDir For example: ./blocks
*/
export async function downloadPackage(registry: string, packageName: string, saveDir: string, clearCache?: boolean) {
const { data: pkgInfo } = await axios.get(`${registry}/${packageName}/latest`);
const dest = path.join(saveDir, pkgInfo.name.replace(/\//, '__') + '@' + pkgInfo.version);
if (fs.existsSync(dest) && !clearCache)
return dest;
const tgzURL = pkgInfo.dist.tarball;
const response = await axios.get(tgzURL, {
responseType: 'stream',
});
const temp = path.resolve(os.tmpdir(), packageName + '-' + new Date().toJSON().replace(/[-:TZ]/g, '').slice(0, -4));
await compressing.tgz.uncompress(response.data, temp);
await fs.move(path.join(temp, 'package'), dest);
fs.removeSync(temp);
fs.removeSync(path.resolve(dest, 'screenshots'));
fs.removeSync(path.resolve(dest, 'public'));
fs.removeSync(path.resolve(dest, 'docs'));
fs.removeSync(path.resolve(dest, 'package.json'));
fs.removeSync(path.resolve(dest, 'README.md'));
return dest;
}