Skip to content

Commit

Permalink
feat(compile): add react-tsx compiler
Browse files Browse the repository at this point in the history
  • Loading branch information
lc-soft committed Aug 13, 2023
1 parent a0c8f5a commit 5a5cf28
Show file tree
Hide file tree
Showing 7 changed files with 623 additions and 456 deletions.
11 changes: 7 additions & 4 deletions lib/compiler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@ import xml from "./xml.js";
import yaml from "./yaml.js";
import json from "./json.js";
import javascript from "./javascript.js";
import tsx from "./react-tsx.js";

const compilers = {
xml,
yaml,
json,
javascript,
tsx,
sass,
css,
};

function compileFile(filePath, options) {
async function compileFile(filePath, options) {
if (fs.statSync(filePath).isDirectory()) {
fs.readdirSync(filePath).forEach((name) => {
compileFile(path.join(filePath, name), options);
Expand All @@ -42,18 +44,19 @@ function compileFile(filePath, options) {

console.log(`[lcui.${type}] compile ${filePath}`);
const content = fs.readFileSync(filePath, { encoding: "utf-8" });
const result = compiler.compile(content, { filePath });
const result = await compiler.compile(content, { ...options, filePath });
fs.writeFileSync(`${filePath}.h`, result, { encoding: "utf-8" });
}

export function compile(file, options) {
export async function compile(file, options) {
const cwd = process.cwd();

if (fs.existsSync(file)) {
compileFile(file, {
await compileFile(file, {
...options,
cwd,
projectDir: cwd,
buildDir: path.join(cwd, ".lcui/build"),
sourceDir: path.join(cwd, "src"),
});
}
Expand Down
192 changes: 192 additions & 0 deletions lib/compiler/react-tsx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import fs from "fs";
import path from "path";
import postcss from "postcss";
import postcssModules from "postcss-modules";
import postcssSass from "postcss-sass";
import React from "react";
import ts from "typescript";
import json from "./json.js";

async function loadCSSFiles(input, options) {
const files = [];

async function loadCSSFile({ ident, filename, start, end }) {
const cssText = fs.readFileSync(filename, { encoding: "utf-8" });
const mainExt = path.parse(filename).ext.toLowerCase();
const secondExt = path.parse(path.parse(filename).name).ext.toLowerCase();
const plugins = [
[".sass", ".scss"].includes(mainExt) && postcssSass(),
secondExt === ".module" &&
postcssModules({
exportGlobals: true,
}),
].filter(Boolean);

const result = await postcss(plugins)
.process(cssText, { from: filename })
.async();

const cssExport = result.messages.find((m) => m.type === "export");
return {
filename,
ident,
css: result.css,
start,
end,
exports: cssExport ? cssExport.exportTokens : {},
};
}

/**
*
* @param {ts.TransformationContext} context
* @returns {ts.Transformer<ts.SourceFile>}
*/
function transformer(context) {
return (sourceFile) => {
/**
*
* @param {ts.Node} node
* @returns {ts.Node}
*/
function visitor(node) {
if (ts.isImportDeclaration(node)) {
const importPath = node.moduleSpecifier.getText().slice(1, -1);
const name = path.parse(importPath).base.toLowerCase();

if (name.endsWith(".css") || name.endsWith(".scss")) {
files.push({
ident: node.importClause.name?.getText(),
filename: path.resolve(
path.parse(sourceFile.fileName).dir,
importPath
),
start: node.getStart(),
end: node.getEnd(),
});
}
}
return ts.visitEachChild(node, visitor, context);
}

return ts.visitNode(sourceFile, visitor);
};
}

ts.transpileModule(input, {
fileName: options.filePath,
compilerOptions: {
target: ts.ScriptTarget.ESNext,
jsx: ts.JsxEmit.React,
},
transformers: {
before: [transformer],
},
});
return Promise.all(files.map(loadCSSFile));
}

function replaceCSSImportStatments(code, files) {
let pos = 0;
const segments = [];

files.forEach(({ start, end, ident, exports }) => {
segments.push(code.substring(pos, start));
segments.push(`const ${ident} = ${JSON.stringify(exports)};`);
pos = end + 1;
});
segments.push(code.substring(pos));
return segments.join("");
}

function transformReactElement(el) {
const node = {
type: "element",
name: "",
text: "",
attributes: {},
children: [],
};

if (typeof el === "string" || typeof el === "number") {
node.text = `${el}`;
node.name = "text";
return node;
}
if (typeof el.type === "string") {
switch (el.type) {
case "div":
case "w":
case "widget":
node.name = "widget";
break;
default:
break;
}
} else if (typeof el.type === "function") {
node.name = el.type.constructor.name;
} else {
return;
}

Object.keys(el.props).forEach((propKey) => {
let key = propKey;

if (key === "children") {
React.Children.forEach(el.props.children, (child) => {
node.children.push(transformReactElement(child));
});
return;
}
if (key === "className") {
key = "class";
}
node.attributes[key] = el.props[propKey];
});
return node;
}

function mergeResourceElements(tree, files, sourceFileName) {
return {
...tree,
children: [
...files.map((file) => ({
type: "element",
name: "resource",
comment: `This css code string is compiled from file ${path.relative(
path.dirname(sourceFileName),
file.filename,
)}`,
attributes: {
type: "text/css",
},
text: file.css,
})),
...tree.children,
],
};
}

async function compile(input, options) {
const files = await loadCSSFiles(input, options);
const tsCode = replaceCSSImportStatments(input, files);
const result = ts.transpileModule(tsCode, {
fileName: options.filePath,
compilerOptions: {
target: ts.ScriptTarget.ESNext,
jsx: ts.JsxEmit.React,
},
});
const output = path.parse(path.join(options.buildDir, options.filePath));
const outputPath = path.join(output.dir, `${output.name}.js`);
fs.writeFileSync(outputPath, result.outputText, { encoding: "utf-8" });
const component = (await import(`file://${outputPath}`)).default;
const reactTree = transformReactElement(component());
const jsonTree = mergeResourceElements(reactTree, files, options.filePath);
return json.compileData(jsonTree, options);
}

export default {
test: /\.tsx$/,
compile,
};
31 changes: 24 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,32 @@
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"version": "npm run changelog && git add CHANGELOG.md"
},
"author": "Liu <[email protected]<",
"author": "Liu",
"license": "MIT",
"husky": {
"hooks": {
"commit-msg": "commitlint --env HUSKY_GIT_PARAMS"
}
},
"repository": {
"url": "https://github.com/lc-ui/lcui-cli.git"
"url": "git+https://github.com/lc-ui/lcui-cli.git"
},
"dependencies": {
"chalk": "^4.1.1",
"change-case": "^4.1.2",
"cli-progress": "^3.9.0",
"commander": "^11.0.0",
"decompress": "^4.2.1",
"fast-xml-parser": "^4.2.2",
"fs-extra": "^10.0.0",
"got": "^12.5.3",
"os-locale": "^6.0.2",
"postcss": "^8.4.27",
"postcss-modules": "^6.0.0",
"postcss-modules-sync": "^1.0.0",
"postcss-sass": "^0.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.64.2",
"simple-git": "^3.3.0",
"typescript": "^5.1.6",
"xml-js": "^1.6.11",
"yaml": "^2.3.1"
},
"devDependencies": {
Expand All @@ -44,5 +48,18 @@
"husky": "^6.0.0",
"mocha": "^10.1.0",
"nyc": "^15.1.0"
}
},
"bugs": {
"url": "https://github.com/lc-ui/lcui-cli/issues"
},
"homepage": "https://github.com/lc-ui/lcui-cli#readme",
"directories": {
"lib": "lib",
"test": "test"
},
"keywords": [
"lcui",
"cli",
"compiler"
]
}
15 changes: 15 additions & 0 deletions test/fixtures/resource/src/ui/home.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
:global(.home) {
padding: 20px;
}

.button {
padding: 5px 10px;
text-align: center;
border: 1px solid #eee;
border-radius: 4px;
}

.text {
color: #f00;
font-size: 24px;
}
11 changes: 11 additions & 0 deletions test/fixtures/resource/src/ui/home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from "react";
import styles from "./home.module.css";

export default function Home() {
return (
<div className="home">
<text className={styles.text}>Hello, World!</text>
<button className={styles.button}>Ok</button>
</div>
);
}
49 changes: 49 additions & 0 deletions test/fixtures/resource/src/ui/home.tsx.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/** This file is generated from home.tsx */

#include <ui.h>

/** This css code string is compiled from file home.module.css */
static const char *css_str_0 = "\
.home {\
padding: 20px;\
}\
\
._button_1ayu2_9 {\
padding: 5px 10px;\
text-align: center;\
border: 1px solid #eee;\
border-radius: 4px;\
}\
\
._text_1ayu2_23 {\
color: #f00;\
font-size: 24px;\
}\
\
";


static void home_load_template(ui_widget_t *home_parent)
{
ui_widget_t *w[5];

w[0] = ui_create_widget(NULL);
ui_widget_add_class(w[0], "home");
w[1] = ui_create_widget(NULL);
ui_widget_add_class(w[1], "_text_1ayu2_23");
w[2] = ui_create_widget("text");
ui_widget_set_text(w[2], "Hello, World!");
ui_widget_append(w[1], w[2]);
w[3] = ui_create_widget(NULL);
ui_widget_add_class(w[3], "_button_1ayu2_9");
w[4] = ui_create_widget("text");
ui_widget_set_text(w[4], "Ok");
ui_widget_append(w[3], w[4]);
ui_widget_append(w[0], w[1]);
ui_widget_append(w[0], w[3]);
}

static void home_load_resources(void)
{
ui_load_css_string(css_str_0, "home.tsx");
}
Loading

0 comments on commit 5a5cf28

Please sign in to comment.