@opentf/web 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/CHANGELOG.md +25 -0
- package/dist/README.md +41 -0
- package/dist/compiler/babel-plugin.cjs +82 -15
- package/dist/package.json +1 -1
- package/dist/router/index.js +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @opentf/web
|
|
2
|
+
|
|
3
|
+
The core engine of the **Web App Framework**, a lightweight, high-performance SPA framework designed for the modern web.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🚀 **Zero-VDOM**: Compiles JSX directly to imperative DOM operations for maximum speed.
|
|
8
|
+
- ⚡ **Signal-Based Reactivity**: Powered by `@preact/signals-core` for fine-grained, surgical updates.
|
|
9
|
+
- 📦 **Web Component Native**: Every component is a standard Custom Element, ensuring perfect encapsulation and interoperability.
|
|
10
|
+
- 🛠️ **Convention-over-Configuration**: File-based routing and automatic component registration.
|
|
11
|
+
- 📏 **Ultra-Lightweight**: Minimal runtime footprint.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @opentf/web
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Basic Usage
|
|
20
|
+
|
|
21
|
+
```jsx
|
|
22
|
+
export default function Counter() {
|
|
23
|
+
let count = $state(0);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="counter">
|
|
27
|
+
<button onclick={() => count--}>-</button>
|
|
28
|
+
<span>{count}</span>
|
|
29
|
+
<button onclick={() => count++}>+</button>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Documentation
|
|
36
|
+
|
|
37
|
+
Visit [web.opentf.workers.dev](https://web.opentf.workers.dev) for full documentation and guides.
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
MIT © [Open Tech Foundation](https://github.com/Open-Tech-Foundation)
|
package/dist/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @opentf/web
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ba984c2: Here is a summary of the changes for your changelog:
|
|
8
|
+
|
|
9
|
+
### 🚀 Features
|
|
10
|
+
|
|
11
|
+
- **Compiler:** Added comprehensive support for **SVG camelCase attributes** (e.g., `viewBox`, `strokeWidth`, `strokeLinecap`). The compiler now preserves the original casing for these attributes, ensuring icons and SVGs render correctly in the browser.
|
|
12
|
+
- **Router:** Improved route matching to support **optional trailing slashes** (e.g., `/about` and `/about/` now match the same route).
|
|
13
|
+
- **Playground:** Added a new **SVG Icons Showcase** page to demonstrate seamless integration with icon libraries like Lucide and Heroicons.
|
|
14
|
+
|
|
15
|
+
### 🐞 Bug Fixes
|
|
16
|
+
|
|
17
|
+
- **Compiler:** Fixed a critical bug where **imported components** were incorrectly compiled as dynamic variables (e.g., `document.createElement(Link)`) instead of using their registered custom element tag names (e.g., `document.createElement("web-link")`).
|
|
18
|
+
- **Playground:** Resolved a build failure in the Icons page by correcting an illegal `const` reassignment of a reactive state variable.
|
|
19
|
+
- **Performance:** Reduced the simulated network delay in `routeGuard.js` from 800ms to 100ms for a snappier development experience.
|
|
20
|
+
|
|
21
|
+
### 🏠 Internal & Maintenance
|
|
22
|
+
|
|
23
|
+
- **Refactoring:** Optimized the Babel plugin by moving large static lists (SVG tags, properties, etc.) to the top-level scope, improving compilation speed.
|
|
24
|
+
- **Testing:** Expanded the test suite with new cases for **icon libraries** and **complex React patterns** (dynamic tags, render props, spread overrides).
|
|
25
|
+
|
|
26
|
+
***
|
|
27
|
+
|
|
3
28
|
## 0.3.0
|
|
4
29
|
|
|
5
30
|
### Minor Changes
|
package/dist/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @opentf/web
|
|
2
|
+
|
|
3
|
+
The core engine of the **Web App Framework**, a lightweight, high-performance SPA framework designed for the modern web.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🚀 **Zero-VDOM**: Compiles JSX directly to imperative DOM operations for maximum speed.
|
|
8
|
+
- ⚡ **Signal-Based Reactivity**: Powered by `@preact/signals-core` for fine-grained, surgical updates.
|
|
9
|
+
- 📦 **Web Component Native**: Every component is a standard Custom Element, ensuring perfect encapsulation and interoperability.
|
|
10
|
+
- 🛠️ **Convention-over-Configuration**: File-based routing and automatic component registration.
|
|
11
|
+
- 📏 **Ultra-Lightweight**: Minimal runtime footprint.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @opentf/web
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Basic Usage
|
|
20
|
+
|
|
21
|
+
```jsx
|
|
22
|
+
export default function Counter() {
|
|
23
|
+
let count = $state(0);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="counter">
|
|
27
|
+
<button onclick={() => count--}>-</button>
|
|
28
|
+
<span>{count}</span>
|
|
29
|
+
<button onclick={() => count++}>+</button>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Documentation
|
|
36
|
+
|
|
37
|
+
Visit [web.opentf.workers.dev](https://web.opentf.workers.dev) for full documentation and guides.
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
MIT © [Open Tech Foundation](https://github.com/Open-Tech-Foundation)
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
const { addNamed } = require("@babel/helper-module-imports");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
|
|
4
|
+
const SVG_TAGS = ['svg', 'path', 'polyline', 'line', 'circle', 'rect', 'ellipse', 'polygon', 'g', 'text', 'tspan', 'defs', 'lineargradient', 'stop'];
|
|
5
|
+
|
|
6
|
+
const SVG_CAMEL_CASE = [
|
|
7
|
+
"allowReorder", "attributeName", "attributeType", "autoReverse", "baseFrequency", "baseProfile", "calcMode", "clipPathUnits", "contentScriptType", "contentStyleType", "cursor", "cx", "cy", "d", "diffuseConstant", "direction", "display", "divisor", "dominantBaseline", "dur", "dx", "dy", "edgeMode", "elevation", "enableBackground", "externalResourcesRequired", "fill", "fillOpacity", "fillRule", "filter", "filterRes", "filterUnits", "floodColor", "floodOpacity", "focusable", "fontFamily", "fontSize", "fontSizeAdjust", "fontStretch", "fontStyle", "fontVariant", "fontWeight", "format", "from", "fx", "fy", "g1", "g2", "glyphName", "glyphOrientationHorizontal", "glyphOrientationVertical", "glyphRef", "gradientTransform", "gradientUnits", "hanging", "horizAdvX", "horizOriginX", "ideographic", "imageRendering", "in", "in2", "intercept", "k", "k1", "k2", "k3", "k4", "kernelMatrix", "kernelUnitLength", "kerning", "keyPoints", "keySplines", "keyTimes", "lengthAdjust", "letterSpacing", "lightingColor", "limitingConeAngle", "local", "markerEnd", "markerMid", "markerStart", "markerHeight", "markerUnits", "markerWidth", "mask", "maskContentUnits", "maskUnits", "mathematical", "mode", "numOctaves", "offset", "opacity", "operator", "order", "orient", "orientation", "origin", "overflow", "overlinePosition", "overlineThickness", "panose1", "paintOrder", "pathLength", "patternContentUnits", "patternTransform", "patternUnits", "pointerEvents", "points", "pointsAtX", "pointsAtY", "pointsAtZ", "preserveAlpha", "preserveAspectRatio", "primitiveUnits", "r", "radius", "refX", "refY", "renderingIntent", "repeatCount", "repeatDur", "requiredExtensions", "requiredFeatures", "restart", "result", "rotate", "rx", "ry", "scale", "seed", "shapeRendering", "slope", "spacing", "specularConstant", "specularExponent", "speed", "spreadMethod", "startOffset", "stdDeviation", "stemh", "stemv", "stitchTiles", "stopColor", "stopOpacity", "strikethroughPosition", "strikethroughThickness", "string", "stroke", "strokeDasharray", "strokeDashoffset", "strokeLinecap", "strokeLinejoin", "strokeMiterlimit", "strokeOpacity", "strokeWidth", "surfaceScale", "systemLanguage", "tableValues", "targetX", "targetY", "textAnchor", "textDecoration", "textLength", "textRendering", "to", "transform", "u1", "u2", "underlinePosition", "underlineThickness", "unicode", "unicodeBidi", "unicodeRange", "unitsPerEm", "vAlphabetic", "vHanging", "vIdeographic", "vMathematical", "values", "version", "vertAdvY", "vertOriginX", "vertOriginY", "viewBox", "viewTarget", "visibility", "widths", "wordSpacing", "writingMode", "x", "x1", "x2", "xChannelSelector", "xHeight", "xlinkActuate", "xlinkArcrole", "xlinkHref", "xlinkRole", "xlinkShow", "xlinkTitle", "xlinkType", "xmlBase", "xmlLang", "xmlSpace", "y", "y1", "y2", "yChannelSelector", "z", "zoomAndPan"
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const IS_PROPERTY = ["className", "style", "value", "checked", "id", "title", "href", "src", "key"];
|
|
11
|
+
|
|
12
|
+
const STANDARD_TAGS = ["div", "span", "p", "a", "ul", "li", "button", "input", "img", "h1", "h2", "h3", "h4", "h5", "h6", "section", "article", "nav", "header", "footer", "main", "aside", "form", "label", "select", "option", "textarea", "table", "tr", "td", "th", "thead", "tbody", "tfoot", "canvas", "video", "audio", "svg"];
|
|
13
|
+
|
|
14
|
+
|
|
4
15
|
module.exports = function (babel) {
|
|
5
16
|
const { types: t } = babel;
|
|
6
17
|
|
|
@@ -515,15 +526,63 @@ module.exports = function (babel) {
|
|
|
515
526
|
|
|
516
527
|
function processNode(n, parentElId) {
|
|
517
528
|
if (t.isJSXElement(n)) {
|
|
518
|
-
const
|
|
519
|
-
|
|
529
|
+
const tagNameNode = n.openingElement.name;
|
|
530
|
+
let tagName = "";
|
|
531
|
+
let isDynamic = false;
|
|
532
|
+
|
|
533
|
+
if (t.isJSXIdentifier(tagNameNode)) {
|
|
534
|
+
tagName = tagNameNode.name;
|
|
535
|
+
// If it's an identifier that starts with Uppercase, it might be a component.
|
|
536
|
+
// But if it's NOT a globally defined component or a local function, it might be a dynamic tag variable.
|
|
537
|
+
// For now, let's assume if it's not a standard HTML tag and it's Uppercase, it's a component.
|
|
538
|
+
// BUT, if it's in scope as a variable, it might be dynamic.
|
|
539
|
+
} else if (t.isJSXMemberExpression(tagNameNode)) {
|
|
540
|
+
const getMemberName = (node) => {
|
|
541
|
+
if (t.isJSXIdentifier(node)) return node.name;
|
|
542
|
+
if (t.isJSXMemberExpression(node)) {
|
|
543
|
+
return `${getMemberName(node.object)}.${getMemberName(node.property)}`;
|
|
544
|
+
}
|
|
545
|
+
return "";
|
|
546
|
+
};
|
|
547
|
+
tagName = getMemberName(tagNameNode);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const isComponent = /^[A-Z]/.test(tagName) || tagName.includes(".");
|
|
520
551
|
const elId = nextId();
|
|
521
552
|
|
|
522
553
|
if (isComponent) {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
554
|
+
// Check if the identifier is actually in scope (not a global component)
|
|
555
|
+
// This is a heuristic: if it's Uppercase and NOT a standard tag, we treat it as a component.
|
|
556
|
+
// For dynamic tags like <Tag />, we need to check if 'Tag' is a variable.
|
|
557
|
+
const binding = path.scope.getBinding(tagName);
|
|
558
|
+
const isStandardTag = /^[a-z]/.test(tagName) && STANDARD_TAGS.includes(tagName.toLowerCase());
|
|
559
|
+
|
|
560
|
+
let componentTagName = tagName;
|
|
561
|
+
if (tagName.includes(".")) {
|
|
562
|
+
componentTagName = "web-" + tagName.replace(/\./g, "-").toLowerCase();
|
|
563
|
+
} else if (!isStandardTag) {
|
|
564
|
+
componentTagName = "web-" + tagName.toLowerCase();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const isFunction = binding && (t.isFunctionDeclaration(binding.path.node) || t.isFunctionExpression(binding.path.node) || t.isArrowFunctionExpression(binding.path.node));
|
|
568
|
+
const isImport = binding && binding.kind === "module";
|
|
569
|
+
const isVariable = binding && t.isVariableDeclarator(binding.path.node);
|
|
570
|
+
|
|
571
|
+
if (isFunction || isImport || (!binding && !isStandardTag)) {
|
|
572
|
+
statements.push(t.variableDeclaration("const", [
|
|
573
|
+
t.variableDeclarator(elId, t.callExpression(t.memberExpression(t.identifier("document"), t.identifier("createElement")), [t.stringLiteral(componentTagName)]))
|
|
574
|
+
]));
|
|
575
|
+
} else if (isVariable) {
|
|
576
|
+
// Truly dynamic tag from local variable
|
|
577
|
+
statements.push(t.variableDeclaration("const", [
|
|
578
|
+
t.variableDeclarator(elId, t.callExpression(t.memberExpression(t.identifier("document"), t.identifier("createElement")), [t.identifier(tagName)]))
|
|
579
|
+
]));
|
|
580
|
+
} else {
|
|
581
|
+
statements.push(t.variableDeclaration("const", [
|
|
582
|
+
t.variableDeclarator(elId, t.callExpression(t.memberExpression(t.identifier("document"), t.identifier("createElement")), [t.stringLiteral(componentTagName)]))
|
|
583
|
+
]));
|
|
584
|
+
}
|
|
585
|
+
|
|
527
586
|
|
|
528
587
|
n.openingElement.attributes.forEach(attr => {
|
|
529
588
|
if (t.isJSXSpreadAttribute(attr)) {
|
|
@@ -532,7 +591,7 @@ module.exports = function (babel) {
|
|
|
532
591
|
return;
|
|
533
592
|
}
|
|
534
593
|
const name = attr.name.name;
|
|
535
|
-
const value = attr.value;
|
|
594
|
+
const value = attr.value || t.booleanLiteral(true);
|
|
536
595
|
const targetProp = name === "class" || name === "className" ? "className" : name;
|
|
537
596
|
|
|
538
597
|
if (t.isJSXExpressionContainer(value)) {
|
|
@@ -565,8 +624,7 @@ module.exports = function (babel) {
|
|
|
565
624
|
return elId;
|
|
566
625
|
}
|
|
567
626
|
|
|
568
|
-
const
|
|
569
|
-
const isSvg = svgTags.includes(tagName.toLowerCase());
|
|
627
|
+
const isSvg = SVG_TAGS.includes(tagName.toLowerCase());
|
|
570
628
|
|
|
571
629
|
statements.push(t.variableDeclaration("const", [
|
|
572
630
|
t.variableDeclarator(
|
|
@@ -577,6 +635,7 @@ module.exports = function (babel) {
|
|
|
577
635
|
)
|
|
578
636
|
]));
|
|
579
637
|
|
|
638
|
+
|
|
580
639
|
n.openingElement.attributes.forEach(attr => {
|
|
581
640
|
if (t.isJSXSpreadAttribute(attr)) {
|
|
582
641
|
const applySpreadId = getImport("applySpread", state.runtimeSource);
|
|
@@ -585,7 +644,7 @@ module.exports = function (babel) {
|
|
|
585
644
|
}
|
|
586
645
|
const originalName = attr.name.name;
|
|
587
646
|
const name = originalName.toLowerCase();
|
|
588
|
-
const value = attr.value;
|
|
647
|
+
const value = attr.value || t.booleanLiteral(true);
|
|
589
648
|
|
|
590
649
|
if (name.startsWith("on")) {
|
|
591
650
|
if (t.isJSXExpressionContainer(value) && t.isJSXEmptyExpression(value.expression)) return;
|
|
@@ -610,26 +669,33 @@ module.exports = function (babel) {
|
|
|
610
669
|
const effectId = getImport("effect", state.runtimeSource);
|
|
611
670
|
const attrProp = (name === "class" || name === "classname") ? "className" : name;
|
|
612
671
|
const isStyle = attrProp === "style";
|
|
613
|
-
const isProperty =
|
|
672
|
+
const isProperty = IS_PROPERTY.includes(attrProp);
|
|
614
673
|
|
|
674
|
+
const isSvgCamel = isSvg && SVG_CAMEL_CASE.includes(originalName);
|
|
675
|
+
const finalAttrName = isSvgCamel ? originalName : (attrProp === "className" ? "class" : toKebabCase(originalName));
|
|
676
|
+
|
|
615
677
|
if (attrProp === "key") {
|
|
616
678
|
statements.push(t.expressionStatement(t.assignmentExpression("=", t.memberExpression(elId, t.identifier("_key")), value.expression)));
|
|
617
679
|
} else {
|
|
618
680
|
statements.push(t.expressionStatement(t.callExpression(effectId, [t.arrowFunctionExpression([],
|
|
619
681
|
isStyle ? t.callExpression(t.memberExpression(t.identifier("Object"), t.identifier("assign")), [t.memberExpression(elId, t.identifier("style")), value.expression])
|
|
620
682
|
: (isProperty && (!isSvg || attrProp !== "className")) ? t.assignmentExpression("=", t.memberExpression(elId, t.identifier(attrProp)), value.expression)
|
|
621
|
-
: t.callExpression(t.memberExpression(elId, t.identifier("setAttribute")), [t.stringLiteral(
|
|
683
|
+
: t.callExpression(t.memberExpression(elId, t.identifier("setAttribute")), [t.stringLiteral(finalAttrName), value.expression])
|
|
622
684
|
)])));
|
|
623
685
|
}
|
|
686
|
+
|
|
624
687
|
} else {
|
|
625
688
|
const attrProp = (name === "class" || name === "classname") ? "className" : name;
|
|
626
|
-
const isProperty =
|
|
689
|
+
const isProperty = IS_PROPERTY.includes(attrProp);
|
|
690
|
+
const isSvgCamel = isSvg && SVG_CAMEL_CASE.includes(originalName);
|
|
691
|
+
const finalAttrName = isSvgCamel ? originalName : (attrProp === "className" ? "class" : toKebabCase(originalName));
|
|
627
692
|
|
|
628
693
|
if (isProperty && (!isSvg || attrProp !== "className")) {
|
|
629
694
|
statements.push(t.expressionStatement(t.assignmentExpression("=", t.memberExpression(elId, t.identifier(attrProp === "key" ? "_key" : attrProp)), value)));
|
|
630
695
|
} else {
|
|
631
|
-
statements.push(t.expressionStatement(t.callExpression(t.memberExpression(elId, t.identifier("setAttribute")), [t.stringLiteral(
|
|
696
|
+
statements.push(t.expressionStatement(t.callExpression(t.memberExpression(elId, t.identifier("setAttribute")), [t.stringLiteral(finalAttrName), value])));
|
|
632
697
|
}
|
|
698
|
+
|
|
633
699
|
}
|
|
634
700
|
});
|
|
635
701
|
|
|
@@ -639,7 +705,8 @@ module.exports = function (babel) {
|
|
|
639
705
|
});
|
|
640
706
|
return elId;
|
|
641
707
|
} else if (t.isJSXText(n)) {
|
|
642
|
-
|
|
708
|
+
if (n.value.includes("\n") && n.value.trim() === "") return null;
|
|
709
|
+
const text = n.value.replace(/\n\s*/g, " ");
|
|
643
710
|
if (!text) return null;
|
|
644
711
|
const textId = nextId("text");
|
|
645
712
|
statements.push(t.variableDeclaration("const", [t.variableDeclarator(textId, t.callExpression(t.memberExpression(t.identifier("document"), t.identifier("createTextNode")), [t.stringLiteral(text)]))]));
|
package/dist/package.json
CHANGED
package/dist/router/index.js
CHANGED
|
@@ -51,7 +51,7 @@ export function registerRoutes(pages) {
|
|
|
51
51
|
function matchRoute(path) {
|
|
52
52
|
for (const route in routes.pages) {
|
|
53
53
|
const pattern = route.replace(/\[\.\.\.([^\]]+)\]/g, '(?<$1>.+)').replace(/\[([^\]]+)\]/g, '(?<$1>[^/]+)');
|
|
54
|
-
const regex = new RegExp(`^${pattern}
|
|
54
|
+
const regex = new RegExp(`^${pattern}/?$`);
|
|
55
55
|
const match = path.match(regex);
|
|
56
56
|
if (match) {
|
|
57
57
|
const params = {
|
package/package.json
CHANGED