Skip to content

Commit

Permalink
feat: 支持编译同一文件中的多个组件
Browse files Browse the repository at this point in the history
  • Loading branch information
lc-soft committed Nov 6, 2024
1 parent 25d1e29 commit 3ed9548
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 79 deletions.
105 changes: 57 additions & 48 deletions lib/compiler/ui-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(["<ui.h>"]);
const indentStr = " ".repeat(indent);
const parentIdent = "parent";
Expand Down Expand Up @@ -139,66 +140,62 @@ 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) {
globalLines.push(node.text);
}
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});`
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -275,31 +272,43 @@ async function compile(rootNode, context, { filePath, indent = 8 }) {
}

function compileNode(node) {
let result;

switch (node.name) {
case "ui":
if (state !== stateEnum.START) {
throw SyntaxError("<ui> must be at the top level");
}
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("<schema> must be at the top level");
throw SyntaxError(`<schema> 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(),
Expand Down
105 changes: 74 additions & 31 deletions src/compiler/ts-loader.ts
Original file line number Diff line number Diff line change
@@ -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<Module>[] = [];
const localFuncNames: string[] = [];
const outputDirPath = path.dirname(loader.resolveModule(loader.resourcePath));

function transformer(context: ts.TransformationContext) {
Expand All @@ -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);
}

Expand All @@ -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;
}

Expand All @@ -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 <ui.h>\n\n${result.declarationCode}${
`#include <ui.h>\n\n${result
.map((item) => item.declarationCode)
.join("\n\n")}${
resourceLoaderName ? `\nvoid ${resourceLoaderName}(void);\n` : ""
}`
);
Expand All @@ -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",
Expand All @@ -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],
},
],
},
})),
],
};
}

0 comments on commit 3ed9548

Please sign in to comment.