@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 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 tagName = n.openingElement.name.name;
519
- const isComponent = /^[A-Z]/.test(tagName);
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
- const componentTagName = "web-" + tagName.toLowerCase();
524
- statements.push(t.variableDeclaration("const", [
525
- t.variableDeclarator(elId, t.callExpression(t.memberExpression(t.identifier("document"), t.identifier("createElement")), [t.stringLiteral(componentTagName)]))
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 svgTags = ['svg', 'path', 'polyline', 'line', 'circle', 'rect', 'ellipse', 'polygon', 'g', 'text', 'tspan', 'defs', 'lineargradient', 'stop'];
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 = ["className", "style", "value", "checked", "id", "title", "href", "src", "key"].includes(attrProp);
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(attrProp === "className" ? "class" : toKebabCase(originalName)), value.expression])
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 = ["className", "style", "value", "checked", "id", "title", "href", "src", "key"].includes(attrProp);
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(attrProp === "className" ? "class" : toKebabCase(originalName)), value])));
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
- const text = n.value.replace(/\n\s*/g, "");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opentf/web",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "The core engine of the Web App Framework, including the zero-VDOM compiler and signal-based runtime.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opentf/web",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "The core engine of the Web App Framework, including the zero-VDOM compiler and signal-based runtime.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",