@varlet/cli
Version:
cli of varlet
220 lines (217 loc) • 8.15 kB
JavaScript
import { dirname, extname, relative, resolve } from 'path';
import { transformAsync } from '@babel/core';
import { pascalCase } from '@varlet/shared';
import jsx from '@vue/babel-plugin-jsx';
import esbuild from 'esbuild';
import fse from 'fs-extra';
import { getVarletConfig } from '../config/varlet.config.js';
import { ES_DIR } from '../shared/constant.js';
import { getVersion, isDir, isJsx, isTsx, replaceExt } from '../shared/fsUtils.js';
import { extractStyleDependencies, IMPORT_CSS_RE, IMPORT_LESS_RE, IMPORT_SCSS_RE } from './compileStyle.js';
const { writeFileSync, readdirSync, readFileSync, removeSync, writeFile, pathExistsSync } = fse;
// https://regexr.com/765a4
export const IMPORT_FROM_DEPENDENCE_RE = /import\s+?[\w\s{},$*]+\s+from\s+?(".*?"|'.*?')/g;
// https://regexr.com/767e6
export const EXPORT_FROM_DEPENDENCE_RE = /export\s+?[\w\s{},$*]+\s+from\s+?(".*?"|'.*?')/g;
// https://regexr.com/764ve
export const IMPORT_DEPENDENCE_RE = /import\s+(".*?"|'.*?')/g;
export const scriptExtNames = ['.vue', '.ts', '.tsx', '.mjs', '.js', '.jsx'];
export const styleExtNames = ['.less', '.scss', '.css'];
export const scriptIndexes = ['index.mjs', 'index.vue', 'index.ts', 'index.tsx', 'index.js', 'index.jsx'];
export const styleIndexes = ['index.less', 'index.scss', 'index.css'];
export const tryMatchExtname = (file, extname) => {
for (const ext of extname) {
const matched = `${file}${ext}`;
if (pathExistsSync(matched)) {
return ext;
}
}
};
export const resolveAlias = (dependence, file, alias = {}) => {
let matchedAliasKey = Object.keys(alias).find((key) => dependence === key);
if (!matchedAliasKey) {
// @/ -> yes
// @varlet/ui -> no
matchedAliasKey = Object.keys(alias).find((key) => dependence.startsWith(`${key}/`));
}
if (!matchedAliasKey) {
return dependence;
}
const matchedAliasValue = alias[matchedAliasKey];
const isRelative = matchedAliasValue.startsWith('.');
const replacedValue = isRelative ? relative(dirname(file), resolve(ES_DIR, matchedAliasValue)) : matchedAliasValue;
return dependence.replace(matchedAliasKey, replacedValue);
};
export const resolveDependence = (file, script, alias = {}) => {
const replacer = (source, dependence) => {
// remove quotes
dependence = dependence.slice(1, dependence.length - 1);
const done = (targetDependence) => source.replace(dependence, targetDependence);
const resolvedDependence = resolveAlias(dependence, file, alias);
const isNodeModule = !resolvedDependence.startsWith('.');
if (isNodeModule) {
// e.g. @varlet/shared
return done(resolvedDependence);
}
const ext = extname(resolvedDependence);
const scriptExtname = getScriptExtname();
if (ext) {
if (scriptExtNames.includes(ext)) {
// e.g. './a.vue' -> './a.mjs'
return done(dependence.replace(ext, scriptExtname));
}
if (styleExtNames.includes(ext)) {
// e.g. './a.css' -> './a.css' './a.less' -> './a.less'
return source;
}
}
const targetDependenceFile = resolve(dirname(file), resolvedDependence);
if (!ext) {
// e.g. ../button/props -> ../button/props.mjs
const matchedScript = tryMatchExtname(targetDependenceFile, scriptExtNames);
if (matchedScript) {
return done(`${resolvedDependence}${scriptExtname}`);
}
const matchedStyle = tryMatchExtname(targetDependenceFile, styleExtNames);
if (matchedStyle) {
return done(`${resolvedDependence}${matchedStyle}`);
}
}
if (!ext && isDir(targetDependenceFile)) {
// e.g. ../button
const files = readdirSync(targetDependenceFile);
const hasScriptIndex = files.some((file) => scriptIndexes.some((name) => file.endsWith(name)));
if (hasScriptIndex) {
// e.g. -> ../button/index.mjs
return done(`${resolvedDependence}/index${scriptExtname}`);
}
const hasStyleIndex = files.some((file) => styleIndexes.some((name) => file.endsWith(name)));
if (hasStyleIndex) {
// e.g. -> ../button/index.css
return done(`${resolvedDependence}/index.css`);
}
}
return '';
};
return script
.replace(IMPORT_FROM_DEPENDENCE_RE, replacer)
.replace(EXPORT_FROM_DEPENDENCE_RE, replacer)
.replace(IMPORT_DEPENDENCE_RE, replacer);
};
export async function compileScriptByBabel(script, file) {
const { code } = (await transformAsync(script, {
filename: file,
babelrc: false,
presets: ['@babel/preset-typescript'],
plugins: [
[
jsx,
{
enableObjectSlots: false,
},
],
],
}));
return code;
}
export async function compileScriptByEsbuild(script) {
const varletConfig = await getVarletConfig();
const { code } = await esbuild.transform(script, {
loader: 'ts',
target: varletConfig?.esbuild?.target,
format: 'esm',
});
return code;
}
export async function compileScript(script, file) {
let code = isJsx(file) || isTsx(file) ? await compileScriptByBabel(script, file) : script;
code = await compileScriptByEsbuild(code);
const { alias } = await getVarletConfig();
if (code) {
code = resolveDependence(file, code, alias);
code = extractStyleDependencies(file, code, IMPORT_CSS_RE);
code = extractStyleDependencies(file, code, IMPORT_LESS_RE);
code = extractStyleDependencies(file, code, IMPORT_SCSS_RE);
}
removeSync(file);
writeFileSync(replaceExt(file, getScriptExtname()), code, 'utf8');
}
export async function compileScriptFile(file) {
const sources = readFileSync(file, 'utf-8');
await compileScript(sources, file);
}
export function getScriptExtname() {
return '.mjs';
}
export function generateEsEntryTemplate(options) {
const { publicDirs, scriptExtname = getScriptExtname(), root = './' } = options;
const imports = [];
const plugins = [];
const exports = [];
const cssImports = [];
const publicComponents = [];
publicDirs.forEach((dirname) => {
const publicComponent = pascalCase(dirname);
const module = `'${root}${dirname}/index${scriptExtname}'`;
publicComponents.push(publicComponent);
imports.push(`import ${publicComponent} from ${module}`);
exports.push(`export * from ${module}`);
plugins.push(`${publicComponent}.install && app.use(${publicComponent})`);
cssImports.push(`import '${root}${dirname}/style/index${scriptExtname}'`);
});
const install = `
function install(app) {
${plugins.join('\n ')}
}
`;
const version = `const version = '${getVersion()}'`;
const indexTemplate = `\
${imports.join('\n')}\n
${exports.join('\n')}\n
${version}
${install}
export {
version,
install,
${publicComponents.join(',\n ')}
}
export default {
version,
install,
${publicComponents.join(',\n ')}
}
`;
const styleTemplate = `\
${cssImports.join('\n')}
`;
const bundleTemplate = `\
${imports.join('\n')}\n
${exports.join('\n')}\n
${cssImports.join('\n')}\n
${version}
${install}
export {
version,
install,
${publicComponents.join(',\n ')}
}
export default {
version,
install,
${publicComponents.join(',\n ')}
}
`;
return {
indexTemplate,
styleTemplate,
bundleTemplate,
};
}
export async function compileESEntry(dir, publicDirs) {
const { indexTemplate, bundleTemplate, styleTemplate } = generateEsEntryTemplate({ publicDirs });
await Promise.all([
writeFile(resolve(dir, 'index.mjs'), indexTemplate, 'utf-8'),
writeFile(resolve(dir, 'index.bundle.mjs'), bundleTemplate, 'utf-8'),
writeFile(resolve(dir, 'style.mjs'), styleTemplate, 'utf-8'),
]);
}