Compare commits

...

9 Commits

Author SHA1 Message Date
winixt
c1fa59fcf8 chore: 优化发布流程 2025-09-10 11:39:33 +08:00
winixt
720296fc82 docs: 完善 docs 2025-09-10 10:06:16 +08:00
winixt
4158a1aee2 chore: 兼容fes3.x 的插件 2025-09-10 10:00:00 +08:00
winixt
8297d91004 chore: 修复部分问题 2025-09-10 09:48:01 +08:00
winixt
77b651c50b chore: 修复 watch 问题 2025-09-09 20:43:03 +08:00
winixt
ad860c8f20 chore: 优化 create app 问题 2025-09-09 19:37:24 +08:00
winixt
fd811c5269 chore: 整理 create-app 2025-09-08 18:09:16 +08:00
winixt
f29492ebe0 chore: 修复部分问题 2025-09-08 17:49:05 +08:00
winixt
3c77c9a2b6 chore: 优化部分问题 2025-09-08 17:25:06 +08:00
86 changed files with 1694 additions and 2060 deletions

2
.npmrc
View File

@ -1,2 +1,4 @@
registry=https://registry.npmmirror.com
link-workspace-packages=true
prefer-workspace-packages=true
shamefully-hoist=true

View File

@ -56,12 +56,10 @@ It mainly has the following functions:
| [@fesjs/plugin-access](http://fesjs.mumblefe.cn/reference/plugin/plugins/access.html) | Provides the ability to control the permissions of page resources |
| [@fesjs/plugin-enums](http://fesjs.mumblefe.cn/reference/plugin/plugins/enums.html#%E4%BB%8B%E7%BB%8D) | Provide unified enumeration access and rich functions to handle enumeration |
| [@fesjs/plugin-icon](http://fesjs.mumblefe.cn/reference/plugin/plugins/icon.html#%E4%BB%8B%E7%BB%8D) | svg file is automatically registered as a component |
| [@fesjs/plugin-jest](http://fesjs.mumblefe.cn/reference/plugin/plugins/jest.html#%E5%90%AF%E7%94%A8%E6%96%B9%E5%BC%8F) | Based on `Jest`, provide unit testing and coverage testing capabilities |
| [@fesjs/plugin-layout](http://fesjs.mumblefe.cn/reference/plugin/plugins/layout.html) | Simple configuration to have a layout, including navigation and sidebar |
| [@fesjs/plugin-locale](http://fesjs.mumblefe.cn/reference/plugin/plugins/locale.html#%E4%BB%8B%E7%BB%8D) | Based on `Vue I18n`, providing internationalization capabilities |
| [@fesjs/plugin-model](http://fesjs.mumblefe.cn/reference/plugin/plugins/model.html#%E4%BB%8B%E7%BB%8D) | Simple data management solution |
| [@fesjs/plugin-request](http://fesjs.mumblefe.cn/reference/plugin/plugins/request.html#%E5%90%AF%E7%94%A8%E6%96%B9%E5%BC%8F) | Based on the request encapsulated by `Axios`, built-in functions such as preventing repeated requests, request throttling, and error handling |
| [@fesjs/plugin-vuex](http://fesjs.mumblefe.cn/reference/plugin/plugins/vuex.html#%E5%90%AF%E7%94%A8%E6%96%B9%E5%BC%8F) | Based on `Vuex`, provide state management capabilities |
| [@fesjs/plugin-qiankun](http://fesjs.mumblefe.cn/reference/plugin/plugins/qiankun.html#%E4%BB%8B%E7%BB%8D) | Based on `qiankun`, provide microservice capabilities |
| [@fesjs/plugin-sass](http://fesjs.mumblefe.cn/reference/plugin/plugins/sass.html#%E4%BB%8B%E7%BB%8D) | Style support sass |
| [@fesjs/plugin-monaco-editor](http://fesjs.mumblefe.cn/reference/plugin/plugins/editor.html#%E4%BB%8B%E7%BB%8D) | Provide code editor capability, based on `monaco-editor` (code editor used by VS Code) |

View File

@ -1,30 +0,0 @@
export default {
pkgs: [
'create-fes-app',
'fes',
'fes-compiler',
'fes-preset-built-in',
'fes-builder-vite',
'fes-builder-webpack',
'fes-runtime',
'fes-utils',
'fes-plugin-access',
'fes-plugin-enums',
'fes-plugin-icon',
'fes-plugin-jest',
'fes-plugin-layout',
'fes-plugin-locale',
'fes-plugin-model',
'fes-plugin-monaco-editor',
'fes-plugin-qiankun',
'fes-plugin-request',
'fes-plugin-sass',
'fes-plugin-vuex',
'fes-plugin-pinia',
'fes-plugin-windicss',
'fes-plugin-watermark',
'fes-plugin-login',
'fes-plugin-swc',
],
copy: [],
};

18
docs/guide/upgrade4.md Normal file
View File

@ -0,0 +1,18 @@
# 从 3.x 迁移到 4.x
构建模块从 commonjs 切换到 esm
## 需调整内容
- package.json 添加 `"type": "module"`
## 版本 4.x 的 break
1. [@fesjs/builder-vite]: vite5 升级到 [vite7](https://cn.vitejs.dev/guide/migration.html)
2. [@fesjs/plugin-pinia]: pinia 2.x > [3.x](https://github.com/vuejs/pinia/releases/tag/v3.0.0)
## 插件
- 移除插件[@fesjs/plugin-vuex]
- 移除插件[@fesjs/plugin-windicss]
- 移除插件[@fesjs/plugin-jest]

View File

@ -32,11 +32,11 @@
"hooks:sync": "simple-git-hooks"
},
"dependencies": {
"chalk": "^5.0.1",
"conventional-changelog-cli": "^4.1.0",
"enquirer": "^2.3.6",
"consola": "^3.4.2",
"conventional-changelog-cli": "^5.0.0",
"execa": "^6.1.0",
"minimist": "^1.2.6",
"picocolors": "^1.1.1",
"semver": "^7.3.6",
"tsup": "^8.5.0",
"turbo": "^2.5.6"
@ -55,8 +55,7 @@
"simple-git-hooks": "^2.9.0",
"typescript": "^5.9.2",
"vitepress": "1.0.0-alpha.73",
"vue": "^3.5.21",
"yargs-parser": "^21.1.1"
"vue": "^3.5.21"
},
"simple-git-hooks": {
"pre-commit": "npx lint-staged",

View File

@ -47,7 +47,7 @@
"@vue/babel-plugin-jsx": "^1.5.0",
"ajv": "^8.12.0",
"autoprefixer": "^10.4.14",
"babel-loader": "^9.1.2",
"babel-loader": "^10.0.0",
"cli-highlight": "^2.1.11",
"cliui": "^8.0.1",
"connect-history-api-fallback": "^2.0.0",

View File

@ -142,10 +142,11 @@ export default class Config {
}
async requireConfigs(configFiles: string[]): Promise<any[]> {
const models = await Promise.all(configFiles.map(f => {
const models = await Promise.all(configFiles.map((f) => {
// 使用 pathToFileURL 确保在 Windows 下路径格式正确
const fileUrl = pathToFileURL(f).href;
return import(fileUrl);
// 避免命中模块缓存
return import(`${fileUrl}?t=${Date.now()}`);
}));
return models.map(m => compatESModuleRequire(m));
}

View File

@ -1 +1 @@
Used in bin/create-fes-app.js to determine if it is in the local debug state.
Used in bin/create-fes-app.mjs to determine if it is in the local debug state.

View File

@ -1,3 +0,0 @@
#!/usr/bin/env node
require('../lib/cli');

View File

@ -0,0 +1,5 @@
#!/usr/bin/env node
import { runMain } from '../dist/index.mjs';
runMain();

View File

@ -17,23 +17,34 @@
"fes"
],
"sideEffects": false,
"main": "lib/index.js",
"main": "dist/index.mjs",
"module": "dist/index.mjs",
"bin": {
"create-fes-app": "bin/create-fes-app.js"
"create-fes-app": "bin/create-fes-app.mjs"
},
"files": [
"bin",
"lib",
"dist",
"templates/**/*"
],
"scripts": {
"dev": "tsup --watch --sourcemap",
"build": "tsup"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@fesjs/utils": "^3.0.3",
"fs-extra": "^11.1.1",
"inquirer": "^7.3.3",
"readline": "^1.3.0",
"validate-npm-package-name": "^3.0.0"
"citty": "^0.1.6",
"consola": "^3.4.2",
"fs-extra": "^11.3.1",
"glob": "^11.0.3",
"mustache": "^4.2.0",
"ora": "^8.2.0",
"semver": "^7.7.2",
"validate-npm-package-name": "^6.0.2"
},
"devDependencies": {
"@types/validate-npm-package-name": "^4.0.2"
}
}

View File

@ -1,51 +0,0 @@
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import process from 'node:process';
import { chalk, yParser } from '@fesjs/utils';
const args = yParser(process.argv.slice(2), {
alias: {
version: ['v'],
help: ['h'],
force: ['f'],
merge: ['m'],
proxy: ['x'],
},
boolean: ['version', 'help', 'merge', 'force'],
});
if (args._.length > 1) {
console.log(chalk.yellow('\n Warning: You provided more than one argument. The first one will be used as the app\'s name, the rest are ignored.'));
}
if (args.version && !args._[0]) {
args._[0] = 'version';
const local = existsSync(join(__dirname, '../.local'))
? chalk.cyan('@local')
: '';
const { name, version } = require('../package.json');
console.log(`${name}@${version}${local}`);
}
else if (args.help && !args._[0]) {
console.log(`
Usage: create-fes-app <name>
Options:
-v, --version Output the current version
-h, --help Display help for command
-f, --force Overwrite target directory if it exists
-m, --merge Merge target directory if it exists
-x, --proxy <proxyUrl> Use specified proxy when creating project
`);
}
else {
require('.')
.default({
cwd: process.cwd(),
args,
})
.catch((err) => {
console.error(`Create failed, ${err.message}`);
console.error(err);
});
}

View File

@ -1,22 +0,0 @@
import { Generator } from '@fesjs/utils';
export default class AppGenerator extends Generator {
constructor({ cwd, args, path, targetDir }) {
super({
cwd,
args,
});
this.path = path;
this.targetDir = targetDir;
}
async writing() {
this.copyDirectory({
context: {
version: require('../../package.json').version,
},
path: this.path,
target: this.targetDir,
});
}
}

View File

@ -1,24 +0,0 @@
import { Generator } from '@fesjs/utils';
export default class AppGenerator extends Generator {
constructor({ cwd, args, path, targetDir, name }) {
super({
cwd,
args,
});
this.path = path;
this.targetDir = targetDir;
this.name = name;
}
async writing() {
this.copyDirectory({
context: {
version: require('../../package.json').version,
name: this.name,
},
path: this.path,
target: this.targetDir,
});
}
}

View File

@ -1,123 +0,0 @@
import path from 'node:path';
import process from 'node:process';
import { chalk } from '@fesjs/utils';
import fs from 'fs-extra';
import inquirer from 'inquirer';
import validateProjectName from 'validate-npm-package-name';
import AppGenerator from './generator/App';
import PluginGenerator from './generator/Plugin';
import { clearConsole } from './utils';
v;
export default async ({ cwd, args }) => {
if (args.proxy) {
process.env.HTTP_PROXY = args.proxy;
}
const projectName = args._[0];
const inCurrent = projectName === '.';
const name = inCurrent ? path.relative('../', cwd) : projectName;
const targetDir = path.resolve(cwd, projectName || '.');
const result = validateProjectName(name);
if (!result.validForNewPackages) {
console.error(chalk.red(`Invalid project name: "${name}"`));
result.errors
&& result.errors.forEach((err) => {
console.error(chalk.red.dim(`Error: ${err}`));
});
result.warnings
&& result.warnings.forEach((warn) => {
console.error(chalk.red.dim(`Warning: ${warn}`));
});
throw new Error('Process exited');
}
if (fs.pathExistsSync(targetDir) && !args.merge) {
if (args.force) {
await fs.remove(targetDir);
}
else if (inCurrent) {
clearConsole();
const { ok } = await inquirer.prompt([
{
name: 'ok',
type: 'confirm',
message: 'Generate project in current directory?',
},
]);
if (!ok) {
return null;
}
}
else {
clearConsole();
const { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: `Target directory ${chalk.cyan(targetDir)} already exists. Pick an action:`,
choices: [
{ name: 'Overwrite', value: 'overwrite' },
{ name: 'Merge', value: 'merge' },
{ name: 'Cancel', value: false },
],
},
]);
if (!action) {
return null;
}
if (action === 'overwrite') {
console.log(`\nRemoving ${chalk.cyan(targetDir)}...`);
await fs.remove(targetDir);
}
}
}
clearConsole();
const { template } = await inquirer.prompt([
{
name: 'template',
type: 'list',
message: 'Pick an template:',
choices: [
{ name: 'PC, suitable for management desk front-end applications', value: 'pc' },
{ name: 'H5, suitable for mobile applications', value: 'h5' },
{ name: 'Plugin, suitable for fes plugin', value: 'plugin' },
{ name: 'Cancel', value: false },
],
},
]);
if (template === 'pc' || template === 'h5') {
const generator = new AppGenerator({
cwd,
args,
targetDir,
path: path.join(__dirname, `../templates/app/${template}`),
});
await generator.run();
console.log();
console.log(chalk.green(`project ${projectName} created successfully, please execute the following command to use:`));
console.log(`$ cd ${projectName}`);
console.log('$ pnpm i');
console.log('$ pnpm dev');
console.log();
}
else if (template === 'plugin') {
const generator = new PluginGenerator({
cwd,
args,
targetDir,
path: path.join(__dirname, '../templates/plugin'),
name,
});
await generator.run();
console.log();
console.log(chalk.green(`plugin ${projectName} created successfully, please execute the following command to use:`));
console.log(`$ cd ${projectName}`);
console.log('$ pnpm i');
console.log('$ pnpm dev');
console.log();
}
};

View File

@ -0,0 +1,2 @@
export { main } from './main';
export { runMain } from './run';

View File

@ -0,0 +1,116 @@
import { join, relative, resolve } from 'node:path';
import process from 'node:process';
import { defineCommand } from 'citty';
import consola from 'consola';
import ora from 'ora';
import validate from 'validate-npm-package-name';
import pkg from '../package.json' assert { type: 'json' };
import { getWorkPath } from './shared';
import { setupGlobalConsole } from './utils/console';
import { checkEngines } from './utils/engines';
import { copyDirectory } from './utils/gen';
export const main = defineCommand({
meta: {
name: pkg.name,
version: pkg.version,
description: pkg.description,
},
args: {
name: {
type: 'positional',
description: '项目名称',
required: true,
},
proxy: {
type: 'string',
description: '代理地址',
alias: ['-p'],
},
merge: {
type: 'boolean',
description: '是否合并',
alias: ['m'],
},
force: {
type: 'boolean',
description: '是否强制覆盖',
alias: ['f'],
},
},
async setup() {
setupGlobalConsole();
await checkEngines();
},
async run({ args }) {
const inCurrent = args.name === '.';
const cwd = getWorkPath();
const projectName = inCurrent ? relative('../', cwd) : args.name;
const result = validate(projectName);
if (!result.validForNewPackages) {
consola.error(`Invalid project name: "${projectName}"`);
result.errors
&& result.errors.forEach((err) => {
consola.error(`Error: ${err}`);
});
result.warnings
&& result.warnings.forEach((warn) => {
consola.warn(`Warning: ${warn}`);
});
process.exit(0);
}
const template = await consola.prompt('Pick an template:', {
type: 'select',
options: [{
label: 'PC, suitable for management desk front-end applications',
value: 'pc',
}, {
label: 'H5, suitable for mobile applications',
value: 'h5',
}, {
label: 'Plugin, suitable for fes plugin',
value: 'plugin',
}, {
label: 'Cancel',
value: 'cancel',
}],
});
const targetDir = resolve(cwd, projectName || '.');
if (template === 'pc' || template === 'h5') {
const spinner = ora('项目生成中加载中...').start();
copyDirectory({
context: {
version: pkg.version,
},
path: join(__dirname, `../templates/app/${template}`),
target: targetDir,
});
spinner.succeed('项目创建成功');
consola.box([
`cd ${projectName}`,
'pnpm i',
'pnpm dev',
].join('\n'));
}
else if (template === 'plugin') {
copyDirectory({
context: {
version: pkg.version,
name: projectName,
},
path: join(__dirname, '../templates/plugin'),
target: targetDir,
});
consola.success(`plugin ${projectName} created successfully, please execute the following command to use:`);
consola.box([
`cd ${projectName}`,
'pnpm i',
'pnpm dev',
].join('\n'));
}
},
});

View File

@ -0,0 +1,5 @@
import { runMain as _runMain } from 'citty';
import { main } from './main';
export const runMain = () => _runMain(main);

View File

@ -0,0 +1,27 @@
import { readFileSync, rmSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
export const OWNER_DIR = join(dirname(fileURLToPath(import.meta.url)), '..');
export function getWorkPath() {
return process.env.PWD || process.cwd();
}
export function removeSync(dir: string) {
rmSync(dir, {
recursive: true,
force: true,
});
}
export function readJsonSync(filePath: string) {
const content = readFileSync(filePath, 'utf-8');
return JSON.parse(content);
}
export function writeJSONSync(filePath: string, data: Record<string, any>) {
writeFileSync(filePath, JSON.stringify(data, null, 2));
}

View File

@ -1,14 +0,0 @@
import process from 'node:process';
import readline from 'node:readline';
export function clearConsole(title) {
if (process.stdout.isTTY) {
const blank = '\n'.repeat(process.stdout.rows);
console.log(blank);
readline.cursorTo(process.stdout, 0, 0);
readline.clearScreenDown(process.stdout);
if (title) {
console.log(title);
}
}
}

View File

@ -0,0 +1,17 @@
import process from 'node:process';
import { consola } from 'consola';
export function setupGlobalConsole(opts: { dev?: boolean } = {}) {
// Wrap all console logs with consola for better DX
if (opts.dev) {
consola.wrapAll();
}
else {
consola.wrapConsole();
}
process.on('unhandledRejection', err => consola.error('[unhandledRejection]', err));
process.on('uncaughtException', err => consola.error('[uncaughtException]', err));
}

View File

@ -0,0 +1,17 @@
import process from 'node:process';
import { logger } from './logger';
export async function checkEngines() {
const satisfies = await import('semver/functions/satisfies.js').then(
r => r.default || (r as any as typeof import('semver/functions/satisfies.js')),
); // npm/node-semver#381
const currentNode = process.versions.node;
const nodeRange = '>= 18.0.0';
if (!satisfies(currentNode, nodeRange)) {
logger.warn(
`Current version of Node.js (\`${currentNode}\`) is unsupported and might cause issues.\n Please upgrade to a compatible version \`${nodeRange}\`.`,
);
}
}

View File

@ -0,0 +1,46 @@
import { readFileSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';
import { copySync, outputFileSync } from 'fs-extra/esm';
import { globSync } from 'glob';
import Mustache from 'mustache';
import { getWorkPath } from '../shared';
function copyTpl(opts: {
templatePath: string;
target: string;
context: Record<string, any>;
}): void {
const tpl = readFileSync(opts.templatePath, 'utf-8');
const content = Mustache.render(tpl, opts.context);
outputFileSync(opts.target, content, 'utf-8');
}
export function copyDirectory(opts: {
path: string;
target: string;
context: Record<string, any>;
}): void {
const files = globSync('**/*', {
cwd: opts.path,
dot: true,
ignore: ['**/node_modules/**'],
});
files.forEach((file) => {
const absFile = join(opts.path, file);
if (statSync(absFile).isDirectory()) {
return;
}
if (file.endsWith('.tpl')) {
return copyTpl({
templatePath: absFile,
target: join(opts.target, file.replace(/\.tpl$/, '')),
context: opts.context,
});
}
const absTarget = join(opts.target, file);
copySync(absFile, absTarget);
});
}

View File

@ -0,0 +1,3 @@
import { consola } from 'consola';
export const logger = consola.withTag('fes');

View File

@ -12,17 +12,18 @@
"access": "public"
},
"dependencies": {
"@fesjs/builder-webpack": "^3.1.0",
"@fesjs/fes": "^3.1.17",
"@fesjs/plugin-icon": "^4.0.0",
"@fesjs/plugin-request": "^4.0.1",
"@fesjs/builder-webpack": "^4.0.0",
"@fesjs/fes": "^4.0.0",
"@fesjs/plugin-icon": "^5.0.0",
"@fesjs/plugin-request": "^5.0.0",
"core-js": "^3.43.0",
"lodash-es": "^4.17.21",
"vue": "^3.5.17"
},
"devDependencies": {
"@antfu/eslint-config": "4.16.1",
"eslint": "9.29.0",
"@antfu/eslint-config": "^5.2.2",
"eslint": "^9.35.0",
"postcss-px-to-viewport-8-plugin": "^1.2.5",
"typescript": "5.8.3"
"typescript": "^5.9.2"
}
}

View File

@ -1,44 +0,0 @@
import { defineRuntimeConfig } from '@fesjs/fes';
export default defineRuntimeConfig({
request: {
// API 前缀
baseURL: '',
dataHandler(data, response) {
// 处理响应内容异常
if (data.code !== '0') {
if (data.code === '20000') {
console.log('hello world');
}
throw new Error(response);
}
// 响应数据格式化
return data?.result ? data.result : data;
},
// http 异常,和插件异常
errorHandler(error) {
if (error.response) {
// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
// 请求已经成功发起,但没有收到响应
// `error.request` 在浏览器中是 XMLHttpRequest 的实例,
// 而在node.js中是 http.ClientRequest 的实例
console.log(error.request);
} else if (error.type) {
// 插件异常
console.log(error.msg);
} else {
// 发送请求时出了点问题
console.log('Error', error.message);
}
console.log(error.config);
},
// 请求拦截器
requestInterceptors: [],
// 响应拦截器
responseInterceptors: [],
},
});

View File

@ -0,0 +1,42 @@
import { defineRuntimeConfig } from '@fesjs/fes'
import { isPlainObject } from 'lodash-es'
export default defineRuntimeConfig({
request: {
baseURL: '',
timeout: 10000, // 默认 10s
method: 'POST', // 默认 post
mergeRequest: false, // 是否合并请求
responseType: null, // 可选 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData',默认根据 content-type 处理
credentials: 'include', // 默认 include, 'include' | 'same-origin' | 'omit'
headers: {}, // 传给服务器的 header
cacheData: false, // 是否缓存
requestInterceptor: (config: Config) => Config,
responseInterceptor: (response: RequestResponse) => RequestResponse,
transformData(data, response) {
// 处理响应内容异常
if (isPlainObject(data)) {
if (data.code === '10000') {
return Promise.reject(data)
}
return data?.result ? data.result : data
}
return data
},
// http 异常,和插件异常
errorHandler(error) {
// 处理业务异常,例如上述 transformData 抛出的异常
if (error.code) {
console.log(error.msg)
}
else if (error.response) {
// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
console.log(`服务异常:${error.response.status}`)
}
else {
// 请求异常
console.log(error.msg || error.message || `请求失败`)
}
},
},
})

View File

@ -1,64 +0,0 @@
// TODO
// 时间格式化
// js 数字精度计算
// 手机号、身份证号 等的校验
// 数字分割
export function resetContainerHeight(dom) {
const originalHeight = document.body.clientHeight || document.documentElement.clientHeight;
window.onresize = function () {
const resizeHeight = document.documentElement.clientHeight || document.body.clientHeight;
if (resizeHeight < originalHeight) {
// 恢复内容区域高度
const container = document.querySelector(dom);
container.style.height = originalHeight;
}
};
}
export function resetInputBlur() {
const isWechat = window.navigator.userAgent.match(/MicroMessenger\/([\d.]+)/i);
if (!isWechat) return;
const wechatVersion = isWechat[1];
const version = navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/);
// 如果设备类型为iOS 12+ 和wechat 6.7.4+,恢复成原来的视口
if (+wechatVersion.replace(/\./g, '') >= 674 && +version[1] >= 12) {
window.scrollTo(0, Math.max(document.body.clientHeight, document.documentElement.clientHeight));
}
}
export function getQueryString(name) {
const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i');
const r = window.location.search.substr(1).match(reg);
if (r != null) {
return decodeURIComponent(r[2]);
}
return null;
}
export function simpleRequest(options) {
const xhr = new XMLHttpRequest();
xhr.timeout = 3000;
if (options.type === 'GET') {
xhr.open(options.type, options.url, options.async || true);
xhr.send(null);
} else if (options.type === 'POST') {
xhr.open(options.type, options.url, options.async || true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(options.data || {}));
}
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
options.successed(xhr.responseText);
} else {
options.failed && options.failed(xhr);
}
}
};
xhr.ontimeout = function () {
options.failed && options.failed(xhr);
};
}

View File

@ -0,0 +1,61 @@
export function resetContainerHeight(dom) {
const originalHeight = document.body.clientHeight || document.documentElement.clientHeight
window.onresize = function () {
const resizeHeight = document.documentElement.clientHeight || document.body.clientHeight
if (resizeHeight < originalHeight) {
// 恢复内容区域高度
const container = document.querySelector(dom)
container.style.height = originalHeight
}
}
}
export function resetInputBlur() {
const isWechat = window.navigator.userAgent.match(/MicroMessenger\/([\d.]+)/i)
if (!isWechat)
return
const wechatVersion = isWechat[1]
const version = navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/)
// 如果设备类型为iOS 12+ 和wechat 6.7.4+,恢复成原来的视口
if (+wechatVersion.replace(/\./g, '') >= 674 && +version[1] >= 12) {
window.scrollTo(0, Math.max(document.body.clientHeight, document.documentElement.clientHeight))
}
}
export function getQueryString(name) {
const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i')
const r = window.location.search.substr(1).match(reg)
if (r != null) {
return decodeURIComponent(r[2])
}
return null
}
export function simpleRequest(options) {
const xhr = new XMLHttpRequest()
xhr.timeout = 3000
if (options.type === 'GET') {
xhr.open(options.type, options.url, options.async || true)
xhr.send(null)
}
else if (options.type === 'POST') {
xhr.open(options.type, options.url, options.async || true)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send(JSON.stringify(options.data || {}))
}
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
options.successed(xhr.responseText)
}
else {
options.failed && options.failed(xhr)
}
}
}
xhr.ontimeout = function () {
options.failed && options.failed(xhr)
}
}

View File

@ -1,43 +1,37 @@
<template>
<div class="onepiece">
fes & 拉夫德鲁 <br />
<fes-icon :spin="true" class="one-icon" type="smile" @click="clickIcon" />
<div v-if="loading" class="loading">loading</div>
<div v-else class="data">{{ data }}</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import { useRouter, useRequest, defineRouteMeta } from '@fesjs/fes';
<script setup lang="ts">
import { defineRouteMeta, useRequest, useRouter } from '@fesjs/fes'
import { onMounted, ref } from 'vue'
defineRouteMeta({
title: '首页',
});
title: '首页',
})
export default {
setup() {
const fes = ref('fes upgrade to vue3');
const rotate = ref(90);
const router = useRouter();
onMounted(() => {
console.log(router);
console.log('mounted1!!');
});
const clickIcon = () => {
console.log('click Icon');
};
const { loading, data } = useRequest('api');
return {
loading,
data,
fes,
rotate,
clickIcon,
};
},
};
const fes = ref('fes upgrade to vue3')
const rotate = ref(90)
const router = useRouter()
onMounted(() => {
console.log(router)
console.log('mounted1!!')
})
function clickIcon() {
console.log('click Icon')
}
const { loading, data } = useRequest('api')
</script>
<template>
<div class="onepiece">
fes & 拉夫德鲁 <br>
<fes-icon :spin="true" class="one-icon" type="smile" @click="clickIcon" />
<div v-if="loading" class="loading">
loading
</div>
<div v-else class="data">
{{ data }}
</div>
</div>
</template>
<style lang="less" scoped>
@import '~@/styles/mixins/hairline';
@import '~@/styles/mixins/hover';

View File

@ -1,20 +1,14 @@
<template>
<div>{{ fes }}</div>
</template>
<script>
import { defineRouteMeta } from '@fesjs/fes';
import { ref } from 'vue';
<script setup lang="ts">
import { defineRouteMeta } from '@fesjs/fes'
import { ref } from 'vue'
defineRouteMeta({
title: 'one piece',
});
title: 'one piece',
})
export default {
setup() {
const fes = ref('fes upgrade to vue3');
return {
fes,
};
},
};
const fes = ref('fes upgrade to vue3')
</script>
<template>
<div>{{ fes }}</div>
</template>

View File

@ -1,32 +1,34 @@
{
"name": "@fesjs/template",
"version": "3.0.0",
"private": true,
"description": "fes项目模版",
"scripts": {
"build": "fes build",
"prod": "FES_ENV=prod fes build",
"analyze": "ANALYZE=1 fes build",
"dev": "fes dev",
"test:unit": "fes test:unit"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@fesjs/builder-webpack": "^3.1.0",
"@fesjs/fes": "^3.1.17",
"@fesjs/fes-design": "^0.8.82",
"@fesjs/plugin-access": "^3.1.9",
"@fesjs/plugin-enums": "^3.0.1",
"@fesjs/plugin-layout": "^5.4.6",
"@fesjs/plugin-model": "^3.0.3",
"core-js": "^3.43.0",
"vue": "^3.5.17"
},
"devDependencies": {
"@antfu/eslint-config": "4.16.1",
"eslint": "9.29.0",
"typescript": "5.8.3"
}
"name": "@fesjs/template",
"version": "3.0.0",
"private": true,
"description": "fes项目模版",
"scripts": {
"build": "fes build",
"prod": "FES_ENV=prod fes build",
"analyze": "ANALYZE=1 fes build",
"dev": "fes dev",
"test:unit": "fes test:unit"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@fesjs/builder-vite": "^5.0.0",
"@fesjs/fes": "^4.0.0",
"@fesjs/fes-design": "^0.8.82",
"@fesjs/plugin-access": "^4.0.0",
"@fesjs/plugin-enums": "^4.0.0",
"@fesjs/plugin-layout": "^6.0.0",
"@fesjs/plugin-model": "^4.0.0",
"core-js": "^3.43.0",
"lodash-es": "^4.17.21",
"vue": "^3.5.17"
},
"devDependencies": {
"@antfu/eslint-config": "^5.2.2",
"eslint": "^9.35.0",
"postcss-px-to-viewport-8-plugin": "^1.2.5",
"typescript": "^5.9.2"
}
}

View File

@ -1,25 +0,0 @@
import { access, defineRuntimeConfig } from '@fesjs/fes';
import PageLoading from '@/components/pageLoading.vue';
import UserCenter from '@/components/userCenter.vue';
export default defineRuntimeConfig({
beforeRender: {
loading: <PageLoading />,
action() {
const { setRole } = access;
return new Promise((resolve) => {
setTimeout(() => {
setRole('admin');
// useModel('@@initialState') @/components/UserCenter
resolve({
userName: '李雷',
});
}, 1000);
});
},
},
layout: {
renderCustom: () => <UserCenter />,
},
});

View File

@ -0,0 +1,61 @@
import { access, defineRuntimeConfig } from '@fesjs/fes'
import { isPlainObject } from 'lodash-es'
import PageLoading from '@/components/pageLoading.vue'
import UserCenter from '@/components/userCenter.vue'
export default defineRuntimeConfig({
beforeRender: {
loading: <PageLoading />,
action() {
const { setRole } = access
return new Promise((resolve) => {
setTimeout(() => {
setRole('admin')
// 初始化应用的全局状态,可以通过 useModel('@@initialState') 获取,具体用法看@/components/UserCenter 文件
resolve({
userName: '李雷',
})
}, 1000)
})
},
},
request: {
baseURL: '',
timeout: 10000, // 默认 10s
method: 'POST', // 默认 post
mergeRequest: false, // 是否合并请求
responseType: null, // 可选 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData',默认根据 content-type 处理
credentials: 'include', // 默认 include, 'include' | 'same-origin' | 'omit'
headers: {}, // 传给服务器的 header
cacheData: false, // 是否缓存
transformData(data, response) {
// 处理响应内容异常
if (isPlainObject(data)) {
if (data.code === '10000') {
return Promise.reject(data)
}
return data?.result ? data.result : data
}
return data
},
// http 异常,和插件异常
errorHandler(error) {
// 处理业务异常,例如上述 transformData 抛出的异常
if (error.code) {
console.log(error.msg)
}
else if (error.response) {
// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
console.log(`服务异常:${error.response.status}`)
}
else {
// 请求异常
console.log(error.msg || error.message || `请求失败`)
}
},
},
layout: {
renderCustom: () => <UserCenter />,
},
})

View File

@ -1,20 +1,13 @@
<template>
<div class="page-loading">
<f-spin size="large" stroke="#5384ff" />
</div>
</template>
<script>
import { FSpin } from '@fesjs/fes-design';
export default {
components: {
FSpin,
},
setup() {
return {};
},
};
<script setup lang="ts">
import { FSpin } from '@fesjs/fes-design'
</script>
<template>
<div class="page-loading">
<FSpin size="large" stroke="#5384ff" />
</div>
</template>
<style>
.page-loading {
position: fixed;

View File

@ -1,18 +1,15 @@
<template>
<div class="right">{{ initialState.userName }}</div>
</template>
<script>
import { useModel } from '@fesjs/fes';
<script setup lang="ts">
import { useModel } from '@fesjs/fes'
export default {
setup() {
const initialState = useModel('@@initialState');
return {
initialState,
};
},
};
const initialState = useModel('@@initialState')
</script>
<template>
<div class="right">
{{ initialState.userName }}
</div>
</template>
<style scope>
.right {
text-align: right;

View File

@ -1,12 +1,14 @@
<template>
<div style="padding: 32px">hello world</div>
</template>
<script setup>
import { defineRouteMeta } from '@fesjs/fes';
<script setup lang="ts">
import { defineRouteMeta } from '@fesjs/fes'
defineRouteMeta({
name: 'index',
title: '首页',
});
name: 'index',
title: '首页',
})
</script>
<template>
<div style="padding: 32px">
hello world
</div>
</template>

View File

@ -1,16 +0,0 @@
# http://editorconfig.org
root = true
lib
[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

View File

@ -1,21 +0,0 @@
module.exports = {
extends: ['@webank/eslint-config-webank/vue.js'],
globals: {
// 这里填入你的项目需要的全局变量
// 这里值为 false 表示这个全局变量不允许被重新赋值,比如:
//
// Vue: false
__DEV__: false,
},
rules: {
'vue/comment-directive': 'off',
'global-require': 'off',
'import/no-unresolved': 'off',
'no-restricted-syntax': 'off',
'no-undefined': 'off',
'vue/valid-template-root': 'off',
},
env: {
jest: true,
},
};

View File

@ -1,2 +1,8 @@
.DS_Store
node_modules
lib
lib
dist
npm-debug.log

View File

@ -1,4 +0,0 @@
{
"singleQuote": true,
"trailingComma": "none"
}

View File

@ -0,0 +1 @@
# Fes.js 插件

View File

@ -1,3 +0,0 @@
module.exports = {
copy: ['runtime'],
};

View File

@ -0,0 +1,36 @@
// eslint.config.js
import antfu from '@antfu/eslint-config';
export default await antfu({
stylistic: {
indent: 4,
quotes: 'single',
semi: 'always',
ignores: ['*.yaml'],
},
typescript: true,
vue: true,
rules: {
'curly': ['error', 'multi-line'],
'vue/block-order': [
'error',
{
order: ['template', 'script', 'style'],
},
],
'style/member-delimiter-style': [
'error',
{
multiline: {
delimiter: 'semi',
requireLast: true,
},
singleline: {
delimiter: 'semi',
requireLast: false,
},
multilineDetection: 'brackets',
},
],
},
});

View File

@ -1,17 +1,17 @@
{
"name": "fes-plugin-{{{name}}}",
"version": "3.0.0",
"version": "1.0.0",
"description": "一个fes.js插件",
"main": "lib/index.js",
"main": "dist/index.mjs",
"module": "dist/index.mjs",
"files": [
"lib",
"dist",
"README.md",
"types.d.ts"
],
"scripts": {
"dev": "node scripts/build.js --watch",
"build": "node scripts/build.js",
"lint": "eslint -c ./.eslintrc.js --ext .js,.jsx,.vue,.ts"
"dev": "tsup --watch --sourcemap",
"build": "tsup"
},
"license": "MIT",
"keywords": [
@ -19,31 +19,14 @@
"dependencies": {
},
"devDependencies": {
"@babel/core": "^7.23.2",
"@babel/preset-env": "^7.23.2",
"@webank/eslint-config-webank": "1.2.7",
"chalk": "^4.1.2",
"chokidar": "^3.5.2",
"deepmerge": "^4.2.2",
"fs-extra": "^11.1.1",
"husky": "^4.3.0",
"lint-staged": "^10.4.0",
"yargs-parser": "^20.2.9"
"@antfu/eslint-config": "^5.2.2",
"tsup": "^8.5.0",
"fs-extra": "^11.3.1",
"eslint": "^9.34.0",
"typescript": "^5.9.2"
},
"peerDependencies": {
"@fesjs/fes": "^3.0.0",
"vue": "^3.2.47"
},
"lint-staged": {
"*.{js,jsx,vue,ts}": [
"eslint --format=codeframe"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"typings": "./types.d.ts"
"@fesjs/fes": "^4.0.0",
"vue": "^3.5.20",
}
}

View File

@ -1,149 +0,0 @@
// 关闭 import 规则
const fs = require('node:fs');
const path = require('node:path');
const chalk = require('chalk');
const chokidar = require('chokidar');
const merge = require('deepmerge');
const fse = require('fs-extra');
const argv = require('yargs-parser')(process.argv.slice(2));
const pkg = require('../package.json');
const compiler = require('./compiler');
const randomColor = require('./randomColor');
const ESM_OUTPUT_DIR = 'es';
const NODE_CJS_OUTPUT_DIR = 'lib';
const SOURCE_DIR = 'src';
const CONFIG_FILE_NAME = 'build.config.js';
const GLOBAL_CONFIG_PATH = path.join(process.cwd(), CONFIG_FILE_NAME);
const DEFAULT_CONFIG = {
target: 'node',
};
function genLog(pkgName) {
return (msg) => {
console.log(`${randomColor(pkgName)}: ${msg}`);
};
}
function genShortPath(filePath) {
const codePath = filePath.split(`/${SOURCE_DIR}/`)[1];
return `${SOURCE_DIR}/${codePath}`;
}
function getPkgSourcePath() {
return path.join(process.cwd(), SOURCE_DIR);
}
function getOutputPath(config) {
if (config.target === 'browser') {
return path.join(process.cwd(), ESM_OUTPUT_DIR);
}
return path.join(process.cwd(), NODE_CJS_OUTPUT_DIR);
}
function getGlobalConfig() {
if (fs.existsSync(GLOBAL_CONFIG_PATH)) {
const userConfig = require(GLOBAL_CONFIG_PATH);
return merge(DEFAULT_CONFIG, userConfig);
}
return DEFAULT_CONFIG;
}
function cleanBeforeCompilerResult(log) {
const esmOutputDir = path.join(process.cwd(), ESM_OUTPUT_DIR);
const cjsOutputDir = path.join(process.cwd(), NODE_CJS_OUTPUT_DIR);
if (fs.existsSync(esmOutputDir)) {
log(chalk.gray(`Clean ${ESM_OUTPUT_DIR} directory`));
fse.removeSync(esmOutputDir);
}
if (fs.existsSync(cjsOutputDir)) {
log(chalk.gray(`Clean ${NODE_CJS_OUTPUT_DIR} directory`));
fse.removeSync(cjsOutputDir);
}
}
function transformFile(filePath, outputPath, config, log) {
if (/\.[jt]sx?$/.test(path.extname(filePath))) {
try {
const code = fs.readFileSync(filePath, 'utf-8');
const shortFilePath = genShortPath(filePath);
const transformedCode = compiler(code, config);
const type = config.target === 'browser' ? ESM_OUTPUT_DIR : NODE_CJS_OUTPUT_DIR;
log(`Transform to ${type} for ${config.target === 'browser' ? chalk.yellow(shortFilePath) : chalk.blue(shortFilePath)}`);
fse.outputFileSync(outputPath, transformedCode);
}
catch (error) {
console.error(error);
}
}
else {
fse.copySync(filePath, outputPath);
}
}
function compilerPkg(codeDir, outputDir, config, log) {
const files = fs.readdirSync(codeDir);
files.forEach((file) => {
const filePath = path.join(codeDir, file);
const outputFilePath = path.join(outputDir, file);
const fileStats = fs.lstatSync(filePath);
if (config.copy.includes(file)) {
fse.copySync(filePath, outputFilePath);
}
else if (fileStats.isDirectory(filePath) && !/__tests__/.test(file)) {
fse.ensureDirSync(outputFilePath);
compilerPkg(filePath, outputFilePath, config, log);
}
else if (fileStats.isFile(filePath)) {
transformFile(filePath, outputFilePath, config, log);
}
});
}
function watchFile(dir, outputDir, config, log) {
chokidar
.watch(dir, {
ignoreInitial: true,
})
.on('all', (event, changeFile) => {
// 修改的可能是一个目录,一个文件,一个需要 copy 的文件 or 目录
const shortChangeFile = genShortPath(changeFile);
const outputPath = changeFile.replace(dir, outputDir);
const stat = fs.lstatSync(changeFile);
log(`[${event}] ${shortChangeFile}`);
if (config.resolveCopy.some(item => changeFile.startsWith(item))) {
fse.copySync(changeFile, outputPath);
}
else if (stat.isFile()) {
transformFile(changeFile, outputPath, config, log);
}
else if (stat.isDirectory()) {
compilerPkg(changeFile, outputPath, config);
}
});
}
function main() {
const sourceCodeDir = getPkgSourcePath();
const pkgName = pkg.name;
if (fs.existsSync(sourceCodeDir)) {
const log = genLog(pkgName);
const config = getGlobalConfig();
const outputDir = getOutputPath(config);
cleanBeforeCompilerResult(log);
const type = config.target === 'browser' ? ESM_OUTPUT_DIR : NODE_CJS_OUTPUT_DIR;
log(chalk.white(`Build ${type} with babel`));
compilerPkg(sourceCodeDir, outputDir, config, log);
if (argv.watch) {
log(chalk.magenta(`Start watch ${SOURCE_DIR} directory...`));
watchFile(sourceCodeDir, outputDir, config, log);
}
}
}
main();

View File

@ -1,52 +0,0 @@
// 关闭 import 规则
/* eslint import/no-extraneous-dependencies: 0 */
const babel = require('@babel/core');
function transform(code, options) {
const result = babel.transformSync(code, options);
return result.code;
}
function transformNodeCode(code) {
return transform(code, {
presets: [
[
'@babel/preset-env',
{
modules: 'cjs',
targets: { node: '12' },
},
],
],
});
}
function transformBrowserCode(code) {
// 因为 fes.js 在生产打包的时候,会处理所有的 node_modules 下的文件,确保不会丢失必要 polyfill
// 因此这里不对 polyfill 进行处理,避免全局污染
return transform(code, {
presets: [
[
'@babel/preset-env',
{
modules: false,
useBuiltIns: false,
targets: { chrome: '64' },
},
],
],
});
}
function compiler(code, config) {
if (!config.target || config.target === 'node') {
return transformNodeCode(code);
}
if (config.target === 'browser') {
return transformBrowserCode(code);
}
throw new Error(`config target error: ${config.target}, only can use 'node' and 'browser'`);
}
module.exports = compiler;

View File

@ -1,35 +0,0 @@
/* eslint import/no-extraneous-dependencies: 0 */
const chalk = require('chalk');
const colors = [
'red',
'green',
'yellow',
'blue',
'magenta',
'cyan',
'gray',
'redBright',
'greenBright',
'yellowBright',
'blueBright',
'magentaBright',
'cyanBright',
];
let index = 0;
const cache = {};
module.exports = function (pkg) {
if (!cache[pkg]) {
const color = colors[index];
const str = chalk[color].bold(pkg);
cache[pkg] = str;
if (index === colors.length - 1) {
index = 0;
} else {
index += 1;
}
}
return cache[pkg];
};

View File

@ -1,5 +1,5 @@
import { join } from 'path';
import { readFileSync } from 'fs';
import { join } from 'node:path';
import { readFileSync } from 'node:fs';
import { name } from '../package.json';
const namespace = 'plugin-{{{name}}}';

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"rootDir": "./src",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

View File

@ -0,0 +1,15 @@
import { copySync } from 'fs-extra/esm';
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
splitting: false,
sourcemap: false,
clean: true,
dts: false,
shims: true,
format: ['esm'],
onSuccess() {
copySync('src/runtime', 'dist/runtime');
},
});

View File

@ -1,10 +0,0 @@
import {} from '@fesjs/fes';
declare module "@fesjs/fes" {
interface PluginBuildConfig {
}
interface PluginRuntimeConfig {
}
}

View File

@ -0,0 +1,9 @@
{
"extends": ["@fesjs/typescript-config/base.json"],
"compilerOptions": {
"rootDir": "./src",
"outDir": "./build"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

View File

@ -0,0 +1,11 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
splitting: false,
sourcemap: false,
clean: true,
dts: false,
shims: true,
format: ['esm'],
});

View File

@ -7,7 +7,7 @@
"author": "qlin",
"license": "MIT",
"scripts": {
"build": "FES_ENV=prod fes build",
"prod": "FES_ENV=prod fes build",
"dev": "fes dev"
},
"dependencies": {

View File

@ -1,13 +1,19 @@
<template>
<div class="onepiece m-10px text-yellow-700">fes h5 & 拉夫德鲁<br /></div>
<div class="onepiece m-10px text-yellow-700">
fes h5 & 拉夫德鲁<br>
</div>
</template>
<script setup>
import { defineRouteMeta } from '@fesjs/fes';
import { defineRouteMeta, request } from '@fesjs/fes';
defineRouteMeta({
title: '首页',
name: 'testIndex',
layout: false,
});
request('/ras-mas/a/b').then((res) => {
console.log('res', res);
});
</script>

View File

@ -6,6 +6,7 @@ export default defineBuildConfig({
define: {
__DEV__: false,
},
publicPath: './',
title: '海贼王',
router: {
mode: 'hash',
@ -71,6 +72,8 @@ export default defineBuildConfig({
plugins: [
tailwindcss(),
],
server: {
},
},
// enums: {
// status: [

View File

@ -7,7 +7,6 @@
"author": "harrywan",
"license": "MIT",
"scripts": {
"build": "fes build",
"prod": "FES_ENV=prod fes build",
"analyze": "ANALYZE=1 fes build",
"dev": "fes dev",

View File

@ -1,5 +0,0 @@
import { resolveConfig } from 'vite';
console.log(await resolveConfig({
}));

View File

@ -7,7 +7,6 @@
"author": "harrywan",
"license": "MIT",
"scripts": {
"build": "fes build",
"prod": "FES_ENV=prod fes build",
"analyze": "ANALYZE=1 fes build",
"dev": "fes dev",

View File

@ -8,9 +8,13 @@ export default defineConfig({
clean: true,
dts: false,
shims: true,
outExtension: () => ({ js: '.mjs' }),
onSuccess() {
copySync('public', 'dist');
},
outExtension({ format }) {
return {
js: format === 'esm' ? '.mjs' : '.cjs',
};
},
format: ['esm'],
});

View File

@ -6,48 +6,23 @@
"description": "fes项目模版",
"author": "harrywan",
"license": "MIT",
"homepage": "https://github.com/WeBankFinTech/fes.js#readme",
"bugs": {
"url": "https://github.com/WeBankFinTech/fes.js/issues"
},
"keywords": [
"管理端",
"fes",
"fast",
"easy",
"strong"
],
"files": [
".eslintrc.js",
".fes.js",
".fes.prod.js",
".gitignore",
"/config",
"/src",
"README.md",
"mock.js",
"package.json",
"tsconfig.json"
],
"scripts": {
"build": "fes build",
"prod": "FES_ENV=prod fes build",
"analyze": "ANALYZE=1 fes build",
"dev": "fes dev",
"test": "fes test"
"dev": "fes dev"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@fesjs/builder-vite": "file:../../../builder-vite",
"@fesjs/fes": "file:../../../fes",
"@fesjs/builder-vite": "workspace:*",
"@fesjs/fes": "workspace:*",
"@fesjs/fes-design": "^0.8.0",
"@fesjs/plugin-access": "file:../../../plugin-access",
"@fesjs/plugin-layout": "file:../../../plugin-layout",
"@fesjs/plugin-model": "file:../../../plugin-model",
"@fesjs/plugin-qiankun": "file:../../../plugin-qiankun",
"@fesjs/plugin-watermark": "file:../../../plugin-watermark",
"@fesjs/plugin-access": "workspace:*",
"@fesjs/plugin-layout": "workspace:*",
"@fesjs/plugin-model": "workspace:*",
"@fesjs/plugin-qiankun": "workspace:*",
"@fesjs/plugin-watermark": "workspace:*",
"vue": "^3.5.21"
}
}

View File

@ -5,50 +5,20 @@
"private": true,
"description": "fes项目模版",
"author": "harrywan",
"license": "MIT",
"homepage": "https://github.com/WeBankFinTech/fes.js#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/WeBankFinTech/fes.js.git",
"directory": "packages/plugin-qiankun"
},
"bugs": {
"url": "https://github.com/WeBankFinTech/fes.js/issues"
},
"keywords": [
"管理端",
"fes",
"fast",
"easy",
"strong"
],
"files": [
".eslintrc.js",
".fes.js",
".fes.prod.js",
".gitignore",
"/config",
"README.md",
"mock.js",
"package.json",
"tsconfig.json"
],
"scripts": {
"build": "fes build",
"prod": "FES_ENV=prod fes build",
"analyze": "ANALYZE=1 fes build",
"dev": "fes dev",
"test": "fes test"
"dev": "fes dev"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@fesjs/builder-vite": "file:../../../builder-vite",
"@fesjs/fes": "file:../../../fes",
"@fesjs/builder-vite": "workspace:*",
"@fesjs/fes": "workspace:*",
"@fesjs/fes-design": "^0.8.0",
"@fesjs/plugin-qiankun": "file:../../../plugin-qiankun",
"@fesjs/plugin-watermark": "file:../../../plugin-watermark",
"@fesjs/plugin-qiankun": "workspace:*",
"@fesjs/plugin-watermark": "workspace:*",
"vue": "^3.5.21"
}
}

View File

@ -1,11 +1,3 @@
// .fes.js 只负责管理编译时配置只能使用plain Object
import { createRequire } from 'node:module';
export function esmResolve(specifier) {
const esmRequire = createRequire(import.meta.url);
return esmRequire.resolve(specifier);
}
export default {
access: {
roles: {

View File

@ -6,31 +6,7 @@
"description": "fes项目模版",
"author": "harrywan",
"license": "MIT",
"homepage": "https://github.com/WeBankFinTech/fes.js#readme",
"bugs": {
"url": "https://github.com/WeBankFinTech/fes.js/issues"
},
"keywords": [
"管理端",
"fes",
"fast",
"easy",
"strong"
],
"files": [
".eslintrc.js",
".fes.js",
".fes.prod.js",
".gitignore",
"/config",
"/src",
"README.md",
"mock.js",
"package.json",
"tsconfig.json"
],
"scripts": {
"build": "fes build",
"prod": "FES_ENV=prod fes build",
"analyze": "ANALYZE=1 fes build",
"dev": "fes dev",
@ -40,14 +16,14 @@
"access": "public"
},
"dependencies": {
"@fesjs/builder-webpack": "file:../../../builder-webpack",
"@fesjs/fes": "file:../../../fes",
"@fesjs/builder-webpack": "workspace:*",
"@fesjs/fes": "workspace:*",
"@fesjs/fes-design": "^0.8.0",
"@fesjs/plugin-access": "file:../../../plugin-access",
"@fesjs/plugin-layout": "file:../../../plugin-layout",
"@fesjs/plugin-model": "file:../../../plugin-model",
"@fesjs/plugin-qiankun": "file:../../../plugin-qiankun",
"@fesjs/plugin-watermark": "file:../../../plugin-watermark",
"@fesjs/plugin-access": "workspace:*",
"@fesjs/plugin-layout": "workspace:*",
"@fesjs/plugin-model": "workspace:*",
"@fesjs/plugin-qiankun": "workspace:*",
"@fesjs/plugin-watermark": "workspace:*",
"vue": "^3.5.21"
}
}

View File

@ -1,37 +1,28 @@
{
"compilerOptions": {
"outDir": "build/dist",
"module": "esnext",
"target": "esnext",
"lib": ["esnext", "dom"],
"sourceMap": true,
"baseUrl": ".",
"jsx": "preserve",
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true,
"allowJs": true,
"skipLibCheck": true,
"experimentalDecorators": true,
"strict": true,
"paths": {
"@/*": ["./src/*"],
"@@/*": ["./src/.fes/*"]
}
},
"include": [
"src/**/*",
"tests/**/*",
"test/**/*",
"__test__/**/*",
"typings/**/*",
"config/**/*",
".eslintrc.js",
".stylelintrc.js",
".prettierrc.js"
],
"exclude": ["node_modules", "build", "dist", "scripts", "src/.fes/*", "webpack", "jest"]
"compilerOptions": {
"target": "esnext",
"jsx": "preserve",
"lib": ["esnext", "dom"],
"experimentalDecorators": true,
"baseUrl": ".",
"module": "esnext",
"moduleResolution": "node",
"paths": {
"@/*": ["./src/*"],
"@@/*": ["./src/.fes/*"]
},
"allowJs": true,
"strict": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"outDir": "build/dist",
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": [
"src/**/*"
]
}

View File

@ -11,21 +11,19 @@
"url": "https://github.com/WeBankFinTech/fes.js/issues"
},
"scripts": {
"build": "fes build",
"prod": "FES_ENV=prod fes build",
"analyze": "ANALYZE=1 fes build",
"dev": "fes dev",
"test": "fes test"
"dev": "fes dev"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@fesjs/builder-webpack": "file:../../../builder-webpack",
"@fesjs/fes": "file:../../../fes",
"@fesjs/builder-webpack": "workspace:*",
"@fesjs/fes": "workspace:*",
"@fesjs/fes-design": "^0.8.0",
"@fesjs/plugin-qiankun": "file:../../../plugin-qiankun",
"@fesjs/plugin-watermark": "file:../../../plugin-watermark",
"@fesjs/plugin-qiankun": "workspace:*",
"@fesjs/plugin-watermark": "workspace:*",
"vue": "^3.5.21"
}
}

View File

@ -44,7 +44,7 @@
"@fesjs/utils": "workspace:*",
"address": "^1.1.2",
"lodash-es": "^4.17.21",
"qiankun": "^2.7.0",
"qiankun": "^2.10.16",
"vite-plugin-qiankun": "^1.0.15"
},
"devDependencies": {

View File

@ -1,6 +1,6 @@
import assert from 'assert';
import { dirname, join } from 'path';
import { existsSync, statSync, readFileSync, writeFileSync, copyFileSync } from 'fs';
import assert from 'node:assert';
import { copyFileSync, existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { startWatch } from './watch/watchMode';
export default function (api) {
@ -43,7 +43,7 @@ export default function (api) {
fn({ namespace, path, ignore }) {
const base = join(api.paths.absTmpPath, namespace);
// copy 行为只需要执行一次
if (cacheCopyPath[base]) return;
if (cacheCopyPath[base]) { return; }
cacheCopyPath[base] = true;
assert(api.stage >= api.ServiceStage.pluginReady, 'api.copyTmpFiles() should not execute in register stage.');
assert(path, 'api.copyTmpFiles() should has param path');
@ -59,11 +59,13 @@ export default function (api) {
}
if (statSync(source).isDirectory()) {
api.utils.mkdirp.sync(target);
} else if (Array.isArray(ignore)) {
if (!ignore.some((pattern) => new RegExp(pattern).test(file))) {
}
else if (Array.isArray(ignore)) {
if (!ignore.some(pattern => new RegExp(pattern).test(file))) {
copyFileSync(source, target);
}
} else {
}
else {
copyFileSync(source, target);
}
});

View File

@ -1,4 +1,5 @@
import { chokidar, winPath, lodash, getAppPath } from '@fesjs/utils';
import process from 'node:process';
import { chokidar, getAppPath, lodash, winPath } from '@fesjs/utils';
import { watchPkg } from './watchPkg';
async function generateWhenFilesChange({ api }) {
@ -36,7 +37,7 @@ async function generateWhenFilesChange({ api }) {
type: api.ApplyPluginsType.add,
initialValue: [paths.absPagesPath, getAppPath(paths.absSrcPath)],
});
lodash.uniq(watcherPaths.map((p) => winPath(p))).forEach((p) => {
lodash.uniq(watcherPaths.map(p => winPath(p))).forEach((p) => {
createWatcher(p);
});
@ -44,7 +45,9 @@ async function generateWhenFilesChange({ api }) {
}
export async function startWatch(api) {
if (process.env.WATCH === 'none') return;
if (process.env.WATCH === 'none') {
return;
}
let unwatchs = [];
const restartServer = () => {
@ -76,7 +79,7 @@ export async function startWatch(api) {
onChange: async ({ pluginChanged, valueChanged }) => {
if (pluginChanged.length) {
console.log();
api.logger.info(`Plugins of ${pluginChanged.map((p) => p.key).join(', ')} changed.`);
api.logger.info(`Plugins of ${pluginChanged.map(p => p.key).join(', ')} changed.`);
restartServer();
}
if (valueChanged.length) {
@ -102,7 +105,8 @@ export async function startWatch(api) {
console.log();
api.logger.info(`Config ${reloadConfigs.join(', ')} changed.`);
restartServer();
} else {
}
else {
api.service.userConfig = api.service.configInstance.getUserConfig();
await api.setConfig();
@ -112,8 +116,9 @@ export async function startWatch(api) {
key: 'onGenerateFiles',
type: api.ApplyPluginsType.event,
});
} else {
fns.forEach((fn) => fn());
}
else {
fns.forEach(fn => fn());
}
}
}

View File

@ -1,18 +1,13 @@
/**
* @copy umi
* https://github.com/umijs/umi/blob/master/packages/preset-built-in/src/plugins/commands/dev/watchPkg.ts
*/
import { join } from 'path';
import { chokidar, winPath, lodash } from '@fesjs/utils';
import { existsSync, readFileSync } from 'fs';
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { isPluginOrPreset, PluginType } from '@fesjs/compiler';
import { chokidar, lodash, winPath } from '@fesjs/utils';
function getPlugins(opts) {
return Object.keys({
...opts.pkg.dependencies,
...opts.pkg.devDependencies,
}).filter((name) => isPluginOrPreset(PluginType.plugin, name) || isPluginOrPreset(PluginType.preset, name));
}).filter(name => isPluginOrPreset(PluginType.plugin, name) || isPluginOrPreset(PluginType.preset, name));
}
function getPluginsFromPkgPath(opts) {
@ -20,7 +15,8 @@ function getPluginsFromPkgPath(opts) {
if (existsSync(opts.pkgPath)) {
try {
pkg = JSON.parse(readFileSync(opts.pkgPath, 'utf-8'));
} catch (e) {
}
catch (e) {
// ignore
}
}

View File

@ -1,77 +0,0 @@
import { copyFileSync, readFileSync, statSync, writeFileSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
import chalk from 'chalk';
import glob from 'glob';
import mkdirp from 'mkdirp';
import Mustache from 'mustache';
interface GeneratorOptions {
cwd: string;
args: Record<string, any>;
}
interface CopyTplOptions {
templatePath: string;
target: string;
context: Record<string, any>;
}
interface CopyDirectoryOptions {
path: string;
target: string;
context: Record<string, any>;
}
class Generator {
cwd: string;
args: Record<string, any>;
constructor({ cwd, args }: GeneratorOptions) {
this.cwd = cwd;
this.args = args;
}
async run(): Promise<void> {
await this.writing();
}
async writing(): Promise<void> { }
copyTpl(opts: CopyTplOptions): void {
const tpl = readFileSync(opts.templatePath, 'utf-8');
const content = Mustache.render(tpl, opts.context);
mkdirp.sync(dirname(opts.target));
// eslint-disable-next-line no-console
console.log(`${chalk.green('Write:')} ${relative(this.cwd, opts.target)}`);
writeFileSync(opts.target, content, 'utf-8');
}
copyDirectory(opts: CopyDirectoryOptions): void {
const files = glob.sync('**/*', {
cwd: opts.path,
dot: true,
ignore: ['**/node_modules/**'],
});
files.forEach((file) => {
const absFile = join(opts.path, file);
if (statSync(absFile).isDirectory()) {
return;
}
if (file.endsWith('.tpl')) {
return this.copyTpl({
templatePath: absFile,
target: join(opts.target, file.replace(/\.tpl$/, '')),
context: opts.context,
});
}
// eslint-disable-next-line no-console
console.log(`${chalk.green('Copy: ')} ${file}`);
const absTarget = join(opts.target, file);
mkdirp.sync(dirname(absTarget));
copyFileSync(absFile, absTarget);
});
}
}
export default Generator;

View File

@ -1,3 +1,6 @@
export default function compatESModuleRequire(m: any): any {
if (m.__esModule && m.default?.default) {
return m.default.default;
}
return m.default ?? m;
}

View File

@ -18,7 +18,6 @@ import yParser from 'yargs-parser';
import changePort from './changePort';
import compatESModuleRequire from './compatESModuleRequire';
import delay from './delay';
import Generator from './Generator';
import getAppPath from './getAppPath';
import getHostName from './getHostName';
import getPort from './getPort';
@ -53,7 +52,6 @@ export {
changePort,
compatESModuleRequire,
delay,
Generator,
getAppPath,
getHostName,
getPort,

1405
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,3 @@
packages:
- 'packages/**'
- '!packages/create-fes-app/templates/**'
- '!packages/plugin-qiankun/examples/**'
- '!packages/fes-template/**'
- '!packages/fes-template-vite/**'
- '!packages/fes-template-h5/**'
- 'packages/**'
- '!packages/create-fes-app/templates/**'

View File

@ -1,198 +0,0 @@
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import chalk from 'chalk';
import chokidar from 'chokidar';
import merge from 'deepmerge';
import fse from 'fs-extra';
import yargsParser from 'yargs-parser';
import buildConfig from '../build.config.js';
import compiler from './compiler.mjs';
import randomColor from './randomColor.mjs';
const argv = yargsParser(process.argv.slice(2));
const ESM_OUTPUT_DIR = 'es';
const NODE_CJS_OUTPUT_DIR = 'lib';
const SOURCE_DIR = 'src';
const CONFIG_FILE_NAME = 'build.config.js';
const GLOBAL_CONFIG_PATH = path.join(process.cwd(), CONFIG_FILE_NAME);
const DEFAULT_CONFIG = {
target: 'node',
pkgs: [],
copy: [],
};
const PACKAGE_PATH = path.join(process.cwd(), './packages');
function genLog(pkgName) {
return (msg) => {
console.log(`${randomColor(pkgName)}: ${msg}`);
};
}
function getPkgPath(pkgName) {
return path.join(PACKAGE_PATH, pkgName);
}
function genShortPath(filePath) {
// 处理 windows 的路径
filePath = filePath.replace(/\\/g, '/');
const codePath = filePath.split(`/${SOURCE_DIR}/`)[1];
return `${SOURCE_DIR}/${codePath}`;
}
function getPkgSourcePath(pkgName) {
return path.join(getPkgPath(pkgName), SOURCE_DIR);
}
function getOutputPath(config, pkgName) {
if (config.target === 'browser') {
return path.join(getPkgPath(pkgName), ESM_OUTPUT_DIR);
}
return path.join(getPkgPath(pkgName), NODE_CJS_OUTPUT_DIR);
}
function getGlobalConfig() {
if (fs.existsSync(GLOBAL_CONFIG_PATH)) {
return merge(DEFAULT_CONFIG, buildConfig);
}
return DEFAULT_CONFIG;
}
async function getPkgConfig(config, pkgName) {
const pkgConfigPath = path.join(getPkgPath(pkgName), CONFIG_FILE_NAME);
if (argv.watch) {
config.sourceMap = true;
}
if (fs.existsSync(pkgConfigPath)) {
const content = await import(process.platform === 'win32' ? `file://${pkgConfigPath}` : pkgConfigPath);
const result = merge(config, content.default);
result.resolveCopy = result.copy.map(item => path.join(getPkgPath(pkgName), 'src', item));
return result;
}
return config;
}
function getNeedCompilerPkg(config) {
// 用户通过 cli 指定的包,优先级最高
if (argv.pkg) {
return Array.isArray(argv.pkg) ? argv.pkg : argv.pkg;
}
// 默认编译所有 packages
if (!config.pkgs?.length) {
const pkgs = fs.readdirSync(PACKAGE_PATH);
return pkgs;
}
return config.pkgs;
}
function cleanBeforeCompilerResult(pkgName, log) {
const esmOutputDir = path.join(getPkgPath(pkgName), ESM_OUTPUT_DIR);
const cjsOutputDir = path.join(getPkgPath(pkgName), NODE_CJS_OUTPUT_DIR);
if (fs.existsSync(esmOutputDir)) {
log(chalk.gray(`Clean ${ESM_OUTPUT_DIR} directory`));
fse.removeSync(esmOutputDir);
}
if (fs.existsSync(cjsOutputDir)) {
log(chalk.gray(`Clean ${NODE_CJS_OUTPUT_DIR} directory`));
fse.removeSync(cjsOutputDir);
}
}
function transformFile(filePath, outputPath, config, log) {
if (/\.[jt]sx?$/.test(path.extname(filePath))) {
try {
const code = fs.readFileSync(filePath, 'utf-8');
const shortFilePath = genShortPath(filePath);
if (config.sourceMap) {
config.sourceFileName = filePath;
}
const transformedCode = compiler(code, config);
const type = config.target === 'browser' ? ESM_OUTPUT_DIR : NODE_CJS_OUTPUT_DIR;
log(`Transform to ${type} for ${config.target === 'browser' ? chalk.yellow(shortFilePath) : chalk.blue(shortFilePath)}`);
fse.outputFileSync(outputPath, transformedCode);
}
catch (error) {
console.error(error);
}
}
else {
fse.copySync(filePath, outputPath);
}
}
function compilerPkg(codeDir, outputDir, config, log) {
const files = fs.readdirSync(codeDir);
files.forEach((file) => {
const filePath = path.join(codeDir, file);
const outputFilePath = path.join(outputDir, file);
const fileStats = fs.lstatSync(filePath);
if (config.copy.includes(file)) {
fse.copySync(filePath, outputFilePath);
}
else if (fileStats.isDirectory(filePath) && !/__tests__/.test(file)) {
fse.ensureDirSync(outputFilePath);
compilerPkg(filePath, outputFilePath, config, log);
}
else if (fileStats.isFile(filePath)) {
transformFile(filePath, outputFilePath, config, log);
}
});
}
function watchFile(dir, outputDir, config, log) {
chokidar
.watch(dir, {
ignoreInitial: true,
})
.on('all', (event, changeFile) => {
// 修改的可能是一个目录,一个文件,一个需要 copy 的文件 or 目录
const shortChangeFile = genShortPath(changeFile);
const outputPath = changeFile.replace(dir, outputDir);
const stat = fs.lstatSync(changeFile);
log(`[${event}] ${shortChangeFile}`);
if (config.resolveCopy?.some(item => changeFile.startsWith(item))) { fse.copySync(changeFile, outputPath); }
else if (stat.isFile()) { transformFile(changeFile, outputPath, config, log); }
else if (stat.isDirectory()) { compilerPkg(changeFile, outputPath, config); }
});
}
function compilerPkgs(pkgs, globalConfig) {
pkgs.forEach(async (pkgName) => {
const sourceCodeDir = getPkgSourcePath(pkgName);
if (fs.existsSync(sourceCodeDir)) {
const log = genLog(pkgName);
const config = await getPkgConfig(globalConfig, pkgName);
const outputDir = getOutputPath(config, pkgName);
cleanBeforeCompilerResult(pkgName, log);
const type = config.target === 'browser' ? ESM_OUTPUT_DIR : NODE_CJS_OUTPUT_DIR;
log(chalk.white(`Build ${type} with babel`));
compilerPkg(sourceCodeDir, outputDir, config, log);
if (argv.watch) {
log(chalk.magenta(`Start watch ${SOURCE_DIR} directory...`));
watchFile(sourceCodeDir, outputDir, config, log);
}
}
});
}
function main() {
const globalConfig = getGlobalConfig();
const pkgs = getNeedCompilerPkg(globalConfig);
compilerPkgs(pkgs, globalConfig);
}
main();

View File

@ -1,51 +0,0 @@
import { transformSync } from '@babel/core';
function transform(code, options) {
const result = transformSync(code, options);
return result.code;
}
function transformNodeCode(code, config) {
return transform(code, {
presets: [
[
'@babel/preset-env',
{
modules: false,
targets: { node: '20' },
},
],
],
sourceFileName: config.sourceMap ? config.sourceFileName : undefined,
sourceMaps: config.sourceMap ? 'inline' : false,
});
}
function transformBrowserCode(code) {
// 因为 fes.js 在生产打包的时候,会处理所有的 node_modules 下的文件,确保不会丢失必要 polyfill
// 因此这里不对 polyfill 进行处理,避免全局污染
return transform(code, {
presets: [
[
'@babel/preset-env',
{
modules: false,
useBuiltIns: false,
targets: { chrome: '51' },
},
],
],
});
}
export default function compiler(code, config) {
if (!config.target || config.target === 'node') {
return transformNodeCode(code, config);
}
if (config.target === 'browser') {
return transformBrowserCode(code);
}
throw new Error(`config target error: ${config.target}, only can use 'node' and 'browser'`);
}

View File

@ -1,24 +0,0 @@
# fes.js 源码编译
优雅的编译和日志输出,约定源码放在 `src` 目录。支持 node 端 cjsbrowseresm 编译。
不支持 browser 端的 cjs 编译,有两个理由:
1. 我们的内部包,目前来看只会在我们内部使用,没必再编译一份 cjs。
2. 即使后来有其他包使用,也不大可能不支持 esm即便不支持到时候再加也没问题。
## 使用方式
- 在项目根目录下添加 `build.config.js` 指定需要编译的 `packages`
- 可以通过 `--watch` cli 参数开启 `watch` 模式
- 如果需要只编译某个包,通过 `--pkg pkgName` 参数指定
## 配置
```
const config = {
target: "node", // 编译目标 "node" | "browser", "node" 输出目录 lib, "browser" 输出目录 lib。默认编译目标 "node“
pkgs: [], // 需要编译的 packages 包,默认编译根目录下所有的 packages 包pkgs 参数只在根目录下的配置有效
copy: [] // 直接拷贝,不进行编译
}
```

View File

@ -1,35 +0,0 @@
/* eslint import/no-extraneous-dependencies: 0 */
import chalk from 'chalk';
const colors = [
'red',
'green',
'yellow',
'blue',
'magenta',
'cyan',
'gray',
'redBright',
'greenBright',
'yellowBright',
'blueBright',
'magentaBright',
'cyanBright',
];
let index = 0;
const cache = {};
export default function (pkg) {
if (!cache[pkg]) {
const color = colors[index];
const str = chalk[color].bold(pkg);
cache[pkg] = str;
if (index === colors.length - 1)
index = 0;
else
index += 1;
}
return cache[pkg];
}

View File

@ -1,42 +1,43 @@
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import * as url from 'node:url';
import path from 'node:path';
import minimist from 'minimist';
import chalk from 'chalk';
import semver from 'semver';
import enquirer from 'enquirer';
import consola from 'consola';
import { execa } from 'execa';
import minimist from 'minimist';
import pc from 'picocolors';
import semver from 'semver';
import { getPublicPkgs } from './shared.mjs';
import buildConfig from '../build.config.js';
const { prompt } = enquirer;
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const { preid: preId, dry: isDryRun } = minimist(process.argv.slice(2));
const packages = buildConfig.pkgs;
const packages = getPublicPkgs();
const versionIncrements = ['patch', 'minor', 'major', 'prepatch', 'preminor', 'premajor', 'prerelease'];
function incVersion(version, i) {
let _preId = preId || semver.prerelease(version)?.[0];
if (!_preId && /pre/.test(i))
if (!_preId && /pre/.test(i)) {
_preId = 'beta';
}
return semver.inc(version, i, _preId);
}
function autoIncVersion(version) {
if (version.includes('-'))
if (version.includes('-')) {
return semver.inc(version, 'prerelease');
}
return semver.inc(version, 'patch');
}
const run = (bin, args, opts = {}) => execa(bin, args, { stdio: 'inherit', ...opts });
const dryRun = (bin, args, opts = {}) => console.log(chalk.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts);
const dryRun = (bin, args, opts = {}) => consola.info(`[dryrun] ${bin} ${args.join(' ')}`, opts);
const runIfNotDry = isDryRun ? dryRun : run;
const getPkgRoot = pkg => path.resolve(__dirname, `../packages/${pkg}`);
const step = msg => console.log(chalk.cyan(msg));
const step = msg => consola.log(msg);
function arrToObj(arr, key) {
return arr.reduce((acc, cur) => {
acc[cur[key]] = cur;
@ -48,12 +49,11 @@ async function publishPackage(pkg, runIfNotDry) {
step(`Publishing ${pkg.name}...`);
try {
let _releaseTag;
if (pkg.newVersion.includes('-'))
if (pkg.newVersion.includes('-')) {
_releaseTag = 'next';
}
await runIfNotDry(
// note: use of pnpm is intentional here as we rely on its publishing
// behavior.
'npm',
['publish', ...(_releaseTag ? ['--tag', _releaseTag] : []), '--access', 'public', '--registry', 'https://registry.npmjs.org'],
{
@ -61,14 +61,14 @@ async function publishPackage(pkg, runIfNotDry) {
stdio: 'pipe',
},
);
console.log('Successfully published :', chalk.green(`${pkg.name}@${pkg.newVersion}`));
console.log('Successfully published :', pc.green(`${pkg.name}@${pkg.newVersion}`));
}
catch (e) {
if (e.stderr.match(/previously published/))
console.log(chalk.red(`Skipping already published: ${pkg.name}`));
if (e.stderr.match(/previously published/)) {
console.log(pc.red(`Skipping already published: ${pkg.name}`));
}
else
throw e;
else { throw e; }
}
}
@ -95,13 +95,15 @@ function updatePackage(pkgName, version, pkgs) {
pkgJson.version = version;
pkgJson.dependencies
&& Object.keys(pkgJson.dependencies).forEach((npmName) => {
if (pkgs[npmName])
if (pkgs[npmName]) {
pkgJson.dependencies[npmName] = `^${pkgs[npmName].newVersion}`;
}
});
pkgJson.peerDependencies
&& Object.keys(pkgJson.peerDependencies).forEach((npmName) => {
if (pkgs[npmName])
if (pkgs[npmName]) {
pkgJson.peerDependencies[npmName] = `^${pkgs[npmName].newVersion}`;
}
});
writePackageJson(pkgName, pkgJson);
}
@ -138,26 +140,25 @@ async function filterChangedPackages() {
async function createPackageNewVersion(name, version) {
// no explicit version, offer suggestions
const { release } = await prompt({
const release = await consola.prompt(`Select release type: ${name}`, {
type: 'select',
name: 'release',
message: `Select release type: ${name}`,
choices: versionIncrements.map(i => `${i} (${incVersion(version, i)})`).concat(['custom']),
options: versionIncrements.map((i) => {
return {
value: incVersion(version, i),
label: `${i} (${incVersion(version, i)})`,
};
}).concat({
value: 'custom',
label: 'custom',
}),
});
let newVersion;
let newVersion = release;
if (release === 'custom') {
newVersion = (
await prompt({
type: 'input',
name: 'version',
message: `Input custom version: ${name}`,
initial: version,
})
).version;
}
else {
newVersion = release.match(/\((.*)\)/)[1];
newVersion = await consola.prompt(`Input custom version: ${name}`, {
type: 'text',
default: version,
});
}
if (!semver.valid(newVersion)) {
@ -212,7 +213,7 @@ async function main() {
const changedPackages = await filterChangedPackages();
if (!changedPackages.length) {
console.log(chalk.yellow(`No changes to commit.`));
consola.warn(`No changes to commit.`);
return;
}
@ -230,16 +231,15 @@ async function main() {
const passiveUpdatePkgs = genOtherPkgsVersion(updatedPkgs);
const packagesVersion = passiveUpdatePkgs.concat(updatedPkgs);
const { yes } = await prompt({
const yes = await consola.prompt(`These packages will be released: \n${packagesVersion
.map(pkg => `${pc.magenta(pkg.name)}: v${pkg.version} > ${pc.green(`v${pkg.newVersion}`)}`)
.join('\n')}\nConfirm?`, {
type: 'confirm',
name: 'yes',
message: `These packages will be released: \n${packagesVersion
.map(pkg => `${chalk.magenta(pkg.name)}: v${pkg.version} > ${chalk.green(`v${pkg.newVersion}`)}`)
.join('\n')}\nConfirm?`,
});
if (!yes)
if (!yes) {
return;
}
const newRootVersion = await genRootPackageVersion();
@ -252,11 +252,11 @@ async function main() {
await run('pnpm', ['i']);
// // build all packages with types
step('\nBuilding all packages...');
if (!isDryRun)
if (!isDryRun) {
await run('pnpm', ['build']);
}
else
console.log(`(skipped build)`);
else { console.log(`(skipped build)`); }
// generate changelog
step('\nGenerating changelog...');
@ -274,8 +274,9 @@ async function main() {
// publish packages
step('\nPublishing packages...');
for (const pkg of packagesVersion)
for (const pkg of packagesVersion) {
await publishPackage(pkg, runIfNotDry);
}
// push to GitHub
step('\nPushing to GitHub...');
@ -283,8 +284,9 @@ async function main() {
await runIfNotDry('git', ['push', 'origin', `refs/tags/v${newRootVersion}`]);
await runIfNotDry('git', ['push']);
if (isDryRun)
if (isDryRun) {
console.log(`\nDry run finished - run git diff to see package changes.`);
}
console.log();
}

View File

@ -1,5 +1,31 @@
import { copyFileSync, mkdirSync, readdirSync } from 'node:fs';
import { copyFileSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import process from 'node:process';
export function getPublicPkgs() {
const packagesDir = join(process.cwd(), 'packages');
const dirs = readdirSync(packagesDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
const publicPkgs = [];
for (const dir of dirs) {
const pkgJsonPath = join(packagesDir, dir, 'package.json');
try {
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
// 如果没有 private 字段或者 private 为 false则是需要发布的包
if (!pkgJson.private) {
publicPkgs.push(dir);
}
}
catch (error) {
// 如果读取或解析 package.json 失败,跳过该目录
console.warn(`Warning: Could not read package.json for ${dir}`, error.message);
}
}
return publicPkgs;
}
export function copyTplFiles(srcDir, dstDir) {
function walk(currentSrc, currentDst) {