mirror of
https://github.com/ziex-dev/ziex.git
synced 2026-07-21 02:59:36 -06:00
wip(ide): devtool data integration (#56)
This commit is contained in:
parent
ee456aaade
commit
f60f735f41
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
@ -31,17 +31,10 @@ jobs:
|
||||
./zig-out/bin/zx --help || echo "Executable built successfully"
|
||||
shell: bash
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: zx-${{ matrix.os }}
|
||||
path: zig-out/
|
||||
retention-days: 1
|
||||
|
||||
test:
|
||||
name: Test (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: build
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
@ -55,16 +48,6 @@ jobs:
|
||||
with:
|
||||
version: 0.15.2
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: zx-${{ matrix.os }}
|
||||
path: zig-out/
|
||||
|
||||
- name: Make executable (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
run: chmod +x zig-out/bin/zx
|
||||
|
||||
- name: Run tests
|
||||
run: zig build test
|
||||
env:
|
||||
|
||||
3
ide/chromium/.gitignore
vendored
3
ide/chromium/.gitignore
vendored
@ -1 +1,2 @@
|
||||
pages/
|
||||
pages/
|
||||
assets/
|
||||
@ -49,6 +49,6 @@
|
||||
}
|
||||
],
|
||||
"content_security_policy": {
|
||||
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
|
||||
"extension_pages": "script-src 'self' 'wasm-unsafe-eval' http://localhost:* http://127.0.0.1:*; connect-src 'self' http://localhost:* http://127.0.0.1:* https://cdn.jsdelivr.net; object-src 'self'"
|
||||
}
|
||||
}
|
||||
@ -1,30 +1,66 @@
|
||||
(function() {
|
||||
var handle = document.querySelector('.devtools-resize-handle');
|
||||
if (!handle) return;
|
||||
var sidebar = handle.previousElementSibling;
|
||||
var dragging = false;
|
||||
(function () {
|
||||
const container = document.querySelector('.devtools-container');
|
||||
const handle = document.querySelector('.devtools-resize-handle');
|
||||
if (!handle || !container) return;
|
||||
|
||||
handle.addEventListener('mousedown', function(e) {
|
||||
const sidebar = handle.previousElementSibling;
|
||||
let dragging = false;
|
||||
|
||||
function startDrag(e) {
|
||||
if (e.type === 'touchstart' && e.touches.length !== 1) return;
|
||||
|
||||
e.preventDefault();
|
||||
dragging = true;
|
||||
const isColumn = getComputedStyle(container).flexDirection === 'column';
|
||||
container.classList.add(isColumn ? 'devtools-dragging-v' : 'devtools-dragging');
|
||||
handle.classList.add('active');
|
||||
document.body.style.cursor = 'col-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
}
|
||||
|
||||
function handleDrag(clientX, clientY) {
|
||||
if (!dragging) return;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
const isColumn = getComputedStyle(container).flexDirection === 'column';
|
||||
|
||||
if (isColumn) {
|
||||
const pct = ((clientY - rect.top) / rect.height) * 100;
|
||||
const clamped = Math.max(15, Math.min(85, pct));
|
||||
sidebar.style.flex = '0 0 ' + clamped + '%';
|
||||
sidebar.style.height = clamped + '%';
|
||||
sidebar.style.width = '100%';
|
||||
} else {
|
||||
const pct = ((clientX - rect.left) / rect.width) * 100;
|
||||
const clamped = Math.max(15, Math.min(85, pct));
|
||||
sidebar.style.flex = '0 0 ' + clamped + '%';
|
||||
sidebar.style.width = clamped + '%';
|
||||
sidebar.style.height = '100%';
|
||||
}
|
||||
}
|
||||
|
||||
handle.addEventListener('mousedown', startDrag);
|
||||
handle.addEventListener('touchstart', startDrag, { passive: false });
|
||||
|
||||
document.addEventListener('mousemove', function (e) {
|
||||
handleDrag(e.clientX, e.clientY);
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', function(e) {
|
||||
if (!dragging) return;
|
||||
var container = sidebar.parentNode;
|
||||
var rect = container.getBoundingClientRect();
|
||||
var w = Math.min(Math.max(200, e.clientX - rect.left), rect.width - 200);
|
||||
sidebar.style.width = w + 'px';
|
||||
});
|
||||
document.addEventListener('touchmove', function (e) {
|
||||
if (!dragging || e.touches.length !== 1) return;
|
||||
const touch = e.touches[0];
|
||||
handleDrag(touch.clientX, touch.clientY);
|
||||
}, { passive: false });
|
||||
|
||||
document.addEventListener('mouseup', function() {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
handle.classList.remove('active');
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
});
|
||||
function stopDrag() {
|
||||
if (dragging) {
|
||||
dragging = false;
|
||||
container.classList.remove('devtools-dragging');
|
||||
container.classList.remove('devtools-dragging-v');
|
||||
handle.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mouseup', stopDrag);
|
||||
document.addEventListener('touchend', stopDrag);
|
||||
})();
|
||||
|
||||
|
||||
|
||||
@ -163,7 +163,7 @@ a {
|
||||
/* Layout */
|
||||
.app-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 60px 1fr;
|
||||
grid-template-columns: 48px 1fr;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
@ -178,14 +178,14 @@ a {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: visible;
|
||||
width: 60px;
|
||||
width: 48px;
|
||||
position: relative;
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
padding: 0 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-weight: 500;
|
||||
font-size: 1.125rem;
|
||||
@ -204,12 +204,12 @@ a {
|
||||
|
||||
.nav-text {
|
||||
position: fixed;
|
||||
left: 70px;
|
||||
left: 54px;
|
||||
background-color: #1a1a1a;
|
||||
color: var(--text-base);
|
||||
padding: 10px 16px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
@ -229,8 +229,8 @@ a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 12px 8px 12px 6px;
|
||||
margin: 4px 8px;
|
||||
padding: 8px 4px 8px 4px;
|
||||
margin: 4px 6px;
|
||||
color: var(--text-muted);
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
@ -252,11 +252,11 @@ a {
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 24px;
|
||||
min-width: 24px;
|
||||
width: 20px;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
margin-right: 8px;
|
||||
font-size: 20px;
|
||||
margin-right: 0;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -748,6 +748,27 @@ input:checked+.slider:before {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.devtools-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.devtools-container > .sidebar {
|
||||
width: 100% !important;
|
||||
height: 50%;
|
||||
min-width: 0 !important;
|
||||
min-height: 15% !important;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.devtools-resize-handle {
|
||||
width: 100% !important;
|
||||
height: 4px !important;
|
||||
cursor: row-resize !important;
|
||||
}
|
||||
}
|
||||
|
||||
.devtools-container > .sidebar {
|
||||
width: 500px;
|
||||
min-width: 200px;
|
||||
@ -772,6 +793,25 @@ input:checked+.slider:before {
|
||||
background: #00d9ff;
|
||||
}
|
||||
|
||||
.devtools-dragging {
|
||||
cursor: col-resize !important;
|
||||
user-select: none !important;
|
||||
}
|
||||
|
||||
.devtools-dragging-v {
|
||||
cursor: row-resize !important;
|
||||
user-select: none !important;
|
||||
}
|
||||
|
||||
.component-state-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.search-box {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
@ -1127,6 +1167,7 @@ label.state-item {
|
||||
flex-direction: column;
|
||||
background: #000000;
|
||||
color: #e5e5e5;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
@ -1180,6 +1221,8 @@ label.state-item {
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
transition: transform 0.3s;
|
||||
max-height: calc(100vh - 150px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.routes-section:hover {
|
||||
@ -1291,6 +1334,11 @@ label.state-item {
|
||||
border: 1px solid rgba(249, 62, 62, 0.3);
|
||||
}
|
||||
|
||||
.routes-children {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.route-path {
|
||||
color: #e5e5e5;
|
||||
font-family: monospace;
|
||||
@ -1298,8 +1346,10 @@ label.state-item {
|
||||
|
||||
.sidebar-logo {
|
||||
color: #00d9ff;
|
||||
min-width: 28px;
|
||||
min-width: 24px;
|
||||
font-weight: 900;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
/// Centralized data for the devtool.
|
||||
/// Pure data only — no logic or computed values.
|
||||
pub const ComponentMeta = struct {
|
||||
version: []const u8 = "v1.0.0",
|
||||
setup_items: []const StateItem = &[_]StateItem{},
|
||||
setup_other_items: []const StateItem = &[_]StateItem{},
|
||||
template_refs_items: []const StateItem = &[_]StateItem{},
|
||||
prop_items: []const StateItem = &[_]StateItem{},
|
||||
signal_items: []const StateItem = &[_]StateItem{},
|
||||
action_items: []const StateItem = &[_]StateItem{},
|
||||
};
|
||||
|
||||
pub const Component = struct {
|
||||
id: []const u8 = "",
|
||||
name: []const u8,
|
||||
has_children: bool,
|
||||
children: []const Component,
|
||||
@ -21,123 +20,187 @@ pub const Route = struct {
|
||||
path: []const u8,
|
||||
};
|
||||
|
||||
pub const StateItem = struct {
|
||||
key: []const u8,
|
||||
value: []const u8,
|
||||
meta: []const u8,
|
||||
children: []const StateItem = &[_]StateItem{},
|
||||
};
|
||||
pub const StateItem = zx.Component.Serializable.StateItem;
|
||||
|
||||
pub const components = [_]Component{
|
||||
.{ .name = "App", .has_children = true, .selected = true, .badge = "fragment", .meta = ComponentMeta{
|
||||
.version = "v1.0.0",
|
||||
.setup_items = &.{
|
||||
.{ .key = "replRef", .value = "Object", .meta = "(Ref)", .children = &[_]StateItem{
|
||||
.{ .key = "value", .value = "null", .meta = "" },
|
||||
.{ .key = "__v_isRef", .value = "true", .meta = "" },
|
||||
} },
|
||||
.{ .key = "AUTO_SAVE_STORAGE_KEY", .value = "\"zx-sfc-playground-auto-save\"", .meta = "" },
|
||||
.{ .key = "initAutoSave", .value = "true", .meta = "" },
|
||||
.{ .key = "autoSave", .value = "true", .meta = "(Ref)" },
|
||||
.{ .key = "productionMode", .value = "false", .meta = "(Ref)" },
|
||||
.{ .key = "zxVersion", .value = "null", .meta = "(Ref)" },
|
||||
.{ .key = "importMap", .value = "Object", .meta = "(Computed)", .children = &[_]StateItem{
|
||||
.{ .key = "imports", .value = "Object", .meta = "", .children = &[_]StateItem{
|
||||
.{ .key = "zx", .value = "\"https://cdn.jsdelivr.net/npm/zx\"", .meta = "" },
|
||||
} },
|
||||
} },
|
||||
.{ .key = "hash", .value = "eNp9UU1LAzEQ/StjLqugXURPZVtQKaBgHFRW85FJ2p9vUBbKS2bWw7H93kqw1Q...", .meta = "" },
|
||||
.{ .key = "sfcOptions", .value = "Object", .meta = "(Computed)", .children = &[_]StateItem{
|
||||
.{ .key = "script", .value = "Object", .meta = "" },
|
||||
.{ .key = "template", .value = "Object", .meta = "" },
|
||||
} },
|
||||
.{ .key = "store", .value = "Reactive", .meta = "", .children = &[_]StateItem{
|
||||
.{ .key = "theme", .value = "\"dark\"", .meta = "(Ref)" },
|
||||
.{ .key = "isVaporSupported", .value = "false", .meta = "(Ref)" },
|
||||
} },
|
||||
.{ .key = "previewOptions", .value = "Object", .meta = "(Computed)", .children = &[_]StateItem{
|
||||
.{ .key = "headHTML", .value = "\"\"", .meta = "" },
|
||||
} },
|
||||
},
|
||||
.setup_other_items = &.{
|
||||
.{ .key = "setVH", .value = "fn i()", .meta = "" },
|
||||
.{ .key = "toggleProdMode", .value = "fn p()", .meta = "" },
|
||||
.{ .key = "toggleSSR", .value = "fn f()", .meta = "" },
|
||||
.{ .key = "toggleAutoSave", .value = "fn m()", .meta = "" },
|
||||
.{ .key = "reloadPage", .value = "fn _()", .meta = "" },
|
||||
.{ .key = "toggleTheme", .value = "fn y(I)", .meta = "" },
|
||||
.{ .key = "Header", .value = "Header", .meta = "" },
|
||||
.{ .key = "Repl", .value = "Object", .meta = "", .children = &[_]StateItem{
|
||||
.{ .key = "setup", .value = "fn()", .meta = "" },
|
||||
.{ .key = "render", .value = "fn()", .meta = "" },
|
||||
} },
|
||||
.{ .key = "Monaco", .value = "Object", .meta = "", .children = &[_]StateItem{
|
||||
.{ .key = "editor", .value = "null", .meta = "(Ref)" },
|
||||
} },
|
||||
},
|
||||
.template_refs_items = &.{
|
||||
.{ .key = "replRef", .value = "Object", .meta = "", .children = &[_]StateItem{
|
||||
.{ .key = "$el", .value = "<div>", .meta = "" },
|
||||
} },
|
||||
},
|
||||
}, .children = &[_]Component{
|
||||
.{ .name = "Header", .has_children = true, .meta = ComponentMeta{
|
||||
.version = "v1.0.0",
|
||||
.setup_items = &.{
|
||||
.{ .key = "title", .value = "\"ZX Playground\"", .meta = "(Ref)" },
|
||||
.{ .key = "showNav", .value = "true", .meta = "(Ref)" },
|
||||
.{ .key = "theme", .value = "\"dark\"", .meta = "(Ref)" },
|
||||
.{ .key = "logo", .value = "Object", .meta = "(Ref)", .children = &[_]StateItem{
|
||||
.{ .key = "src", .value = "\"/assets/logo.svg\"", .meta = "" },
|
||||
.{ .key = "alt", .value = "\"ZX Logo\"", .meta = "" },
|
||||
} },
|
||||
.{ .key = "navItems", .value = "Object", .meta = "(Computed)", .children = &[_]StateItem{
|
||||
.{ .key = "docs", .value = "\"/docs\"", .meta = "" },
|
||||
.{ .key = "playground", .value = "\"/playground\"", .meta = "" },
|
||||
.{ .key = "github", .value = "\"https://github.com\"", .meta = "" },
|
||||
} },
|
||||
.{ .key = "version", .value = "\"v1.0.0\"", .meta = "(Computed)" },
|
||||
.{ .key = "isMenuOpen", .value = "false", .meta = "(Ref)" },
|
||||
},
|
||||
.setup_other_items = &.{
|
||||
.{ .key = "toggleTheme", .value = "fn y(I)", .meta = "" },
|
||||
.{ .key = "toggleMenu", .value = "fn m()", .meta = "" },
|
||||
.{ .key = "VersionSelect", .value = "Object", .meta = "", .children = &[_]StateItem{
|
||||
.{ .key = "setup", .value = "fn()", .meta = "" },
|
||||
.{ .key = "render", .value = "fn()", .meta = "" },
|
||||
} },
|
||||
},
|
||||
.template_refs_items = &.{
|
||||
.{ .key = "headerRef", .value = "Object", .meta = "", .children = &[_]StateItem{
|
||||
.{ .key = "$el", .value = "<header>", .meta = "" },
|
||||
} },
|
||||
},
|
||||
}, .children = &[_]Component{
|
||||
.{ .name = "VersionSelect", .has_children = false, .children = &[_]Component{} },
|
||||
.{ .name = "VersionSelect", .has_children = false, .children = &[_]Component{} },
|
||||
.{ .name = "Sun", .has_children = false, .children = &[_]Component{} },
|
||||
.{ .name = "Moon", .has_children = false, .children = &[_]Component{} },
|
||||
.{ .name = "Share", .has_children = false, .children = &[_]Component{} },
|
||||
.{ .name = "Reload", .has_children = false, .children = &[_]Component{} },
|
||||
.{ .name = "Download", .has_children = false, .children = &[_]Component{} },
|
||||
.{ .name = "GitHub", .has_children = false, .children = &[_]Component{} },
|
||||
} },
|
||||
.{
|
||||
.name = "Repl",
|
||||
.has_children = true,
|
||||
.children = &[_]Component{
|
||||
.{
|
||||
.name = "App",
|
||||
.has_children = true,
|
||||
.selected = true,
|
||||
.badge = "fragment",
|
||||
.meta = ComponentMeta{
|
||||
.prop_items = &.{
|
||||
.{
|
||||
.name = "SplitPane",
|
||||
.has_children = true,
|
||||
.children = &[_]Component{
|
||||
.{ .name = "Panes", .has_children = false, .children = &[_]Component{} },
|
||||
.{ .name = "PanesTwo", .has_children = false, .children = &[_]Component{} },
|
||||
.{ .name = "PanesThree", .has_children = false, .children = &[_]Component{} },
|
||||
.key = "replRef",
|
||||
.value = "Object",
|
||||
.meta = "(Ref)",
|
||||
.children = &[_]StateItem{
|
||||
.{ .key = "value", .value = "null", .meta = "" },
|
||||
.{ .key = "__v_isRef", .value = "true", .meta = "" },
|
||||
},
|
||||
},
|
||||
.{ .key = "AUTO_SAVE_STORAGE_KEY", .value = "\"zx-sfc-playground-auto-save\"", .meta = "" },
|
||||
.{ .key = "initAutoSave", .value = "true", .meta = "" },
|
||||
.{ .key = "autoSave", .value = "true", .meta = "(Ref)" },
|
||||
.{ .key = "productionMode", .value = "false", .meta = "(Ref)" },
|
||||
.{ .key = "zxVersion", .value = "null", .meta = "(Ref)" },
|
||||
.{
|
||||
.key = "importMap",
|
||||
.value = "Object",
|
||||
.meta = "(Computed)",
|
||||
.children = &[_]StateItem{
|
||||
.{ .key = "imports", .value = "Object", .meta = "", .children = &[_]StateItem{
|
||||
.{ .key = "zx", .value = "\"https://cdn.jsdelivr.net/npm/zx\"", .meta = "" },
|
||||
} },
|
||||
},
|
||||
},
|
||||
.{ .key = "hash", .value = "eNp9UU1LAzEQ/StjLqugXURPZVtQKaBgHFRW85FJ2p9vUBbKS2bWw7H93kqw1Q...", .meta = "" },
|
||||
.{
|
||||
.key = "sfcOptions",
|
||||
.value = "Object",
|
||||
.meta = "(Computed)",
|
||||
.children = &[_]StateItem{
|
||||
.{ .key = "script", .value = "Object", .meta = "" },
|
||||
.{ .key = "template", .value = "Object", .meta = "" },
|
||||
},
|
||||
},
|
||||
.{
|
||||
.key = "store",
|
||||
.value = "Reactive",
|
||||
.meta = "",
|
||||
.children = &[_]StateItem{
|
||||
.{ .key = "theme", .value = "\"dark\"", .meta = "(Ref)" },
|
||||
.{ .key = "isVaporSupported", .value = "false", .meta = "(Ref)" },
|
||||
},
|
||||
},
|
||||
.{
|
||||
.key = "previewOptions",
|
||||
.value = "Object",
|
||||
.meta = "(Computed)",
|
||||
.children = &[_]StateItem{
|
||||
.{ .key = "headHTML", .value = "\"\"", .meta = "" },
|
||||
},
|
||||
},
|
||||
},
|
||||
.signal_items = &.{
|
||||
.{ .key = "setVH", .value = "fn i()", .meta = "" },
|
||||
.{ .key = "toggleProdMode", .value = "fn p()", .meta = "" },
|
||||
.{ .key = "toggleSSR", .value = "fn f()", .meta = "" },
|
||||
.{ .key = "toggleAutoSave", .value = "fn m()", .meta = "" },
|
||||
.{ .key = "reloadPage", .value = "fn _()", .meta = "" },
|
||||
.{ .key = "toggleTheme", .value = "fn y(I)", .meta = "" },
|
||||
.{ .key = "Header", .value = "Header", .meta = "" },
|
||||
.{
|
||||
.key = "Repl",
|
||||
.value = "Object",
|
||||
.meta = "",
|
||||
.children = &[_]StateItem{
|
||||
.{ .key = "setup", .value = "fn()", .meta = "" },
|
||||
.{ .key = "render", .value = "fn()", .meta = "" },
|
||||
},
|
||||
},
|
||||
.{
|
||||
.key = "Monaco",
|
||||
.value = "Object",
|
||||
.meta = "",
|
||||
.children = &[_]StateItem{
|
||||
.{ .key = "editor", .value = "null", .meta = "(Ref)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
.action_items = &.{
|
||||
.{
|
||||
.key = "replRef",
|
||||
.value = "Object",
|
||||
.meta = "",
|
||||
.children = &[_]StateItem{
|
||||
.{ .key = "$el", .value = "<div>", .meta = "" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} },
|
||||
.children = &[_]Component{
|
||||
.{
|
||||
.name = "Header",
|
||||
.has_children = true,
|
||||
.meta = ComponentMeta{
|
||||
.prop_items = &.{
|
||||
.{ .key = "title", .value = "\"ZX Playground\"", .meta = "(Ref)" },
|
||||
.{ .key = "showNav", .value = "true", .meta = "(Ref)" },
|
||||
.{ .key = "theme", .value = "\"dark\"", .meta = "(Ref)" },
|
||||
.{
|
||||
.key = "logo",
|
||||
.value = "Object",
|
||||
.meta = "(Ref)",
|
||||
.children = &[_]StateItem{
|
||||
.{ .key = "src", .value = "\"/assets/logo.svg\"", .meta = "" },
|
||||
.{ .key = "alt", .value = "\"ZX Logo\"", .meta = "" },
|
||||
},
|
||||
},
|
||||
.{
|
||||
.key = "navItems",
|
||||
.value = "Object",
|
||||
.meta = "(Computed)",
|
||||
.children = &[_]StateItem{
|
||||
.{ .key = "docs", .value = "\"/docs\"", .meta = "" },
|
||||
.{ .key = "playground", .value = "\"/playground\"", .meta = "" },
|
||||
.{ .key = "github", .value = "\"https://github.com\"", .meta = "" },
|
||||
},
|
||||
},
|
||||
.{ .key = "isMenuOpen", .value = "false", .meta = "(Ref)" },
|
||||
},
|
||||
.signal_items = &.{
|
||||
.{ .key = "toggleTheme", .value = "fn y(I)", .meta = "" },
|
||||
.{ .key = "toggleMenu", .value = "fn m()", .meta = "" },
|
||||
.{
|
||||
.key = "VersionSelect",
|
||||
.value = "Object",
|
||||
.meta = "",
|
||||
.children = &[_]StateItem{
|
||||
.{ .key = "setup", .value = "fn()", .meta = "" },
|
||||
.{ .key = "render", .value = "fn()", .meta = "" },
|
||||
},
|
||||
},
|
||||
},
|
||||
.action_items = &.{
|
||||
.{
|
||||
.key = "headerRef",
|
||||
.value = "Object",
|
||||
.meta = "",
|
||||
.children = &[_]StateItem{
|
||||
.{ .key = "$el", .value = "<header>", .meta = "" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
.children = &[_]Component{
|
||||
.{ .name = "VersionSelect", .has_children = false, .children = &[_]Component{} },
|
||||
.{ .name = "VersionSelect", .has_children = false, .children = &[_]Component{} },
|
||||
.{ .name = "Sun", .has_children = false, .children = &[_]Component{} },
|
||||
.{ .name = "Moon", .has_children = false, .children = &[_]Component{} },
|
||||
.{ .name = "Share", .has_children = false, .children = &[_]Component{} },
|
||||
.{ .name = "Reload", .has_children = false, .children = &[_]Component{} },
|
||||
.{ .name = "Download", .has_children = false, .children = &[_]Component{} },
|
||||
.{ .name = "GitHub", .has_children = false, .children = &[_]Component{} },
|
||||
},
|
||||
},
|
||||
.{
|
||||
.name = "Repl",
|
||||
.has_children = true,
|
||||
.children = &[_]Component{
|
||||
.{
|
||||
.name = "SplitPane",
|
||||
.has_children = true,
|
||||
.children = &[_]Component{
|
||||
.{ .name = "Panes", .has_children = false, .children = &[_]Component{} },
|
||||
.{ .name = "PanesTwo", .has_children = false, .children = &[_]Component{} },
|
||||
.{ .name = "PanesThree", .has_children = false, .children = &[_]Component{} },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
pub const routes = [_]Route{
|
||||
@ -150,3 +213,98 @@ pub const routes = [_]Route{
|
||||
.{ .method = "GET", .path = "/docs" },
|
||||
.{ .method = "GET", .path = "/settings" },
|
||||
};
|
||||
|
||||
pub fn fromSerializable(allocator: std.mem.Allocator, s: zx.Component.Serializable, path: []const u8) anyerror!Component {
|
||||
var name: []const u8 = "unknown";
|
||||
var badge: []const u8 = "";
|
||||
|
||||
if (s.component) |c| {
|
||||
name = c;
|
||||
} else if (s.tag) |t| {
|
||||
name = @tagName(t);
|
||||
} else if (s.text) |_| {
|
||||
name = "text";
|
||||
badge = "text";
|
||||
}
|
||||
|
||||
var children: []const Component = &[_]Component{};
|
||||
if (s.children) |sc| {
|
||||
var children_mut = try allocator.alloc(Component, sc.len);
|
||||
for (sc, 0..) |child_s, i| {
|
||||
const child_path = try std.fmt.allocPrint(allocator, "{s}.{d}", .{ path, i });
|
||||
children_mut[i] = try fromSerializable(allocator, child_s, child_path);
|
||||
}
|
||||
children = children_mut;
|
||||
}
|
||||
|
||||
var meta: ?ComponentMeta = null;
|
||||
if (s.props) |p| {
|
||||
var props_list = std.ArrayList(StateItem).empty;
|
||||
var signals_list = std.ArrayList(StateItem).empty;
|
||||
|
||||
for (p) |item| {
|
||||
if (std.mem.eql(u8, item.meta, "(Ref)") or std.mem.eql(u8, item.meta, "(Computed)")) {
|
||||
try signals_list.append(allocator, item);
|
||||
} else {
|
||||
try props_list.append(allocator, item);
|
||||
}
|
||||
}
|
||||
|
||||
meta = ComponentMeta{
|
||||
.prop_items = try props_list.toOwnedSlice(allocator),
|
||||
.signal_items = try signals_list.toOwnedSlice(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
return Component{
|
||||
.id = path,
|
||||
.name = name,
|
||||
.children = children,
|
||||
.has_children = children.len > 0,
|
||||
.badge = badge,
|
||||
.meta = meta,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn fromSerializableSlice(allocator: std.mem.Allocator, sc: []const zx.Component.Serializable) anyerror![]const Component {
|
||||
var children = try allocator.alloc(Component, sc.len);
|
||||
for (sc, 0..) |child_s, i| {
|
||||
const path = try std.fmt.allocPrint(allocator, "{d}", .{i});
|
||||
children[i] = try fromSerializable(allocator, child_s, path);
|
||||
}
|
||||
|
||||
var root_page = try allocator.alloc(Component, 1);
|
||||
root_page[0] = Component{
|
||||
.id = "0.root.layout.page",
|
||||
.name = "Page",
|
||||
.children = children,
|
||||
.has_children = children.len > 0,
|
||||
.badge = "",
|
||||
.meta = null,
|
||||
};
|
||||
|
||||
var root_layout = try allocator.alloc(Component, 1);
|
||||
root_layout[0] = Component{
|
||||
.id = "0.root.layout",
|
||||
.name = "Layout",
|
||||
.children = root_page,
|
||||
.has_children = root_page.len > 0,
|
||||
.badge = "",
|
||||
.meta = null,
|
||||
};
|
||||
|
||||
var root_component = try allocator.alloc(Component, 1);
|
||||
root_component[0] = Component{
|
||||
.id = "0.root",
|
||||
.name = "App",
|
||||
.children = root_layout,
|
||||
.has_children = root_layout.len > 0,
|
||||
.badge = "fragment",
|
||||
.meta = null,
|
||||
};
|
||||
|
||||
return root_component;
|
||||
}
|
||||
|
||||
const zx = @import("zx");
|
||||
const std = @import("std");
|
||||
|
||||
@ -21,9 +21,9 @@ pub fn Layout(ctx: zx.LayoutContext, children: zx.Component) zx.Component {
|
||||
</a>
|
||||
)}
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
v1.0.0
|
||||
</div>
|
||||
// <div class="sidebar-footer">
|
||||
// {zx.info.version}
|
||||
// </div>
|
||||
</aside>
|
||||
<main class="main-content">
|
||||
{children}
|
||||
|
||||
@ -10,7 +10,7 @@ const component_count_label = std.fmt.comptimePrint("{d} components", .{countCom
|
||||
const route_count_label = std.fmt.comptimePrint("{d} Routes", .{data.routes.len});
|
||||
|
||||
const features = [_]Feature{
|
||||
.{ .icon_fn = icons.Lightning, .title = data.components[0].meta.?.version, .link = "overview" },
|
||||
.{ .icon_fn = icons.Lightning, .title = zx.info.version, .link = "overview" },
|
||||
.{ .icon_fn = icons.Hive, .title = component_count_label, .link = "/" },
|
||||
.{ .icon_fn = icons.Workflow, .title = route_count_label, .link = "routes" },
|
||||
};
|
||||
@ -30,7 +30,7 @@ pub fn Page(ctx: zx.PageContext) zx.Component {
|
||||
</p>
|
||||
<p class="hero-description">
|
||||
Developer Tools for Ziex Framework
|
||||
<code class="version-badge">{data.components[0].meta.?.version}</code>
|
||||
<code class="version-badge">{zx.info.version}</code>
|
||||
</p>
|
||||
|
||||
<div class="features-grid">
|
||||
|
||||
@ -1,18 +1,20 @@
|
||||
const components = data.components;
|
||||
var components: []const data.Component = &.{};
|
||||
|
||||
var inputvalue: []const u8 = "";
|
||||
var stateFilter: []const u8 = "";
|
||||
|
||||
var selected_component = zx.Signal([]const u8).init("App");
|
||||
var selected_component = zx.Signal([]const u8).init("0");
|
||||
|
||||
// Helper function to find a component by name recursively
|
||||
fn findComponentByName(comps: []const data.Component, name: []const u8) ?data.Component {
|
||||
var fetched = false;
|
||||
// Helper function to find a component by ID recursively
|
||||
fn findComponentById(comps: []const data.Component, id: []const u8) ?data.Component {
|
||||
fetch();
|
||||
for (comps) |comp| {
|
||||
if (std.mem.eql(u8, comp.name, name)) {
|
||||
if (std.mem.eql(u8, comp.id, id)) {
|
||||
return comp;
|
||||
}
|
||||
if (comp.has_children) {
|
||||
if (findComponentByName(comp.children, name)) |found| {
|
||||
if (findComponentById(comp.children, id)) |found| {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
@ -20,6 +22,29 @@ fn findComponentByName(comps: []const data.Component, name: []const u8) ?data.Co
|
||||
return null;
|
||||
}
|
||||
|
||||
fn fetch() void {
|
||||
if (fetched) return;
|
||||
fetched = true;
|
||||
_ = zx.fetch(.wasm(&onFetchText), zx.client_allocator, "http://localhost:5588/.well-known/_zx/devtool?path=/", .{ .method = .GET }) catch {};
|
||||
}
|
||||
|
||||
fn onFetchText(res: ?*zx.Fetch.Response, _: ?zx.Fetch.FetchError) void {
|
||||
if (res) |r| {
|
||||
defer r.deinit();
|
||||
if (r.text()) |p| {
|
||||
const parsed = zx.prop.parse(zx.Component.Serializable, zx.client_allocator, p);
|
||||
if (parsed.children) |sc| {
|
||||
const mapped = data.fromSerializableSlice(zx.client_allocator, sc) catch unreachable;
|
||||
std.log.info("Fetched {d} components", .{mapped.len});
|
||||
components = mapped;
|
||||
}
|
||||
} else |_| {}
|
||||
}
|
||||
zx.requestRender();
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ComponentSearchInputProps = struct { };
|
||||
const ComponentStateProps = struct { };
|
||||
const ComponentTreeProps = struct { };
|
||||
@ -36,10 +61,11 @@ pub fn ComponentSearchInput(
|
||||
pub fn ComponentTree(
|
||||
ctx: *zx.ComponentCtx(ComponentTreeProps),
|
||||
) zx.Component {
|
||||
const sel = selected_component.get();
|
||||
return (
|
||||
<div @allocator={ctx.allocator} class="component-tree">
|
||||
<div class="component-list">
|
||||
{for (components) |comp| (<ComponentItem allocator={ctx.allocator} name={comp.name} has_children={comp.has_children} children={comp.children} depth={0} selected={comp.selected} badge={comp.badge} />)}
|
||||
{for (components) |comp| (<ComponentItem allocator={ctx.allocator} id={comp.id} name={comp.name} has_children={comp.has_children} children={comp.children} depth={0} selected={std.mem.eql(u8, comp.id, sel)} badge={comp.badge} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -49,16 +75,16 @@ pub fn ComponentState(
|
||||
ctx: *zx.ComponentCtx(ComponentStateProps),
|
||||
) zx.Component {
|
||||
const sel = selected_component.get();
|
||||
const maybe_component = findComponentByName(&components, sel);
|
||||
const maybe_component = findComponentById(components, sel);
|
||||
|
||||
if (maybe_component) |comp| {
|
||||
if (comp.meta) |metadata| {
|
||||
return (
|
||||
<div @allocator={ctx.allocator} class="component-state-wrapper">
|
||||
<div @allocator={ctx.allocator} class="state-content">
|
||||
<StateSection allocator={ctx.allocator} title="Props" items={metadata.setup_items} />
|
||||
<StateSection allocator={ctx.allocator} title="Signals" items={metadata.setup_other_items} />
|
||||
<StateSection allocator={ctx.allocator} title="Actions" items={metadata.template_refs_items} />
|
||||
<StateSection allocator={ctx.allocator} title="Props" items={metadata.prop_items} />
|
||||
<StateSection allocator={ctx.allocator} title="Signals" items={metadata.signal_items} />
|
||||
<StateSection allocator={ctx.allocator} title="Actions" items={metadata.action_items} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -69,7 +95,7 @@ pub fn ComponentState(
|
||||
<div @allocator={ctx.allocator} class="component-state-wrapper">
|
||||
<div @allocator={ctx.allocator} class="state-content">
|
||||
<div class="state-empty">
|
||||
<h2>{sel}</h2>
|
||||
<h2>{if (maybe_component) |c| c.name else sel}</h2>
|
||||
<p>{"No state data available for this component."}</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -92,14 +118,14 @@ pub fn ComponentStateFilter(
|
||||
) zx.Component {
|
||||
return (
|
||||
<div @allocator={ctx.allocator} class="filter-wrapper">
|
||||
<input @allocator={ctx.allocator} oninput={handleStateFilterInput} type="text" placeholder="Filter State..." class="filter-input" />
|
||||
<input @allocator={ctx.allocator} oninput={handleStateFilterInput} type="text" placeholder="Search..." class="filter-input" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
pub fn handleComponentClick(ctx: zx.EventContext) void {
|
||||
const name = ctx.value() orelse "App";
|
||||
selected_component.set(name);
|
||||
const id = ctx.value() orelse "0";
|
||||
selected_component.set(id);
|
||||
zx.requestRender();
|
||||
}
|
||||
|
||||
@ -124,14 +150,14 @@ pub fn Page(ctx: zx.PageContext) zx.Component {
|
||||
|
||||
<div class="state-panel">
|
||||
<div class="state-header">
|
||||
<span class="state-title" id="state-title">{&selected_component}</span>
|
||||
// <span class="state-title" id="state-title">{&selected_component}</span>
|
||||
<ComponentStateFilter @rendering={.client} />
|
||||
<div class="state-actions">
|
||||
{icons.StateEye(ctx.arena)}
|
||||
{icons.StateArrows(ctx.arena)}
|
||||
{icons.StateList(ctx.arena)}
|
||||
{icons.StateExport(ctx.arena)}
|
||||
</div>
|
||||
// <div class="state-actions">
|
||||
// {icons.StateEye(ctx.arena)}
|
||||
// {icons.StateArrows(ctx.arena)}
|
||||
// {icons.StateList(ctx.arena)}
|
||||
// {icons.StateExport(ctx.arena)}
|
||||
// </div>
|
||||
</div>
|
||||
|
||||
<ComponentState @rendering={.client} />
|
||||
@ -142,25 +168,28 @@ pub fn Page(ctx: zx.PageContext) zx.Component {
|
||||
}
|
||||
|
||||
const Component = data.Component;
|
||||
const ComponentItemProps = struct { name: []const u8, has_children: bool, children: []const Component, depth: usize = 0, selected: bool = false, badge: []const u8 = "" };
|
||||
const ComponentItemProps = struct { id: []const u8, name: []const u8, has_children: bool, children: []const Component, depth: usize = 0, selected: bool = false, badge: []const u8 = "" };
|
||||
|
||||
fn ComponentItem(allocator: zx.Allocator, props: ComponentItemProps) zx.Component {
|
||||
const id = std.fmt.allocPrint(allocator, "comp-{d}-{s}", .{ props.depth, props.name }) catch "";
|
||||
const sel = selected_component.get();
|
||||
const dom_id = std.fmt.allocPrint(allocator, "comp-{s}", .{props.id}) catch "";
|
||||
const padding = props.depth * 24 + 16;
|
||||
const style_str = std.fmt.allocPrint(allocator, "padding-left: {d}px", .{padding}) catch "";
|
||||
const item_class = if (props.selected) "component-root" else "component-item";
|
||||
const name_class = if (props.selected) "component-name-root" else "component-name";
|
||||
const is_selected = std.mem.eql(u8, props.id, sel);
|
||||
const item_class = if (is_selected) "component-root" else "component-item";
|
||||
const name_class = if (is_selected) "component-name-root" else "component-name";
|
||||
const group_class = getComponentGroupClass(props.name, props.has_children, props.children);
|
||||
|
||||
return (
|
||||
<div @allocator={allocator} class={group_class}>
|
||||
{if (props.has_children) (
|
||||
<>
|
||||
<input type="checkbox" id={id} class="component-toggle" checked />
|
||||
<input type="checkbox" id={dom_id} class="component-toggle" checked />
|
||||
<div class={item_class} style={style_str} >
|
||||
<label for={id} class="component-toggle-label">
|
||||
<label for={dom_id} class="component-toggle-label">
|
||||
<span class="tree-arrow"></span>
|
||||
</label>
|
||||
<button value={props.name} onclick={handleComponentClick} class="component-select-btn">
|
||||
<button value={props.id} onclick={handleComponentClick} class="component-select-btn">
|
||||
<span class="bracket">{"<"}</span>
|
||||
<span class={name_class}>{props.name}</span>
|
||||
<span class="bracket">{">"}</span>
|
||||
@ -169,13 +198,13 @@ fn ComponentItem(allocator: zx.Allocator, props: ComponentItemProps) zx.Componen
|
||||
</div>
|
||||
<div class="component-children">
|
||||
{for (props.children) |child| (
|
||||
<ComponentItem allocator={allocator} name={child.name} has_children={child.has_children} children={child.children} depth={props.depth + 1} selected={child.selected} badge={child.badge} />
|
||||
<ComponentItem allocator={allocator} id={child.id} name={child.name} has_children={child.has_children} children={child.children} depth={props.depth + 1} selected={std.mem.eql(u8, child.id, sel)} badge={child.badge} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) else (
|
||||
<div class={item_class} style={style_str}>
|
||||
<button value={props.name} onclick={handleComponentClick} class="component-select-btn-leaf">
|
||||
<button value={props.id} onclick={handleComponentClick} class="component-select-btn-leaf">
|
||||
<span class="tree-arrow-spacer"></span>
|
||||
<span class="bracket">{"<"}</span>
|
||||
<span class={name_class}>{props.name}</span>
|
||||
@ -189,7 +218,8 @@ fn ComponentItem(allocator: zx.Allocator, props: ComponentItemProps) zx.Componen
|
||||
}
|
||||
|
||||
const StateItem = data.StateItem;
|
||||
fn StateSection(allocator: zx.Allocator, props: struct { title: []const u8, items: []const StateItem }) zx.Component {
|
||||
fn StateSection(allocator: zx.Allocator, props: struct { title: []const u8, items: []const StateItem }) ?zx.Component {
|
||||
if (props.items.len == 0) return null;
|
||||
const id = std.fmt.allocPrint(allocator, "state-section-{s}", .{props.title}) catch "";
|
||||
return (
|
||||
<div @allocator={allocator} class="state-section">
|
||||
@ -199,21 +229,21 @@ fn StateSection(allocator: zx.Allocator, props: struct { title: []const u8, item
|
||||
<span class="section-title">{props.title}</span>
|
||||
</label>
|
||||
<div class="section-items">
|
||||
{for (props.items) |item| (
|
||||
<StateItemRow allocator={allocator} item={item} />
|
||||
{for (props.items, 0..) |item, i| (
|
||||
<StateItemRow allocator={allocator} item={item} path={std.fmt.allocPrint(allocator, "ss-{s}-{d}", .{props.title, i}) catch ""} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
fn StateItemRow(allocator: zx.Allocator, props: struct { item: StateItem, depth: usize = 0 }) zx.Component {
|
||||
fn StateItemRow(allocator: zx.Allocator, props: struct { item: StateItem, depth: usize = 0, path: []const u8 }) zx.Component {
|
||||
const item = props.item;
|
||||
const has_children = item.children.len > 0;
|
||||
const value_color = getValueColor(item.value);
|
||||
const padding = props.depth * 18;
|
||||
const style_str = std.fmt.allocPrint(allocator, "padding-left: {d}px", .{padding}) catch "";
|
||||
const id = std.fmt.allocPrint(allocator, "sr-{d}-{s}", .{ props.depth, item.key }) catch "";
|
||||
const id = std.fmt.allocPrint(allocator, "sr-{s}", .{ props.path }) catch "";
|
||||
const group_class = getStateItemGroupClass(item);
|
||||
return (
|
||||
<div @allocator={allocator} class={group_class} style={style_str}>
|
||||
@ -228,8 +258,8 @@ fn StateItemRow(allocator: zx.Allocator, props: struct { item: StateItem, depth:
|
||||
{if (item.meta.len > 0) (<span class="item-meta">{item.meta}</span>) else ""}
|
||||
</label>
|
||||
<div class="state-item-children">
|
||||
{for (item.children) |child| (
|
||||
<StateItemRow allocator={allocator} item={child} depth={props.depth + 1} />
|
||||
{for (item.children, 0..) |child, i| (
|
||||
<StateItemRow allocator={allocator} item={child} depth={props.depth + 1} path={std.fmt.allocPrint(allocator, "{s}-{d}", .{props.path, i}) catch ""} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
@ -246,6 +276,7 @@ fn StateItemRow(allocator: zx.Allocator, props: struct { item: StateItem, depth:
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
fn getComponentGroupClass(name: []const u8, has_children: bool, children: []const Component) []const u8 {
|
||||
if (componentOrDescendantMatches(name, has_children, children)) return "component-group";
|
||||
return "component-group component-group-hidden";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const routes = data.routes;
|
||||
var routes: []const AppRoute = &[_]AppRoute{};
|
||||
|
||||
pub fn Page(ctx: zx.PageContext) zx.Component {
|
||||
return (
|
||||
@ -23,6 +23,32 @@ pub fn Page(ctx: zx.PageContext) zx.Component {
|
||||
);
|
||||
}
|
||||
|
||||
var fetched = false;
|
||||
fn fetch() void {
|
||||
if (fetched) return;
|
||||
fetched = true;
|
||||
_ = zx.fetch(.wasm(&onFetchText), zx.client_allocator, "http://localhost:5588/.well-known/_zx/devtool?meta=true", .{ .method = .GET }) catch {};
|
||||
}
|
||||
|
||||
pub const AppRoute = struct {
|
||||
path: []const u8,
|
||||
method: []const u8 = "NA",
|
||||
has_notfound: bool = false,
|
||||
is_dynamic: bool = false,
|
||||
};
|
||||
|
||||
|
||||
fn onFetchText(res: ?*zx.Fetch.Response, _: ?zx.Fetch.FetchError) void {
|
||||
if (res) |r| {
|
||||
defer r.deinit();
|
||||
if (r.text()) |p| {
|
||||
routes = zx.prop.parse([]const AppRoute, zx.client_allocator, p);
|
||||
} else |_| {}
|
||||
}
|
||||
zx.requestRender();
|
||||
}
|
||||
|
||||
|
||||
fn getMethodClass(method: []const u8) []const u8 {
|
||||
if (std.mem.eql(u8, method, "GET")) return "route-method method-get";
|
||||
if (std.mem.eql(u8, method, "POST")) return "route-method method-post";
|
||||
@ -35,6 +61,7 @@ const ComponentRoutesProps = struct { };
|
||||
pub fn ComponentRotes(
|
||||
ctx: *zx.ComponentCtx(ComponentRoutesProps),
|
||||
) zx.Component {
|
||||
fetch();
|
||||
return (
|
||||
<div @allocator={ctx.allocator} class="routes-section" >
|
||||
<input type="checkbox" id="routes-toggle" class="routes-toggle" checked />
|
||||
|
||||
@ -80,6 +80,7 @@ fn @"export"(ctx: zli.CommandContext) !void {
|
||||
for (static_params.items) |expanded_path| {
|
||||
const expanded_route = zx.App.SerilizableAppMeta.Route{
|
||||
.path = expanded_path,
|
||||
.method = route.method,
|
||||
.has_notfound = route.has_notfound,
|
||||
.is_dynamic = false,
|
||||
};
|
||||
|
||||
@ -945,11 +945,14 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
|
||||
const client_cmp = try ClientComponentMetadata.init(ctx.allocator, tag, full_path, rendering_type, component_index);
|
||||
try ctx.client_components.append(ctx.allocator, client_cmp);
|
||||
|
||||
// Write _zx.cmp(Component, .{ .client = .{ .name = ..., .path = ..., .id = ... } }, .{ props })
|
||||
// Write _zx.cmp(Component, .{ .name = ..., .client = .{ .name = ..., .id = ... } }, .{ props })
|
||||
try ctx.writeM("_zx.cmp", node.startByte(), self);
|
||||
try ctx.write("(");
|
||||
try ctx.write(tag);
|
||||
try ctx.write(", .{ .client = .{ .name = \"");
|
||||
try ctx.write(", ");
|
||||
try ctx.write(".{ .name = \"");
|
||||
try ctx.write(tag);
|
||||
try ctx.write("\", .client = .{ .name = \"");
|
||||
try ctx.write(tag);
|
||||
// try ctx.write("\", .path = \"");
|
||||
// try ctx.write(full_path);
|
||||
@ -982,7 +985,7 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
|
||||
}
|
||||
|
||||
{
|
||||
// Regular cmp component: _zx.cmp(Func, .{ options }, .{ props })
|
||||
// Regular cmp component: _zx.cmp(Func, .{ .name = ..., options }, .{ props })
|
||||
try ctx.writeM("_zx.cmp", node.startByte(), self);
|
||||
try ctx.write("(");
|
||||
try ctx.write(tag);
|
||||
@ -1012,9 +1015,11 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
|
||||
const has_regular_props = regular_props.items.len > 0;
|
||||
const has_children = children.len > 0;
|
||||
|
||||
// Write options parameter first (builtin attributes)
|
||||
try ctx.write(".{");
|
||||
try writeComponentBuiltinOptions(self, builtin_attrs.items, ctx);
|
||||
// Write options parameter first (name + builtin attributes)
|
||||
try ctx.write(".{ .name = \"");
|
||||
try ctx.write(tag);
|
||||
try ctx.write("\"");
|
||||
try writeComponentBuiltinOptions(self, builtin_attrs.items, ctx, true);
|
||||
try ctx.write(" }, ");
|
||||
|
||||
// Case 1: Single spread
|
||||
@ -1105,9 +1110,11 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
|
||||
}
|
||||
}
|
||||
|
||||
/// Write builtin options for component (cmp) calls
|
||||
fn writeComponentBuiltinOptions(self: *Ast, builtin_attrs: []const ZxAttribute, ctx: *TranspileContext) !void {
|
||||
var first = true;
|
||||
/// Write builtin options for component (cmp) calls.
|
||||
/// `has_prior_field` should be true when a field (e.g. `.name`) was already written
|
||||
/// so the first builtin attr is prefixed with a comma separator.
|
||||
fn writeComponentBuiltinOptions(self: *Ast, builtin_attrs: []const ZxAttribute, ctx: *TranspileContext, has_prior_field: bool) !void {
|
||||
var first = !has_prior_field;
|
||||
for (builtin_attrs) |attr| {
|
||||
// Skip @rendering which is handled separately for CSR components
|
||||
if (std.mem.eql(u8, attr.name, "@rendering")) continue;
|
||||
|
||||
283
src/devtool.zig
Normal file
283
src/devtool.zig
Normal file
@ -0,0 +1,283 @@
|
||||
const std = @import("std");
|
||||
const zx = @import("root.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const ComponentSerializable = struct {
|
||||
pub const StateItem = struct {
|
||||
key: []const u8,
|
||||
value: []const u8,
|
||||
meta: []const u8 = "",
|
||||
children: []const StateItem = &[_]StateItem{},
|
||||
};
|
||||
|
||||
pub fn isSignalType(comptime T: type) bool {
|
||||
const ti = @typeInfo(T);
|
||||
if (ti == .pointer) {
|
||||
const Child = ti.pointer.child;
|
||||
if (@typeInfo(Child) == .@"struct") {
|
||||
return @hasField(Child, "id") and
|
||||
@hasField(Child, "value") and
|
||||
@hasDecl(Child, "get") and
|
||||
@hasDecl(Child, "set") and
|
||||
@hasDecl(Child, "notifyChange");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn isComputedType(comptime T: type) bool {
|
||||
const ti = @typeInfo(T);
|
||||
if (ti == .pointer) {
|
||||
const Child = ti.pointer.child;
|
||||
if (@typeInfo(Child) == .@"struct") {
|
||||
return @hasField(Child, "id") and
|
||||
@hasDecl(Child, "get") and
|
||||
!@hasDecl(Child, "set");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn toStateItems(allocator: Allocator, comptime T: type, value: T) anyerror![]const StateItem {
|
||||
const ti = @typeInfo(T);
|
||||
if (ti != .@"struct") return &[_]StateItem{};
|
||||
|
||||
const fields = ti.@"struct".fields;
|
||||
var items = try allocator.alloc(StateItem, fields.len);
|
||||
inline for (fields, 0..) |field, i| {
|
||||
items[i] = try toStateItem(allocator, field.type, field.name, @field(value, field.name), 0);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
pub fn createGetStateItemsFn(comptime func: anytype) *const fn (Allocator, *const anyopaque) anyerror![]const StateItem {
|
||||
return struct {
|
||||
fn getStateItems(alloc: Allocator, ptr: *const anyopaque) anyerror![]const StateItem {
|
||||
const FuncInfo = @typeInfo(@TypeOf(func));
|
||||
const param_count = FuncInfo.@"fn".params.len;
|
||||
if (param_count == 0) return &[_]StateItem{};
|
||||
|
||||
const FirstPropType = FuncInfo.@"fn".params[0].type.?;
|
||||
const first_is_allocator = FirstPropType == std.mem.Allocator;
|
||||
const first_is_ctx_ptr = @typeInfo(FirstPropType) == .pointer and
|
||||
@hasField(@typeInfo(FirstPropType).pointer.child, "allocator") and
|
||||
@hasField(@typeInfo(FirstPropType).pointer.child, "children");
|
||||
|
||||
if (first_is_allocator and param_count == 2) {
|
||||
const SecondPropType = FuncInfo.@"fn".params[1].type.?;
|
||||
const typed_p: *const SecondPropType = @ptrCast(@alignCast(ptr));
|
||||
return toStateItems(alloc, SecondPropType, typed_p.*);
|
||||
} else if (first_is_ctx_ptr) {
|
||||
const CtxType = @typeInfo(FirstPropType).pointer.child;
|
||||
const ctx_ptr: *const CtxType = @ptrCast(@alignCast(ptr));
|
||||
if (@hasField(CtxType, "props")) {
|
||||
const props_val = ctx_ptr.props;
|
||||
const PropsT = @TypeOf(props_val);
|
||||
if (PropsT != void) {
|
||||
return toStateItems(alloc, PropsT, props_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
return &[_]StateItem{};
|
||||
}
|
||||
}.getStateItems;
|
||||
}
|
||||
|
||||
pub fn toStateItem(allocator: Allocator, comptime T: type, key: []const u8, value: T, depth: usize) anyerror!StateItem {
|
||||
var item: StateItem = .{
|
||||
.key = key,
|
||||
.value = "",
|
||||
.meta = "",
|
||||
.children = &[_]StateItem{},
|
||||
};
|
||||
|
||||
if (depth > 6) {
|
||||
item.value = "...";
|
||||
return item;
|
||||
}
|
||||
|
||||
if (comptime isSignalType(T)) {
|
||||
item.meta = "(Ref)";
|
||||
const val = value.get();
|
||||
const ValueT = @TypeOf(val);
|
||||
const sub = try toStateItem(allocator, ValueT, key, val, depth + 1);
|
||||
item.value = sub.value;
|
||||
item.children = sub.children;
|
||||
return item;
|
||||
}
|
||||
|
||||
if (comptime isComputedType(T)) {
|
||||
item.meta = "(Computed)";
|
||||
const val = value.get();
|
||||
const ValueT = @TypeOf(val);
|
||||
const sub = try toStateItem(allocator, ValueT, key, val, depth + 1);
|
||||
item.value = sub.value;
|
||||
item.children = sub.children;
|
||||
return item;
|
||||
}
|
||||
|
||||
const ti = @typeInfo(T);
|
||||
switch (ti) {
|
||||
.@"struct" => |s| {
|
||||
item.value = "Object";
|
||||
var children = try allocator.alloc(StateItem, s.fields.len);
|
||||
inline for (s.fields, 0..) |field, i| {
|
||||
children[i] = try toStateItem(allocator, field.type, field.name, @field(value, field.name), depth + 1);
|
||||
}
|
||||
item.children = children;
|
||||
},
|
||||
.pointer => |p| {
|
||||
if (p.size == .slice and p.child == u8) {
|
||||
item.value = try std.json.Stringify.valueAlloc(allocator, value, .{});
|
||||
} else if (p.size == .slice) {
|
||||
item.value = "Array";
|
||||
var children = try allocator.alloc(StateItem, value.len);
|
||||
for (value, 0..) |v, i| {
|
||||
var buf: [32]u8 = undefined;
|
||||
const index_key = std.fmt.bufPrint(&buf, "{d}", .{i}) catch "item";
|
||||
children[i] = try toStateItem(allocator, p.child, try allocator.dupe(u8, index_key), v, depth + 1);
|
||||
}
|
||||
item.children = children;
|
||||
} else {
|
||||
item.value = "Pointer";
|
||||
}
|
||||
},
|
||||
.optional => |opt| {
|
||||
if (value) |v| {
|
||||
return try toStateItem(allocator, opt.child, key, v, depth);
|
||||
} else {
|
||||
item.value = "null";
|
||||
}
|
||||
},
|
||||
.int, .float, .bool => {
|
||||
item.value = try std.json.Stringify.valueAlloc(allocator, value, .{});
|
||||
},
|
||||
.@"fn" => {
|
||||
item.value = "fn()";
|
||||
},
|
||||
.error_union => {
|
||||
if (value) |v| {
|
||||
return try toStateItem(allocator, @TypeOf(v), key, v, depth);
|
||||
} else |err| {
|
||||
item.value = @errorName(err);
|
||||
}
|
||||
},
|
||||
else => {
|
||||
item.value = @typeName(T);
|
||||
},
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/// Serializable attribute (excludes handler which is a function pointer)
|
||||
const AttributeSerializable = struct {
|
||||
name: []const u8,
|
||||
value: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
tag: ?zx.ElementTag = null,
|
||||
component: ?[]const u8 = null,
|
||||
text: ?[]const u8 = null,
|
||||
props: ?[]const StateItem = null,
|
||||
attributes: ?[]const AttributeSerializable = null,
|
||||
children: ?[]ComponentSerializable = null,
|
||||
|
||||
/// Convert Element.Attribute slice to serializable form (strips handlers)
|
||||
fn serializeAttributes(allocator: Allocator, attrs: ?[]const zx.Element.Attribute) !?[]const AttributeSerializable {
|
||||
const attributes = attrs orelse return null;
|
||||
const serializable = try allocator.alloc(AttributeSerializable, attributes.len);
|
||||
for (attributes, 0..) |attr, i| {
|
||||
serializable[i] = .{
|
||||
.name = attr.name,
|
||||
.value = attr.value,
|
||||
// handler is intentionally excluded - not serializable
|
||||
};
|
||||
}
|
||||
return serializable;
|
||||
}
|
||||
|
||||
fn serializeProps(allocator: Allocator, getStateItems: ?*const anyopaque, props_ptr: ?*const anyopaque) !?[]const StateItem {
|
||||
const gsi_opaque = getStateItems orelse return null;
|
||||
const pp = props_ptr orelse return null;
|
||||
|
||||
const gsi: *const fn (Allocator, *const anyopaque) anyerror![]const StateItem = @ptrCast(@alignCast(gsi_opaque));
|
||||
return try gsi(allocator, pp);
|
||||
}
|
||||
|
||||
pub fn init(allocator: Allocator, component: zx.Component, options: zx.Component.SerializeOptions) anyerror!ComponentSerializable {
|
||||
return switch (component) {
|
||||
.none => .{},
|
||||
.text => |text| .{ .text = text },
|
||||
.signal_text => |sig| .{ .text = sig.current_text },
|
||||
.element => |element| blk: {
|
||||
const children_serializable = if (element.children) |children| blk2: {
|
||||
break :blk2 try ComponentSerializable.initChildren(allocator, children, options);
|
||||
} else null;
|
||||
break :blk .{
|
||||
.tag = element.tag,
|
||||
.attributes = if (options.include_attributes) try serializeAttributes(allocator, element.attributes) else null,
|
||||
.children = children_serializable,
|
||||
};
|
||||
},
|
||||
.component_csr => |component_csr| blk: {
|
||||
const children_serializable = if (component_csr.children) |children| blk2: {
|
||||
const serializable = try allocator.alloc(ComponentSerializable, 1);
|
||||
serializable[0] = try ComponentSerializable.init(allocator, children.*, options);
|
||||
break :blk2 serializable;
|
||||
} else null;
|
||||
break :blk .{
|
||||
.component = component_csr.name,
|
||||
.props = if (options.include_props) try serializeProps(allocator, component_csr.getStateItems, component_csr.props_ptr) else null,
|
||||
.children = children_serializable,
|
||||
};
|
||||
},
|
||||
.component_fn => |comp_fn| blk: {
|
||||
// Resolve component_fn by calling it, then serialize the result
|
||||
// This avoids serializing anyopaque fields
|
||||
const resolved = try comp_fn.call();
|
||||
|
||||
const resolved_serializable = try ComponentSerializable.init(allocator, resolved, options);
|
||||
const children_slice = try allocator.alloc(ComponentSerializable, 1);
|
||||
children_slice[0] = resolved_serializable;
|
||||
break :blk .{
|
||||
.component = comp_fn.name,
|
||||
.props = if (options.include_props) try serializeProps(allocator, comp_fn.getStateItems, comp_fn.propsPtr) else null,
|
||||
.children = children_slice,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initChildren(allocator: Allocator, children: []const zx.Component, options: zx.Component.SerializeOptions) anyerror![]ComponentSerializable {
|
||||
if (!options.only_components) {
|
||||
const children_serializable = try allocator.alloc(ComponentSerializable, children.len);
|
||||
for (children, 0..) |child, i| {
|
||||
children_serializable[i] = try ComponentSerializable.init(allocator, child, options);
|
||||
}
|
||||
return children_serializable;
|
||||
}
|
||||
|
||||
var list = std.ArrayList(ComponentSerializable).empty;
|
||||
for (children) |child| {
|
||||
switch (child) {
|
||||
.element => |elem| {
|
||||
if (elem.children) |child_elements| {
|
||||
const sub = try initChildren(allocator, child_elements, options);
|
||||
try list.appendSlice(allocator, sub);
|
||||
}
|
||||
},
|
||||
.component_fn, .component_csr => {
|
||||
try list.append(allocator, try ComponentSerializable.init(allocator, child, options));
|
||||
},
|
||||
else => {}, // Skip text, none, etc.
|
||||
}
|
||||
}
|
||||
return list.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
pub fn serialize(self: ComponentSerializable, writer: *std.Io.Writer) !void {
|
||||
try zx.prop.serialize(ComponentSerializable, self, writer);
|
||||
}
|
||||
};
|
||||
142
src/root.zig
142
src/root.zig
@ -2,13 +2,14 @@
|
||||
//! This module provides the core component system, rendering engine, and utilities
|
||||
//! for creating type-safe, high-performance web applications with server-side rendering.
|
||||
const std = @import("std");
|
||||
pub const devtool = @import("devtool.zig");
|
||||
pub const Ast = @import("core/Ast.zig");
|
||||
pub const Parse = @import("core/Parse.zig");
|
||||
/// Global cache for components and pages
|
||||
pub const cache = @import("runtime/core//Cache.zig");
|
||||
|
||||
// HTML Tags
|
||||
const ElementTag = enum {
|
||||
pub const ElementTag = enum {
|
||||
aside,
|
||||
fragment,
|
||||
iframe,
|
||||
@ -302,95 +303,9 @@ fn coerceProps(comptime TargetType: type, props: anytype) TargetType {
|
||||
return result;
|
||||
}
|
||||
|
||||
const ComponentSerializable = struct {
|
||||
/// Serializable attribute (excludes handler which is a function pointer)
|
||||
const AttributeSerializable = struct {
|
||||
name: []const u8,
|
||||
value: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
tag: ?ElementTag = null,
|
||||
text: ?[]const u8 = null,
|
||||
attributes: ?[]const AttributeSerializable = null,
|
||||
children: ?[]ComponentSerializable = null,
|
||||
|
||||
/// Convert Element.Attribute slice to serializable form (strips handlers)
|
||||
fn serializeAttributes(allocator: Allocator, attrs: ?[]const Element.Attribute) !?[]const AttributeSerializable {
|
||||
const attributes = attrs orelse return null;
|
||||
const serializable = try allocator.alloc(AttributeSerializable, attributes.len);
|
||||
for (attributes, 0..) |attr, i| {
|
||||
serializable[i] = .{
|
||||
.name = attr.name,
|
||||
.value = attr.value,
|
||||
// handler is intentionally excluded - not serializable
|
||||
};
|
||||
}
|
||||
return serializable;
|
||||
}
|
||||
|
||||
pub fn init(allocator: Allocator, component: Component) !ComponentSerializable {
|
||||
return switch (component) {
|
||||
.text => |text| .{ .text = text },
|
||||
.signal_text => |sig| .{ .text = sig.current_text },
|
||||
.element => |element| blk: {
|
||||
const children_serializable = if (element.children) |children| blk2: {
|
||||
const serializable = try allocator.alloc(ComponentSerializable, children.len);
|
||||
for (children, 0..) |child, i| {
|
||||
serializable[i] = try ComponentSerializable.init(allocator, child);
|
||||
}
|
||||
break :blk2 serializable;
|
||||
} else null;
|
||||
break :blk .{
|
||||
.tag = element.tag,
|
||||
.attributes = try serializeAttributes(allocator, element.attributes),
|
||||
.children = children_serializable,
|
||||
};
|
||||
},
|
||||
.component_csr => |component_csr| blk: {
|
||||
// Serialize the SSR children content (comments are not serializable)
|
||||
if (component_csr.children) |children| {
|
||||
break :blk try ComponentSerializable.init(allocator, children.*);
|
||||
}
|
||||
break :blk .{};
|
||||
},
|
||||
.component_fn => |comp_fn| blk: {
|
||||
// Resolve component_fn by calling it, then serialize the result
|
||||
// This avoids serializing anyopaque fields
|
||||
const resolved = try comp_fn.call();
|
||||
const serialized = try ComponentSerializable.init(allocator, resolved);
|
||||
break :blk serialized;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initChildren(allocator: Allocator, children: []const Component) ![]ComponentSerializable {
|
||||
const children_serializable = try allocator.alloc(ComponentSerializable, children.len);
|
||||
for (children, 0..) |child, i| {
|
||||
children_serializable[i] = try ComponentSerializable.init(allocator, child);
|
||||
}
|
||||
return children_serializable;
|
||||
}
|
||||
|
||||
pub fn serialize(self: ComponentSerializable, writer: *std.Io.Writer) !void {
|
||||
|
||||
// try std.zon.stringify.serializeArbitraryDepth(
|
||||
// self,
|
||||
// .{
|
||||
// .whitespace = true,
|
||||
// .emit_default_optional_fields = false,
|
||||
// },
|
||||
// writer,
|
||||
// );
|
||||
|
||||
try std.json.Stringify.value(
|
||||
self,
|
||||
.{ .whitespace = .indent_2 },
|
||||
writer,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Component = union(enum) {
|
||||
pub const Serializable = devtool.ComponentSerializable;
|
||||
|
||||
none,
|
||||
text: []const u8,
|
||||
element: Element,
|
||||
@ -412,6 +327,7 @@ pub const Component = union(enum) {
|
||||
id: []const u8,
|
||||
props_ptr: ?*const anyopaque = null,
|
||||
writeProps: ?*const fn (*std.Io.Writer, *const anyopaque) anyerror!void = null,
|
||||
getStateItems: ?*const anyopaque = null,
|
||||
/// SSR-rendered content of the component (for hydration)
|
||||
children: ?*const Component = null,
|
||||
/// Whether this is a React component (uses JSON) or Zig component (uses ZON)
|
||||
@ -421,13 +337,15 @@ pub const Component = union(enum) {
|
||||
pub const ComponentFn = struct {
|
||||
propsPtr: ?*const anyopaque,
|
||||
callFn: *const fn (propsPtr: ?*const anyopaque, allocator: Allocator) anyerror!Component,
|
||||
getStateItems: ?*const anyopaque = null,
|
||||
allocator: Allocator,
|
||||
deinitFn: ?*const fn (propsPtr: ?*const anyopaque, allocator: Allocator) void,
|
||||
async_mode: BuiltinAttribute.Async = .sync,
|
||||
fallback: ?*const Component = null,
|
||||
caching: ?BuiltinAttribute.Caching = null,
|
||||
name: []const u8,
|
||||
|
||||
pub fn init(comptime func: anytype, allocator: Allocator, props: anytype) ComponentFn {
|
||||
pub fn init(comptime func: anytype, name: []const u8, allocator: Allocator, props: anytype) ComponentFn {
|
||||
const FuncInfo = @typeInfo(@TypeOf(func));
|
||||
const param_count = FuncInfo.@"fn".params.len;
|
||||
const fn_name = @typeName(@TypeOf(func));
|
||||
@ -546,8 +464,10 @@ pub const Component = union(enum) {
|
||||
return .{
|
||||
.propsPtr = props_copy,
|
||||
.callFn = Wrapper.call,
|
||||
.getStateItems = @ptrCast(devtool.ComponentSerializable.createGetStateItemsFn(func)),
|
||||
.allocator = allocator,
|
||||
.deinitFn = Wrapper.deinit,
|
||||
.name = name,
|
||||
};
|
||||
}
|
||||
|
||||
@ -915,17 +835,31 @@ pub const Component = union(enum) {
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn format(
|
||||
// self: *const Component,
|
||||
// w: *std.Io.Writer,
|
||||
// ) error{WriteFailed}!void {
|
||||
// var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||
// defer arena.deinit();
|
||||
// const allocator = arena.allocator();
|
||||
pub const SerializeOptions = struct {
|
||||
only_components: bool = true,
|
||||
include_props: bool = true,
|
||||
include_attributes: bool = true,
|
||||
};
|
||||
|
||||
// var serializable = ComponentSerializable.init(allocator, self.*) catch return error.WriteFailed;
|
||||
// try serializable.serialize(w);
|
||||
// }
|
||||
pub fn format(
|
||||
self: *const Component,
|
||||
w: *std.Io.Writer,
|
||||
) error{WriteFailed}!void {
|
||||
self.formatWithOptions(w, .{}) catch return error.WriteFailed;
|
||||
}
|
||||
|
||||
pub fn formatWithOptions(
|
||||
self: *const Component,
|
||||
w: *std.Io.Writer,
|
||||
options: SerializeOptions,
|
||||
) anyerror!void {
|
||||
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||
defer arena.deinit();
|
||||
const allocator = arena.allocator();
|
||||
|
||||
var serializable = try devtool.ComponentSerializable.init(allocator, self.*, options);
|
||||
try serializable.serialize(w);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Element = struct {
|
||||
@ -965,6 +899,9 @@ const ZxOptions = struct {
|
||||
fallback: ?*const Component = null,
|
||||
caching: ?BuiltinAttribute.Caching = null,
|
||||
client: ?ComponentClientOptions = null,
|
||||
/// Component name used for devtools / debugging.
|
||||
/// Pass `null` (or omit) in release builds to reduce binary size.
|
||||
name: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub fn zx(tag: ElementTag, options: ZxOptions) Component {
|
||||
@ -1528,13 +1465,14 @@ pub const ZxContext = struct {
|
||||
@hasField(@typeInfo(FirstPropType).pointer.child, "allocator") and
|
||||
@hasField(@typeInfo(FirstPropType).pointer.child, "children");
|
||||
|
||||
const name = options.name orelse "";
|
||||
// Context-based component or function with props parameter
|
||||
var comp_fn = if (first_is_ctx_ptr or param_count == 2) blk: {
|
||||
const PropsType = if (first_is_ctx_ptr) @TypeOf(props) else FuncInfo.@"fn".params[1].type.?;
|
||||
const coerced_props = coerceProps(PropsType, props);
|
||||
break :blk Component.ComponentFn.init(func, allocator, coerced_props);
|
||||
break :blk Component.ComponentFn.init(func, name, allocator, coerced_props);
|
||||
} else blk: {
|
||||
break :blk Component.ComponentFn.init(func, allocator, props);
|
||||
break :blk Component.ComponentFn.init(func, name, allocator, props);
|
||||
};
|
||||
|
||||
// Apply builtin attributes from options
|
||||
|
||||
@ -16,7 +16,7 @@ pub const PropsParser = struct {
|
||||
return self.parseValue(T, allocator);
|
||||
}
|
||||
|
||||
fn parseValue(self: *PropsParser, comptime T: type, allocator: std.mem.Allocator) !T {
|
||||
fn parseValue(self: *PropsParser, comptime T: type, allocator: std.mem.Allocator) anyerror!T {
|
||||
const type_info = @typeInfo(T);
|
||||
|
||||
switch (type_info) {
|
||||
|
||||
@ -57,7 +57,7 @@ pub fn Server(comptime H: type) type {
|
||||
else
|
||||
&self.app_ctx;
|
||||
|
||||
self.handler = try HandlerType.init(allocator, &self.meta, config.cache, app_ctx_ptr);
|
||||
self.handler = try HandlerType.init(allocator, &self.meta, config, app_ctx_ptr);
|
||||
errdefer self.handler.deinit();
|
||||
self.server = try httpz.Server(*HandlerType).init(allocator, config.server, &self.handler);
|
||||
|
||||
@ -273,7 +273,7 @@ pub fn Server(comptime H: type) type {
|
||||
var aw = std.Io.Writer.Allocating.init(self.allocator);
|
||||
defer aw.deinit();
|
||||
|
||||
var serilizable_meta = try SerilizableAppMeta.init(self.allocator, self);
|
||||
var serilizable_meta = try SerilizableAppMeta.init(self.allocator, &self.meta, self.server.config);
|
||||
defer serilizable_meta.deinit(self.allocator);
|
||||
try serilizable_meta.serialize(&aw.writer);
|
||||
|
||||
@ -285,8 +285,11 @@ pub fn Server(comptime H: type) type {
|
||||
if (self.meta.cli_command == .dev) {
|
||||
var router = try self.server.router(.{});
|
||||
var zx_routes = router.group("/.well-known/_zx", .{});
|
||||
|
||||
zx_routes.get("/devsocket", HandlerType.devsocket, .{});
|
||||
zx_routes.get("/devscript.js", HandlerType.devscript, .{});
|
||||
zx_routes.all("/devtool", HandlerType.devtool, .{});
|
||||
zx_routes.all("/devtool", HandlerType.devtool, .{});
|
||||
}
|
||||
|
||||
try stdout.flush();
|
||||
@ -295,6 +298,7 @@ pub fn Server(comptime H: type) type {
|
||||
pub const SerilizableAppMeta = struct {
|
||||
pub const Route = struct {
|
||||
path: []const u8,
|
||||
method: []const u8,
|
||||
has_notfound: bool = false,
|
||||
is_dynamic: bool = false,
|
||||
};
|
||||
@ -309,13 +313,14 @@ pub fn Server(comptime H: type) type {
|
||||
version: []const u8,
|
||||
cli_command: ?ServerMeta.CliCommand = null,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, srv: *const Self) !SerilizableAppMeta {
|
||||
var routes = try allocator.alloc(Route, srv.meta.routes.len);
|
||||
pub fn init(allocator: std.mem.Allocator, meta: *ServerMeta, config: httpz.Config) !SerilizableAppMeta {
|
||||
var routes = try allocator.alloc(Route, meta.routes.len);
|
||||
|
||||
for (srv.meta.routes, 0..) |route, i| {
|
||||
for (meta.routes, 0..) |route, i| {
|
||||
const is_dynamic = std.mem.indexOf(u8, route.path, ":") != null;
|
||||
routes[i] = Route{
|
||||
.path = try allocator.dupe(u8, route.path),
|
||||
.method = "NA",
|
||||
.has_notfound = route.notfound != null,
|
||||
.is_dynamic = is_dynamic,
|
||||
};
|
||||
@ -324,11 +329,11 @@ pub fn Server(comptime H: type) type {
|
||||
return SerilizableAppMeta{
|
||||
.routes = routes,
|
||||
.config = SerilizableAppMeta.Config{
|
||||
.server = srv.server.config,
|
||||
.server = config,
|
||||
},
|
||||
.version = Self.version,
|
||||
.rootdir = srv.meta.rootdir,
|
||||
.cli_command = srv.meta.cli_command,
|
||||
.rootdir = meta.rootdir,
|
||||
.cli_command = meta.cli_command,
|
||||
};
|
||||
}
|
||||
|
||||
@ -349,6 +354,9 @@ pub fn Server(comptime H: type) type {
|
||||
.emit_default_optional_fields = true,
|
||||
}, writer);
|
||||
}
|
||||
pub fn serializeRoutes(self: SerilizableAppMeta, writer: anytype) !void {
|
||||
try zx.prop.serialize([]const SerilizableAppMeta.Route, self.routes, writer);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@ -280,11 +280,13 @@ pub fn Handler(comptime AppCtxType: type) type {
|
||||
const Self = @This();
|
||||
|
||||
meta: *App.Meta,
|
||||
config: App.Config,
|
||||
page_cache: PageCache,
|
||||
allocator: std.mem.Allocator,
|
||||
app_ctx: *AppCtxType,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, meta: *App.Meta, cache_config: CacheConfig, app_ctx: *AppCtxType) !Self {
|
||||
pub fn init(allocator: std.mem.Allocator, meta: *App.Meta, config: App.Config, app_ctx: *AppCtxType) !Self {
|
||||
const cache_config = config.cache;
|
||||
// Initialize unified component cache
|
||||
try zx.cache.init(allocator, .{
|
||||
.max_size = cache_config.max_size,
|
||||
@ -292,6 +294,7 @@ pub fn Handler(comptime AppCtxType: type) type {
|
||||
|
||||
return Self{
|
||||
.meta = meta,
|
||||
.config = config,
|
||||
.allocator = allocator,
|
||||
.page_cache = try PageCache.init(allocator, cache_config),
|
||||
.app_ctx = app_ctx,
|
||||
@ -940,6 +943,8 @@ pub fn Handler(comptime AppCtxType: type) type {
|
||||
return self.uncaughtError(req, res, err);
|
||||
};
|
||||
|
||||
const is_devtool = is_dev_mode and std.mem.eql(u8, req.url.path, "/.well-known/_zx/devtool");
|
||||
|
||||
// Find and apply parent layouts based on path hierarchy
|
||||
// Collect all parent layouts from root to this route
|
||||
var layouts_to_apply: [10]App.Meta.LayoutHandler = undefined;
|
||||
@ -947,7 +952,8 @@ pub fn Handler(comptime AppCtxType: type) type {
|
||||
|
||||
// Build the path segments to traverse from root to current route
|
||||
var path_segments = std.array_list.Managed([]const u8).init(pagectx.arena);
|
||||
var path_iter = std.mem.splitScalar(u8, req.url.path, '/');
|
||||
const segments_path = if (is_devtool) (try req.query()).get("path") orelse "/" else req.url.path;
|
||||
var path_iter = std.mem.splitScalar(u8, segments_path, '/');
|
||||
while (path_iter.next()) |segment| {
|
||||
if (segment.len > 0) {
|
||||
path_segments.append(segment) catch break :blk;
|
||||
@ -1048,6 +1054,12 @@ pub fn Handler(comptime AppCtxType: type) type {
|
||||
}
|
||||
}
|
||||
|
||||
if (is_devtool) {
|
||||
res.content_type = .JSON;
|
||||
try page_component.format(res.writer());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if streaming is enabled
|
||||
const streaming_enabled = if (route.page_opts) |opts| opts.streaming else false;
|
||||
|
||||
@ -1073,6 +1085,34 @@ pub fn Handler(comptime AppCtxType: type) type {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn devtool(self: *Self, req: *httpz.Request, res: *httpz.Response) !void {
|
||||
// Add cors headers
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||
res.header("Access-Control-Allow-Headers", "Content-Type");
|
||||
if (req.method == .OPTIONS) {
|
||||
res.status = 200;
|
||||
return;
|
||||
}
|
||||
|
||||
const query = try req.query();
|
||||
const is_meta = query.get("meta") != null;
|
||||
if (is_meta) {
|
||||
const meta = try App.SerilizableAppMeta.init(req.arena, self.meta, self.config.server);
|
||||
res.content_type = .JSON;
|
||||
try meta.serializeRoutes(res.writer());
|
||||
return;
|
||||
}
|
||||
const target_path = query.get("path") orelse "/";
|
||||
|
||||
if (self.findRoute(target_path, .{ .match = .exact })) |route| {
|
||||
req.route_data = @constCast(route);
|
||||
return self.page(req, res);
|
||||
} else {
|
||||
return self.notFound(req, res);
|
||||
}
|
||||
}
|
||||
|
||||
fn resolveStaticParams(self: *Self, allocator: Allocator, static_opts: zx.PageOptions.Static) ![]const []const zx.PageOptions.StaticParam {
|
||||
_ = self; // currently unused but keeps signature flexible
|
||||
var params = std.ArrayList([]const zx.PageOptions.StaticParam).empty;
|
||||
|
||||
@ -8,12 +8,12 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
.children = &.{
|
||||
_zx.cmp(
|
||||
ArgToBuiltin,
|
||||
.{},
|
||||
.{ .name = "ArgToBuiltin" },
|
||||
.{},
|
||||
),
|
||||
_zx.cmp(
|
||||
StructToBuiltin,
|
||||
.{},
|
||||
.{ .name = "StructToBuiltin" },
|
||||
.{},
|
||||
),
|
||||
},
|
||||
|
||||
@ -19,12 +19,12 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
.children = &.{
|
||||
_zx.cmp(
|
||||
ChildComponent,
|
||||
.{},
|
||||
.{ .name = "ChildComponent" },
|
||||
.{ .children = hello_child },
|
||||
),
|
||||
_zx.cmp(
|
||||
ChildComponent,
|
||||
.{},
|
||||
.{ .name = "ChildComponent" },
|
||||
.{ .children = _zx.ele(
|
||||
.div,
|
||||
.{
|
||||
|
||||
@ -20,12 +20,12 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
.children = &.{
|
||||
_zx.cmp(
|
||||
Input,
|
||||
.{},
|
||||
.{ .name = "Input" },
|
||||
input_props,
|
||||
),
|
||||
_zx.cmp(
|
||||
Input,
|
||||
.{},
|
||||
.{ .name = "Input" },
|
||||
_zx.propsM(input_props, .{ .extra = "override" }),
|
||||
),
|
||||
},
|
||||
|
||||
@ -7,7 +7,7 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
.children = &.{
|
||||
_zx.cmp(
|
||||
Button,
|
||||
.{},
|
||||
.{ .name = "Button" },
|
||||
.{ .title = "Custom Button" },
|
||||
),
|
||||
},
|
||||
|
||||
@ -7,12 +7,12 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
.children = &.{
|
||||
_zx.cmp(
|
||||
Button,
|
||||
.{ .caching = comptime .tag("10s:button") },
|
||||
.{ .name = "Button", .caching = comptime .tag("10s:button") },
|
||||
.{ .title = "Custom Button" },
|
||||
),
|
||||
_zx.cmp(
|
||||
Button,
|
||||
.{ .caching = comptime .tag("10s") },
|
||||
.{ .name = "Button", .caching = comptime .tag("10s") },
|
||||
.{ .title = "Custom Button" },
|
||||
),
|
||||
},
|
||||
|
||||
@ -7,7 +7,7 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
.children = &.{
|
||||
_zx.cmp(
|
||||
Wrapper,
|
||||
.{},
|
||||
.{ .name = "Wrapper" },
|
||||
.{ .children = _zx.ele(.fragment, .{ .children = &.{
|
||||
_zx.ele(
|
||||
.p,
|
||||
@ -21,7 +21,7 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
),
|
||||
_zx.cmp(
|
||||
Container,
|
||||
.{},
|
||||
.{ .name = "Container" },
|
||||
.{ .children = _zx.ele(.fragment, .{ .children = &.{
|
||||
_zx.ele(
|
||||
.span,
|
||||
|
||||
@ -7,7 +7,7 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
.children = &.{
|
||||
_zx.cmp(
|
||||
Wrapper,
|
||||
.{},
|
||||
.{ .name = "Wrapper" },
|
||||
.{ .children = _zx.ele(.fragment, .{ .children = &.{
|
||||
_zx.ele(
|
||||
.p,
|
||||
@ -21,7 +21,7 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
),
|
||||
_zx.cmp(
|
||||
Card,
|
||||
.{},
|
||||
.{ .name = "Card" },
|
||||
.{ .children = _zx.ele(.fragment, .{ .children = &.{
|
||||
_zx.ele(
|
||||
.span,
|
||||
|
||||
@ -7,17 +7,17 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
.children = &.{
|
||||
_zx.cmp(
|
||||
Button,
|
||||
.{},
|
||||
.{ .name = "Button" },
|
||||
.{ .title = "Click me" },
|
||||
),
|
||||
_zx.cmp(
|
||||
Alert,
|
||||
.{},
|
||||
.{ .name = "Alert" },
|
||||
.{ .message = "This is an alert", .level = "warning" },
|
||||
),
|
||||
_zx.cmp(
|
||||
Panel,
|
||||
.{},
|
||||
.{ .name = "Panel" },
|
||||
.{ .title = "Panel Title", .children = _zx.ele(.fragment, .{ .children = &.{
|
||||
_zx.ele(
|
||||
.p,
|
||||
|
||||
@ -7,17 +7,17 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
.children = &.{
|
||||
_zx.cmp(
|
||||
CounterComponent,
|
||||
.{ .client = .{ .name = "CounterComponent", .id = "c8fee6a" } },
|
||||
.{ .name = "CounterComponent", .client = .{ .name = "CounterComponent", .id = "c8fee6a" } },
|
||||
.{},
|
||||
),
|
||||
_zx.cmp(
|
||||
CounterComponent,
|
||||
.{},
|
||||
.{ .name = "CounterComponent" },
|
||||
.{},
|
||||
),
|
||||
_zx.cmp(
|
||||
Button,
|
||||
.{ .client = .{ .name = "Button", .id = "cd02624" } },
|
||||
.{ .name = "Button", .client = .{ .name = "Button", .id = "cd02624" } },
|
||||
.{ .title = "Custom Button" },
|
||||
),
|
||||
},
|
||||
|
||||
@ -8,17 +8,17 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
.children = &.{
|
||||
_zx.cmp(
|
||||
Counter,
|
||||
.{ .client = .{ .name = "Counter", .id = "c24eadf" } },
|
||||
.{ .name = "Counter", .client = .{ .name = "Counter", .id = "c24eadf" } },
|
||||
.{ .initial = 5, .label = "Main Counter" },
|
||||
),
|
||||
_zx.cmp(
|
||||
Counter,
|
||||
.{ .client = .{ .name = "Counter", .id = "cd768fc" } },
|
||||
.{ .name = "Counter", .client = .{ .name = "Counter", .id = "cd768fc" } },
|
||||
.{ .initial = 10, .label = "Secondary" },
|
||||
),
|
||||
_zx.cmp(
|
||||
Counter,
|
||||
.{ .client = .{ .name = "Counter", .id = "c9e599a" } },
|
||||
.{ .name = "Counter", .client = .{ .name = "Counter", .id = "c9e599a" } },
|
||||
.{ .initial = 0 },
|
||||
),
|
||||
},
|
||||
|
||||
@ -8,12 +8,12 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
.children = &.{
|
||||
_zx.cmp(
|
||||
Fallible,
|
||||
.{},
|
||||
.{ .name = "Fallible" },
|
||||
.{ .success = true },
|
||||
),
|
||||
_zx.cmp(
|
||||
FallibleCtx,
|
||||
.{},
|
||||
.{ .name = "FallibleCtx" },
|
||||
.{ .success = true },
|
||||
),
|
||||
},
|
||||
|
||||
@ -7,7 +7,7 @@ pub fn Page(allocator: z.Allocator) z.Component {
|
||||
.children = &.{
|
||||
_zx.cmp(
|
||||
Button,
|
||||
.{},
|
||||
.{ .name = "Button" },
|
||||
.{ .title = "Custom Button" },
|
||||
),
|
||||
},
|
||||
|
||||
@ -7,27 +7,27 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
.children = &.{
|
||||
_zx.cmp(
|
||||
Button,
|
||||
.{},
|
||||
.{ .name = "Button" },
|
||||
.{ .title = "Submit" },
|
||||
),
|
||||
_zx.cmp(
|
||||
Button,
|
||||
.{},
|
||||
.{ .name = "Button" },
|
||||
.{ .title = "Cancel" },
|
||||
),
|
||||
_zx.cmp(
|
||||
AsyncScore,
|
||||
.{},
|
||||
.{ .name = "AsyncScore" },
|
||||
.{ .index = 1, .label = "Score" },
|
||||
),
|
||||
_zx.cmp(
|
||||
AsyncScore,
|
||||
.{},
|
||||
.{ .name = "AsyncScore" },
|
||||
.{ .index = 2, .label = "Points" },
|
||||
),
|
||||
_zx.cmp(
|
||||
AsyncScore,
|
||||
.{},
|
||||
.{ .name = "AsyncScore" },
|
||||
.{ .index = 3, .label = "Rating" },
|
||||
),
|
||||
},
|
||||
|
||||
@ -7,11 +7,11 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
.children = &.{
|
||||
_zx.cmp(
|
||||
Card,
|
||||
.{},
|
||||
.{ .name = "Card" },
|
||||
.{ .title = "Welcome", .children = _zx.ele(.fragment, .{ .children = &.{
|
||||
_zx.cmp(
|
||||
Button,
|
||||
.{},
|
||||
.{ .name = "Button" },
|
||||
.{ .label = "Click me" },
|
||||
),
|
||||
} }) },
|
||||
|
||||
@ -7,12 +7,12 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
.children = &.{
|
||||
_zx.cmp(
|
||||
None,
|
||||
.{},
|
||||
.{ .name = "None" },
|
||||
.{},
|
||||
),
|
||||
_zx.cmp(
|
||||
Null,
|
||||
.{},
|
||||
.{ .name = "Null" },
|
||||
.{},
|
||||
),
|
||||
},
|
||||
|
||||
@ -8,12 +8,12 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
.children = &.{
|
||||
_zx.cmp(
|
||||
MaybeContent,
|
||||
.{},
|
||||
.{ .name = "MaybeContent" },
|
||||
.{ .show = true },
|
||||
),
|
||||
_zx.cmp(
|
||||
MaybeContent,
|
||||
.{},
|
||||
.{ .name = "MaybeContent" },
|
||||
.{ .show = false },
|
||||
),
|
||||
},
|
||||
|
||||
@ -2,7 +2,7 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
var _zx = @import("zx").allocInit(allocator);
|
||||
return _zx.cmp(
|
||||
Button,
|
||||
.{},
|
||||
.{ .name = "Button" },
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
@ -150,7 +150,7 @@ pub fn StructCaptureToComponent(allocator: zx.Allocator) zx.Component {
|
||||
for (users, 0..) |user, _zx_i_5| {
|
||||
__zx_children_5[_zx_i_5] = _zx.cmp(
|
||||
UserComponent,
|
||||
.{},
|
||||
.{ .name = "UserComponent" },
|
||||
.{ .name = user.name, .age = user.age },
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
.children = &.{
|
||||
_zx.cmp(
|
||||
FragmentComponent,
|
||||
.{},
|
||||
.{ .name = "FragmentComponent" },
|
||||
.{},
|
||||
),
|
||||
},
|
||||
|
||||
@ -32,7 +32,7 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
),
|
||||
_zx.cmp(
|
||||
Component,
|
||||
.{},
|
||||
.{ .name = "Component" },
|
||||
.{ .text = _zx.propf("hello {s}", .{_zx.propv(count)}), .name = _zx.propf("test {s} {s} more-text", .{ _zx.propv(name), _zx.propv(getThemeClass(.dark)) }) },
|
||||
),
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user