From 430dace767199625cf4897d6584668480c9747d2 Mon Sep 17 00:00:00 2001 From: wkj <278599135@qq.com> Date: Thu, 21 Apr 2022 17:38:22 +0800 Subject: [PATCH] =?UTF-8?q?oak-cli=20=E9=A1=B9=E7=9B=AE=E6=96=B0=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- config/env.js | 30 + config/webpack.config.js | 171 ++++++ lib/build.d.ts | 1 + lib/build.js | 22 + lib/config.d.ts | 8 + lib/config.js | 11 + lib/create.d.ts | 1 + lib/create.js | 145 +++++ lib/enum.d.ts | 9 + lib/enum.js | 13 + lib/file-handle.d.ts | 63 ++ lib/file-handle.js | 206 +++++++ lib/index.d.ts | 2 + lib/index.js | 75 +++ lib/interface.d.ts | 56 ++ lib/interface.js | 2 + lib/make.d.ts | 1 + lib/make.js | 37 ++ lib/template.d.ts | 5 + lib/template.js | 203 +++++++ lib/tip-style.d.ts | 8 + lib/tip-style.js | 26 + lib/utils.d.ts | 59 ++ lib/utils.js | 99 +++ package.json | 48 ++ plugins/WechatMpPlugin.js | 595 +++++++++++++++++++ scripts/build-app-domain.js | 9 + scripts/webpack.js | 27 + src/build.ts | 29 + src/config.ts | 13 + src/create.ts | 209 +++++++ src/enum.ts | 9 + src/file-handle.ts | 197 ++++++ src/index.ts | 78 +++ src/interface.ts | 61 ++ src/make.ts | 50 ++ src/template.ts | 213 +++++++ src/tip-style.ts | 13 + src/utils.ts | 98 +++ template/.gitignore | 86 +++ template/.stylelintrc.js | 24 + template/src/aspects/index.ts | 12 + template/src/aspects/sample.ts | 6 + template/src/entities/House.ts | 12 + template/src/features/Sample.ts | 26 + template/src/features/index.ts | 16 + template/src/typings/polyfill.d.ts | 0 template/wechatMp/src/app.json | 15 + template/wechatMp/src/app.less | 10 + template/wechatMp/src/app.ts | 18 + template/wechatMp/src/init.ts | 19 + template/wechatMp/src/pages/index/index.json | 3 + template/wechatMp/src/pages/index/index.less | 20 + template/wechatMp/src/pages/index/index.ts | 47 ++ template/wechatMp/src/pages/index/index.wxml | 23 + template/wechatMp/src/sitemap.json | 7 + template/wechatMp/src/utils/util.ts | 19 + tsconfig.json | 71 +++ 59 files changed, 3339 insertions(+), 1 deletion(-) create mode 100644 config/env.js create mode 100644 config/webpack.config.js create mode 100644 lib/build.d.ts create mode 100644 lib/build.js create mode 100644 lib/config.d.ts create mode 100644 lib/config.js create mode 100644 lib/create.d.ts create mode 100644 lib/create.js create mode 100644 lib/enum.d.ts create mode 100644 lib/enum.js create mode 100644 lib/file-handle.d.ts create mode 100644 lib/file-handle.js create mode 100644 lib/index.d.ts create mode 100755 lib/index.js create mode 100644 lib/interface.d.ts create mode 100644 lib/interface.js create mode 100644 lib/make.d.ts create mode 100644 lib/make.js create mode 100644 lib/template.d.ts create mode 100644 lib/template.js create mode 100644 lib/tip-style.d.ts create mode 100644 lib/tip-style.js create mode 100644 lib/utils.d.ts create mode 100644 lib/utils.js create mode 100644 package.json create mode 100644 plugins/WechatMpPlugin.js create mode 100644 scripts/build-app-domain.js create mode 100644 scripts/webpack.js create mode 100644 src/build.ts create mode 100644 src/config.ts create mode 100644 src/create.ts create mode 100644 src/enum.ts create mode 100644 src/file-handle.ts create mode 100644 src/index.ts create mode 100644 src/interface.ts create mode 100644 src/make.ts create mode 100644 src/template.ts create mode 100644 src/tip-style.ts create mode 100644 src/utils.ts create mode 100644 template/.gitignore create mode 100644 template/.stylelintrc.js create mode 100644 template/src/aspects/index.ts create mode 100644 template/src/aspects/sample.ts create mode 100644 template/src/entities/House.ts create mode 100644 template/src/features/Sample.ts create mode 100644 template/src/features/index.ts create mode 100644 template/src/typings/polyfill.d.ts create mode 100644 template/wechatMp/src/app.json create mode 100644 template/wechatMp/src/app.less create mode 100644 template/wechatMp/src/app.ts create mode 100644 template/wechatMp/src/init.ts create mode 100644 template/wechatMp/src/pages/index/index.json create mode 100644 template/wechatMp/src/pages/index/index.less create mode 100644 template/wechatMp/src/pages/index/index.ts create mode 100644 template/wechatMp/src/pages/index/index.wxml create mode 100644 template/wechatMp/src/sitemap.json create mode 100644 template/wechatMp/src/utils/util.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index bf78570..6377096 100644 --- a/.gitignore +++ b/.gitignore @@ -83,4 +83,6 @@ dist .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz -.pnp.* \ No newline at end of file +.pnp.* + +package-lock.json \ No newline at end of file diff --git a/config/env.js b/config/env.js new file mode 100644 index 0000000..ac95f07 --- /dev/null +++ b/config/env.js @@ -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', + }, +}; diff --git a/config/webpack.config.js b/config/webpack.config.js new file mode 100644 index 0000000..1595c14 --- /dev/null +++ b/config/webpack.config.js @@ -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, + }, +}; diff --git a/lib/build.d.ts b/lib/build.d.ts new file mode 100644 index 0000000..e670f7c --- /dev/null +++ b/lib/build.d.ts @@ -0,0 +1 @@ +export default function build(env: string): Promise; diff --git a/lib/build.js b/lib/build.js new file mode 100644 index 0000000..94f682f --- /dev/null +++ b/lib/build.js @@ -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; diff --git a/lib/config.d.ts b/lib/config.d.ts new file mode 100644 index 0000000..5d634f0 --- /dev/null +++ b/lib/config.d.ts @@ -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"; diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 0000000..8f28f6e --- /dev/null +++ b/lib/config.js @@ -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'; diff --git a/lib/create.d.ts b/lib/create.d.ts new file mode 100644 index 0000000..e50fb0c --- /dev/null +++ b/lib/create.d.ts @@ -0,0 +1 @@ +export default function create(dirName: string, env: string): Promise; diff --git a/lib/create.js b/lib/create.js new file mode 100644 index 0000000..5405c22 --- /dev/null +++ b/lib/create.js @@ -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; diff --git a/lib/enum.d.ts b/lib/enum.d.ts new file mode 100644 index 0000000..fd35e3e --- /dev/null +++ b/lib/enum.d.ts @@ -0,0 +1,9 @@ +/** + * @name 检查文件时用到的枚举 + * @export + * @enum {number} + */ +export declare enum checkFileExistsAndCreateType { + DIRECTORY = 0, + FILE = 1 +} diff --git a/lib/enum.js b/lib/enum.js new file mode 100644 index 0000000..8606401 --- /dev/null +++ b/lib/enum.js @@ -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 = {})); diff --git a/lib/file-handle.d.ts b/lib/file-handle.d.ts new file mode 100644 index 0000000..733ec77 --- /dev/null +++ b/lib/file-handle.d.ts @@ -0,0 +1,63 @@ +/// +import { PathLike } from 'fs'; +import { checkFileExistsAndCreateType } from './enum'; +/** + * @name 读取目录下所有文件 + * @export + * @param {string} entry 目录名称 + */ +export declare function readDirPath(entry: string): Set; +/** + * @name 读取指定目录的文件(不进行深度遍历,只获取根目录) + * @export + * @param {*} entry + * @returns + */ +export declare function readDirGetFile(entry: string): string[]; +/** + * @name 解析json文件(数组) + * @export + * @param {Array} arr + * @returns + */ +export declare function parseJsonFiles(arr: Set): 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; diff --git a/lib/file-handle.js b/lib/file-handle.js new file mode 100644 index 0000000..b180e0b --- /dev/null +++ b/lib/file-handle.js @@ -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} 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; diff --git a/lib/index.d.ts b/lib/index.d.ts new file mode 100644 index 0000000..b798801 --- /dev/null +++ b/lib/index.d.ts @@ -0,0 +1,2 @@ +#!/usr/bin/env node +export {}; diff --git a/lib/index.js b/lib/index.js new file mode 100755 index 0000000..661cd8f --- /dev/null +++ b/lib/index.js @@ -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(' [options]'); +commander_1.default + .command('make') + .description('build oak app domain of make on demand') + .action(make_1.default); +commander_1.default + .command('start ') + .usage('') + .description('build we chat mp of start on demand') + .action(build_1.default); +commander_1.default + .command('build ') + .usage('') + .description('build we chat mp of build on demand') + .action(build_1.default); +commander_1.default + .command('create [env]') + .usage('') + // .option('-e, --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('').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); diff --git a/lib/interface.d.ts b/lib/interface.d.ts new file mode 100644 index 0000000..eab36c0 --- /dev/null +++ b/lib/interface.d.ts @@ -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; +} +/** + * @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; +} diff --git a/lib/interface.js b/lib/interface.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/lib/interface.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/make.d.ts b/lib/make.d.ts new file mode 100644 index 0000000..b98bfb3 --- /dev/null +++ b/lib/make.d.ts @@ -0,0 +1 @@ +export default function make(): Promise; diff --git a/lib/make.js b/lib/make.js new file mode 100644 index 0000000..7c76b43 --- /dev/null +++ b/lib/make.js @@ -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; diff --git a/lib/template.d.ts b/lib/template.d.ts new file mode 100644 index 0000000..deae865 --- /dev/null +++ b/lib/template.d.ts @@ -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; diff --git a/lib/template.js b/lib/template.js new file mode 100644 index 0000000..aa2728e --- /dev/null +++ b/lib/template.js @@ -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; diff --git a/lib/tip-style.d.ts b/lib/tip-style.d.ts new file mode 100644 index 0000000..9368fa5 --- /dev/null +++ b/lib/tip-style.d.ts @@ -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; diff --git a/lib/tip-style.js b/lib/tip-style.js new file mode 100644 index 0000000..74d6fa8 --- /dev/null +++ b/lib/tip-style.js @@ -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; diff --git a/lib/utils.d.ts b/lib/utils.d.ts new file mode 100644 index 0000000..e332d51 --- /dev/null +++ b/lib/utils.d.ts @@ -0,0 +1,59 @@ +/** + * @name 从一组路径里查找到所有json文件 + * @export + * @param {Array} pathArr + * @returns {Set} + */ +export declare function findJson(pathArr: Set): Set; +/** + * @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} current + * @param {Set} target + * @returns {Set} + */ +export declare function difference(current: Set, target: Set): Set; +/** + * @name 获取交集 + * @export + * @template T + * @param {Set} current + * @param {Set} target + * @returns {Set} + */ +export declare function intersect(current: Set, target: Set): Set; +/** + * @name 获取并集 + * @export + * @template T + * @param {Set} current + * @param {Set} target + * @returns {Set} + */ +export declare function union(current: Set, target: Set): Set; +/** + * @name 格式化json + * @export + * @template T + * @param {T} data + * @returns {string} + */ +export declare function formatJsonByFile(data: T): string; +/** + * @name 数组对象去重 + * @export + * @param {Array} arr 需要去重的数组或set + * @param {*} [type] 需要根据哪个字段去重 + * @returns + */ +export declare function deWeight(arr: Array | Set, type: any): Set; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..19c6772 --- /dev/null +++ b/lib/utils.js @@ -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} pathArr + * @returns {Set} + */ +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} current + * @param {Set} target + * @returns {Set} + */ +function difference(current, target) { + return new Set([...target].filter(x => !current.has(x))); +} +exports.difference = difference; +/** + * @name 获取交集 + * @export + * @template T + * @param {Set} current + * @param {Set} target + * @returns {Set} + */ +function intersect(current, target) { + return new Set([...target].filter(x => current.has(x))); +} +exports.intersect = intersect; +/** + * @name 获取并集 + * @export + * @template T + * @param {Set} current + * @param {Set} target + * @returns {Set} + */ +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} 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; diff --git a/package.json b/package.json new file mode 100644 index 0000000..fbd96a0 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/plugins/WechatMpPlugin.js b/plugins/WechatMpPlugin.js new file mode 100644 index 0000000..b5b3882 --- /dev/null +++ b/plugins/WechatMpPlugin.js @@ -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; diff --git a/scripts/build-app-domain.js b/scripts/build-app-domain.js new file mode 100644 index 0000000..eaff5cd --- /dev/null +++ b/scripts/build-app-domain.js @@ -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`); \ No newline at end of file diff --git a/scripts/webpack.js b/scripts/webpack.js new file mode 100644 index 0000000..e596561 --- /dev/null +++ b/scripts/webpack.js @@ -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) + ); + } + } +}); diff --git a/src/build.ts b/src/build.ts new file mode 100644 index 0000000..b27b699 --- /dev/null +++ b/src/build.ts @@ -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(`执行失败`)}`); + } +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..4b1b49b --- /dev/null +++ b/src/config.ts @@ -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' \ No newline at end of file diff --git a/src/create.ts b/src/create.ts new file mode 100644 index 0000000..394bf9f --- /dev/null +++ b/src/create.ts @@ -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 = 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)); + } +} diff --git a/src/enum.ts b/src/enum.ts new file mode 100644 index 0000000..cf62135 --- /dev/null +++ b/src/enum.ts @@ -0,0 +1,9 @@ +/** + * @name 检查文件时用到的枚举 + * @export + * @enum {number} + */ +export enum checkFileExistsAndCreateType { + DIRECTORY, + FILE +} \ No newline at end of file diff --git a/src/file-handle.ts b/src/file-handle.ts new file mode 100644 index 0000000..c930d08 --- /dev/null +++ b/src/file-handle.ts @@ -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 = new Set() +/** + * @name 读取目录下所有文件 + * @export + * @param {string} entry 目录名称 + */ +export function readDirPath(entry: string): Set { + 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} arr + * @returns + */ +export function parseJsonFiles(arr: Set) { + 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; + } + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..02e8e5e --- /dev/null +++ b/src/index.ts @@ -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(' [options]'); + +program + .command('make') + .description('build oak app domain of make on demand') + .action(make); +program + .command('start ') + .usage('') + .description('build we chat mp of start on demand') + .action(build); +program + .command('build ') + .usage('') + .description('build we chat mp of build on demand') + .action(build); +program + .command('create [env]') + .usage('') + // .option('-e, --env ', 'A env') + .description(`create a new project powered by ${CLI_NAME}`) + .action(create); +// output help information on unknown commands +program.arguments('').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); diff --git a/src/interface.ts b/src/interface.ts new file mode 100644 index 0000000..94339f4 --- /dev/null +++ b/src/interface.ts @@ -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 +} +/** + * @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, +} \ No newline at end of file diff --git a/src/make.ts b/src/make.ts new file mode 100644 index 0000000..ec0d516 --- /dev/null +++ b/src/make.ts @@ -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); + } +} diff --git a/src/template.ts b/src/template.ts new file mode 100644 index 0000000..485c8ac --- /dev/null +++ b/src/template.ts @@ -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" + } +}`; +} \ No newline at end of file diff --git a/src/tip-style.ts b/src/tip-style.ts new file mode 100644 index 0000000..1c9062c --- /dev/null +++ b/src/tip-style.ts @@ -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) diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..91bbdbd --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,98 @@ + +/** + * @name 从一组路径里查找到所有json文件 + * @export + * @param {Array} pathArr + * @returns {Set} + */ +export function findJson(pathArr: Set): Set { + const result: Set = 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} current + * @param {Set} target + * @returns {Set} + */ +export function difference(current: Set, target: Set): Set { + return new Set( + [...target].filter(x => !current.has(x)) + ) +} + +/** + * @name 获取交集 + * @export + * @template T + * @param {Set} current + * @param {Set} target + * @returns {Set} + */ +export function intersect(current: Set, target: Set): Set { + return new Set([...target].filter(x => current.has(x))) +} + +/** + * @name 获取并集 + * @export + * @template T + * @param {Set} current + * @param {Set} target + * @returns {Set} + */ +export function union(current: Set, target: Set): Set { + return new Set([...current, ...target]) +} + +/** + * @name 格式化json + * @export + * @template T + * @param {T} data + * @returns {string} + */ +export function formatJsonByFile(data: T): string { + return JSON.stringify(data, null, 2) +} + +/** + * @name 数组对象去重 + * @export + * @param {Array} arr 需要去重的数组或set + * @param {*} [type] 需要根据哪个字段去重 + * @returns + */ +export function deWeight(arr: Array | Set, 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()]); +} diff --git a/template/.gitignore b/template/.gitignore new file mode 100644 index 0000000..bf78570 --- /dev/null +++ b/template/.gitignore @@ -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.* \ No newline at end of file diff --git a/template/.stylelintrc.js b/template/.stylelintrc.js new file mode 100644 index 0000000..969b363 --- /dev/null +++ b/template/.stylelintrc.js @@ -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 + }, +}; \ No newline at end of file diff --git a/template/src/aspects/index.ts b/template/src/aspects/index.ts new file mode 100644 index 0000000..14ae94c --- /dev/null +++ b/template/src/aspects/index.ts @@ -0,0 +1,12 @@ +import { test } from './sample'; + +const aspectDict = { + test +}; + +type AspectDict = typeof aspectDict; + +export { + aspectDict, + AspectDict, +} \ No newline at end of file diff --git a/template/src/aspects/sample.ts b/template/src/aspects/sample.ts new file mode 100644 index 0000000..c5fb6ed --- /dev/null +++ b/template/src/aspects/sample.ts @@ -0,0 +1,6 @@ +import { RuntimeContext } from "oak-general-business"; +import { EntityDict } from '../../app-domain'; + +export async function test(params: string, context: RuntimeContext) { + return 'hello world'; +} diff --git a/template/src/entities/House.ts b/template/src/entities/House.ts new file mode 100644 index 0000000..7999a01 --- /dev/null +++ b/template/src/entities/House.ts @@ -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; +}; diff --git a/template/src/features/Sample.ts b/template/src/features/Sample.ts new file mode 100644 index 0000000..a5f2ea2 --- /dev/null +++ b/template/src/features/Sample.ts @@ -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 { + get(params: any) { + throw new Error('Method not implemented.'); + } + action(action: DoSthAcion) { + throw new Error('Method not implemented.'); + } + cache: Cache; + + constructor(cache: Cache) { + super(); + this.cache = cache; + }; +}; diff --git a/template/src/features/index.ts b/template/src/features/index.ts new file mode 100644 index 0000000..9eea615 --- /dev/null +++ b/template/src/features/index.ts @@ -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) { + const { cache } = features; + + const sample = new Sample.Sample(cache); + + return { + sample, + }; +} + +export type FeatureDict = ReturnType; diff --git a/template/src/typings/polyfill.d.ts b/template/src/typings/polyfill.d.ts new file mode 100644 index 0000000..e69de29 diff --git a/template/wechatMp/src/app.json b/template/wechatMp/src/app.json new file mode 100644 index 0000000..3537d16 --- /dev/null +++ b/template/wechatMp/src/app.json @@ -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" +} diff --git a/template/wechatMp/src/app.less b/template/wechatMp/src/app.less new file mode 100644 index 0000000..06c6fc9 --- /dev/null +++ b/template/wechatMp/src/app.less @@ -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; +} diff --git a/template/wechatMp/src/app.ts b/template/wechatMp/src/app.ts new file mode 100644 index 0000000..3b83786 --- /dev/null +++ b/template/wechatMp/src/app.ts @@ -0,0 +1,18 @@ +import { OakPage, OakComponent } from './init' +export interface IAppOption { + globalData: {}; +} + +App({ + globalData: { + OakPage, + OakComponent, + }, + async onLaunch() { + console.log('onLaunch'); + }, + + onHide() { + console.log('onHide'); + }, +}); diff --git a/template/wechatMp/src/init.ts b/template/wechatMp/src/init.ts new file mode 100644 index 0000000..973851f --- /dev/null +++ b/template/wechatMp/src/init.ts @@ -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, typeof aspectDict, {}>( + storageSchema, + () => ({}), + (store) => new RuntimeContext(store, '123'), + triggers as any, + checkers as any, + aspectDict, + data as any); +export { + OakPage, + OakComponent, +}; \ No newline at end of file diff --git a/template/wechatMp/src/pages/index/index.json b/template/wechatMp/src/pages/index/index.json new file mode 100644 index 0000000..8835af0 --- /dev/null +++ b/template/wechatMp/src/pages/index/index.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/template/wechatMp/src/pages/index/index.less b/template/wechatMp/src/pages/index/index.less new file mode 100644 index 0000000..e1a30da --- /dev/null +++ b/template/wechatMp/src/pages/index/index.less @@ -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; +} diff --git a/template/wechatMp/src/pages/index/index.ts b/template/wechatMp/src/pages/index/index.ts new file mode 100644 index 0000000..667d115 --- /dev/null +++ b/template/wechatMp/src/pages/index/index.ts @@ -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 + }) + } +}) diff --git a/template/wechatMp/src/pages/index/index.wxml b/template/wechatMp/src/pages/index/index.wxml new file mode 100644 index 0000000..f00d294 --- /dev/null +++ b/template/wechatMp/src/pages/index/index.wxml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + 请使用1.4.4及以上版本基础库 + + + + {{userInfo.nickName}} + + + + {{motto}} + + diff --git a/template/wechatMp/src/sitemap.json b/template/wechatMp/src/sitemap.json new file mode 100644 index 0000000..ca02add --- /dev/null +++ b/template/wechatMp/src/sitemap.json @@ -0,0 +1,7 @@ +{ + "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", + "rules": [{ + "action": "allow", + "page": "*" + }] +} \ No newline at end of file diff --git a/template/wechatMp/src/utils/util.ts b/template/wechatMp/src/utils/util.ts new file mode 100644 index 0000000..69a2e19 --- /dev/null +++ b/template/wechatMp/src/utils/util.ts @@ -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 +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..889e9f3 --- /dev/null +++ b/tsconfig.json @@ -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", + ] +} \ No newline at end of file