build: add files of vant-cli 2

This commit is contained in:
陈嘉涵
2019-11-18 16:27:12 +08:00
parent f6650aa1cd
commit 08e9d99699
46 changed files with 12125 additions and 249 deletions

View File

@@ -0,0 +1,54 @@
import { join } from 'path';
import { remove, copy, readdirSync } from 'fs-extra';
import { clean } from './clean';
import { compileJs } from '../compiler/compile-js';
import { compileSfc } from '../compiler/compile-sfc';
import { compileStyle } from '../compiler/compile-style';
import { SRC_DIR, LIB_DIR, ES_DIR } from '../common/constant';
import {
isDir,
isSfc,
isDemoDir,
isTestDir,
isScript,
isStyle
} from '../common';
async function compileDir(dir: string) {
const files = readdirSync(dir);
files.forEach(async filename => {
const filePath = join(dir, filename);
if (isDemoDir(filePath) || isTestDir(filePath)) {
await remove(filePath);
} else if (isDir(filePath)) {
await compileDir(filePath);
} else if (isSfc(filePath)) {
await compileSfc(filePath);
} else if (isScript(filePath)) {
await compileJs(filePath);
} else if (isStyle(filePath)) {
await compileStyle(filePath);
} else {
await remove(filePath);
}
});
}
function setModuleEnv(value: string) {
process.env.BABEL_MODULE = value;
}
export async function build() {
clean();
await copy(SRC_DIR, ES_DIR);
await copy(SRC_DIR, LIB_DIR);
setModuleEnv('esmodule');
await compileDir(ES_DIR);
setModuleEnv('commonjs');
await compileDir(LIB_DIR);
}

View File

@@ -1,11 +1,11 @@
const path = require('path');
const shelljs = require('shelljs');
import { join } from 'path';
import { exec } from 'shelljs';
function changelog(dist, cmd) {
export function changelog(dist: string, cmd: { tag?: string }) {
const basepath = process.cwd();
const tag = cmd.tag || 'v1.0.0';
shelljs.exec(`
exec(`
basepath=${basepath}
github_changelog_generator \
@@ -18,9 +18,6 @@ function changelog(dist, cmd) {
--no-author \
--no-unreleased \
--since-tag ${tag} \
-o ${path.join(basepath, dist)}
`
);
-o ${join(basepath, dist)}
`);
}
module.exports = changelog;

View File

@@ -0,0 +1,8 @@
import { emptyDirSync } from 'fs-extra';
import { ES_DIR, LIB_DIR, DIST_DIR } from '../common/constant';
export function clean() {
emptyDirSync(ES_DIR);
emptyDirSync(LIB_DIR);
emptyDirSync(DIST_DIR);
}

View File

@@ -1,11 +1,11 @@
const fs = require('fs');
const signale = require('signale');
import signale from 'signale';
import { readFileSync } from 'fs';
const commitRE = /^(revert: )?(fix|feat|docs|perf|test|types|build|chore|refactor|breaking change)(\(.+\))?: .{1,50}/;
function commitLint() {
const gitParams = process.env.HUSKY_GIT_PARAMS;
const commitMsg = fs.readFileSync(gitParams, 'utf-8').trim();
export function commitLint() {
const gitParams = process.env.HUSKY_GIT_PARAMS as string;
const commitMsg = readFileSync(gitParams, 'utf-8').trim();
if (!commitRE.test(commitMsg)) {
signale.error(`Error: invalid commit message: "${commitMsg}".
@@ -34,5 +34,3 @@ Allowed Types:
process.exit(1);
}
}
module.exports = commitLint;

View File

@@ -0,0 +1,39 @@
import webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
import webpackDevConfig from '../config/webpack.site.dev';
import { getPort } from 'portfinder';
import { clean } from '../commands/clean';
import { genMobileConfig } from '../compiler/gen-mobile-config';
import { genDesktopConfig } from '../compiler/gen-desktop-config';
function runWebpack() {
const server = new WebpackDevServer(
webpack(webpackDevConfig),
(webpackDevConfig as any).devServer
);
getPort(
{
port: 8080
},
(err, port) => {
if (err) {
console.log(err);
return;
}
server.listen(port, 'localhost', (err?: Error) => {
if (err) {
console.log(err);
}
});
}
);
}
export function dev() {
clean();
genMobileConfig();
genDesktopConfig();
runWebpack();
}

View File

@@ -0,0 +1,48 @@
import { start, error, success } from 'signale';
import { lint as stylelint } from 'stylelint';
import { CLIEngine } from 'eslint';
function lintScript() {
start('ESLint Start');
const cli = new CLIEngine({
fix: true,
extensions: ['.js', '.jsx', '.vue', '.ts', '.tsx']
});
const report = cli.executeOnFiles(['src/']);
const formatter = cli.getFormatter();
CLIEngine.outputFixes(report);
// output lint errors
const formatted = formatter(report.results);
if (formatted) {
error('ESLint Failed');
console.log(formatter(report.results));
} else {
success('ESLint Passed');
}
}
function lintStyle() {
start('Stylelint Start');
stylelint({
fix: true,
formatter: 'string',
files: ['src/**/*.css', 'src/**/*.less', 'src/**/*.scss', 'src/**/*.vue']
}).then(result => {
if (result.errored) {
error('Stylelint Failed');
console.log(result.output);
} else {
success('Stylelint Passed');
}
});
}
export function lint() {
lintScript();
lintStyle();
}

View File

@@ -0,0 +1,14 @@
/* eslint-disable no-template-curly-in-string */
import { build } from './build';
// @ts-ignore
import releaseIt from 'release-it';
export async function release() {
await build();
await releaseIt({
git: {
tagName: 'v${version}',
commitMessage: 'chore: release ${version}'
}
});
}

View File

@@ -0,0 +1,14 @@
import { runCLI } from 'jest';
import { CWD, JEST_CONFIG_FILE } from '../common/constant';
export function test(command: any) {
process.env.NODE_ENV = 'test';
const config = {
rootDir: CWD,
watch: command.watch,
config: JEST_CONFIG_FILE
} as any;
runCLI(config, [CWD]);
}

View File

@@ -0,0 +1,17 @@
import { join } from 'path';
export const CWD = process.cwd();
export const SRC_DIR = join(CWD, 'src');
export const CONFIG_FILE = join(CWD, 'components.config.js');
export const ES_DIR = join(CWD, 'es');
export const LIB_DIR = join(CWD, 'lib');
export const DIST_DIR = join(__dirname, '../../dist');
export const CONFIG_DIR = join(__dirname, '../config');
export const MOBILE_CONFIG_FILE = join(DIST_DIR, 'mobile-config.js');
export const DESKTOP_CONFIG_FILE = join(DIST_DIR, 'desktop-config.js');
export const BABEL_CONFIG_FILE = join(CONFIG_DIR, 'babel.config.js');
export const JEST_CONFIG_FILE = join(CONFIG_DIR, 'jest.config.js');
export const JEST_FILE_MOCK_FILE = join(CONFIG_DIR, 'jest.file-mock.js');
export const JEST_STYLE_MOCK_FILE = join(CONFIG_DIR, 'jest.style-mock.js');
export const JEST_TRANSFORM_FILE = join(CONFIG_DIR, 'jest.transform.js');
export const POSTCSS_CONFIG_FILE = join(CONFIG_DIR, 'postcss.config.js');

View File

@@ -0,0 +1,67 @@
import fs from 'fs-extra';
import { join } from 'path';
import { SRC_DIR } from './constant';
export const EXT_REGEXP = /\.\w+$/;
export const SFC_REGEXP = /\.(vue)$/;
export const DEMO_REGEXP = /\/demo$/;
export const TEST_REGEXP = /\/test$/;
export const STYLE_REGEXP = /\.(css|less|scss)$/;
export const SCRIPT_REGEXP = /\.(js|ts|jsx|tsx)$/;
export const ENTRY_EXTS = ['js', 'ts', 'tsx', 'jsx', 'vue'];
export function removeExt(path: string) {
return path.replace('.js', '');
}
export function replaceExt(path: string, ext: string) {
return path.replace(EXT_REGEXP, ext);
}
export function getComponents() {
const EXCLUDES = ['.DS_Store'];
const dirs = fs.readdirSync(SRC_DIR);
return dirs
.filter(dir => !EXCLUDES.includes(dir))
.filter(dir =>
ENTRY_EXTS.some(ext => fs.existsSync(join(SRC_DIR, dir, `index.${ext}`)))
);
}
export function isDir(dir: string) {
return fs.lstatSync(dir).isDirectory();
}
export function isDemoDir(dir: string) {
return DEMO_REGEXP.test(dir);
}
export function isTestDir(dir: string) {
return TEST_REGEXP.test(dir);
}
export function isSfc(path: string) {
return SFC_REGEXP.test(path);
}
export function isStyle(path: string) {
return STYLE_REGEXP.test(path);
}
export function isScript(path: string) {
return SCRIPT_REGEXP.test(path);
}
const camelizeRE = /-(\w)/g;
const pascalizeRE = /(\w)(\w*)/g;
export function camelize(str: string): string {
return str.replace(camelizeRE, (_, c) => c.toUpperCase());
}
export function pascalize(str: string): string {
return camelize(str).replace(
pascalizeRE,
(_, c1, c2) => c1.toUpperCase() + c2
);
}

View File

@@ -0,0 +1,14 @@
import { transformFileSync } from '@babel/core';
import { removeSync, outputFileSync } from 'fs-extra';
import { replaceExt } from '../common';
export function compileJs(filePath: string) {
const result = transformFileSync(filePath);
if (result) {
const jsFilePath = replaceExt(filePath, '.js');
removeSync(filePath);
outputFileSync(jsFilePath, result.code);
}
}

View File

@@ -0,0 +1,100 @@
import * as compiler from 'vue-template-compiler';
import * as compileUtils from '@vue/component-compiler-utils';
import { parse } from 'path';
import { removeSync, writeFileSync, readFileSync } from 'fs-extra';
import { replaceExt } from '../common';
import { compileJs } from './compile-js';
import { compileStyle } from './compile-style';
const RENDER_FN = '__vue_render__';
const STATIC_RENDER_FN = '__vue_staticRenderFns__';
const EXPORT = 'export default {';
// trim some unused code
function trim(code: string) {
return code.replace(/\/\/\n/g, '').trim();
}
// inject render fn to script
function injectRender(script: string, render: string) {
script = trim(script);
render = render
.replace('var render', `var ${RENDER_FN}`)
.replace('var staticRenderFns', `var ${STATIC_RENDER_FN}`);
return script.replace(
EXPORT,
`${render}\n${EXPORT}\n render: ${RENDER_FN},\n\n staticRenderFns: ${STATIC_RENDER_FN},\n`
);
}
function injectStyle(
script: string,
styles: compileUtils.SFCBlock[],
filePath: string
) {
if (styles.length) {
const imports = styles
.map((style, index) => {
const prefix = index !== 0 ? `-${index + 1}` : '';
const { base } = parse(replaceExt(filePath, `${prefix}.css`));
return `import './${base}';`;
})
.join('\n');
return script.replace(EXPORT, `${imports}\n\n${EXPORT}`);
}
return script;
}
function compileTemplate(template: string) {
const result = compileUtils.compileTemplate({
compiler,
source: template,
isProduction: true
} as any);
return result.code;
}
export async function compileSfc(filePath: string) {
const source = readFileSync(filePath, 'utf-8');
const jsFilePath = replaceExt(filePath, '.js');
const descriptor = compileUtils.parse({
source,
compiler,
needMap: false
} as any);
const { template, styles } = descriptor;
removeSync(filePath);
// compile js part
if (descriptor.script) {
let script = descriptor.script.content;
script = injectStyle(script, styles, filePath);
if (template) {
const render = compileTemplate(template.content);
script = injectRender(script, render);
}
writeFileSync(jsFilePath, script);
await compileJs(jsFilePath);
}
// compile style part
styles.forEach(async (style, index: number) => {
const prefix = index !== 0 ? `-${index + 1}` : '';
const ext = style.lang || 'css';
const cssFilePath = replaceExt(filePath, `${prefix}.${ext}`);
writeFileSync(cssFilePath, trim(style.content));
await compileStyle(cssFilePath);
});
}

View File

@@ -0,0 +1,46 @@
import postcss from 'postcss';
import postcssrc from 'postcss-load-config';
import { parse } from 'path';
import { render as renderLess } from 'less';
import { renderSync as renderSass } from 'sass';
import { readFileSync, writeFileSync } from 'fs';
import { replaceExt } from '../common';
import { POSTCSS_CONFIG_FILE } from '../common/constant';
async function compilePostcss(filePath: string, source: string | Buffer) {
const config = await postcssrc({}, POSTCSS_CONFIG_FILE);
const output = await postcss(config.plugins as any).process(source, {
from: undefined
});
writeFileSync(filePath, output);
}
async function compileLess(filePath: string) {
const source = readFileSync(filePath, 'utf-8');
const { css } = await renderLess(source, {
filename: filePath
});
return css;
}
async function compileSass(filePath: string) {
const { css } = renderSass({ file: filePath });
return css;
}
export async function compileStyle(filePath: string) {
const parsedPath = parse(filePath);
if (parsedPath.ext === '.less') {
const source = await compileLess(filePath);
await compilePostcss(replaceExt(filePath, '.css'), source);
} else if (parsedPath.ext === '.scss') {
const source = await compileSass(filePath);
await compilePostcss(replaceExt(filePath, '.css'), source);
} else {
const source = readFileSync(filePath, 'utf-8');
await compilePostcss(filePath, source);
}
}

View File

@@ -0,0 +1,55 @@
import { join, relative } from 'path';
import { existsSync, writeFileSync } from 'fs-extra';
import { pascalize, removeExt, getComponents } from '../common';
import {
SRC_DIR,
CONFIG_FILE,
DIST_DIR,
DESKTOP_CONFIG_FILE
} from '../common/constant';
function checkDocumentExists(component: string) {
const absolutePath = join(SRC_DIR, component, 'README.md');
return existsSync(absolutePath);
}
function genImportDocuments(components: string[]) {
return components
.filter(component => checkDocumentExists(component))
.map(component => {
const absolutePath = join(SRC_DIR, component, 'README.md');
const relativePath = relative(DIST_DIR, absolutePath);
return `import ${pascalize(component)} from '${relativePath}';`;
})
.join('\n');
}
function genExportDocuments(components: string[]) {
return `export const documents = {
${components
.filter(component => checkDocumentExists(component))
.map(component => pascalize(component))
.join(',\n ')}
};`;
}
function genImportConfig() {
const configRelative = relative(DIST_DIR, CONFIG_FILE);
return `import config from '${removeExt(configRelative)}';`;
}
function genExportConfig() {
return 'export { config };';
}
export function genDesktopConfig() {
const components = getComponents();
const code = `${genImportConfig()}
${genImportDocuments(components)}
${genExportConfig()}
${genExportDocuments(components)}
`;
writeFileSync(DESKTOP_CONFIG_FILE, code);
}

View File

@@ -0,0 +1,42 @@
import { join, relative } from 'path';
import { existsSync, ensureDirSync, writeFileSync } from 'fs-extra';
import { pascalize, removeExt, getComponents } from '../common';
import { SRC_DIR, DIST_DIR, MOBILE_CONFIG_FILE } from '../common/constant';
function checkDemoExists(component: string) {
const absolutePath = join(SRC_DIR, component, 'demo/index.vue');
return existsSync(absolutePath);
}
function genImports(components: string[]) {
return components
.filter(component => checkDemoExists(component))
.map(component => {
const absolutePath = join(SRC_DIR, component, 'demo/index.vue');
const relativePath = relative(DIST_DIR, absolutePath);
return `import ${pascalize(component)} from '${removeExt(
relativePath
)}';`;
})
.join('\n');
}
function genExports(components: string[]) {
return `export const demos = {\n ${components
.filter(component => checkDemoExists(component))
.map(component => pascalize(component))
.join(',\n ')}\n};`;
}
function genCode(components: string[]) {
return `${genImports(components)}\n\n${genExports(components)}\n`;
}
export function genMobileConfig() {
const components = getComponents();
const code = genCode(components);
ensureDirSync(DIST_DIR);
writeFileSync(MOBILE_CONFIG_FILE, code);
}

View File

@@ -0,0 +1,42 @@
module.exports = function(api: any) {
const { BABEL_MODULE, NODE_ENV } = process.env;
const isTest = NODE_ENV === 'test';
const useESModules = BABEL_MODULE !== 'commonjs' && !isTest;
api && api.cache(false);
return {
presets: [
[
'@babel/preset-env',
{
loose: true,
modules: useESModules ? false : 'commonjs'
}
],
[
'@vue/babel-preset-jsx',
{
functional: false
}
],
'@babel/preset-typescript'
],
plugins: [
[
'@babel/plugin-transform-runtime',
{
corejs: false,
helpers: true,
regenerator: isTest,
useESModules
}
],
'@babel/plugin-transform-object-assign',
'@babel/plugin-proposal-optional-chaining'
]
};
};
export default module.exports;

View File

@@ -0,0 +1,28 @@
import {
JEST_FILE_MOCK_FILE,
JEST_STYLE_MOCK_FILE
} from '../common/constant';
module.exports = {
moduleNameMapper: {
'\\.(css|less|scss)$': JEST_STYLE_MOCK_FILE,
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': JEST_FILE_MOCK_FILE
},
moduleFileExtensions: ['js', 'jsx', 'vue', 'ts', 'tsx'],
transform: {
'\\.(vue)$': 'vue-jest',
'\\.(js|jsx|ts|tsx)$': 'babel-jest'
},
transformIgnorePatterns: ['node_modules/(?!(vant|@babel\\/runtime)/)'],
snapshotSerializers: ['jest-serializer-vue'],
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx,vue}',
'!**/style/**',
'!**/demo/**',
'!**/locale/lang/**',
'!**/sku/**'
],
collectCoverage: true,
coverageReporters: ['html', 'lcov', 'text-summary'],
coverageDirectory: './test/coverage'
};

View File

@@ -0,0 +1 @@
module.exports = 'test-file-stub';

View File

@@ -0,0 +1 @@
module.exports = {};

View File

@@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {}
}
};

View File

@@ -0,0 +1,78 @@
import sass from 'sass';
import { VueLoaderPlugin } from 'vue-loader';
import { POSTCSS_CONFIG_FILE } from '../common/constant';
const CSS_LOADERS = [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
config: {
path: POSTCSS_CONFIG_FILE
}
}
}
];
module.exports = {
mode: 'development',
resolve: {
extensions: ['.js', '.ts', '.tsx', '.jsx', '.vue', '.less']
},
module: {
rules: [
{
test: /\.vue$/,
use: [
{
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
}
}
}
]
},
{
test: /\.(js|ts|jsx|tsx)$/,
exclude: /node_modules\/(?!(@youzan\/create-vue-components))/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.css$/,
sideEffects: true,
use: CSS_LOADERS
},
{
test: /\.less$/,
sideEffects: true,
use: [...CSS_LOADERS, 'less-loader']
},
{
test: /\.scss$/,
sideEffects: true,
use: [
...CSS_LOADERS,
{
loader: 'sass-loader',
options: {
implementation: sass
}
}
]
},
{
test: /\.md$/,
use: ['vue-loader', '@vant/markdown-loader']
}
]
},
plugins: [new VueLoaderPlugin()]
};
export default module.exports;

View File

@@ -0,0 +1,48 @@
import { join } from 'path';
import merge from 'webpack-merge';
import config from './webpack.base';
import HtmlWebpackPlugin from 'html-webpack-plugin';
module.exports = merge(config, {
entry: {
'site-desktop': join(__dirname, '../../site/desktop/main.js'),
'site-mobile': join(__dirname, '../../site/mobile/main.js')
},
devServer: {
open: true,
host: '0.0.0.0',
stats: 'errors-only',
disableHostCheck: true,
},
output: {
path: join(__dirname, '../../site/dist'),
publicPath: '/',
chunkFilename: 'async_[name].js'
},
optimization: {
splitChunks: {
cacheGroups: {
chunks: {
chunks: 'all',
minChunks: 2,
minSize: 0,
name: 'chunks'
}
}
}
},
plugins: [
new HtmlWebpackPlugin({
chunks: ['chunks', 'site-desktop'],
template: join(__dirname, '../../site/desktop/index.html'),
filename: 'index.html'
}),
new HtmlWebpackPlugin({
chunks: ['chunks', 'site-mobile'],
template: join(__dirname, '../../site/mobile/index.html'),
filename: 'mobile.html'
})
]
});
export default module.exports;

View File

@@ -0,0 +1,15 @@
import { join } from 'path';
import merge from 'webpack-merge';
import config from './webpack.site.dev';
module.exports = merge(config, {
mode: 'production',
output: {
path: join(__dirname, '../../site/dist'),
publicPath: 'https://b.yzcdn.cn/vant/',
filename: '[name].[hash:8].js',
chunkFilename: 'async_[name].[chunkhash:8].js'
}
});
export default module.exports;

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env node
const commander = require('commander');
const changelog = require('./changelog');
const commitLint = require('./commit-lint');
commander
.command('changelog <dir>')
.option('--tag [tag]', 'Since tag')
.action(changelog);
commander
.command('commit-lint')
.action(commitLint);
commander.parse(process.argv);

33
packages/vant-cli/src/index.ts Executable file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env node
import { command, parse } from 'commander';
import { dev } from './commands/dev';
import { test } from './commands/test';
import { lint } from './commands/lint';
import { clean } from './commands/clean';
import { build } from './commands/build';
import { release } from './commands/release';
import { changelog } from './commands/changelog';
import { commitLint } from './commands/commit-lint';
command('dev').action(dev);
command('lint').action(lint);
command('clean').action(clean);
command('build').action(build);
command('release').action(release);
command('changelog <dir>')
.option('--tag [tag]', 'Since tag')
.action(changelog);
command('commit-lint').action(commitLint);
command('test')
.option('--watch')
.action(test);
parse(process.argv);