解析entity定义和创建oak组件功能初始化

This commit is contained in:
Pan Qiancheng 2024-10-19 19:59:40 +08:00
parent dab1e738b1
commit 8fab2de5dc
16 changed files with 932 additions and 133 deletions

View File

@ -1,45 +1,90 @@
const esbuild = require("esbuild");
const production = process.argv.includes('--production');
const watch = process.argv.includes('--watch');
const production = process.argv.includes("--production");
const watch = process.argv.includes("--watch");
const path = require("path");
const fs = require("fs").promises;
/**
* 递归复制目录
* @param {string} src 源目录
* @param {string} dest 目标目录
*/
async function copyDir(src, dest) {
await fs.mkdir(dest, { recursive: true });
let entries = await fs.readdir(src, { withFileTypes: true });
for (let entry of entries) {
let srcPath = path.join(src, entry.name);
let destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
await copyDir(srcPath, destPath);
} else {
await fs.copyFile(srcPath, destPath);
console.log(`Copied ${srcPath} to ${destPath}`);
}
}
}
/**
* @type {import('esbuild').Plugin}
*/
const copyTemplatesPlugin = {
name: "copy-templates",
setup(build) {
build.onEnd(async () => {
const srcDir = path.join(__dirname, "src", "templates");
const destDir = path.join(__dirname, "dist", "templates");
try {
await copyDir(srcDir, destDir);
console.log("Templates copied successfully");
} catch (err) {
console.error("Error copying templates:", err);
}
});
},
};
/**
* @type {import('esbuild').Plugin}
*/
const esbuildProblemMatcherPlugin = {
name: 'esbuild-problem-matcher',
name: "esbuild-problem-matcher",
setup(build) {
build.onStart(() => {
console.log('[watch] build started');
console.log("[watch] build started");
});
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`);
console.error(` ${location.file}:${location.line}:${location.column}:`);
console.error(
` ${location.file}:${location.line}:${location.column}:`
);
});
console.log('[watch] build finished');
console.log("[watch] build finished");
});
},
};
async function main() {
const ctx = await esbuild.context({
entryPoints: [
'src/extension.ts'
],
entryPoints: ["src/extension.ts"],
bundle: true,
format: 'cjs',
format: "cjs",
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: 'node',
outfile: 'dist/extension.js',
external: ['vscode'],
logLevel: 'silent',
platform: "node",
outfile: "dist/extension.js",
external: ["vscode"],
logLevel: "silent",
plugins: [
/* add to the end of plugins array */
esbuildProblemMatcherPlugin,
copyTemplatesPlugin,
],
});
if (watch) {
@ -50,7 +95,7 @@ async function main() {
}
}
main().catch(e => {
main().catch((e) => {
console.error(e);
process.exit(1);
});

View File

@ -22,8 +22,21 @@
{
"command": "oak-assistant.check-pages-and-namespace",
"title": "检查page和namespace对应关系"
},
{
"command": "oak-assistant.create-oak-component",
"title": "创建OAK组件"
}
]
],
"menus": {
"explorer/context": [
{
"when": "explorerResourceIsFolder",
"command": "oak-assistant.create-oak-component",
"group": "navigation"
}
]
}
},
"scripts": {
"vscode:prepublish": "pnpm run package",
@ -40,16 +53,22 @@
"test": "vscode-test"
},
"devDependencies": {
"@types/vscode": "^1.94.0",
"@types/lodash": "^4.17.12",
"@types/mocha": "^10.0.8",
"@types/node": "20.x",
"@types/node-schedule": "^2.1.7",
"@types/vscode": "^1.94.0",
"@typescript-eslint/eslint-plugin": "^8.7.0",
"@typescript-eslint/parser": "^8.7.0",
"eslint": "^9.11.1",
"esbuild": "^0.24.0",
"npm-run-all": "^4.1.5",
"typescript": "^5.6.2",
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.4.1"
"@vscode/test-electron": "^2.4.1",
"esbuild": "^0.24.0",
"eslint": "^9.11.1",
"npm-run-all": "^4.1.5",
"oak-domain": "^5.1.6",
"typescript": "^5.6.2"
},
"dependencies": {
"lodash": "^4.17.21"
}
}
}

View File

@ -4,13 +4,24 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
lodash:
specifier: ^4.17.21
version: 4.17.21
devDependencies:
'@types/lodash':
specifier: ^4.17.12
version: 4.17.12
'@types/mocha':
specifier: ^10.0.8
version: 10.0.9
'@types/node':
specifier: 20.x
version: 20.16.12
'@types/node-schedule':
specifier: ^2.1.7
version: 2.1.7
'@types/vscode':
specifier: ^1.94.0
version: 1.94.0
@ -35,6 +46,9 @@ devDependencies:
npm-run-all:
specifier: ^4.1.5
version: 4.1.5
oak-domain:
specifier: ^5.1.6
version: 5.1.6
typescript:
specifier: ^5.6.2
version: 5.6.3
@ -422,10 +436,20 @@ packages:
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
dev: true
/@types/lodash@4.17.12:
resolution: {integrity: sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ==}
dev: true
/@types/mocha@10.0.9:
resolution: {integrity: sha512-sicdRoWtYevwxjOHNMPTl3vSfJM6oyW8o1wXeI7uww6b6xHg8eBznQDNSGBCDJmsE8UMxP05JgZRtsKbTqt//Q==}
dev: true
/@types/node-schedule@2.1.7:
resolution: {integrity: sha512-G7Z3R9H7r3TowoH6D2pkzUHPhcJrDF4Jz1JOQ80AX0K2DWTHoN9VC94XzFAPNMdbW9TBzMZ3LjpFi7RYdbxtXA==}
dependencies:
'@types/node': 20.16.12
dev: true
/@types/node@20.16.12:
resolution: {integrity: sha512-LfPFB0zOeCeCNQV3i+67rcoVvoN5n0NVuR2vLG0O5ySQMgchuZlC4lgz546ZOJyDtj5KIgOxy+lacOimfqZAIA==}
dependencies:
@ -888,6 +912,13 @@ packages:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
dev: true
/cron-parser@4.9.0:
resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==}
engines: {node: '>=12.0.0'}
dependencies:
luxon: 3.5.0
dev: true
/cross-spawn@6.0.5:
resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==}
engines: {node: '>=4.8'}
@ -935,6 +966,10 @@ packages:
is-data-view: 1.0.1
dev: true
/dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
dev: true
/debug@4.3.7(supports-color@8.1.1):
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
engines: {node: '>=6.0'}
@ -1854,6 +1889,10 @@ packages:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true
/lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: false
/log-symbols@4.1.0:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
engines: {node: '>=10'}
@ -1870,10 +1909,19 @@ packages:
is-unicode-supported: 1.3.0
dev: true
/long-timeout@0.1.1:
resolution: {integrity: sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==}
dev: true
/lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
dev: true
/luxon@3.5.0:
resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==}
engines: {node: '>=12'}
dev: true
/make-dir@4.0.0:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
@ -1968,6 +2016,15 @@ packages:
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
dev: true
/node-schedule@2.1.1:
resolution: {integrity: sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==}
engines: {node: '>=6'}
dependencies:
cron-parser: 4.9.0
long-timeout: 0.1.1
sorted-array-functions: 1.3.0
dev: true
/normalize-package-data@2.5.0:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
dependencies:
@ -1998,6 +2055,15 @@ packages:
string.prototype.padend: 3.1.6
dev: true
/oak-domain@5.1.6:
resolution: {integrity: sha512-rxWWaC22WpV67h92c6c9nUHgvGA2T61yml4/oFEpVqd7HejV6NlAVJKloZa1oVdW1TUeCrf8XhHe2AYZIB0nYg==}
dependencies:
dayjs: 1.11.13
node-schedule: 2.1.1
uuid: 9.0.1
webidl-conversions: 5.0.0
dev: true
/object-inspect@1.13.2:
resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==}
engines: {node: '>= 0.4'}
@ -2381,6 +2447,10 @@ packages:
engines: {node: '>=14'}
dev: true
/sorted-array-functions@1.3.0:
resolution: {integrity: sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==}
dev: true
/spdx-correct@3.2.0:
resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
dependencies:
@ -2655,6 +2725,11 @@ packages:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
/uuid@9.0.1:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
dev: true
/v8-to-istanbul@9.3.0:
resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
engines: {node: '>=10.12.0'}
@ -2671,6 +2746,11 @@ packages:
spdx-expression-parse: 3.0.1
dev: true
/webidl-conversions@5.0.0:
resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==}
engines: {node: '>=8'}
dev: true
/which-boxed-primitive@1.0.2:
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
dependencies:

View File

@ -1,93 +1,123 @@
import * as vscode from "vscode";
import { setProjectHome, pathConfig } from "./paths";
import { setProjectHome, pathConfig } from "./utils/paths";
import { join } from "path";
import { Uri } from "vscode";
let projectHome: string | undefined;
import checkPagesAndNamespace from "./plugins/checkPagesAndNamespace";
import { OakConfiog } from "./types/OakConfig";
import createOakComponent from "./plugins/createOakComponent";
import { analyzeOakAppDomain } from "./utils/entities";
// 初始化配置
// 查找工作区的根目录中的oak.config.json文件
vscode.workspace.findFiles("oak.config.json").then((uris) => {
if (uris.length === 0) {
vscode.window.showErrorMessage("未找到oak.config.json文件");
return;
}
const uri = uris[0];
const fs = vscode.workspace.fs;
fs.readFile(uri).then((content) => {
const config = JSON.parse(content.toString());
projectHome = join(uri.path, "../", config.projectHome);
console.log("projectHome:", projectHome);
// 设置projectHome
setProjectHome(projectHome);
});
// 查找工作区的根目录中的oak.config.json文件排除src和node_modules目录
const exclude: vscode.GlobPattern = new vscode.RelativePattern(
"**",
"{src,node_modules,lib,configuration}"
);
vscode.workspace.findFiles("oak.config.json", exclude).then((uris) => {
if (uris.length === 0) {
// 获取当前工作区
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
vscode.window.showErrorMessage(
"未找到工作区,请打开一个文件夹后再试。"
);
return;
}
// 弹出提示消息,询问是否以根目录为工作区
vscode.window
.showInformationMessage(
"未找到oak.config.json文件是否以当前工作区根目录为项目主目录",
"是",
"否"
)
.then((value) => {
if (value === "是") {
const rootPath = workspaceFolders[0].uri.fsPath;
const projectPath = join(rootPath, "./");
setProjectHome(projectPath);
vscode.window.showInformationMessage(
`已将项目主目录设置为: ${projectPath}`
);
afterPathSet();
}
});
return;
}
const uri = uris[0];
const fs = vscode.workspace.fs;
fs.readFile(uri).then((content) => {
const config = JSON.parse(content.toString()) as OakConfiog;
const projectHome = join(uri.fsPath, "..", config.projectHome);
console.log("projectHome:", projectHome);
// 设置projectHome
setProjectHome(projectHome);
// 通知已经启用
vscode.window.showInformationMessage("已启用oak-assistant!");
afterPathSet();
});
});
const afterPathSet = async () => {
const stepList: {
name: string;
description: string;
function: () => Promise<void>;
}[] = [
{
name: "解析 Entity",
description: "解析项目中的 Entity 结构",
function: async () => {
await analyzeOakAppDomain(pathConfig.oakAppDomainHome);
},
},
];
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: "解析oak项目结构",
cancellable: false,
},
async (progress) => {
progress.report({ message: "开始分析..." });
try {
for (let i = 0; i < stepList.length; i++) {
const step = stepList[i];
progress.report({
message: step.description,
increment: 100 / stepList.length,
});
await step.function();
}
vscode.window.showInformationMessage("分析完成");
} catch (error) {
vscode.window.showErrorMessage(`分析过程中出错: ${error}`);
}
}
);
};
export async function activate(context: vscode.ExtensionContext) {
console.log(
'Congratulations, your extension "oak-assistant" is now active!'
);
console.log(
'Congratulations, your extension "oak-assistant" is now active!'
);
const helloOak = vscode.commands.registerCommand(
"oak-assistant.hello-oak",
() => {
vscode.window.showInformationMessage(
"Hello OAK from oak-assistant!"
);
}
);
const helloOak = vscode.commands.registerCommand(
"oak-assistant.hello-oak",
() => {
vscode.window.showInformationMessage(
"Hello OAK from oak-assistant!"
);
}
);
const checkPagesAndNamespace = vscode.commands.registerCommand(
"oak-assistant.check-pages-and-namespace",
async () => {
if (!projectHome) {
vscode.window.showErrorMessage(
"配置未初始化请检查oak.config.json文件"
);
return;
}
let errorNums: number = 0;
const fs = vscode.workspace.fs;
const pagesHome = pathConfig.pagesHome;
const namespacesHome = pathConfig.namespacesHome;
console.log("checking:", pagesHome, namespacesHome);
try {
const namespaces = await fs.readDirectory(
Uri.file(namespacesHome)
);
for (const [namespaceName, namespaceType] of namespaces) {
if (namespaceType === vscode.FileType.Directory) {
// 只检查 namespacesHome 下的第一层目录
const pagePath = join(pagesHome, namespaceName);
try {
const stat = await fs.stat(Uri.file(pagePath));
if (stat.type !== vscode.FileType.Directory) {
vscode.window.showErrorMessage(
`页面${namespaceName}不存在或不是目录`
);
errorNums++;
}
} catch (error) {
vscode.window.showErrorMessage(
`页面${namespaceName}不存在`
);
errorNums++;
}
}
}
if (errorNums === 0) {
vscode.window.showInformationMessage("检查通过");
}
} catch (error) {
vscode.window.showErrorMessage(`检查过程中发生错误: ${error}`);
}
}
);
context.subscriptions.push(helloOak, checkPagesAndNamespace);
context.subscriptions.push(
helloOak,
checkPagesAndNamespace(),
createOakComponent()
);
}
export function deactivate() {}

View File

@ -1,29 +0,0 @@
export const pathConfig: {
projectHome: string;
get entityHome(): string;
get triggerHome(): string;
get checkerHome(): string;
get pagesHome(): string;
get namespacesHome(): string;
} = {
projectHome: __dirname,
get entityHome() {
return `${this.projectHome}\\src\\entities`;
},
get triggerHome() {
return `${this.projectHome}\\src\\triggers`;
},
get checkerHome() {
return `${this.projectHome}\\src\\checkers`;
},
get pagesHome() {
return `${this.projectHome}\\src\\pages`;
},
get namespacesHome() {
return `${this.projectHome}\\web\\src\\app\\namespaces`;
}
};
export const setProjectHome = (projectHome: string) => {
pathConfig.projectHome = projectHome.substring(1, projectHome.length - 1);
}

View File

@ -0,0 +1,80 @@
import * as vscode from "vscode";
import { isConfigReady, pathConfig } from "../utils/paths";
import { Uri } from "vscode";
import { join } from "path";
const checkPageAndNamespacePlugin = () => {
const checkPagesAndNamespace = vscode.commands.registerCommand(
"oak-assistant.check-pages-and-namespace",
async () => {
if (!isConfigReady()) {
vscode.window.showErrorMessage(
"配置未初始化请检查oak.config.json文件"
);
return;
}
let errorNums: number = 0;
const fs = vscode.workspace.fs;
const pagesHome = pathConfig.pagesHome;
const namespacesHome = pathConfig.namespacesHome;
console.log("checking:", pagesHome, namespacesHome);
const namespacesList: string[] = [];
try {
const namespaces = await fs.readDirectory(
Uri.file(namespacesHome)
);
for (const [namespaceName, namespaceType] of namespaces) {
namespacesList.push(namespaceName);
if (namespaceType === vscode.FileType.Directory) {
// 只检查 namespacesHome 下的第一层目录
const pagePath = join(pagesHome, namespaceName);
try {
const stat = await fs.stat(Uri.file(pagePath));
if (stat.type !== vscode.FileType.Directory) {
vscode.window.showErrorMessage(
`页面${namespaceName}不存在或不是目录`
);
errorNums++;
}
} catch (error) {
vscode.window.showErrorMessage(
`页面${namespaceName}不存在`
);
errorNums++;
}
}
}
if (errorNums === 0) {
vscode.window
.showInformationMessage(
"检查通过",
...namespacesList,
"关闭"
)
.then((value) => {
if (value) {
if (value === "取消") {
return;
}
// 根据选择的namespace左侧目录结构定位到对应的页面目录
const pagePath = join(pagesHome, value);
vscode.commands.executeCommand(
"revealInExplorer",
Uri.file(pagePath)
);
}
});
}
} catch (error) {
vscode.window.showErrorMessage(`检查过程中发生错误: ${error}`);
}
}
);
return checkPagesAndNamespace;
};
export default checkPageAndNamespacePlugin;

View File

@ -0,0 +1,119 @@
import * as vscode from "vscode";
import { isFileInDirectory } from "../utils/paths";
import { entityConfig } from "../utils/entities";
type CreateComponentConfig = {
entityName: string;
isList: boolean;
autoProjection: boolean;
};
type ConfigStep = {
name: keyof CreateComponentConfig;
description: string;
inputType: "input" | "select" | "confirm";
options?: string[] | (() => string[] | Promise<string[]>);
result?: string;
};
const createComponentSteps: ConfigStep[] = [
{
name: "entityName",
description: "请选择实体名称",
inputType: "select",
options: () => entityConfig.entityNameList,
},
{
name: "isList",
description: "是否为列表",
inputType: "confirm",
},
{
name: "autoProjection",
description: "是否注入Projection",
inputType: "confirm",
},
];
const createOakComponent = () => {
const plugin = vscode.commands.registerCommand(
"oak-assistant.create-oak-component",
async (uri: vscode.Uri) => {
if (!uri) {
vscode.window.showErrorMessage("请在文件夹上右键选择此命令。");
return;
}
const folderPath = uri.fsPath;
const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri);
if (!workspaceFolder) {
vscode.window.showErrorMessage("无法确定工作区文件夹。");
return;
}
if (!isFileInDirectory(folderPath, "pagesHome", "componentsHome")) {
vscode.window.showWarningMessage(
"选择的文件夹不在 pages或components 目录下,无法创建 OAK 组件。"
);
}
const createComponentConfig: CreateComponentConfig = {
entityName: "",
isList: false,
autoProjection: false,
};
for (const step of createComponentSteps) {
if (step.inputType === "input") {
const result = await vscode.window.showInputBox({
prompt: step.description,
});
if (result) {
(createComponentConfig as any)[step.name] = result;
} else {
vscode.window.showErrorMessage(
"未输入有效值,退出创建。"
);
return;
}
} else if (step.inputType === "select") {
const options = step.options
? step.options instanceof Function
? await step.options()
: step.options
: [];
const result = await vscode.window.showQuickPick(options, {
placeHolder: step.description,
});
if (result) {
(createComponentConfig as any)[step.name] = result;
} else {
vscode.window.showErrorMessage(
"未选择有效值,退出创建。"
);
return;
}
} else if (step.inputType === "confirm") {
const result = await vscode.window.showInformationMessage(
step.description,
"是",
"否"
);
if (result === "是") {
(createComponentConfig as any)[step.name] = true;
} else {
(createComponentConfig as any)[step.name] = false;
}
}
}
vscode.window.showInformationMessage(
`创建组件: ${createComponentConfig.entityName}`
);
}
);
return plugin;
};
export default createOakComponent;

View File

View File

View File

3
src/types/OakConfig.ts Normal file
View File

@ -0,0 +1,3 @@
export type OakConfiog = {
projectHome: string;
}

337
src/utils/entities.ts Normal file
View File

@ -0,0 +1,337 @@
import * as vscode from "vscode";
import { EntityShape } from "oak-domain/lib/types";
import { StorageDesc } from "oak-domain/lib/types/Storage";
import { join, dirname } from "path";
import fs from "fs";
import * as ts from "typescript";
export type EntityDict = {
[key: string]: StorageDesc<EntityShape>;
};
const entityDict: EntityDict = {};
const genEntityNameList = (): string[] => {
return Object.keys(entityDict);
};
export const entityConfig = {
get entityNameList() {
return genEntityNameList();
},
};
function resolveImportPath(importPath: string, currentDir: string): string {
if (importPath.startsWith(".")) {
return join(currentDir, `${importPath}.ts`);
}
// 处理非相对路径的导入(如 node_modules
return importPath;
}
function getEvaluateNodeForShorthandProperty(
program: ts.Program,
node: ts.ShorthandPropertyAssignment,
typeChecker: ts.TypeChecker
): any {
const symbol = typeChecker.getSymbolAtLocation(node.name);
if (!symbol) {
return undefined;
}
// 获取符号的声明
const declarations = symbol.declarations;
if (!declarations || declarations.length === 0) {
return undefined;
}
const declaration = declarations[0];
// 从当前文件的所有导入中找到对应的导入
const sourceFile = declaration.getSourceFile();
let propertyName = "";
const importDeclaration = sourceFile.statements.find((statement) => {
// 在这里找到import { actions } from "./Actions" 这样的形式
if (ts.isImportDeclaration(statement)) {
const moduleSpecifier = statement.moduleSpecifier;
if (ts.isStringLiteral(moduleSpecifier)) {
const imports = statement.importClause?.namedBindings;
// 如果导入了node.name
if (imports && ts.isNamedImports(imports)) {
// 这里需要注意如果是import { generalActions as actions } from "./Actions" 这样的形式要拿到as的内容和node.name进行比较
return imports.elements.some((element) => {
if (ts.isImportSpecifier(element)) {
if (element.propertyName) {
propertyName = element.propertyName.getText();
// 这里要确保是as actions里的actions和node.name进行比较
return element
.getText()
.endsWith(` as ${node.getText()}`);
}
// 这里是import { actions } from "./Actions" 这样的形式
propertyName = element.name.getText();
return element.name.getText() === node.getText();
}
return false;
});
}
}
}
return false;
}) as ts.ImportDeclaration | undefined;
// 这里对包内的genericActions做特殊处理
if (propertyName === "genericActions") {
return [
"count",
"stat",
"download",
"select",
"aggregate",
"create",
"remove",
"update",
];
}
if (importDeclaration) {
// 得到导入的路径
const importPath = (
importDeclaration.moduleSpecifier as ts.StringLiteral
).text;
const currentSourceFile = node.getSourceFile();
const resolvedPath = resolveImportPath(
importPath,
dirname(currentSourceFile.fileName)
);
// 创建新的程序来解析导入的文件
const importProgram = ts.createProgram(
[resolvedPath],
program.getCompilerOptions()
);
const importSourceFile = importProgram.getSourceFile(resolvedPath);
if (importSourceFile) {
let foundDeclaration: ts.Node | undefined;
ts.forEachChild(importSourceFile, (child) => {
if (ts.isVariableStatement(child)) {
const declaration = child.declarationList.declarations[0];
if (
ts.isIdentifier(declaration.name) &&
declaration.name.text === propertyName
) {
foundDeclaration = declaration;
}
} else if (
ts.isFunctionDeclaration(child) &&
child.name &&
child.name.text === propertyName
) {
foundDeclaration = child;
} else if (
ts.isExportAssignment(child) &&
ts.isIdentifier(child.expression) &&
child.expression.text === propertyName
) {
foundDeclaration = child;
}
});
if (foundDeclaration) {
return evaluateNode(
importProgram,
foundDeclaration,
importProgram.getTypeChecker()
);
}
}
}
// 如果没有找到导入声明,则假设它是当前文件中的定义
return evaluateNode(program, declaration, typeChecker);
}
function evaluateNode(
program: ts.Program,
node: ts.Node,
typeChecker: ts.TypeChecker
): any {
if (ts.isObjectLiteralExpression(node)) {
return node.properties.reduce((obj: any, prop) => {
if (ts.isShorthandPropertyAssignment(prop)) {
// 得到标识符的名称
const name = prop.name.getText();
const evaluated = getEvaluateNodeForShorthandProperty(
program,
prop,
typeChecker
);
obj[name] = evaluated;
} else if (ts.isPropertyAssignment(prop)) {
const name = prop.name.getText();
obj[name] = evaluateNode(
program,
prop.initializer,
typeChecker
);
}
return obj;
}, {});
} else if (ts.isArrayLiteralExpression(node)) {
return node.elements.map((element) =>
evaluateNode(program, element, typeChecker)
);
} else if (ts.isStringLiteral(node)) {
return node.text;
} else if (ts.isNumericLiteral(node)) {
return Number(node.text);
} else if (node.kind === ts.SyntaxKind.TrueKeyword) {
return true;
} else if (node.kind === ts.SyntaxKind.FalseKeyword) {
return false;
} else if (ts.isIdentifier(node)) {
// 处理导入的标识符
const symbol = typeChecker.getSymbolAtLocation(node);
if (symbol && symbol.valueDeclaration) {
return evaluateNode(program, symbol.valueDeclaration, typeChecker);
}
} else if (ts.isVariableDeclaration(node) && node.initializer) {
// 处理变量声明
return evaluateNode(program, node.initializer, typeChecker);
}
// 对于其他类型的节点,可能需要进一步处理
return undefined;
}
function parseDescFile(
filePath: string,
program: ts.Program
): StorageDesc<EntityShape> | null {
const sourceFile = program.getSourceFile(filePath);
if (!sourceFile) {
vscode.window.showWarningMessage(`无法解析文件: ${filePath}`);
return null;
}
const typeChecker = program.getTypeChecker();
let descObject: StorageDesc<EntityShape> | null = null;
ts.forEachChild(sourceFile, (node) => {
if (ts.isVariableStatement(node)) {
const declaration = node.declarationList.declarations[0];
if (
ts.isIdentifier(declaration.name) &&
declaration.name.text === "desc"
) {
if (
declaration.initializer &&
ts.isObjectLiteralExpression(declaration.initializer)
) {
descObject = evaluateNode(
program,
declaration.initializer,
typeChecker
);
}
}
}
});
return descObject;
}
export const analyzeOakAppDomain = (path: string) => {
const storageFile = join(path, "Storage.ts");
if (!fs.existsSync(storageFile)) {
vscode.window.showErrorMessage(
"Storage.ts文件不存在请先尝试make:domain"
);
return;
}
const program = ts.createProgram([storageFile], {});
const sourceFile = program.getSourceFile(storageFile);
if (!sourceFile) {
vscode.window.showErrorMessage("无法解析Storage.ts文件");
return;
}
let storageSchemaNode: ts.Node | undefined;
ts.forEachChild(sourceFile, (node) => {
if (ts.isVariableStatement(node)) {
const declaration = node.declarationList.declarations[0];
if (
ts.isIdentifier(declaration.name) &&
declaration.name.text === "storageSchema"
) {
storageSchemaNode = declaration.initializer;
}
}
});
if (
!storageSchemaNode ||
!ts.isObjectLiteralExpression(storageSchemaNode)
) {
vscode.window.showErrorMessage("无法找到storageSchema或格式不正确");
return;
}
const importMap: { [key: string]: string } = {};
ts.forEachChild(sourceFile, (node) => {
if (ts.isImportDeclaration(node)) {
const moduleSpecifier = node.moduleSpecifier;
if (ts.isStringLiteral(moduleSpecifier)) {
const importPath = moduleSpecifier.text;
const importClause = node.importClause;
if (
importClause &&
importClause.namedBindings &&
ts.isNamedImports(importClause.namedBindings)
) {
importClause.namedBindings.elements.forEach((element) => {
if (
element.propertyName &&
element.propertyName.text === "desc"
) {
importMap[element.name.text] = importPath;
}
});
}
}
}
});
storageSchemaNode.properties.forEach((prop) => {
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
const entityName = prop.name.text;
if (ts.isIdentifier(prop.initializer)) {
const descName = prop.initializer.text;
const importPath = importMap[descName];
if (importPath) {
const resolvedPath = resolveImportPath(
importPath,
dirname(storageFile)
);
const descObject = parseDescFile(resolvedPath, program);
if (descObject) {
entityDict[entityName] = descObject;
}
} else {
vscode.window.showWarningMessage(
`未找到 ${descName} 的导入路径`
);
}
} else {
vscode.window.showWarningMessage(
`${entityName} 的值不是预期的标识符`
);
}
}
});
console.log("entityDict:", entityDict);
};

75
src/utils/paths.ts Normal file
View File

@ -0,0 +1,75 @@
export const pluginPaths: {
root: string;
get templates(): string;
} = {
root: __dirname,
get templates() {
return `${this.root}\\templates`;
},
};
console.log("plugin inited:", pluginPaths);
export const internalPath = {
entities: "src\\entities",
triggers: "src\\triggers",
checkers: "src\\checkers",
pages: "src\\pages",
namespaces: "web\\src\\app\\namespaces",
oakAppDomain: "src\\oak-app-domain",
components: "src\\components",
};
export const pathConfig: {
projectHome: string;
get entityHome(): string;
get triggerHome(): string;
get checkerHome(): string;
get pagesHome(): string;
get namespacesHome(): string;
get oakAppDomainHome(): string;
get componentsHome(): string;
} = {
projectHome: "",
get entityHome() {
return `${this.projectHome}\\${internalPath.entities}`;
},
get triggerHome() {
return `${this.projectHome}\\${internalPath.triggers}`;
},
get checkerHome() {
return `${this.projectHome}\\${internalPath.checkers}`;
},
get pagesHome() {
return `${this.projectHome}\\${internalPath.pages}`;
},
get namespacesHome() {
return `${this.projectHome}\\${internalPath.namespaces}`;
},
get oakAppDomainHome() {
return `${this.projectHome}\\${internalPath.oakAppDomain}`;
},
get componentsHome() {
return `${this.projectHome}\\${internalPath.components}`;
},
};
export const isConfigReady = (): boolean => {
return pathConfig.projectHome !== "";
};
export const setProjectHome = (projectHome: string) => {
pathConfig.projectHome = projectHome.endsWith("\\")
? projectHome.slice(0, -1)
: projectHome;
};
export const isFileInDirectory = (
file: string,
...directory: (keyof typeof pathConfig)[]
): boolean => {
return directory.some((dir) => {
const pathGetter = pathConfig[dir];
return file.startsWith(pathGetter);
});
};

40
src/utils/template.ts Normal file
View File

@ -0,0 +1,40 @@
import { join } from "path";
import { pluginPaths } from "./paths";
import fs from "fs";
export const templateNames = {
index: "index.ts",
webPcTsx: "web.pc.tsx",
webTsx: "web.tsx",
localeZhCN: "locale\\zh_CN.json",
styleLess: "style.module.less",
} as const;
export type TemplateName = keyof typeof templateNames;
export function getTemplateContext(name: TemplateName): string {
const templateFolder = pluginPaths.templates;
if (!fs.existsSync(templateFolder)) {
throw new Error(`Template folder not found: ${templateFolder}`);
}
const templatePath = join(templateFolder, templateNames[name] + ".template");
if (!fs.existsSync(templatePath)) {
throw new Error(`Template not found: ${templatePath}`);
}
const template = fs.readFileSync(templatePath, "utf-8");
return template;
}
export function fillTemplate(
template: string,
data: Record<string, any>
): string {
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
return data[key] !== undefined ? data[key] : `{{${key}}}`;
});
}
export const getFilledTemplate = (name: TemplateName, data: Record<string, any>) => {
const template = getTemplateContext(name);
return fillTemplate(template, data);
};