From 3ed9548ec86788c138b32707331a21baf881aab3 Mon Sep 17 00:00:00 2001 From: Liu Date: Sat, 18 May 2024 23:38:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E5=90=8C=E4=B8=80=E6=96=87=E4=BB=B6=E4=B8=AD=E7=9A=84=E5=A4=9A?= =?UTF-8?q?=E4=B8=AA=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/compiler/ui-loader.js | 105 +++++++++++++++++++++----------------- src/compiler/ts-loader.ts | 105 +++++++++++++++++++++++++++----------- 2 files changed, 131 insertions(+), 79 deletions(-) diff --git a/lib/compiler/ui-loader.js b/lib/compiler/ui-loader.js index 0bfbc09..23c96be 100644 --- a/lib/compiler/ui-loader.js +++ b/lib/compiler/ui-loader.js @@ -42,6 +42,7 @@ async function compile(rootNode, context, { filePath, indent = 8 }) { const { name: fileName, base: fileBase } = path.parse(filePath); const globalLines = []; const resourceLines = []; + const assets = []; const headerFiles = new Set([""]); const indentStr = " ".repeat(indent); const parentIdent = "parent"; @@ -139,7 +140,7 @@ async function compile(rootNode, context, { filePath, indent = 8 }) { ].join("\n"); } - async function compileResourceNode(node) { + function compileResourceNode(node) { const attrs = node.attributes || {}; if (attrs.type === "text/c") { if (node.text) { @@ -147,58 +148,54 @@ async function compile(rootNode, context, { filePath, indent = 8 }) { } return; } - const asset = await context.importModule(attrs.src); - asset.metadata.headerFiles.forEach((file) => headerFiles.add(file)); - resourceLines.push(asset.metadata.initCode); + assets.push(attrs.src); } - async function compileSchemaNode(node) { + function compileSchemaNode(node) { currentSchema = createSchema(); - await Promise.all( - node.children.map(async (child) => { - switch (child.name) { - case "name": - currentSchema.name = child.text; - break; - case "include": - headerFiles.add(child.text); - break; - case "ref": - currentSchema.refs.push(child.text); - break; - case "code": - if (child.attributes?.kind === "types") { - currentSchema.typesCode += child.text; - } else { - currentSchema.code += child.text; - } - break; - case "template": - currentSchema.template = child; - await compileUINode(child); - break; - default: - throw SyntaxError(`Unknown node: ${child.name}`); - } - }) - ); + node.children.forEach((child) => { + switch (child.name) { + case "name": + currentSchema.name = child.text; + console.log("currentSchema.name", child.text); + break; + case "include": + headerFiles.add(child.text); + break; + case "ref": + currentSchema.refs.push(child.text); + break; + case "code": + if (child.attributes?.kind === "types") { + currentSchema.typesCode += child.text; + } else { + currentSchema.code += child.text; + } + break; + case "template": + currentSchema.template = child; + compileUINode(child); + break; + default: + throw SyntaxError(`Unknown node: ${child.name}`); + } + }); if (!currentSchema.name) { throw SyntaxError("The schema has no name"); } + console.log("currentSchema", currentSchema.name, node); schemas[currentSchema.name] = currentSchema; } - async function compileWidgetNodeChildren(node, ident) { + function compileWidgetNodeChildren(node, ident) { if (!Array.isArray(node.children)) { return; } - const identList = await Promise.all( - node.children.map(async (node) => { - const childIdent = allocWidgetNodeIdent(node); - await compileWidgetNode(node, childIdent); - return childIdent; - }) - ); + const identList = node.children.map((node) => { + const childIdent = allocWidgetNodeIdent(node); + compileWidgetNode(node, childIdent); + return childIdent; + }); identList.forEach((childIdent) => { currentSchema.templateLines.push( `ui_widget_append(${ident}, ${childIdent});` @@ -226,14 +223,14 @@ async function compile(rootNode, context, { filePath, indent = 8 }) { return ident; } - async function compileUINode(node) { + function compileUINode(node) { return compileWidgetNode( node.children.length == 1 ? node.children[0] : node, parentIdent ); } - async function compileWidgetNode(node, ident) { + function compileWidgetNode(node, ident) { const attrs = node.attributes || {}; Object.keys(attrs).forEach((attrName) => { @@ -275,6 +272,8 @@ async function compile(rootNode, context, { filePath, indent = 8 }) { } function compileNode(node) { + let result; + switch (node.name) { case "ui": if (state !== stateEnum.START) { @@ -282,24 +281,34 @@ async function compile(rootNode, context, { filePath, indent = 8 }) { } state = stateEnum.PARSE_UI; currentSchema.body = node; - return compileUINode(node); + result = compileUINode(node); + state = stateEnum.START; + return result; case "lcui-app": break; case "resource": return compileResourceNode(node); case "schema": if (state !== stateEnum.START) { - throw SyntaxError(" must be at the top level"); + throw SyntaxError(` must be at the top level`); } state = stateEnum.PARSE_SCHEMA; - return compileSchemaNode(node); + result = compileSchemaNode(node); + state = stateEnum.START; + return result; default: throw SyntaxError(`Unknown node: ${node.name}`); } - return Promise.all(node.children.map(compileNode)); + return node.children.map(compileNode); } - await compileNode(rootNode); + compileNode(rootNode); + ( + await Promise.all(assets.map((asset) => context.importModule(asset))) + ).forEach((asset) => { + asset.metadata.headerFiles.forEach((file) => headerFiles.add(file)); + resourceLines.push(asset.metadata.initCode); + }); return [ `/** This file is generated from ${fileBase} */`, generateIncluding(), diff --git a/src/compiler/ts-loader.ts b/src/compiler/ts-loader.ts index 0883bae..5d58bc4 100644 --- a/src/compiler/ts-loader.ts +++ b/src/compiler/ts-loader.ts @@ -1,15 +1,21 @@ import fs from "fs"; import path from "path"; import ts from "typescript"; +import React from "react"; import { getResourceLoaderName, parsePageRoute } from "../utils.js"; import { LoaderContext, LoaderInput, Module } from "../types.js"; +function isComponentFunc(name: string) { + return name.charAt(0) >= "A" && name.charAt(0) <= "Z"; +} + export default async function TsLoader( this: LoaderContext, content: LoaderInput ) { const loader = this; const modules: Promise[] = []; + const localFuncNames: string[] = []; const outputDirPath = path.dirname(loader.resolveModule(loader.resourcePath)); function transformer(context: ts.TransformationContext) { @@ -33,6 +39,20 @@ export default async function TsLoader( node.attributes ); } + if ( + ts.isFunctionDeclaration(node) && + node.name && + isComponentFunc(node.name.getText()) + ) { + localFuncNames.push(node.name.getText()); + } else if ( + ts.isVariableDeclaration(node) && + node.initializer && + ts.isArrowFunction(node.initializer) && + isComponentFunc(node.name.getText()) + ) { + localFuncNames.push(node.name.getText()); + } return ts.visitEachChild(node, visitor, context); } @@ -54,16 +74,18 @@ export default async function TsLoader( const assets = (await Promise.all(modules)).filter( (m) => m?.metadata?.type === "asset" ); - await loader.generateModule(loader.resourcePath, () => - tsResult.outputText.replace( - "react/jsx-runtime", - "@lcui/react/lib/jsx-runtime.js" - ) - ); - const { default: componentFunc } = await loader.importModule( - loader.resourcePath + await loader.generateModule( + loader.resourcePath, + () => + tsResult.outputText.replace( + "react/jsx-runtime", + "@lcui/react/lib/jsx-runtime.js" + ) + `\n\nexport const componentList = [${localFuncNames.join(", ")}];\n` ); - if (!(componentFunc instanceof Function)) { + const { default: defaultComponentFunc, componentList } = + await loader.importModule(loader.resourcePath); + + if (componentList.length < 1) { return; } @@ -78,36 +100,57 @@ export default async function TsLoader( ); const options = this.getOptions(); const { dir, name, base } = path.parse(loader.resourcePath); - let componentName = componentFunc.displayName || componentFunc.name || name; + let defaultComponentName = + defaultComponentFunc?.displayName || defaultComponentFunc?.name || name; if (options.target === "AppRouter") { - componentName = parsePageRoute(loader.appDir, loader.resourcePath).ident; + defaultComponentName = parsePageRoute( + loader.appDir, + loader.resourcePath + ).ident; } - // TODO: 处理一个 tsx 文件内有多个组件的情况 - const result = compile( - componentFunc, - {}, - { - target: options.target, - name: componentName, - } + const result = (componentList as React.FC[]).map( + (component, i) => + compile( + component, + {}, + { + target: + defaultComponentFunc === component ? options.target : undefined, + name: + component.displayName || component.name || `UnnamedComponent${i}`, + } + ) as { + name: string; + node: any; + refs: string[]; + headerFiles: string[]; + typesCode: string; + reactCode: string; + sourceCode: string; + declarationCode: string; + } ); const basePath = path.join(dir, name); const sourceFilePath = `${basePath}.c`; const headerFilePath = `${basePath}.h`; - const resourceLoaderName = getResourceLoaderName(name, componentName); + const resourceLoaderName = getResourceLoaderName(name, defaultComponentName); if (!fs.existsSync(sourceFilePath)) { loader.emitFile( sourceFilePath, - `#include "${base}.h"\n#include "${name}.h"\n\n${result.sourceCode}` + `#include "${base}.h"\n#include "${name}.h"\n\n${result + .map((item) => item.sourceCode) + .join("\n\n")}` ); } if (!fs.existsSync(headerFilePath)) { loader.emitFile( headerFilePath, - `#include \n\n${result.declarationCode}${ + `#include \n\n${result + .map((item) => item.declarationCode) + .join("\n\n")}${ resourceLoaderName ? `\nvoid ${resourceLoaderName}(void);\n` : "" }` ); @@ -121,7 +164,7 @@ export default async function TsLoader( resourceLoaderName, headerFilePath: path.relative(loader.rootContext, headerFilePath), assets, - components: [componentName], + components: result.map((item) => item.name), }; return { name: "lcui-app", @@ -132,38 +175,38 @@ export default async function TsLoader( src: asset.metadata.path, }, })), - { + ...result.map((item) => ({ name: "schema", children: [ { name: "name", - text: componentName, + text: item.name, }, - ...result.refs.map((ref) => ({ + ...item.refs.map((ref) => ({ name: "ref", text: ref, })), - ...result.headerFiles.map((file) => ({ + ...item.headerFiles.map((file) => ({ name: "include", text: file, })), { name: "code", - text: result.typesCode, + text: item.typesCode, attributes: { kind: "types", }, }, { name: "code", - text: result.reactCode, + text: item.reactCode, }, { name: "template", - children: [result.node], + children: [item.node], }, ], - }, + })), ], }; }