oak-cli 项目新建

This commit is contained in:
Wang Kejun 2022-04-21 17:38:22 +08:00
parent 8511d289be
commit 430dace767
59 changed files with 3339 additions and 1 deletions

4
.gitignore vendored
View File

@ -83,4 +83,6 @@ dist
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.pnp.*
package-lock.json

30
config/env.js Normal file
View File

@ -0,0 +1,30 @@
const path = require('path');
/** 环境变量 */
exports.NODE_ENV = process.argv.splice(2, 1)[0];
/** 项目路径 */
exports.ROOT = path.join(process.cwd(), 'wechatMp');
/** 源代码存放路径 */
exports.SOURCE = path.resolve(this.ROOT, 'src');
/** 目标代码存放路径 */
exports.DESTINATION = path.resolve(this.ROOT, 'dist');
/** 配置脚本文件路径 */
exports.SCRIPTS = path.resolve(this.ROOT, 'scripts');
/** .env 配置文件路径 */
exports.ENV_CONFIG = path.resolve(this.ROOT, '.env');
/** 默认配置文件 */
exports.DEFAULT_CONFIG = {
platform: 'wx',
css_unit_ratio: 1,
};
/** 平台映射字典 */
exports.PLATFORM_CONFIG = {
wx: {
template: '.wxml',
style: '.wxss',
},
swan: {
template: '.swan',
style: '.css',
},
};

171
config/webpack.config.js Normal file
View File

@ -0,0 +1,171 @@
const webpack = require('webpack');
const chalk = require('chalk');
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const StylelintPlugin = require('stylelint-webpack-plugin');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const UiExtractPlugin = require('ui-extract-webpack-plugin');
const Dotenv = require('dotenv-webpack');
// const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const OakWeChatMpPlugin = require('../plugins/WechatMpPlugin');
const {
ROOT,
SOURCE,
DESTINATION,
NODE_ENV,
PLATFORM_CONFIG,
ENV_CONFIG,
} = require('./env');
const __DEV__ = NODE_ENV === 'development';
const relativeFileLoader = (ext = '[ext]') => {
return {
loader: 'file-loader',
options: {
useRelativePath: true,
name: `[path][name].${ext}`,
context: SOURCE,
},
};
};
const oakLoader = (ext = '[ext]') => {
return {
loader: 'file-loader',
options: {
useRelativePath: true,
name: `[path][name].${ext}`,
outputPath: (url, resourcePath, context) => {
const outputPath = url.split(
'oak-general-business/src/platforms/wechatMp/'
)[1];
return outputPath;
},
context: SOURCE,
},
};
};
module.exports = {
context: SOURCE,
devtool: __DEV__ ? 'source-map' : false,
mode: NODE_ENV,
target: 'web',
entry: {
app: '../src/app',
},
output: {
filename: '[name].js',
path: DESTINATION,
publicPath: '/',
globalObject: 'global',
},
resolve: {
alias: {
'@': SOURCE,
assert: require.resolve('assert'),
},
extensions: ['.ts', '.js', 'json'],
symlinks: true,
fallback: {
crypto: require.resolve('crypto-browserify'),
},
},
module: {
rules: [
{
test: /\.less$/,
include: /src/,
exclude: /node_modules/,
use: [
relativeFileLoader('wxss'),
{
loader: 'less-loader',
},
],
},
{
test: /\.less$/,
include: /oak-general-business/,
type: 'javascript/auto',
use: [
oakLoader('wxss'),
{
loader: 'less-loader',
options: {
lessOptions: () => {
const oakConfigJson = require(`${SOURCE}/oak.config.json`);
return {
javascriptEnabled: true,
modifyVars: oakConfigJson.theme,
};
},
},
},
],
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.ts$/,
exclude: /node_modules/,
loader: 'ts-loader',
},
// {
// test: /\.json$/,
// include: /src/,
// type: 'asset/resource',
// generator: {
// filename: `[path][name].[ext]`,
// },
// // type: 'javascript/auto',
// // use: [relativeFileLoader('json')],
// },
{
test: /\.(xml|wxml)$/,
include: /src/,
type: 'asset/resource',
generator: {
filename: `[path][name].[ext]`,
},
// use: [relativeFileLoader('wxml')],
},
],
},
plugins: [
// new CleanWebpackPlugin(),
new UiExtractPlugin({ context: SOURCE }),
new OakWeChatMpPlugin({
exclude: ['*/weui-miniprogram/*'],
include: ['project.config.json', 'sitemap.json'],
}),
/* new webpack.DefinePlugin({
['process.env.NODE_ENV']: JSON.stringify(NODE_ENV),
}), */
// new MiniCssExtractPlugin({ filename: `[name]${PLATFORM_CONFIG[yamlConfig.platform].style}` }),
new StylelintPlugin({
fix: true,
files: '**/*.(sa|sc|le|wx|c)ss',
}),
new ProgressBarPlugin({
summary: false,
format: ':msg :percent (:elapsed seconds)',
customSummary: (buildTime) =>
console.log(
chalk.gray(`\n[${new Date().toLocaleDateString()}`),
chalk.green(`Compiled successfully!(${buildTime})\n`)
),
}),
new Dotenv({ path: ENV_CONFIG, silent: true }),
],
watch: true,
watchOptions: {
aggregateTimeout: 600,
ignored: '**/node_modules',
followSymlinks: true,
},
};

1
lib/build.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export default function build(env: string): Promise<void>;

22
lib/build.js Normal file
View File

@ -0,0 +1,22 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const tip_style_1 = require("./tip-style");
const cross_spawn_1 = __importDefault(require("cross-spawn"));
async function build(env) {
(0, tip_style_1.Success)(`${(0, tip_style_1.success)(`build环境:${env}`)}`);
const result = cross_spawn_1.default.sync(process.execPath, [require.resolve('../scripts/' + 'webpack.js')].concat([env]), {
stdio: 'inherit',
shell: true,
});
// const result = spawn.sync('npm -v', [], { stdio: 'inherit', shell: true });
if (result.status === 0) {
(0, tip_style_1.Success)(`${(0, tip_style_1.success)(`执行完成`)}`);
}
else {
(0, tip_style_1.Error)(`${(0, tip_style_1.error)(`执行失败`)}`);
}
}
exports.default = build;

8
lib/config.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
export declare const CLI_VERSION: any;
export declare const CLI_NAME: any;
export declare const BASE_DIR: string;
export declare const USER_CONFIG_FILE_NAME = "oak.config.json";
export declare const USER_CONFIG_FILE: string;
export declare const NODE_MODULES_DIR_NAME = "node_modules";
export declare const CNPM_BASE_URL = "";
export declare const MINI_VERSION_URL = "https://mp.weixin.qq.com/debug/getpublibpercentage";

11
lib/config.js Normal file
View File

@ -0,0 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MINI_VERSION_URL = exports.CNPM_BASE_URL = exports.NODE_MODULES_DIR_NAME = exports.USER_CONFIG_FILE = exports.USER_CONFIG_FILE_NAME = exports.BASE_DIR = exports.CLI_NAME = exports.CLI_VERSION = void 0;
exports.CLI_VERSION = require('../package.json')['version'];
exports.CLI_NAME = require('../package.json')['name'];
exports.BASE_DIR = process.cwd();
exports.USER_CONFIG_FILE_NAME = 'oak.config.json';
exports.USER_CONFIG_FILE = exports.BASE_DIR + '/' + exports.USER_CONFIG_FILE_NAME;
exports.NODE_MODULES_DIR_NAME = 'node_modules';
exports.CNPM_BASE_URL = ''; //oak-cli
exports.MINI_VERSION_URL = 'https://mp.weixin.qq.com/debug/getpublibpercentage';

1
lib/create.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export default function create(dirName: string, env: string): Promise<void>;

145
lib/create.js Normal file
View File

@ -0,0 +1,145 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const file_handle_1 = require("./file-handle");
const enum_1 = require("./enum");
const config_1 = require("./config");
const template_1 = require("./template");
const path_1 = require("path");
const inquirer_1 = __importDefault(require("inquirer"));
const axios_1 = __importDefault(require("axios"));
const tip_style_1 = require("./tip-style");
const shelljs_1 = __importDefault(require("shelljs"));
const prompt = [
{
type: 'input',
name: 'version',
message: 'version',
default: '1.0.0',
},
{
type: 'input',
name: 'description',
message: 'description',
},
];
/**
* @name 检查项目名是否已存在
* @param dirName
*/
function checkProjectName(dirName) {
// 项目根路径
const rootPath = process.cwd() + '/' + dirName;
const isExists = (0, file_handle_1.checkFileExists)(rootPath);
if (isExists) {
console.error((0, tip_style_1.error)(`Cannot create a project named ${(0, tip_style_1.success)(`"${dirName}"`)} because a path with the same name exists.\n`) + (0, tip_style_1.error)('Please choose a different project name.'));
process.exit(1);
}
}
/**
* @name 获取oak-cli最新版本号
* @returns
*/
async function getOakCliVersion() {
const res = await axios_1.default.get(config_1.CNPM_BASE_URL);
return res.data['dist-tags']['latest'];
}
/**
* @name 获取微信小程序稳定基础版本库
* @returns
*/
async function getMiniVersion() {
const res = await axios_1.default.get(config_1.MINI_VERSION_URL);
const versions = JSON.parse(res.data['json_data'])['total'];
const versionsSort = versions.sort((a, b) => {
return b['percentage'] - a['percentage'];
});
return versionsSort[0]['sdkVer'];
}
async function create(dirName, env) {
const nameOption = {
type: 'input',
name: 'name',
message: `name`,
default: dirName,
};
prompt.unshift(nameOption);
const isDev = env === 'dev' || env === 'development';
const { name, version, description } = await inquirer_1.default.prompt(prompt);
// 获取微信小程序稳定基础版本库
const miniVersion = await getMiniVersion();
// 获取package.json内容
const packageJson = (0, template_1.packageJsonContent)({
name,
version,
description,
cliversion: config_1.CLI_VERSION,
cliname: config_1.CLI_NAME,
isDev,
});
// 获取tsconfig.json内容
const tsconfigJson = (0, template_1.tsConfigJsonContent)();
// 获取小程序项目project.config.json内容
const projectConfigWithWeChatMp = (0, template_1.projectConfigContentWithWeChatMp)(config_1.USER_CONFIG_FILE_NAME, 'wechatMp', miniVersion);
// 获取小程序项目oak.config.json内容
const oakConfigWithWeChatMp = (0, template_1.oakConfigContentWithWeChatMp)();
// 项目根路径
const rootPath = process.cwd() + '/' + dirName;
// package.json路径
const packageJsonPath = `${rootPath}/package.json`;
// tsconfig.json路径
const tsconfigJsonPath = `${rootPath}/tsconfig.json`;
// web项目根路径
const webRootPath = `${rootPath}/web`;
// 小程序项目根路径
const weChatMpRootPath = `${rootPath}/wechatMp`;
// 小程序项目project.config.json路径
const projectConfigPathWithWeChatMp = `${weChatMpRootPath}/src/project.config.json`;
// 小程序项目project.config.json路径
const oakConfigPathWithWeChatMp = `${weChatMpRootPath}/src/${config_1.USER_CONFIG_FILE_NAME}`;
// 被复制的文件夹路径
const currentPath = (0, path_1.join)(__dirname, '..') + '/template';
//检查项目名是否存在
checkProjectName(dirName);
try {
// 创建根目录
(0, file_handle_1.checkFileExistsAndCreate)(rootPath);
// 创建package.json
(0, file_handle_1.checkFileExistsAndCreate)(packageJsonPath, packageJson, enum_1.checkFileExistsAndCreateType.FILE);
// 创建tsconfig.json
(0, file_handle_1.checkFileExistsAndCreate)(tsconfigJsonPath, tsconfigJson, enum_1.checkFileExistsAndCreateType.FILE);
// 复制项目文件
(0, file_handle_1.copyFolder)(currentPath, rootPath);
// 创建小程序项目project.config.json
(0, file_handle_1.checkFileExistsAndCreate)(projectConfigPathWithWeChatMp, projectConfigWithWeChatMp, enum_1.checkFileExistsAndCreateType.FILE);
// 创建小程序项目oak.config.json
(0, file_handle_1.checkFileExistsAndCreate)(oakConfigPathWithWeChatMp, oakConfigWithWeChatMp, enum_1.checkFileExistsAndCreateType.FILE);
if (!shelljs_1.default.which('npm')) {
(0, tip_style_1.Warn)((0, tip_style_1.warn)('Sorry, this script requires npm! Please install npm!'));
shelljs_1.default.exit(1);
}
(0, tip_style_1.Success)(`${(0, tip_style_1.success)(`Waiting...`)}`);
(0, tip_style_1.Success)(`${(0, tip_style_1.success)(`Dependencies are now being installed`)}`);
shelljs_1.default.cd(dirName).exec('npm install');
// checkFileExistsAndCreate(weChatMpRootPath + '/src/styles');
// //const data = readFile(
// // `${rootPath}/node_modules/oak-frontend-boilerplate/src/platforms/wechatMp/styles/base.less`
// //);
// const data = readFile(
// `${rootPath}/node_modules/oak-general-business/src/platforms/wechatMp/styles/base.less`
// );
// checkFileExistsAndCreate(
// weChatMpRootPath + '/src/styles/base.less',
// data,
// checkFileExistsAndCreateType.FILE
// );
(0, tip_style_1.Success)(`${(0, tip_style_1.success)(`Successfully created project ${(0, tip_style_1.primary)(name)}, directory name is ${(0, tip_style_1.primary)(dirName)}`)}`);
}
catch (err) {
(0, tip_style_1.Error)((0, tip_style_1.error)('create error'));
(0, tip_style_1.Error)((0, tip_style_1.error)(err));
}
}
exports.default = create;

9
lib/enum.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
/**
* @name
* @export
* @enum {number}
*/
export declare enum checkFileExistsAndCreateType {
DIRECTORY = 0,
FILE = 1
}

13
lib/enum.js Normal file
View File

@ -0,0 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkFileExistsAndCreateType = void 0;
/**
* @name 检查文件时用到的枚举
* @export
* @enum {number}
*/
var checkFileExistsAndCreateType;
(function (checkFileExistsAndCreateType) {
checkFileExistsAndCreateType[checkFileExistsAndCreateType["DIRECTORY"] = 0] = "DIRECTORY";
checkFileExistsAndCreateType[checkFileExistsAndCreateType["FILE"] = 1] = "FILE";
})(checkFileExistsAndCreateType = exports.checkFileExistsAndCreateType || (exports.checkFileExistsAndCreateType = {}));

63
lib/file-handle.d.ts vendored Normal file
View File

@ -0,0 +1,63 @@
/// <reference types="node" />
import { PathLike } from 'fs';
import { checkFileExistsAndCreateType } from './enum';
/**
* @name
* @export
* @param {string} entry
*/
export declare function readDirPath(entry: string): Set<string>;
/**
* @name
* @export
* @param {*} entry
* @returns
*/
export declare function readDirGetFile(entry: string): string[];
/**
* @name json文件()
* @export
* @param {Array<string>} arr
* @returns
*/
export declare function parseJsonFiles(arr: Set<string>): any[];
/**
* @name json
* @export
* @param {PathLike} file
* @returns
*/
export declare function parseJsonFile(file: string): any;
/**
* @name
* @export
* @param {string} entry
*/
export declare function deleteFolderRecursive(entry: string): void;
export declare function writeFile(path: string | PathLike, data: any): void;
export declare function readFile(path: string | PathLike, options?: {
encoding?: null | undefined;
flag?: string | undefined;
} | null): Buffer | undefined;
/**
* @name
* @export
* @param {PathLike} currentDir
* @param {PathLike} targetDir
*/
export declare function copyFolder(currentDir: PathLike, targetDir: PathLike): void;
/**
* @name /
* @export
* @param {(PathLike | string)} path
* @returns
*/
export declare function checkFileExists(path: PathLike | string): boolean;
/**
* @name /
* @export
* @param {(PathLike | string)} path
* @param {*} [data]
* @param {checkFileExistsAndCreateType} [type=checkFileExistsAndCreateType.DIRECTORY]
*/
export declare function checkFileExistsAndCreate(path: PathLike | string, data?: any, type?: checkFileExistsAndCreateType): void;

206
lib/file-handle.js Normal file
View File

@ -0,0 +1,206 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkFileExistsAndCreate = exports.checkFileExists = exports.copyFolder = exports.readFile = exports.writeFile = exports.deleteFolderRecursive = exports.parseJsonFile = exports.parseJsonFiles = exports.readDirGetFile = exports.readDirPath = void 0;
const fs_1 = require("fs");
const path_1 = require("path");
const enum_1 = require("./enum");
const tip_style_1 = require("./tip-style");
const pathList = new Set();
/**
* @name 读取目录下所有文件
* @export
* @param {string} entry 目录名称
*/
function readDirPath(entry) {
const dirInfo = (0, fs_1.readdirSync)(entry);
for (let item of dirInfo) {
const location = (0, path_1.join)(entry, item);
const info = (0, fs_1.statSync)(location);
if (info.isDirectory()) {
readDirPath(location);
}
else {
pathList.add(location);
}
}
return pathList;
}
exports.readDirPath = readDirPath;
/**
* @name 读取指定目录的文件不进行深度遍历只获取根目录
* @export
* @param {*} entry
* @returns
*/
function readDirGetFile(entry) {
const dirInfo = (0, fs_1.readdirSync)(entry);
return dirInfo;
}
exports.readDirGetFile = readDirGetFile;
/**
* @name 解析json文件(数组)
* @export
* @param {Array<string>} arr
* @returns
*/
function parseJsonFiles(arr) {
const result = [];
for (let item of arr) {
const data = parseJsonFile(item);
result.push(data);
}
return result;
}
exports.parseJsonFiles = parseJsonFiles;
/**
* @name 解析单个文件json
* @export
* @param {PathLike} file
* @returns
*/
function parseJsonFile(file) {
try {
const data = (0, fs_1.readFileSync)(file, 'utf8');
return JSON.parse(data);
}
catch (error) {
return;
}
}
exports.parseJsonFile = parseJsonFile;
/**
* @name 删除文件夹
* @export
* @param {string} entry
*/
function deleteFolderRecursive(entry) {
let files = [];
// 判断给定的路径是否存在
if ((0, fs_1.existsSync)(entry)) {
// 返回文件和子目录的数组
files = (0, fs_1.readdirSync)(entry);
for (let file of files) {
const curPath = (0, path_1.join)(entry, file);
// fs.statSync同步读取文件夹文件如果是文件夹在重复触发函数
if ((0, fs_1.statSync)(curPath).isDirectory()) { // recurse
deleteFolderRecursive(curPath);
// 是文件delete file
}
else {
(0, fs_1.unlinkSync)(curPath);
}
}
// 清除文件夹
(0, fs_1.rmdirSync)(entry);
}
else {
// console.log("文件夹不存在");
}
}
exports.deleteFolderRecursive = deleteFolderRecursive;
;
function writeFile(path, data) {
try {
(0, fs_1.writeFileSync)(path, data);
}
catch (err) {
(0, tip_style_1.Error)((0, tip_style_1.error)(err));
(0, tip_style_1.Error)((0, tip_style_1.error)('文件写入失败'));
}
}
exports.writeFile = writeFile;
function readFile(path, options) {
try {
const data = (0, fs_1.readFileSync)(path, options);
return data;
}
catch (err) {
(0, tip_style_1.Error)((0, tip_style_1.error)(err));
(0, tip_style_1.Error)((0, tip_style_1.error)('文件读取失败'));
}
}
exports.readFile = readFile;
/**
* @name 拷贝文件夹
* @export
* @param {PathLike} currentDir
* @param {PathLike} targetDir
*/
function copyFolder(currentDir, targetDir) {
function handleFolder(currentDir, targetDir) {
const files = (0, fs_1.readdirSync)(currentDir, {
withFileTypes: true
});
for (let file of files) {
// 拼接文件绝对路径
const copyCurrentFileInfo = currentDir + '/' + file.name;
const copyTargetFileInfo = targetDir + '/' + file.name;
// 判断文件是否存在
const readCurrentFile = (0, fs_1.existsSync)(copyCurrentFileInfo);
const readTargetFile = (0, fs_1.existsSync)(copyTargetFileInfo);
if (readCurrentFile && !readTargetFile) {
// 判断是否为文件,如果为文件则复制,文件夹则递归
if (file.isFile()) {
const readStream = (0, fs_1.createReadStream)(copyCurrentFileInfo);
const writeStream = (0, fs_1.createWriteStream)(copyTargetFileInfo);
readStream.pipe(writeStream);
}
else {
try {
(0, fs_1.accessSync)((0, path_1.join)(copyTargetFileInfo, '..'), fs_1.constants.W_OK);
copyFolder(copyCurrentFileInfo, copyTargetFileInfo);
}
catch (error) {
(0, tip_style_1.Warn)('权限不足' + error);
}
}
}
else {
(0, tip_style_1.Error)((0, tip_style_1.error)('操作失败target文件夹已存在或current文件夹不存在'));
}
}
}
if ((0, fs_1.existsSync)(currentDir)) {
if (!(0, fs_1.existsSync)(targetDir)) {
(0, fs_1.mkdirSync)(targetDir);
}
handleFolder(currentDir, targetDir);
}
else {
(0, tip_style_1.Warn)((0, tip_style_1.warn)('需要copy的文件夹不存在:' + currentDir));
}
}
exports.copyFolder = copyFolder;
/**
* @name 检测文件/文件夹是否存在
* @export
* @param {(PathLike | string)} path
* @returns
*/
function checkFileExists(path) {
return (0, fs_1.existsSync)(path);
}
exports.checkFileExists = checkFileExists;
/**
* @name 检测文件/文件夹是否存在不存在则创建
* @export
* @param {(PathLike | string)} path
* @param {*} [data]
* @param {checkFileExistsAndCreateType} [type=checkFileExistsAndCreateType.DIRECTORY]
*/
function checkFileExistsAndCreate(path, data, type = enum_1.checkFileExistsAndCreateType.DIRECTORY) {
if (!checkFileExists(path)) {
switch (type) {
case enum_1.checkFileExistsAndCreateType.DIRECTORY:
(0, fs_1.mkdirSync)(path);
break;
case enum_1.checkFileExistsAndCreateType.FILE:
writeFile(path, data);
break;
default:
(0, fs_1.mkdirSync)(path);
break;
}
}
}
exports.checkFileExistsAndCreate = checkFileExistsAndCreate;

2
lib/index.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
#!/usr/bin/env node
export {};

75
lib/index.js Executable file
View File

@ -0,0 +1,75 @@
#!/usr/bin/env node
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const commander_1 = __importDefault(require("commander"));
const create_1 = __importDefault(require("./create"));
const build_1 = __importDefault(require("./build"));
const make_1 = __importDefault(require("./make"));
const config_1 = require("./config");
const tip_style_1 = require("./tip-style");
/**
* @name 未知参数错误提示
* @param {string} methodName
* @param {Function} log
*/
function enhanceErrorMessages(methodName, log) {
commander_1.default.Command.prototype[methodName] = function (...args) {
if (methodName === 'unknownOption' && this._allowUnknownOption) {
return;
}
this.outputHelp();
console.log(` ` + (0, tip_style_1.error)(log(...args)));
console.log();
process.exit(1);
};
}
const currentNodeVersion = process.versions.node;
const semver = currentNodeVersion.split('.');
const major = semver[0];
const minNodeVersion = 14;
if (Number(major) < minNodeVersion) {
console.error('You are running Node ' +
currentNodeVersion +
'.\n' +
'Create React App requires Node ' +
minNodeVersion +
' or higher. \n' +
'Please update your version of Node.');
process.exit(1);
}
commander_1.default.version(config_1.CLI_VERSION, '-v, --version').usage('<command> [options]');
commander_1.default
.command('make')
.description('build oak app domain of make on demand')
.action(make_1.default);
commander_1.default
.command('start <env>')
.usage('<env>')
.description('build we chat mp of start on demand')
.action(build_1.default);
commander_1.default
.command('build <env>')
.usage('<env>')
.description('build we chat mp of build on demand')
.action(build_1.default);
commander_1.default
.command('create <name> [env]')
.usage('<name>')
// .option('-e, --env <env>', 'A env')
.description(`create a new project powered by ${config_1.CLI_NAME}`)
.action(create_1.default);
// output help information on unknown commands
commander_1.default.arguments('<command>').action((cmd) => {
commander_1.default.outputHelp();
console.log();
console.log(` ` + `${(0, tip_style_1.error)(`Unknown command ${(0, tip_style_1.warn)(cmd)}`)}`);
console.log();
});
enhanceErrorMessages('missingArgument', (argName) => {
console.log();
return `${(0, tip_style_1.error)(`Missing required argument ${(0, tip_style_1.warn)(argName)}.`)}`;
});
commander_1.default.parse(process.argv);

56
lib/interface.d.ts vendored Normal file
View File

@ -0,0 +1,56 @@
/**
* @name package.json需要输入的参数
* @export
* @interface PackageJsonInput
*/
export interface PackageJsonInput {
name: string;
version?: string;
description?: string;
cliversion: string;
cliname: string;
isDev?: boolean;
}
/**
* @name Prompt需要输入的参数
* @export
* @interface PromptInput
*/
export interface PromptInput {
name: string;
version: string;
description: string;
}
/**
* @name project.config.json
* @export
* @interface ProjectConfigInterface
*/
export interface ProjectConfigInterface {
packOptions: PackOptions;
}
/**
* @name project.config.json PackOptions
* @export
* @interface PackOptions
*/
export interface PackOptions {
ignore: Array<PackOptionsIgnore>;
}
/**
* @name project.config.json PackOptions PackOptionsIgnore
* @export
* @interface PackOptionsIgnore
*/
export interface PackOptionsIgnore {
type: string;
value: string;
}
/**
* @name oak-cli需要输入的参数
* @export
* @interface OakInput
*/
export interface OakInput {
mode: string;
}

2
lib/interface.js Normal file
View File

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

1
lib/make.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export default function make(): Promise<void>;

37
lib/make.js Normal file
View File

@ -0,0 +1,37 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const tip_style_1 = require("./tip-style");
const cross_spawn_1 = __importDefault(require("cross-spawn"));
async function make() {
(0, tip_style_1.Success)(`${(0, tip_style_1.success)(`build oak app domain`)}`);
// ts-node scripts/build-app-domain & npm link ./app-domain
const result = cross_spawn_1.default.sync('ts-node', [require.resolve('../scripts/' + 'build-app-domain.js')], {
stdio: 'inherit',
shell: true,
});
// const result2 = spawn.sync('npm -v', [], { stdio: 'inherit', shell: true });
if (result.status === 0) {
(0, tip_style_1.Success)(`${(0, tip_style_1.success)(`build 执行完成`)}`);
}
else {
(0, tip_style_1.Error)(`${(0, tip_style_1.error)(`build 执行失败`)}`);
process.exit(1);
}
(0, tip_style_1.Success)(`${(0, tip_style_1.success)(`npm link oak app domain`)}`);
const isMac = process.platform === 'darwin';
const result2 = cross_spawn_1.default.sync(`${isMac ? 'sudo' : ''} npm`, [`link ${process.cwd()}/oak-app-domain`], {
stdio: 'inherit',
shell: true,
});
if (result2.status === 0) {
(0, tip_style_1.Success)(`${(0, tip_style_1.success)(`link 执行完成`)}`);
}
else {
(0, tip_style_1.Error)(`${(0, tip_style_1.error)(`link 执行失败`)}`);
process.exit(1);
}
}
exports.default = make;

5
lib/template.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import { PackageJsonInput } from './interface';
export declare function packageJsonContent({ name, version, description, cliversion, cliname, isDev, }: PackageJsonInput): string;
export declare function tsConfigJsonContent(): string;
export declare function projectConfigContentWithWeChatMp(oakConfigName: string, projectname: string, miniVersion: string): string;
export declare function oakConfigContentWithWeChatMp(): string;

203
lib/template.js Normal file
View File

@ -0,0 +1,203 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.oakConfigContentWithWeChatMp = exports.projectConfigContentWithWeChatMp = exports.tsConfigJsonContent = exports.packageJsonContent = void 0;
function packageJsonContent({ name, version, description, cliversion, cliname, isDev, }) {
let oakPackageStr;
if (isDev) {
oakPackageStr = `"${cliname}": "file:../${cliname}",
"oak-domain": "file:../oak-domain",
"oak-frontend-base": "file:../oak-frontend-base",
"oak-general-business": "file:../oak-general-business",
"oak-memory-tree-store": "file:../oak-memory-tree-store",`;
}
else {
oakPackageStr = `"${cliname}": "^${cliversion}",
"oak-domain": "^1.0.0",
"oak-frontend-base": "^1.0.0",
"oak-general-business": "^1.0.0",
"oak-memory-tree-store": "^1.0.0",`;
}
return `{
"name": "${name}",
"version": "${version}",
"description": "${description}",
"scripts": {
"make:domain": "${cliname} make",
"start:mp": "${cliname} start development",
"build:mp": "${cliname} build production"
},
"keywords": [],
"author": "",
"license": "",
"dependencies": {
"@reduxjs/toolkit": "^1.7.2",
"crypto-browserify": "^3.12.0",
"lodash": "^4.17.21"
},
"devDependencies": {
"@babel/cli": "^7.12.13",
"@babel/core": "^7.12.13",
"@babel/plugin-proposal-class-properties": "^7.12.13",
"@babel/preset-env": "^7.12.13",
"@babel/preset-typescript": "^7.12.13",
"@types/assert": "^1.5.6",
"@types/fs-extra": "^9.0.13",
"@types/lodash": "^4.14.179",
"@types/mocha": "^8.2.0",
"@types/node": "^14.14.25",
"@types/react": "^17.0.2",
"@types/shelljs": "^0.8.11",
"@types/uuid": "^8.3.0",
"@types/wechat-miniprogram": "^3.4.0",
"assert": "^2.0.0",
"babel-loader": "^8.2.3",
"chalk": "^4.1.2",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.2.4",
"cross-env": "^7.0.2",
"css-loader": "^6.6.0",
"dotenv-webpack": "^7.1.0",
"ensure-posix-path": "^1.1.1",
"file-loader": "^6.2.0",
"fs-extra": "^10.0.0",
"globby": "^11.1.0",
"less-loader": "^10.2.0",
"mini-css-extract-plugin": "^2.5.3",
"miniprogram-api-typings": "^3.4.5",
"mocha": "^8.2.1",
${oakPackageStr}
"postcss-less": "^6.0.0",
"progress": "^2.0.3",
"progress-bar-webpack-plugin": "^2.1.0",
"required-path": "^1.0.1",
"shelljs": "^0.8.5",
"style-loader": "^3.3.1",
"stylelint-config-standard": "^25.0.0",
"stylelint-webpack-plugin": "^3.1.1",
"ts-loader": "^9.2.6",
"ts-node": "^9.1.1",
"typescript": "^4.5.2",
"ui-extract-webpack-plugin": "^1.0.0",
"webpack": "^5.69.1",
"webpack-dashboard": "^3.3.7"
}
}
`;
}
exports.packageJsonContent = packageJsonContent;
function tsConfigJsonContent() {
return `{
"compilerOptions": {
"module": "CommonJS",
"target": "esnext",
"allowJs": false,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"strict": true,
"lib": [
"ES2020"
],
"typeRoots": [
"./src/typings"
],
"types": [
"node",
"miniprogram-api-typings"
],
"resolveJsonModule": true /* Disallow inconsistently-cased references to the same file. */
},
"include": [
"./**/*.ts",
"scripts/webpack.js"
],
"exclude": [
"node_modules"
]
}`;
}
exports.tsConfigJsonContent = tsConfigJsonContent;
function projectConfigContentWithWeChatMp(oakConfigName, projectname, miniVersion) {
return `{
"description": "项目配置文件",
"packOptions": {
"ignore": [{
"type": "file",
"value": "${oakConfigName}"
}]
},
"setting": {
"urlCheck": true,
"es6": true,
"enhance": true,
"postcss": true,
"preloadBackgroundData": false,
"minified": true,
"newFeature": false,
"coverView": true,
"nodeModules": true,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"scopeDataCheck": false,
"uglifyFileName": false,
"checkInvalidKey": true,
"checkSiteMap": true,
"uploadWithSourceMap": true,
"compileHotReLoad": false,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"useIsolateContext": true,
"useCompilerModule": false,
"userConfirmedUseCompilerModuleSwitch": false
},
"compileType": "miniprogram",
"libVersion": "${miniVersion}",
"appid": "",
"projectname": "${projectname}",
"debugOptions": {
"hidedInDevtools": []
},
"scripts": "",
"isGameTourist": false,
"simulatorType": "wechat",
"simulatorPluginLibVersion": {},
"condition": {
"search": {
"current": -1,
"list": []
},
"conversation": {
"current": -1,
"list": []
},
"game": {
"current": -1,
"list": []
},
"plugin": {
"current": -1,
"list": []
},
"gamePlugin": {
"current": -1,
"list": []
},
"miniprogram": {
"current": -1,
"list": []
}
}
}`;
}
exports.projectConfigContentWithWeChatMp = projectConfigContentWithWeChatMp;
function oakConfigContentWithWeChatMp() {
return `{
"theme": {
"@primary-color": "#2d8cf0"
}
}`;
}
exports.oakConfigContentWithWeChatMp = oakConfigContentWithWeChatMp;

8
lib/tip-style.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
export declare const Success: (content: any) => void;
export declare const Error: (content: any) => void;
export declare const Start: (content: any) => void;
export declare const Warn: (content: any) => void;
export declare const success: (content: any) => string;
export declare const error: (content: any) => string;
export declare const primary: (content: any) => string;
export declare const warn: (content: any) => string;

26
lib/tip-style.js Normal file
View File

@ -0,0 +1,26 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.warn = exports.primary = exports.error = exports.success = exports.Warn = exports.Start = exports.Error = exports.Success = void 0;
const chalk_1 = __importDefault(require("chalk"));
const consola_1 = __importDefault(require("consola"));
// tip type
const Success = (content) => consola_1.default.success(content);
exports.Success = Success;
const Error = (content) => consola_1.default.error(content);
exports.Error = Error;
const Start = (content) => consola_1.default.start(content);
exports.Start = Start;
const Warn = (content) => consola_1.default.warn(content);
exports.Warn = Warn;
// tip style
const success = (content) => chalk_1.default.greenBright(content);
exports.success = success;
const error = (content) => chalk_1.default.redBright(content);
exports.error = error;
const primary = (content) => chalk_1.default.blueBright(content);
exports.primary = primary;
const warn = (content) => chalk_1.default.yellowBright(content);
exports.warn = warn;

59
lib/utils.d.ts vendored Normal file
View File

@ -0,0 +1,59 @@
/**
* @name json文件
* @export
* @param {Array<string>} pathArr
* @returns {Set<string>}
*/
export declare function findJson(pathArr: Set<string>): Set<string>;
/**
* @name
* @export
* @param {string} str
* @param {string} start
* @param {string} end
* @returns {(string | null)}
*/
export declare function getStr(str: string, start: string, end: string): string | null;
/**
* @name
* @export
* @template T
* @param {Set<T>} current
* @param {Set<T>} target
* @returns {Set<T>}
*/
export declare function difference<T>(current: Set<T>, target: Set<T>): Set<T>;
/**
* @name
* @export
* @template T
* @param {Set<T>} current
* @param {Set<T>} target
* @returns {Set<T>}
*/
export declare function intersect<T>(current: Set<T>, target: Set<T>): Set<T>;
/**
* @name
* @export
* @template T
* @param {Set<T>} current
* @param {Set<T>} target
* @returns {Set<T>}
*/
export declare function union<T>(current: Set<T>, target: Set<T>): Set<T>;
/**
* @name json
* @export
* @template T
* @param {T} data
* @returns {string}
*/
export declare function formatJsonByFile<T extends Object>(data: T): string;
/**
* @name
* @export
* @param {Array<any>} arr set
* @param {*} [type]
* @returns
*/
export declare function deWeight(arr: Array<any> | Set<any>, type: any): Set<any>;

99
lib/utils.js Normal file
View File

@ -0,0 +1,99 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.deWeight = exports.formatJsonByFile = exports.union = exports.intersect = exports.difference = exports.getStr = exports.findJson = void 0;
/**
* @name 从一组路径里查找到所有json文件
* @export
* @param {Array<string>} pathArr
* @returns {Set<string>}
*/
function findJson(pathArr) {
const result = new Set();
for (let item of pathArr) {
const endIndex = item.length;
const str = item.substring(endIndex - 5, endIndex);
if (str === '.json') {
result.add(item);
}
}
return result;
}
exports.findJson = findJson;
/**
* @name 已知前后文取中间文本
* @export
* @param {string} str
* @param {string} start
* @param {string} end
* @returns {(string | null)}
*/
function getStr(str, start, end) {
const reg = new RegExp(`${start}(.*?)${end}`);
let res = str.match(reg);
return res ? res[1] : null;
}
exports.getStr = getStr;
/**
* @name 差集
* @export
* @template T
* @param {Set<T>} current
* @param {Set<T>} target
* @returns {Set<T>}
*/
function difference(current, target) {
return new Set([...target].filter(x => !current.has(x)));
}
exports.difference = difference;
/**
* @name 获取交集
* @export
* @template T
* @param {Set<T>} current
* @param {Set<T>} target
* @returns {Set<T>}
*/
function intersect(current, target) {
return new Set([...target].filter(x => current.has(x)));
}
exports.intersect = intersect;
/**
* @name 获取并集
* @export
* @template T
* @param {Set<T>} current
* @param {Set<T>} target
* @returns {Set<T>}
*/
function union(current, target) {
return new Set([...current, ...target]);
}
exports.union = union;
/**
* @name 格式化json
* @export
* @template T
* @param {T} data
* @returns {string}
*/
function formatJsonByFile(data) {
return JSON.stringify(data, null, 2);
}
exports.formatJsonByFile = formatJsonByFile;
/**
* @name 数组对象去重
* @export
* @param {Array<any>} arr 需要去重的数组或set
* @param {*} [type] 需要根据哪个字段去重
* @returns
*/
function deWeight(arr, type) {
let map = new Map();
for (let item of arr) {
if (!map.has(item[type])) {
map.set(item[type], item);
}
}
return new Set([...map.values()]);
}
exports.deWeight = deWeight;

48
package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "oak-cli",
"version": "1.0.0",
"description": "oak-cli脚手架",
"main": "lib/index.js",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"bin": {
"oak-cli": "lib/index.js"
},
"author": "",
"license": "",
"bugs": {
"url": ""
},
"homepage": "",
"devDependencies": {
"@types/cross-spawn": "^6.0.2",
"@types/inquirer": "^7.3.1",
"@types/node": "^12.0.27",
"@types/shelljs": "^0.8.8"
},
"dependencies": {
"axios": ">=0.21.1",
"chalk": "^4.1.0",
"clean-webpack-plugin": "^4.0.0",
"commander": "^6.0.0",
"consola": "^2.15.0",
"copy-webpack-plugin": "^10.2.4",
"cross-spawn": "^6.0.5",
"crypto-browserify": "^3.12.0",
"dotenv-webpack": "^7.1.0",
"ensure-posix-path": "^1.1.1",
"fs-extra": "^10.0.0",
"globby": "^11.1.0",
"inquirer": "^7.3.3",
"mini-css-extract-plugin": "^2.5.3",
"postcss-less": "^6.0.0",
"progress-bar-webpack-plugin": "^2.1.0",
"required-path": "^1.0.1",
"shelljs": "^0.8.4",
"stylelint-webpack-plugin": "^3.2.0",
"ui-extract-webpack-plugin": "^1.0.0",
"webpack": "^5.72.0"
}
}

595
plugins/WechatMpPlugin.js Normal file
View File

@ -0,0 +1,595 @@
/**
* 把目录下所有的.ts和.less文件加入entry
*/
const fs = require('fs');
const path = require('path');
const fsExtra = require('fs-extra');
const globby = require('globby');
const { optimize, sources } = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
// const LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin');
// const NodeSourcePlugin = require('webpack/lib/node/NodeSourcePlugin');
// const JsonpTemplatePlugin = require('webpack/lib/web/JsonpTemplatePlugin');
const JavascriptModulesPlugin = require('webpack/lib/javascript/JavascriptModulesPlugin');
const EntryPlugin = require('webpack/lib/EntryPlugin');
const ensurePosix = require('ensure-posix-path');
const requiredPath = require('required-path');
const pluginName = 'OakWeChatMpPlugin';
const oakPagePrefix = 'oak#';
const oakPagePath = 'node_modules/oak-general-business/src/platforms/wechatMp/';
// const oakPagePathRegExp = /node_modules\/oak-general-business\/src\/platforms\/wechatMp\//;
function getIsOak(str) {
return str.indexOf(oakPagePrefix) === 0;
}
class OakWeChatMpPlugin {
constructor(options = {}) {
this.options = Object.assign(
{},
{
clear: true,
extensions: ['.js', '.ts'], // script ext
include: [], // include assets file
exclude: [], // ignore assets file
assetsChunkName: '__assets_chunk__',
commonsChunkName: 'commons',
vendorChunkName: 'vendor',
runtimeChunkName: 'runtime',
},
options
);
}
apply(compiler) {
this.setBasePath(compiler);
const catchError = (handler) => async (arg) => {
try {
await handler(arg);
} catch (err) {
console.warn(err);
}
};
this.firstClean = false;
compiler.hooks.run.tapPromise(
pluginName,
catchError((compiler) => this.setAppEntries(compiler))
);
compiler.hooks.watchRun.tapPromise(
pluginName,
catchError((compiler) => this.setAppEntries(compiler))
);
compiler.hooks.compilation.tap(
pluginName,
catchError((compilation) => this.compilationHooks(compilation))
);
compiler.hooks.emit.tapPromise(
pluginName,
catchError(async (compilation) => {
const { clear } = this.options;
if (clear && !this.firstClean) {
this.firstClean = true;
await OakWeChatMpPlugin.clearOutPut(compilation);
}
})
);
}
compilationHooks(compilation) {
const { assetsChunkName } = this.options;
JavascriptModulesPlugin.getCompilationHooks(compilation).render.tap(
pluginName,
(source, { chunkGraph, chunk }) => {
// 非动态入口
const hasEntryModule =
chunkGraph.getNumberOfEntryModules(chunk) > 0;
if (!hasEntryModule) return source;
// 收集动态入口所有的依赖
let dependences = [];
[...chunk.groupsIterable].forEach((group) => {
[...group.chunks].forEach((chunk2) => {
const filename = ensurePosix(
path.relative(path.dirname(chunk.name), chunk2.name)
);
// console.log(filename)
if (
chunk === chunk2 ||
dependences.includes(filename)
) {
return;
}
dependences.push(filename);
});
});
// 没有依赖
if (dependences.length == 0) return;
// 源文件拼接依赖
const concatStr = dependences.reduce((prev, file) => {
prev += `require('${requiredPath(file)}');`;
return prev;
}, ';');
return new sources.ConcatSource(concatStr, source);
}
);
// splice assets module
compilation.hooks.beforeChunkAssets.tap(pluginName, () => {
let assetsChunk;
compilation.chunks.forEach((chunk) => {
if (chunk.name === assetsChunkName) {
assetsChunk = chunk;
}
});
if (assetsChunk) {
compilation.chunks.delete(assetsChunk);
}
});
compilation.hooks.processAssets.tapPromise(
{
name: pluginName,
stage: compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
},
async (assets) => {
await this.emitAssetsFile(compilation);
}
);
}
async setAppEntries(compiler) {
this.npmComponents = new Set();
this.oakPages = new Set();
this.oakComponents = new Set();
this.appEntries = await this.resolveAppEntries(compiler);
await Promise.all([
this.addScriptEntry(compiler),
this.addAssetsEntries(compiler),
]);
this.applyPlugin(compiler);
}
// resolve tabbar page compoments
async resolveAppEntries(compiler) {
const {
tabBar = {},
pages = [],
subpackages = [],
} = fsExtra.readJSONSync(path.join(this.basePath, 'app.json'));
let tabBarAssets = new Set();
let components = new Set();
let subPageRoots = [];
let independentPageRoots = [];
let realPages = [];
this.subpackRoot = [];
for (const { iconPath, selectedIconPath } of tabBar.list || []) {
if (iconPath) {
tabBarAssets.add(iconPath);
}
if (selectedIconPath) {
tabBarAssets.add(selectedIconPath);
}
}
// parse subpage
for (const subPage of subpackages) {
subPageRoots.push(subPage.root);
if (subPage.independent) {
independentPageRoots.push(subPage.root);
}
for (const page of subPage.pages || []) {
realPages.push(path.join(subPage.root, page));
}
}
// add app.[ts/js]
realPages.push('app');
// resolve page components
for (const page of pages) {
let instance;
let isOak = getIsOak(page);
if (isOak) {
const oakPage = page.replace(
new RegExp(oakPagePrefix),
oakPagePath
);
instance = path.resolve(process.cwd(), oakPage);
if (!this.oakPages.has(oakPage)) {
this.oakPages.add(oakPage);
realPages.push(oakPage);
}
} else {
realPages.push(page);
instance = path.resolve(this.basePath, page);
}
await this.getComponents(components, instance, isOak);
}
components = Array.from(components) || [];
tabBarAssets = Array.from(tabBarAssets) || [];
const ret = [...realPages, ...components];
Object.defineProperties(ret, {
pages: {
get: () => realPages,
},
components: {
get: () => components,
},
tabBarAssets: {
get: () => tabBarAssets,
},
subPageRoots: {
get: () => subPageRoots,
},
independentPageRoots: {
get: () => independentPageRoots,
},
});
return ret;
}
// parse components
async getComponents(components, instance, isOak) {
try {
const { usingComponents = {} } = fsExtra.readJSONSync(
`${instance}.json`
);
const instanceDir = path.parse(instance).dir;
for (const c of Object.values(usingComponents)) {
if (c.indexOf('plugin://') === 0) {
break;
}
if (c.indexOf('/miniprogram_npm') === 0) {
const component = c.replace(
/\/miniprogram_npm/,
'node_modules'
);
if (!this.npmComponents.has(component)) {
this.npmComponents.add(component);
components.add(component);
this.getComponents(
components,
path.resolve(process.cwd(), component)
);
}
break;
}
if (isOak) {
const component = path
.resolve(instanceDir, c)
.replace(/\\/g, '/');
const component2 = component.replace(
process.cwd() + '/',
''
);
if (!this.oakComponents.has(component2)) {
this.oakComponents.add(component2);
components.add(component2);
await this.getComponents(
components,
path.resolve(process.cwd(), component2),
isOak
);
}
} else {
const component = path
.resolve(instanceDir, c)
.replace(/\\/g, '/');
if (!components.has(component)) {
// components.add(path.relative(this.basePath, component));
// await this.getComponents(components, component);
const component2 = path
.relative(this.basePath, component)
.replace(/\\/g, '/');
if (!components.has(component2)) {
components.add(component2);
await this.getComponents(components, component);
}
}
}
}
} catch (error) {}
}
// add script entry
async addScriptEntry(compiler) {
this.appEntries
.filter((resource) => resource !== 'app')
.forEach((resource) => {
if (this.npmComponents.has(resource)) {
new EntryPlugin(
this.basePath,
path.join(process.cwd(), resource),
resource.replace(/node_modules/, 'miniprogram_npm')
).apply(compiler);
} else if (this.oakPages.has(resource)) {
new EntryPlugin(
this.basePath,
path.join(process.cwd(), resource),
resource.replace(new RegExp(oakPagePath), '')
).apply(compiler);
} else if (this.oakComponents.has(resource)) {
new EntryPlugin(
this.basePath,
path.join(process.cwd(), resource),
resource.replace(new RegExp(oakPagePath), '')
).apply(compiler);
} else {
const fullPath = this.getFullScriptPath(resource);
if (fullPath) {
new EntryPlugin(
this.basePath,
fullPath,
resource
).apply(compiler);
} else {
console.warn(`file ${resource} is exists`);
}
}
});
}
// add assets entry
async addAssetsEntries(compiler) {
const { include, exclude, extensions, assetsChunkName } = this.options;
const patterns = this.appEntries
.map((resource) => `${resource}.*`)
.concat(include);
const entries = await globby(patterns, {
cwd: this.basePath,
nodir: true,
realpath: false,
ignore: [...extensions.map((ext) => `**/*${ext}`), ...exclude],
dot: false,
});
this.assetsEntry = [...entries, ...this.appEntries.tabBarAssets];
this.assetsEntry.forEach((resource) => {
new EntryPlugin(
this.basePath,
path.resolve(this.basePath, resource),
assetsChunkName
).apply(compiler);
});
const oakPageAssetsEntry = await globby(
[...this.oakPages]
.map((resource) => `${path.parse(resource).dir}/**/*.*`)
.concat(include),
{
cwd: process.cwd(),
nodir: true,
realpath: false,
ignore: [...extensions.map((ext) => `**/*${ext}`), ...exclude],
dot: false,
}
);
const oakComponentAssetsEntry = await globby(
[...this.oakComponents]
.map((resource) => `${path.parse(resource).dir}/**/*.*`)
.concat(include),
{
cwd: process.cwd(),
nodir: true,
realpath: false,
ignore: [...extensions.map((ext) => `**/*${ext}`), ...exclude],
dot: false,
}
);
this.oakAssetsEntry = [
...oakPageAssetsEntry,
...oakComponentAssetsEntry,
];
this.oakAssetsEntry.forEach((resource) => {
new EntryPlugin(
this.basePath,
path.resolve(process.cwd(), resource),
assetsChunkName
).apply(compiler);
});
const npmAssetsEntry = await globby(
[...this.npmComponents]
.map((resource) => `${path.parse(resource).dir}/**/*.*`)
.concat(include),
{
cwd: process.cwd(),
nodir: true,
realpath: false,
ignore: [...extensions.map((ext) => `**/*${ext}`), ...exclude],
dot: false,
}
);
if (npmAssetsEntry.length > 0) {
new CopyWebpackPlugin({
patterns: [
...npmAssetsEntry.map((resource) => {
return {
from: path.resolve(
process.cwd().replace(/\\/g, '/'),
resource
),
to: resource.replace(
/node_modules/,
'miniprogram_npm'
),
globOptions: {
ignore: [
...extensions.map((ext) => `**/*${ext}`),
...exclude,
],
},
};
}),
],
}).apply(compiler);
}
}
// code splite
applyPlugin(compiler) {
const { runtimeChunkName, commonsChunkName, vendorChunkName } =
this.options;
const subpackRoots = this.appEntries.subPageRoots;
const independentPageRoots = this.appEntries.independentPageRoots;
new optimize.RuntimeChunkPlugin({
name({ name }) {
const index = independentPageRoots.findIndex((item) =>
name.includes(item)
);
if (index !== -1) {
return path.join(
independentPageRoots[index],
runtimeChunkName
);
}
return runtimeChunkName;
},
}).apply(compiler);
new optimize.SplitChunksPlugin({
hidePathInfo: false,
chunks: 'async',
minSize: 10000,
minChunks: 1,
maxAsyncRequests: Infinity,
automaticNameDelimiter: '~',
maxInitialRequests: Infinity,
name: true,
cacheGroups: {
default: false,
// node_modules
vendor: {
chunks: 'all',
test: /[\\/]node_modules[\\/]/,
name: vendorChunkName,
minChunks: 0,
},
// 其他公用代码
common: {
chunks: 'all',
test: /[\\/]src[\\/]/,
minChunks: 2,
name({ context }) {
const index = subpackRoots.findIndex((item) =>
context.includes(item)
);
if (index !== -1) {
return path.join(
subpackRoots[index],
commonsChunkName
);
}
return commonsChunkName;
},
minSize: 0,
},
},
}).apply(compiler);
}
async emitAssetsFile(compilation) {
const emitAssets = [];
for (let entry of this.assetsEntry) {
const assets = path.resolve(this.basePath, entry);
if (/\.(sass|scss|css|less|styl)$/.test(assets)) {
continue;
}
const toTmit = async () => {
const stat = await fsExtra.stat(assets);
let size = stat.size;
let source = await fsExtra.readFile(assets);
if (entry === 'app.json') {
const appJson = JSON.parse(source.toString());
let pages = [];
for (let page of appJson.pages) {
let isOak = getIsOak(page);
if (isOak) {
page = page.replace(new RegExp(oakPagePrefix), '');
}
pages.push(page);
}
appJson.pages = pages;
source = Buffer.from(JSON.stringify(appJson, null, 2));
size = source.length;
}
compilation.assets[entry] = {
size: () => size,
source: () => source,
};
};
emitAssets.push(toTmit());
}
for (let entry of this.oakAssetsEntry) {
const assets = path.resolve(process.cwd(), entry);
if (/\.(sass|scss|css|less|styl)$/.test(assets)) {
continue;
}
const toTmit = async () => {
const stat = await fsExtra.stat(assets);
const source = await fsExtra.readFile(assets);
compilation.assets[entry.replace(new RegExp(oakPagePath), '')] =
{
size: () => stat.size,
source: () => source,
};
};
emitAssets.push(toTmit());
}
await Promise.all(emitAssets);
}
setBasePath(compiler) {
this.basePath = compiler.options.context;
}
// async enforceTarget(compiler) {
// const { options } = compiler;
// // set jsonp obj motuned obj
// options.output.globalObject = 'global';
// options.node = {
// ...(options.node || {}),
// global: false,
// };
// // set target to web
// new JsonpTemplatePlugin(options.output).apply(compiler);
// new NodeSourcePlugin(options.node).apply(compiler);
// new LoaderTargetPlugin('web').apply(compiler);
// }
// script full path
getFullScriptPath(script) {
const {
basePath,
options: { extensions },
} = this;
for (const ext of extensions) {
const fullPath = path.resolve(basePath, script + ext);
if (fsExtra.existsSync(fullPath)) {
return fullPath;
}
}
}
static async clearOutPut(compilation) {
const { path } = compilation.options.output;
await fsExtra.remove(path);
}
}
module.exports = OakWeChatMpPlugin;

View File

@ -0,0 +1,9 @@
const {
buildSchema,
analyzeEntities,
} = require(`${process.cwd()}/node_modules/oak-domain/src/compiler/schemalBuilder`);
analyzeEntities(`${process.cwd()}/node_modules/oak-general-business/src/entities`);
analyzeEntities(`${process.cwd()}/src/entities`);
buildSchema(`${process.cwd()}/oak-app-domain`);

27
scripts/webpack.js Normal file
View File

@ -0,0 +1,27 @@
const webpack = require('webpack');
const chalk = require('chalk');
const webpackConfig = require('../config/webpack.config');
webpack(webpackConfig, (err, stats) => {
if (err) {
console.log(chalk.red(err.stack || err));
if (err.details) {
console.log(chalk.red(err.details));
}
return undefined;
}
if (stats) {
const info = stats.toJson();
if (stats.hasErrors()) {
info.errors.forEach(
ele => console.warn(ele)
);
}
if (stats.hasWarnings()) {
info.warnings.forEach(
ele => console.warn(ele)
);
}
}
});

29
src/build.ts Normal file
View File

@ -0,0 +1,29 @@
import {
Success,
Error,
error,
primary,
success,
warn,
Warn,
} from './tip-style';
import spawn from 'cross-spawn';
export default async function build(env: string) {
Success(`${success(`build环境:${env}`)}`);
const result = spawn.sync(
process.execPath,
[require.resolve('../scripts/' + 'webpack.js')].concat([env]),
{
stdio: 'inherit',
shell: true,
}
);
// const result = spawn.sync('npm -v', [], { stdio: 'inherit', shell: true });
if (result.status === 0) {
Success(`${success(`执行完成`)}`);
} else {
Error(`${error(`执行失败`)}`);
}
}

13
src/config.ts Normal file
View File

@ -0,0 +1,13 @@
export const CLI_VERSION = require('../package.json')['version']
export const CLI_NAME = require('../package.json')['name']
export const BASE_DIR = process.cwd()
export const USER_CONFIG_FILE_NAME = 'oak.config.json'
export const USER_CONFIG_FILE = BASE_DIR + '/' + USER_CONFIG_FILE_NAME
export const NODE_MODULES_DIR_NAME = 'node_modules'
export const CNPM_BASE_URL = '' //oak-cli
export const MINI_VERSION_URL = 'https://mp.weixin.qq.com/debug/getpublibpercentage'

209
src/create.ts Normal file
View File

@ -0,0 +1,209 @@
import {
copyFolder,
checkFileExistsAndCreate,
readFile,
checkFileExists,
} from './file-handle';
import { checkFileExistsAndCreateType } from './enum';
import {
CNPM_BASE_URL,
CLI_VERSION,
USER_CONFIG_FILE_NAME,
CLI_NAME,
MINI_VERSION_URL,
} from './config';
import {
packageJsonContent,
tsConfigJsonContent,
projectConfigContentWithWeChatMp,
oakConfigContentWithWeChatMp,
} from './template';
import { PromptInput, OakInput } from './interface';
import { join } from 'path';
import inquirer from 'inquirer';
import axios from 'axios';
import {
Success,
Error,
error,
primary,
success,
warn,
Warn,
} from './tip-style';
import shell from 'shelljs';
const prompt = [
{
type: 'input',
name: 'version',
message: 'version',
default: '1.0.0',
},
{
type: 'input',
name: 'description',
message: 'description',
},
];
/**
* @name
* @param dirName
*/
function checkProjectName(dirName: string) {
// 项目根路径
const rootPath = process.cwd() + '/' + dirName;
const isExists = checkFileExists(rootPath);
if (isExists) {
console.error(
error(
`Cannot create a project named ${success(
`"${dirName}"`
)} because a path with the same name exists.\n`
) + error('Please choose a different project name.')
);
process.exit(1);
}
}
/**
* @name oak-cli最新版本号
* @returns
*/
async function getOakCliVersion() {
const res = await axios.get(CNPM_BASE_URL);
return res.data['dist-tags']['latest'];
}
/**
* @name
* @returns
*/
async function getMiniVersion() {
const res = await axios.get(MINI_VERSION_URL);
const versions: Array<any> = JSON.parse(res.data['json_data'])['total'];
const versionsSort = versions.sort((a: any, b: any) => {
return b['percentage'] - a['percentage'];
});
return versionsSort[0]['sdkVer'];
}
export default async function create(dirName: string, env: string) {
const nameOption = {
type: 'input',
name: 'name',
message: `name`,
default: dirName,
};
prompt.unshift(nameOption);
const isDev = env === 'dev' || env === 'development';
const { name, version, description }: PromptInput = await inquirer.prompt(
prompt
);
// 获取微信小程序稳定基础版本库
const miniVersion = await getMiniVersion();
// 获取package.json内容
const packageJson = packageJsonContent({
name,
version,
description,
cliversion: CLI_VERSION,
cliname: CLI_NAME,
isDev,
});
// 获取tsconfig.json内容
const tsconfigJson = tsConfigJsonContent();
// 获取小程序项目project.config.json内容
const projectConfigWithWeChatMp = projectConfigContentWithWeChatMp(
USER_CONFIG_FILE_NAME,
'wechatMp',
miniVersion
);
// 获取小程序项目oak.config.json内容
const oakConfigWithWeChatMp = oakConfigContentWithWeChatMp();
// 项目根路径
const rootPath = process.cwd() + '/' + dirName;
// package.json路径
const packageJsonPath = `${rootPath}/package.json`;
// tsconfig.json路径
const tsconfigJsonPath = `${rootPath}/tsconfig.json`;
// web项目根路径
const webRootPath = `${rootPath}/web`;
// 小程序项目根路径
const weChatMpRootPath = `${rootPath}/wechatMp`;
// 小程序项目project.config.json路径
const projectConfigPathWithWeChatMp = `${weChatMpRootPath}/src/project.config.json`;
// 小程序项目project.config.json路径
const oakConfigPathWithWeChatMp = `${weChatMpRootPath}/src/${USER_CONFIG_FILE_NAME}`;
// 被复制的文件夹路径
const currentPath = join(__dirname, '..') + '/template';
//检查项目名是否存在
checkProjectName(dirName);
try {
// 创建根目录
checkFileExistsAndCreate(rootPath);
// 创建package.json
checkFileExistsAndCreate(
packageJsonPath,
packageJson,
checkFileExistsAndCreateType.FILE
);
// 创建tsconfig.json
checkFileExistsAndCreate(
tsconfigJsonPath,
tsconfigJson,
checkFileExistsAndCreateType.FILE
);
// 复制项目文件
copyFolder(currentPath, rootPath);
// 创建小程序项目project.config.json
checkFileExistsAndCreate(
projectConfigPathWithWeChatMp,
projectConfigWithWeChatMp,
checkFileExistsAndCreateType.FILE
);
// 创建小程序项目oak.config.json
checkFileExistsAndCreate(
oakConfigPathWithWeChatMp,
oakConfigWithWeChatMp,
checkFileExistsAndCreateType.FILE
);
if (!shell.which('npm')) {
Warn(warn('Sorry, this script requires npm! Please install npm!'));
shell.exit(1);
}
Success(`${success(`Waiting...`)}`);
Success(`${success(`Dependencies are now being installed`)}`);
shell.cd(dirName).exec('npm install');
// checkFileExistsAndCreate(weChatMpRootPath + '/src/styles');
// //const data = readFile(
// // `${rootPath}/node_modules/oak-frontend-boilerplate/src/platforms/wechatMp/styles/base.less`
// //);
// const data = readFile(
// `${rootPath}/node_modules/oak-general-business/src/platforms/wechatMp/styles/base.less`
// );
// checkFileExistsAndCreate(
// weChatMpRootPath + '/src/styles/base.less',
// data,
// checkFileExistsAndCreateType.FILE
// );
Success(
`${success(
`Successfully created project ${primary(
name
)}, directory name is ${primary(dirName)}`
)}`
);
} catch (err) {
Error(error('create error'));
Error(error(err));
}
}

9
src/enum.ts Normal file
View File

@ -0,0 +1,9 @@
/**
* @name
* @export
* @enum {number}
*/
export enum checkFileExistsAndCreateType {
DIRECTORY,
FILE
}

197
src/file-handle.ts Normal file
View File

@ -0,0 +1,197 @@
import { readdirSync, statSync, writeFileSync, PathLike, existsSync, unlinkSync, mkdirSync, rmdirSync, createReadStream, accessSync, createWriteStream, constants, readFileSync } from 'fs'
import { join } from 'path'
import { checkFileExistsAndCreateType } from './enum'
import { Error, error, Warn, warn } from './tip-style'
const pathList: Set<string> = new Set()
/**
* @name
* @export
* @param {string} entry
*/
export function readDirPath(entry: string): Set<string> {
const dirInfo = readdirSync(entry);
for (let item of dirInfo) {
const location = join(entry, item);
const info = statSync(location);
if (info.isDirectory()) {
readDirPath(location);
} else {
pathList.add(location)
}
}
return pathList
}
/**
* @name
* @export
* @param {*} entry
* @returns
*/
export function readDirGetFile(entry: string) {
const dirInfo = readdirSync(entry);
return dirInfo
}
/**
* @name json文件()
* @export
* @param {Array<string>} arr
* @returns
*/
export function parseJsonFiles(arr: Set<string>) {
const result = []
for (let item of arr) {
const data = parseJsonFile(item);
result.push(data)
}
return result
}
/**
* @name json
* @export
* @param {PathLike} file
* @returns
*/
export function parseJsonFile(file: string) {
try {
const data = readFileSync(file, 'utf8');
return JSON.parse(data)
} catch (error) {
return
}
}
/**
* @name
* @export
* @param {string} entry
*/
export function deleteFolderRecursive(entry: string) {
let files = [];
// 判断给定的路径是否存在
if (existsSync(entry)) {
// 返回文件和子目录的数组
files = readdirSync(entry);
for (let file of files) {
const curPath = join(entry, file);
// fs.statSync同步读取文件夹文件如果是文件夹在重复触发函数
if (statSync(curPath).isDirectory()) { // recurse
deleteFolderRecursive(curPath);
// 是文件delete file
} else {
unlinkSync(curPath);
}
}
// 清除文件夹
rmdirSync(entry);
} else {
// console.log("文件夹不存在");
}
};
export function writeFile(path: string | PathLike, data: any) {
try {
writeFileSync(path, data)
} catch (err) {
Error(error(err))
Error(error('文件写入失败'))
}
}
export function readFile(
path: string | PathLike,
options?: { encoding?: null | undefined; flag?: string | undefined } | null
) {
try {
const data = readFileSync(path, options);
return data;
} catch (err) {
Error(error(err));
Error(error('文件读取失败'));
}
}
/**
* @name
* @export
* @param {PathLike} currentDir
* @param {PathLike} targetDir
*/
export function copyFolder(currentDir: PathLike, targetDir: PathLike) {
function handleFolder(currentDir: PathLike, targetDir: PathLike) {
const files = readdirSync(currentDir, {
withFileTypes: true
})
for (let file of files) {
// 拼接文件绝对路径
const copyCurrentFileInfo = currentDir + '/' + file.name
const copyTargetFileInfo = targetDir + '/' + file.name
// 判断文件是否存在
const readCurrentFile = existsSync(copyCurrentFileInfo)
const readTargetFile = existsSync(copyTargetFileInfo)
if (readCurrentFile && !readTargetFile) {
// 判断是否为文件,如果为文件则复制,文件夹则递归
if (file.isFile()) {
const readStream = createReadStream(copyCurrentFileInfo)
const writeStream = createWriteStream(copyTargetFileInfo)
readStream.pipe(writeStream)
} else {
try {
accessSync(join(copyTargetFileInfo, '..'), constants.W_OK)
copyFolder(copyCurrentFileInfo, copyTargetFileInfo)
} catch (error) {
Warn('权限不足' + error)
}
}
} else {
Error(error('操作失败target文件夹已存在或current文件夹不存在'))
}
}
}
if (existsSync(currentDir)) {
if (!existsSync(targetDir)) {
mkdirSync(targetDir)
}
handleFolder(currentDir, targetDir)
} else {
Warn(warn('需要copy的文件夹不存在:' + currentDir))
}
}
/**
* @name /
* @export
* @param {(PathLike | string)} path
* @returns
*/
export function checkFileExists(path: PathLike | string) {
return existsSync(path)
}
/**
* @name /
* @export
* @param {(PathLike | string)} path
* @param {*} [data]
* @param {checkFileExistsAndCreateType} [type=checkFileExistsAndCreateType.DIRECTORY]
*/
export function checkFileExistsAndCreate(path: PathLike | string, data?: any, type: checkFileExistsAndCreateType = checkFileExistsAndCreateType.DIRECTORY): void {
if (!checkFileExists(path)) {
switch (type) {
case checkFileExistsAndCreateType.DIRECTORY:
mkdirSync(path)
break;
case checkFileExistsAndCreateType.FILE:
writeFile(path, data)
break;
default:
mkdirSync(path)
break;
}
}
}

78
src/index.ts Normal file
View File

@ -0,0 +1,78 @@
#!/usr/bin/env node
import program from 'commander';
import create from './create';
import build from './build';
import make from './make';
import { CLI_VERSION, CLI_NAME } from './config';
import { error, warn } from './tip-style';
/**
* @name
* @param {string} methodName
* @param {Function} log
*/
function enhanceErrorMessages(methodName: string, log: Function) {
program.Command.prototype[methodName] = function (...args: any) {
if (methodName === 'unknownOption' && this._allowUnknownOption) {
return;
}
this.outputHelp();
console.log(` ` + error(log(...args)));
console.log();
process.exit(1);
};
}
const currentNodeVersion = process.versions.node;
const semver = currentNodeVersion.split('.');
const major = semver[0];
const minNodeVersion = 14;
if (Number(major) < minNodeVersion) {
console.error(
'You are running Node ' +
currentNodeVersion +
'.\n' +
'Create React App requires Node ' +
minNodeVersion +
' or higher. \n' +
'Please update your version of Node.'
);
process.exit(1);
}
program.version(CLI_VERSION, '-v, --version').usage('<command> [options]');
program
.command('make')
.description('build oak app domain of make on demand')
.action(make);
program
.command('start <env>')
.usage('<env>')
.description('build we chat mp of start on demand')
.action(build);
program
.command('build <env>')
.usage('<env>')
.description('build we chat mp of build on demand')
.action(build);
program
.command('create <name> [env]')
.usage('<name>')
// .option('-e, --env <env>', 'A env')
.description(`create a new project powered by ${CLI_NAME}`)
.action(create);
// output help information on unknown commands
program.arguments('<command>').action((cmd) => {
program.outputHelp();
console.log();
console.log(` ` + `${error(`Unknown command ${warn(cmd)}`)}`);
console.log();
});
enhanceErrorMessages('missingArgument', (argName: string) => {
console.log();
return `${error(`Missing required argument ${warn(argName)}.`)}`;
});
program.parse(process.argv);

61
src/interface.ts Normal file
View File

@ -0,0 +1,61 @@
/**
* @name package.json需要输入的参数
* @export
* @interface PackageJsonInput
*/
export interface PackageJsonInput {
name: string;
version?: string;
description?: string;
cliversion: string;
cliname: string;
isDev?: boolean;
}
/**
* @name Prompt需要输入的参数
* @export
* @interface PromptInput
*/
export interface PromptInput {
name: string,
version: string,
description: string,
}
/**
* @name project.config.json
* @export
* @interface ProjectConfigInterface
*/
export interface ProjectConfigInterface {
packOptions: PackOptions
}
/**
* @name project.config.json PackOptions
* @export
* @interface PackOptions
*/
export interface PackOptions {
ignore: Array<PackOptionsIgnore>
}
/**
* @name project.config.json PackOptions PackOptionsIgnore
* @export
* @interface PackOptionsIgnore
*/
export interface PackOptionsIgnore {
type: string,
value: string
}
/**
* @name oak-cli需要输入的参数
* @export
* @interface OakInput
*/
export interface OakInput {
mode: string,
}

50
src/make.ts Normal file
View File

@ -0,0 +1,50 @@
import {
Success,
Error,
error,
primary,
success,
warn,
Warn,
} from './tip-style';
import spawn from 'cross-spawn';
export default async function make() {
Success(`${success(`build oak app domain`)}`);
// ts-node scripts/build-app-domain & npm link ./app-domain
const result = spawn.sync(
'ts-node',
[require.resolve('../scripts/' + 'build-app-domain.js')],
{
stdio: 'inherit',
shell: true,
}
);
// const result2 = spawn.sync('npm -v', [], { stdio: 'inherit', shell: true });
if (result.status === 0) {
Success(`${success(`build 执行完成`)}`);
} else {
Error(`${error(`build 执行失败`)}`);
process.exit(1);
}
Success(`${success(`npm link oak app domain`)}`);
const isMac = process.platform === 'darwin';
const result2 = spawn.sync(
`${isMac ? 'sudo' : ''} npm`,
[`link ${process.cwd()}/oak-app-domain`],
{
stdio: 'inherit',
shell: true,
}
);
if (result2.status === 0) {
Success(`${success(`link 执行完成`)}`);
} else {
Error(`${error(`link 执行失败`)}`);
process.exit(1);
}
}

213
src/template.ts Normal file
View File

@ -0,0 +1,213 @@
import { PackageJsonInput } from './interface'
export function packageJsonContent({
name,
version,
description,
cliversion,
cliname,
isDev,
}: PackageJsonInput) {
let oakPackageStr;
if (isDev) {
oakPackageStr = `"${cliname}": "file:../${cliname}",
"oak-domain": "file:../oak-domain",
"oak-frontend-base": "file:../oak-frontend-base",
"oak-general-business": "file:../oak-general-business",
"oak-memory-tree-store": "file:../oak-memory-tree-store",`;
}
else {
oakPackageStr = `"${cliname}": "^${cliversion}",
"oak-domain": "^1.0.0",
"oak-frontend-base": "^1.0.0",
"oak-general-business": "^1.0.0",
"oak-memory-tree-store": "^1.0.0",`;
}
return `{
"name": "${name}",
"version": "${version}",
"description": "${description}",
"scripts": {
"make:domain": "${cliname} make",
"start:mp": "${cliname} start development",
"build:mp": "${cliname} build production"
},
"keywords": [],
"author": "",
"license": "",
"dependencies": {
"@reduxjs/toolkit": "^1.7.2",
"crypto-browserify": "^3.12.0",
"lodash": "^4.17.21"
},
"devDependencies": {
"@babel/cli": "^7.12.13",
"@babel/core": "^7.12.13",
"@babel/plugin-proposal-class-properties": "^7.12.13",
"@babel/preset-env": "^7.12.13",
"@babel/preset-typescript": "^7.12.13",
"@types/assert": "^1.5.6",
"@types/fs-extra": "^9.0.13",
"@types/lodash": "^4.14.179",
"@types/mocha": "^8.2.0",
"@types/node": "^14.14.25",
"@types/react": "^17.0.2",
"@types/shelljs": "^0.8.11",
"@types/uuid": "^8.3.0",
"@types/wechat-miniprogram": "^3.4.0",
"assert": "^2.0.0",
"babel-loader": "^8.2.3",
"chalk": "^4.1.2",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.2.4",
"cross-env": "^7.0.2",
"css-loader": "^6.6.0",
"dotenv-webpack": "^7.1.0",
"ensure-posix-path": "^1.1.1",
"file-loader": "^6.2.0",
"fs-extra": "^10.0.0",
"globby": "^11.1.0",
"less-loader": "^10.2.0",
"mini-css-extract-plugin": "^2.5.3",
"miniprogram-api-typings": "^3.4.5",
"mocha": "^8.2.1",
${oakPackageStr}
"postcss-less": "^6.0.0",
"progress": "^2.0.3",
"progress-bar-webpack-plugin": "^2.1.0",
"required-path": "^1.0.1",
"shelljs": "^0.8.5",
"style-loader": "^3.3.1",
"stylelint-config-standard": "^25.0.0",
"stylelint-webpack-plugin": "^3.1.1",
"ts-loader": "^9.2.6",
"ts-node": "^9.1.1",
"typescript": "^4.5.2",
"ui-extract-webpack-plugin": "^1.0.0",
"webpack": "^5.69.1",
"webpack-dashboard": "^3.3.7"
}
}
`;
}
export function tsConfigJsonContent() {
return `{
"compilerOptions": {
"module": "CommonJS",
"target": "esnext",
"allowJs": false,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"strict": true,
"lib": [
"ES2020"
],
"typeRoots": [
"./src/typings"
],
"types": [
"node",
"miniprogram-api-typings"
],
"resolveJsonModule": true /* Disallow inconsistently-cased references to the same file. */
},
"include": [
"./**/*.ts",
"scripts/webpack.js"
],
"exclude": [
"node_modules"
]
}`;
}
export function projectConfigContentWithWeChatMp(
oakConfigName: string,
projectname: string,
miniVersion: string
) {
return `{
"description": "项目配置文件",
"packOptions": {
"ignore": [{
"type": "file",
"value": "${oakConfigName}"
}]
},
"setting": {
"urlCheck": true,
"es6": true,
"enhance": true,
"postcss": true,
"preloadBackgroundData": false,
"minified": true,
"newFeature": false,
"coverView": true,
"nodeModules": true,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"scopeDataCheck": false,
"uglifyFileName": false,
"checkInvalidKey": true,
"checkSiteMap": true,
"uploadWithSourceMap": true,
"compileHotReLoad": false,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"useIsolateContext": true,
"useCompilerModule": false,
"userConfirmedUseCompilerModuleSwitch": false
},
"compileType": "miniprogram",
"libVersion": "${miniVersion}",
"appid": "",
"projectname": "${projectname}",
"debugOptions": {
"hidedInDevtools": []
},
"scripts": "",
"isGameTourist": false,
"simulatorType": "wechat",
"simulatorPluginLibVersion": {},
"condition": {
"search": {
"current": -1,
"list": []
},
"conversation": {
"current": -1,
"list": []
},
"game": {
"current": -1,
"list": []
},
"plugin": {
"current": -1,
"list": []
},
"gamePlugin": {
"current": -1,
"list": []
},
"miniprogram": {
"current": -1,
"list": []
}
}
}`;
}
export function oakConfigContentWithWeChatMp() {
return `{
"theme": {
"@primary-color": "#2d8cf0"
}
}`;
}

13
src/tip-style.ts Normal file
View File

@ -0,0 +1,13 @@
import chalk from 'chalk'
import consola from 'consola'
// tip type
export const Success = (content: any) => consola.success(content)
export const Error = (content: any) => consola.error(content)
export const Start = (content: any) => consola.start(content)
export const Warn = (content: any) => consola.warn(content)
// tip style
export const success = (content: any) => chalk.greenBright(content)
export const error = (content: any) => chalk.redBright(content)
export const primary = (content: any) => chalk.blueBright(content)
export const warn = (content: any) => chalk.yellowBright(content)

98
src/utils.ts Normal file
View File

@ -0,0 +1,98 @@
/**
* @name json文件
* @export
* @param {Array<string>} pathArr
* @returns {Set<string>}
*/
export function findJson(pathArr: Set<string>): Set<string> {
const result: Set<string> = new Set()
for (let item of pathArr) {
const endIndex = item.length
const str = item.substring(endIndex - 5, endIndex)
if (str === '.json') {
result.add(item)
}
}
return result
}
/**
* @name
* @export
* @param {string} str
* @param {string} start
* @param {string} end
* @returns {(string | null)}
*/
export function getStr(str: string, start: string, end: string): string | null {
const reg = new RegExp(`${start}(.*?)${end}`)
let res = str.match(reg)
return res ? res[1] : null
}
/**
* @name
* @export
* @template T
* @param {Set<T>} current
* @param {Set<T>} target
* @returns {Set<T>}
*/
export function difference<T>(current: Set<T>, target: Set<T>): Set<T> {
return new Set(
[...target].filter(x => !current.has(x))
)
}
/**
* @name
* @export
* @template T
* @param {Set<T>} current
* @param {Set<T>} target
* @returns {Set<T>}
*/
export function intersect<T>(current: Set<T>, target: Set<T>): Set<T> {
return new Set([...target].filter(x => current.has(x)))
}
/**
* @name
* @export
* @template T
* @param {Set<T>} current
* @param {Set<T>} target
* @returns {Set<T>}
*/
export function union<T>(current: Set<T>, target: Set<T>): Set<T> {
return new Set([...current, ...target])
}
/**
* @name json
* @export
* @template T
* @param {T} data
* @returns {string}
*/
export function formatJsonByFile<T extends Object>(data: T): string {
return JSON.stringify(data, null, 2)
}
/**
* @name
* @export
* @param {Array<any>} arr set
* @param {*} [type]
* @returns
*/
export function deWeight(arr: Array<any> | Set<any>, type: any) {
let map = new Map();
for (let item of arr) {
if (!map.has(item[type])) {
map.set(item[type], item);
}
}
return new Set([...map.values()]);
}

86
template/.gitignore vendored Normal file
View File

@ -0,0 +1,86 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

24
template/.stylelintrc.js Normal file
View File

@ -0,0 +1,24 @@
module.exports = {
customSyntax: 'postcss-less',
extends: 'stylelint-config-standard',
rules: {
'at-rule-no-vendor-prefix': true,
'media-feature-name-no-vendor-prefix': true,
'property-no-vendor-prefix': true,
'selector-max-compound-selectors': 10,
'selector-no-vendor-prefix': true,
'value-no-vendor-prefix': true,
'unit-no-unknown': [true, { ignoreUnits: ['rpx', 'px'] }],
'no-descending-specificity': null,
'at-rule-no-unknown': null,
'declaration-block-no-duplicate-properties': null,
'no-duplicate-selectors': null,
'selector-class-pattern': null,
'font-family-no-missing-generic-family-keyword': null,
'function-no-unknown': [true, { ignoreFunctions: ['alpha', 'constant', 'fadeout'] }],
'declaration-block-no-shorthand-property-overrides': null,
'no-empty-source': null,
'selector-type-no-unknown': null
},
};

View File

@ -0,0 +1,12 @@
import { test } from './sample';
const aspectDict = {
test
};
type AspectDict = typeof aspectDict;
export {
aspectDict,
AspectDict,
}

View File

@ -0,0 +1,6 @@
import { RuntimeContext } from "oak-general-business";
import { EntityDict } from '../../app-domain';
export async function test(params: string, context: RuntimeContext<EntityDict>) {
return 'hello world';
}

View File

@ -0,0 +1,12 @@
import { String, Int, Datetime, Image, Boolean, Text } from 'oak-domain/lib/types/DataType';
import { Schema as Area } from 'oak-general-business/lib/entities/Area';
import { Schema as User } from 'oak-general-business/lib/entities/User';
import { Schema as ExtraFile } from 'oak-general-business/lib/entities/ExtraFile';
import { EntityShape } from 'oak-domain/lib/types/Entity';
export interface Schema extends EntityShape {
district: String<16>;
area: Area;
owner: User;
dd: Array<ExtraFile>;
};

View File

@ -0,0 +1,26 @@
import { EntityDict } from '../../app-domain';
import { Feature } from 'oak-frontend-base';
import { AspectDict } from '../aspects';
import { Cache } from 'oak-frontend-base';
type DoSthAcion = {
type: 'doSth',
payload: {
args: string;
}
}
export class Sample extends Feature<EntityDict, AspectDict> {
get(params: any) {
throw new Error('Method not implemented.');
}
action(action: DoSthAcion) {
throw new Error('Method not implemented.');
}
cache: Cache<EntityDict, AspectDict>;
constructor(cache: Cache<EntityDict, AspectDict>) {
super();
this.cache = cache;
};
};

View File

@ -0,0 +1,16 @@
import { EntityDict } from 'oak-app-domain';
import { BasicFeatures } from 'oak-frontend-base';
import * as Sample from './Sample';
import { AspectDict } from '../aspects';
export function initialize(features: BasicFeatures<EntityDict, AspectDict>) {
const { cache } = features;
const sample = new Sample.Sample(cache);
return {
sample,
};
}
export type FeatureDict = ReturnType<typeof initialize>;

0
template/src/typings/polyfill.d.ts vendored Normal file
View File

View File

@ -0,0 +1,15 @@
{
"pages":[
"pages/index/index",
"oak#pages/address/list/index",
"oak#pages/address/upsert/index"
],
"window":{
"backgroundTextStyle":"light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "Weixin",
"navigationBarTextStyle":"black"
},
"style": "v2",
"sitemapLocation": "sitemap.json"
}

View File

@ -0,0 +1,10 @@
/**app.wxss**/
.container {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 200rpx 0;
box-sizing: border-box;
}

View File

@ -0,0 +1,18 @@
import { OakPage, OakComponent } from './init'
export interface IAppOption {
globalData: {};
}
App<IAppOption>({
globalData: {
OakPage,
OakComponent,
},
async onLaunch() {
console.log('onLaunch');
},
onHide() {
console.log('onHide');
},
});

View File

@ -0,0 +1,19 @@
import { InitializeWechatMp, } from 'oak-frontend-base';
import { EntityDict } from 'oak-app-domain/EntityDict';
import { storageSchema } from 'oak-app-domain/Storage';
import { triggers, aspectDict, data, checkers, RuntimeContext } from 'oak-general-business';
const { OakComponent, OakPage } = InitializeWechatMp<EntityDict, RuntimeContext<EntityDict>, typeof aspectDict, {}>(
storageSchema,
() => ({}),
(store) => new RuntimeContext(store, '123'),
triggers as any,
checkers as any,
aspectDict,
data as any);
export {
OakPage,
OakComponent,
};

View File

@ -0,0 +1,3 @@
{
"usingComponents": {}
}

View File

@ -0,0 +1,20 @@
/** index.wxss **/
.userinfo {
display: flex;
flex-direction: column;
align-items: center;
color: #aaa;
}
.userinfo-avatar {
overflow: hidden;
width: 128rpx;
height: 128rpx;
margin: 20rpx;
border-radius: 50%;
}
.usermotto {
margin-top: 200px;
}

View File

@ -0,0 +1,47 @@
// index.ts
Page({
data: {
motto: 'Hello World',
userInfo: {},
hasUserInfo: false,
canIUse: wx.canIUse('button.open-type.getUserInfo'),
canIUseGetUserProfile: false,
canIUseOpenData: wx.canIUse('open-data.type.userAvatarUrl') && wx.canIUse('open-data.type.userNickName') // 如需尝试获取用户信息可改为false
},
// 事件处理函数
bindViewTap() {
wx.navigateTo({
url: '../logs/logs',
})
},
onLoad() {
// @ts-ignore
if (wx.getUserProfile) {
this.setData({
canIUseGetUserProfile: true
})
}
},
getUserProfile() {
// 推荐使用wx.getUserProfile获取用户信息开发者每次通过该接口获取用户个人信息均需用户确认开发者妥善保管用户快速填写的头像昵称避免重复弹窗
wx.getUserProfile({
desc: '展示用户信息', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
success: (res) => {
console.log(res)
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
})
}
})
},
getUserInfo(e: any) {
// 不推荐使用getUserInfo获取用户信息预计自2021年4月13日起getUserInfo将不再弹出弹窗并直接返回匿名的用户个人信息
console.log(e)
this.setData({
userInfo: e.detail.userInfo,
hasUserInfo: true
})
}
})

View File

@ -0,0 +1,23 @@
<!--index.wxml-->
<view class="container">
<view class="userinfo">
<block wx:if="{{canIUseOpenData}}">
<view class="userinfo-avatar" bindtap="bindViewTap">
<open-data type="userAvatarUrl"></open-data>
</view>
<open-data type="userNickName"></open-data>
</block>
<block wx:elif="{{!hasUserInfo}}">
<button wx:if="{{canIUseGetUserProfile}}" bindtap="getUserProfile"> 获取头像昵称 </button>
<button wx:elif="{{canIUse}}" open-type="getUserInfo" bindgetuserinfo="getUserInfo"> 获取头像昵称 </button>
<view wx:else> 请使用1.4.4及以上版本基础库 </view>
</block>
<block wx:else>
<image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
<text class="userinfo-nickname">{{userInfo.nickName}}</text>
</block>
</view>
<view class="usermotto">
<text class="user-motto">{{motto}}</text>
</view>
</view>

View File

@ -0,0 +1,7 @@
{
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
"rules": [{
"action": "allow",
"page": "*"
}]
}

View File

@ -0,0 +1,19 @@
export const formatTime = (date: Date) => {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hour = date.getHours()
const minute = date.getMinutes()
const second = date.getSeconds()
return (
[year, month, day].map(formatNumber).join('/') +
' ' +
[hour, minute, second].map(formatNumber).join(':')
)
}
const formatNumber = (n: number) => {
const s = n.toString()
return s[1] ? s : '0' + s
}

71
tsconfig.json Normal file
View File

@ -0,0 +1,71 @@
{
"compilerOptions": {
"jsx": "preserve",
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": [
"dom",
"dom.iterable",
"esnext"
],
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "lib", /* Redirect output structure to the directory. */
"rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
"downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true /* Disallow inconsistently-cased references to the same file. */
},
"include": [ "src/**/*" ],
"exclude": [
"node_modules",
]
}