sygnal 7.0.0 → 7.2.0-preview.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/CHANGELOG.md +632 -0
- package/dist/agent-ui.cjs.js +592 -0
- package/dist/agent-ui.cjs.js.map +1 -0
- package/dist/agent-ui.esm.js +582 -0
- package/dist/agent-ui.esm.js.map +1 -0
- package/dist/api.cjs.js +63 -0
- package/dist/api.cjs.js.map +1 -0
- package/dist/api.esm.js +2 -0
- package/dist/api.esm.js.map +1 -0
- package/dist/astro/index.cjs.js +968 -4
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +968 -4
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js +73 -45
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +73 -45
- package/dist/astro/server.mjs.map +1 -1
- package/dist/canvas.cjs.js +2919 -0
- package/dist/canvas.cjs.js.map +1 -0
- package/dist/canvas.esm.js +2866 -0
- package/dist/canvas.esm.js.map +1 -0
- package/dist/devtools.cjs.js +38 -6
- package/dist/devtools.cjs.js.map +1 -1
- package/dist/devtools.esm.js +38 -6
- package/dist/devtools.esm.js.map +1 -1
- package/dist/diagnostics.cjs.js +224 -20
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +224 -20
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/guide/accessibility.md +1 -1
- package/dist/guide/ai-compose.md +150 -0
- package/dist/guide/behaviors.md +2 -1
- package/dist/guide/collaborative-state.md +265 -0
- package/dist/guide/element-commands.md +36 -2
- package/dist/guide/forms-reference.md +5 -5
- package/dist/guide/forms.md +3 -2
- package/dist/guide/http.md +28 -1
- package/dist/guide/islands.md +240 -0
- package/dist/guide/recipes/code-editor.md +1 -1
- package/dist/guide/renderers/agent-ui.md +138 -0
- package/dist/guide/renderers/custom-targets.md +126 -0
- package/dist/guide/renderers/overview.md +120 -0
- package/dist/guide/renderers/react-native.md +128 -0
- package/dist/guide/renderers/terminal.md +306 -0
- package/dist/guide/renderers/three.md +332 -0
- package/dist/guide/renderers/write-once.md +137 -0
- package/dist/guide/resources.md +2 -0
- package/dist/guide/sandboxed-components.md +223 -0
- package/dist/guide/timers.md +32 -5
- package/dist/guide/typed-apis.md +442 -0
- package/dist/guide/ui/accordion.md +11 -0
- package/dist/guide/ui/combobox.md +112 -8
- package/dist/guide/ui/dialog.md +96 -20
- package/dist/guide/ui/menu.md +57 -5
- package/dist/guide/ui/overview.md +8 -4
- package/dist/guide/ui/popover.md +69 -9
- package/dist/guide/ui/select.md +54 -4
- package/dist/guide/ui/tabs.md +1 -1
- package/dist/guide/ui/tooltip.md +7 -5
- package/dist/guide/virtual-collections.md +3 -0
- package/dist/guide/widgets.md +90 -1
- package/dist/host.cjs.js +2181 -0
- package/dist/host.cjs.js.map +1 -0
- package/dist/host.esm.js +2146 -0
- package/dist/host.esm.js.map +1 -0
- package/dist/index.cjs.js +3948 -586
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +584 -44
- package/dist/index.esm.js +3918 -586
- package/dist/index.esm.js.map +1 -1
- package/dist/primitives.cjs.js +143 -0
- package/dist/primitives.cjs.js.map +1 -0
- package/dist/primitives.esm.js +133 -0
- package/dist/primitives.esm.js.map +1 -0
- package/dist/react-native.cjs.js +248 -0
- package/dist/react-native.cjs.js.map +1 -0
- package/dist/react-native.esm.js +244 -0
- package/dist/react-native.esm.js.map +1 -0
- package/dist/replay.cjs.js +39 -0
- package/dist/replay.cjs.js.map +1 -0
- package/dist/replay.esm.js +2 -0
- package/dist/replay.esm.js.map +1 -0
- package/dist/sandbox-build.cjs.js +140 -0
- package/dist/sandbox-build.cjs.js.map +1 -0
- package/dist/sandbox-build.esm.js +137 -0
- package/dist/sandbox-build.esm.js.map +1 -0
- package/dist/sandbox-kernel.esm.js +212 -0
- package/dist/sandbox-kernel.esm.js.map +1 -0
- package/dist/sandbox-worker.cjs.js +857 -0
- package/dist/sandbox-worker.cjs.js.map +1 -0
- package/dist/sandbox-worker.esm.js +854 -0
- package/dist/sandbox-worker.esm.js.map +1 -0
- package/dist/sandbox.cjs.js +1425 -0
- package/dist/sandbox.cjs.js.map +1 -0
- package/dist/sandbox.esm.js +1406 -0
- package/dist/sandbox.esm.js.map +1 -0
- package/dist/sygnal.min.js +1 -1
- package/dist/sygnal.min.js.map +1 -1
- package/dist/terminal.cjs.js +1314 -0
- package/dist/terminal.cjs.js.map +1 -0
- package/dist/terminal.esm.js +1304 -0
- package/dist/terminal.esm.js.map +1 -0
- package/dist/three.cjs.js +1111 -0
- package/dist/three.cjs.js.map +1 -0
- package/dist/three.esm.js +1079 -0
- package/dist/three.esm.js.map +1 -0
- package/dist/ui-combobox.cjs.js +256 -18
- package/dist/ui-combobox.cjs.js.map +1 -1
- package/dist/ui-combobox.esm.js +256 -18
- package/dist/ui-combobox.esm.js.map +1 -1
- package/dist/ui-menu.cjs.js +150 -16
- package/dist/ui-menu.cjs.js.map +1 -1
- package/dist/ui-menu.esm.js +151 -17
- package/dist/ui-menu.esm.js.map +1 -1
- package/dist/ui-select.cjs.js +95 -9
- package/dist/ui-select.cjs.js.map +1 -1
- package/dist/ui-select.esm.js +95 -9
- package/dist/ui-select.esm.js.map +1 -1
- package/dist/ui.cjs.js +679 -173
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.esm.js +679 -175
- package/dist/ui.esm.js.map +1 -1
- package/dist/vike/config/package.json +1 -1
- package/dist/vite/plugin.cjs.js +968 -4
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +968 -4
- package/dist/vite/plugin.mjs.map +1 -1
- package/dist/worker-page.cjs.js +669 -0
- package/dist/worker-page.cjs.js.map +1 -0
- package/dist/worker-page.esm.js +665 -0
- package/dist/worker-page.esm.js.map +1 -0
- package/dist/worker.cjs.js +377 -0
- package/dist/worker.cjs.js.map +1 -0
- package/dist/worker.esm.js +374 -0
- package/dist/worker.esm.js.map +1 -0
- package/dist/yjs-testing.cjs.js +98 -0
- package/dist/yjs-testing.cjs.js.map +1 -0
- package/dist/yjs-testing.esm.js +77 -0
- package/dist/yjs-testing.esm.js.map +1 -0
- package/dist/yjs.cjs.js +923 -0
- package/dist/yjs.cjs.js.map +1 -0
- package/dist/yjs.esm.js +900 -0
- package/dist/yjs.esm.js.map +1 -0
- package/dist/zag.cjs.js +11 -1
- package/dist/zag.cjs.js.map +1 -1
- package/dist/zag.esm.js +11 -1
- package/dist/zag.esm.js.map +1 -1
- package/llms/ai.md +65 -0
- package/llms/collaboration.md +5 -0
- package/llms/graphics.md +10 -0
- package/llms/renderers.md +7 -0
- package/llms/replay.md +5 -0
- package/llms/sandbox.md +5 -0
- package/llms/typed-apis.md +5 -0
- package/llms/ui.md +10 -0
- package/llms-full.txt +444 -0
- package/llms.txt +37 -35
- package/package.json +139 -3
- package/src/agent-ui/mcp.ts +152 -0
- package/src/agent-ui/snapshot.ts +212 -0
- package/src/agent-ui.d.ts +106 -0
- package/src/agent-ui.ts +249 -0
- package/src/ai.d.ts +95 -2
- package/src/api.d.ts +303 -0
- package/src/api.ts +9 -0
- package/src/canvas/Canvas.ts +585 -0
- package/src/canvas/backend.ts +627 -0
- package/src/canvas/geometry.ts +187 -0
- package/src/canvas/gesture.ts +23 -0
- package/src/canvas/image.ts +197 -0
- package/src/canvas/kit.ts +130 -0
- package/src/canvas/paint.ts +260 -0
- package/src/canvas/pointer.ts +25 -0
- package/src/canvas/snap.ts +68 -0
- package/src/canvas/svg.ts +99 -0
- package/src/canvas.d.ts +373 -0
- package/src/canvas.ts +46 -0
- package/src/core/actions.ts +14 -3
- package/src/core/cell.ts +12 -4
- package/src/core/debug.ts +8 -3
- package/src/core/hooks.ts +16 -2
- package/src/core/hosts/collection.ts +1 -1
- package/src/core/hosts/switchable.ts +26 -8
- package/src/core/instance.ts +98 -45
- package/src/core/markers/portal.ts +4 -0
- package/src/core/registry.ts +4 -0
- package/src/core/runtime.ts +34 -25
- package/src/core/statics.ts +5 -5
- package/src/cycle/dom/EventDelegator.ts +37 -33
- package/src/cycle/dom/MainDOMSource.ts +16 -14
- package/src/cycle/dom/enrichEventStream.ts +13 -9
- package/src/cycle/dom/fromEvent.ts +6 -10
- package/src/cycle/dom/index.ts +11 -7
- package/src/cycle/dom/isolate.ts +13 -6
- package/src/cycle/dom/makeDOMDriver.ts +6 -2
- package/src/cycle/dom/utils.ts +9 -2
- package/src/cycle/run/adapt.ts +4 -12
- package/src/devtools.d.ts +9 -0
- package/src/devtools.ts +2 -1
- package/src/extra/ai/compose/catalog.ts +2 -1
- package/src/extra/ai/compose/decisions.ts +1 -2
- package/src/extra/ai/compose/generated.ts +234 -0
- package/src/extra/ai/compose/index.ts +6 -2
- package/src/extra/ai/compose/layout.ts +3 -1
- package/src/extra/ai/compose/llm.ts +7 -3
- package/src/extra/ai/compose/types.ts +2 -2
- package/src/extra/ai/transports/shared.ts +3 -1
- package/src/extra/api/contract.ts +485 -0
- package/src/extra/api/index.ts +26 -0
- package/src/extra/behaviors.ts +3 -3
- package/src/extra/controlled.ts +44 -0
- package/src/extra/copyAsTest.ts +22 -0
- package/src/extra/devtools.ts +5 -3
- package/src/extra/devtoolsActions.ts +8 -0
- package/src/extra/devtoolsNext.ts +1 -1
- package/src/extra/diagnostics/checks/actionLog.ts +8 -3
- package/src/extra/diagnostics/checks/behaviors.ts +25 -0
- package/src/extra/diagnostics/checks/elementCommands.ts +17 -5
- package/src/extra/diagnostics/checks/index.ts +1 -1
- package/src/extra/diagnostics/checks/next.ts +78 -4
- package/src/extra/diagnostics/checks/props.ts +6 -1
- package/src/extra/diagnostics/checks/replies.ts +11 -7
- package/src/extra/diagnostics/checks/timers.ts +8 -2
- package/src/extra/diagnostics/checks/virtual.ts +11 -0
- package/src/extra/diagnostics/codes.ts +51 -1
- package/src/extra/elementCommands.ts +20 -4
- package/src/extra/fetchDriver.ts +4 -1
- package/src/extra/fetchEventSource.ts +6 -2
- package/src/extra/field.ts +29 -0
- package/src/extra/form.ts +42 -16
- package/src/extra/frame.ts +54 -0
- package/src/extra/keymap.ts +41 -0
- package/src/extra/pager.ts +2 -0
- package/src/extra/persist.ts +2 -1
- package/src/extra/pointer.ts +262 -0
- package/src/extra/processForm.ts +34 -1
- package/src/extra/replay/address.ts +49 -0
- package/src/extra/replay/entropy.ts +99 -0
- package/src/extra/replay/format.ts +263 -0
- package/src/extra/replay/index.ts +8 -0
- package/src/extra/replay/inert.ts +22 -0
- package/src/extra/replay/recorder.ts +247 -0
- package/src/extra/replay/replayer.ts +234 -0
- package/src/extra/replay/report.ts +126 -0
- package/src/extra/replay/schema.ts +52 -0
- package/src/extra/replay/version.ts +3 -0
- package/src/extra/router.ts +7 -3
- package/src/extra/selection.ts +2 -0
- package/src/extra/socketDriver.ts +51 -6
- package/src/extra/ssr.ts +128 -42
- package/src/extra/testing.ts +295 -39
- package/src/extra/timers.ts +96 -7
- package/src/extra/undo.ts +2 -0
- package/src/extra/virtual.ts +25 -2
- package/src/extra/widget.ts +106 -17
- package/src/host/actions.ts +383 -0
- package/src/host/delegation.ts +101 -0
- package/src/host/dom.ts +553 -0
- package/src/host/driver.ts +185 -0
- package/src/host/selector.ts +119 -0
- package/src/host/style.ts +51 -0
- package/src/host.d.ts +298 -0
- package/src/host.ts +46 -0
- package/src/index.d.ts +302 -38
- package/src/index.ts +15 -3
- package/src/pragma/blocks.ts +211 -0
- package/src/pragma/index.ts +44 -10
- package/src/primitives.d.ts +54 -0
- package/src/primitives.ts +136 -0
- package/src/react-native.d.ts +57 -0
- package/src/react-native.ts +285 -0
- package/src/react-peers.d.ts +21 -3
- package/src/replay.d.ts +185 -0
- package/src/replay.ts +6 -0
- package/src/sandbox/build.ts +119 -0
- package/src/sandbox/create.ts +187 -0
- package/src/sandbox/engine.ts +267 -0
- package/src/sandbox/generated.ts +195 -0
- package/src/sandbox/kernel.ts +180 -0
- package/src/sandbox/mirror.ts +269 -0
- package/src/sandbox/protocol.ts +128 -0
- package/src/sandbox/quickjs.ts +74 -0
- package/src/sandbox/ssr.ts +41 -0
- package/src/sandbox/widget.ts +60 -0
- package/src/sandbox/worker-entry.ts +59 -0
- package/src/sandbox/worker.ts +137 -0
- package/src/sandbox-build.d.ts +12 -0
- package/src/sandbox-build.ts +6 -0
- package/src/sandbox.d.ts +299 -0
- package/src/sandbox.ts +25 -0
- package/src/shared.ts +12 -0
- package/src/terminal/paint.ts +712 -0
- package/src/terminal.d.ts +167 -0
- package/src/terminal.ts +530 -0
- package/src/three/assets.ts +170 -0
- package/src/three/host.ts +36 -0
- package/src/three/scene.ts +217 -0
- package/src/three/target.ts +252 -0
- package/src/three.d.ts +221 -0
- package/src/three.ts +30 -0
- package/src/ui/accordion.ts +21 -7
- package/src/ui/dialog.ts +192 -90
- package/src/ui/disclosure.ts +2 -0
- package/src/ui/dom.ts +30 -0
- package/src/ui/layers.ts +132 -0
- package/src/ui/popover.ts +120 -21
- package/src/ui/shared.ts +98 -12
- package/src/ui/stack.ts +18 -0
- package/src/ui/tabs.ts +2 -0
- package/src/ui/toaster.ts +16 -15
- package/src/ui/tooltip.ts +24 -8
- package/src/ui/tooltips.ts +26 -14
- package/src/ui/zag/combobox.ts +180 -13
- package/src/ui/zag/menu.ts +97 -17
- package/src/ui/zag/select.ts +15 -4
- package/src/ui/zag/shared.ts +60 -8
- package/src/ui-combobox.d.ts +16 -1
- package/src/ui-menu.d.ts +22 -7
- package/src/ui-zag-types.d.ts +13 -5
- package/src/ui.d.ts +62 -7
- package/src/ui.ts +2 -2
- package/src/vite/api.ts +186 -0
- package/src/vite/compile.ts +280 -0
- package/src/vite/plugin.d.ts +34 -0
- package/src/vite/plugin.ts +78 -4
- package/src/vite/sandbox.ts +16 -0
- package/src/worker/page.ts +396 -0
- package/src/worker/protocol.ts +48 -0
- package/src/worker/target.ts +282 -0
- package/src/worker-page.d.ts +42 -0
- package/src/worker-page.ts +12 -0
- package/src/worker.d.ts +65 -0
- package/src/worker.ts +16 -0
- package/src/yjs/driver.ts +253 -0
- package/src/yjs/mapping.ts +389 -0
- package/src/yjs/shared.ts +138 -0
- package/src/yjs/testing.ts +91 -0
- package/src/yjs-testing.d.ts +34 -0
- package/src/yjs-testing.ts +7 -0
- package/src/yjs.d.ts +109 -0
- package/src/yjs.ts +18 -0
- package/src/zag.d.ts +2 -0
- package/src/zag.ts +8 -1
package/dist/astro/index.mjs
CHANGED
|
@@ -2,6 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import { MagicString } from 'magic-string';
|
|
6
|
+
import { Readable } from 'node:stream';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* G-099: xstream does `require('globalthis').getPolyfill()`, which pulls the
|
|
@@ -889,6 +891,922 @@ function printGraphUrl(server, available = true) {
|
|
|
889
891
|
};
|
|
890
892
|
}
|
|
891
893
|
|
|
894
|
+
/**
|
|
895
|
+
* CV-2: compiled views, tier A (dev-plans/research/compile-wasm/compiled-views-plan.md). Rewrites a
|
|
896
|
+
* module's JSX before the JSX transform: an HTML element (not a form field, custom element or SVG
|
|
897
|
+
* tag) with plain attributes becomes a call to the pragma's `__cv.e(sel, data, children, key)`,
|
|
898
|
+
* its data made by the pragma's own sanitizeData (`__cv.sd`): once at module load (hoisted) when
|
|
899
|
+
* every attribute is static, else per render. Everything else (components, markers, fragments,
|
|
900
|
+
* fields, spreads, refs, ...) is left as JSX for the JSX transform; their children may still
|
|
901
|
+
* compile. The output vnodes are the ones the pragma would make (test/cv-compile.test.js).
|
|
902
|
+
*
|
|
903
|
+
* CV-1 (`ssr`, server builds): the outermost compiled element stays a vnode (`__cvs.r`: the
|
|
904
|
+
* root's `data-sygnal-ssr` stamp and the SSR walk need one) whose inside is HTML parts made by
|
|
905
|
+
* ssr.ts (`__cvs.f`, `__cvs.s`): open tags and static subtrees serialized by its own vnodeToHtml
|
|
906
|
+
* (cached per escaping mode), text escaped by its escapeHtml, anything not compiled (components,
|
|
907
|
+
* markers, fields, ...) kept as a vnode at the child path the JSX runtime's tree would give it.
|
|
908
|
+
* The HTML is renderToString's (test/cv-ssr.test.js).
|
|
909
|
+
*
|
|
910
|
+
* `parse` is Vite 8's parseAst (ESTree + JSX, UTF-16 offsets). The source map maps each rewritten
|
|
911
|
+
* JSX expression to its start (magic-string).
|
|
912
|
+
*/
|
|
913
|
+
// the named export: magic-string's CJS build has no plain default for the plugin's CJS output
|
|
914
|
+
// HTML elements the compiler takes. Not here: the form fields (input, textarea, select, option:
|
|
915
|
+
// the controlled-input module re-syncs them; the pragma never makes them plain), SVG and MathML
|
|
916
|
+
// (the pragma sets the namespace), hyphenated custom elements, and any other lowercase tag (a host
|
|
917
|
+
// renderer's own tags go through the pragma, which knows its run-time tag lists)
|
|
918
|
+
const HTML = new Set(('a abbr address article aside audio b bdi bdo blockquote br button canvas caption cite code col colgroup ' +
|
|
919
|
+
'data datalist dd del details dfn dialog div dl dt em fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hgroup hr i ' +
|
|
920
|
+
'iframe img ins kbd label legend li main map mark menu meter nav noscript object ol optgroup output p picture pre progress q rp rt ' +
|
|
921
|
+
'ruby s samp search section small source span strong sub summary sup table tbody td template tfoot th thead time tr track u ul var ' +
|
|
922
|
+
'video wbr').split(' '));
|
|
923
|
+
// attributes the pragma gives run-time behaviour (hooks) or that change what the element is
|
|
924
|
+
const BAIL_ATTR = /^(ref|autoFocus|autoSelect|hook|children|key)$/;
|
|
925
|
+
// in server builds also: the SSR serializer gives these their own content
|
|
926
|
+
const SSR_BAIL_ATTR = /^(innerHTML|textContent)$/;
|
|
927
|
+
// an HTML entity: JSX decodes them, the parser keeps them raw (the pragma path decodes through the JSX transform)
|
|
928
|
+
const ENTITY = /&(#\d+|#x[\da-f]+|[a-z][a-z\d]*);/i;
|
|
929
|
+
function compileViews(code, id, parse, opts = {}) {
|
|
930
|
+
const ssr = !!opts.ssr, blocks = !ssr && !!opts.blocks;
|
|
931
|
+
let nblocks = 0;
|
|
932
|
+
if (!/<[a-z]/.test(code))
|
|
933
|
+
return null;
|
|
934
|
+
let ast;
|
|
935
|
+
try {
|
|
936
|
+
ast = parse(code, { lang: /\.tsx$/.test(id) ? 'tsx' : /\.ts$/.test(id) ? 'ts' : 'jsx' });
|
|
937
|
+
}
|
|
938
|
+
catch {
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
const hoisted = [];
|
|
942
|
+
let compiled = 0;
|
|
943
|
+
// CV-3: the JSX a component (a capitalized function) returns directly: its view's root
|
|
944
|
+
const roots = new Set();
|
|
945
|
+
if (blocks) {
|
|
946
|
+
const un = (n) => n?.type == 'ParenthesizedExpression' ? un(n.expression) : n;
|
|
947
|
+
const fnRoots = (f) => {
|
|
948
|
+
if (!f)
|
|
949
|
+
return;
|
|
950
|
+
if (f.expression) {
|
|
951
|
+
roots.add(un(f.body));
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
for (const st of f.body?.body || [])
|
|
955
|
+
if (st.type == 'ReturnStatement')
|
|
956
|
+
roots.add(un(st.argument));
|
|
957
|
+
};
|
|
958
|
+
const scan = (n) => {
|
|
959
|
+
if (!n || typeof n != 'object')
|
|
960
|
+
return;
|
|
961
|
+
if (n.type == 'FunctionDeclaration' && /^[A-Z]/.test(n.id?.name || ''))
|
|
962
|
+
fnRoots(n);
|
|
963
|
+
if (n.type == 'VariableDeclarator' && /^[A-Z]/.test(n.id?.name || '') && /Function/.test(n.init?.type || ''))
|
|
964
|
+
fnRoots(n.init);
|
|
965
|
+
for (const k in n) {
|
|
966
|
+
if (k == 'start' || k == 'end' || k == 'loc')
|
|
967
|
+
continue;
|
|
968
|
+
const v = n[k];
|
|
969
|
+
if (Array.isArray(v))
|
|
970
|
+
v.forEach(scan);
|
|
971
|
+
else if (v && typeof v == 'object' && typeof v.type == 'string')
|
|
972
|
+
scan(v);
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
scan(ast);
|
|
976
|
+
}
|
|
977
|
+
const src = (s, e, subs) => {
|
|
978
|
+
let out = '', at = s;
|
|
979
|
+
for (const [a, b, t] of subs.sort((x, y) => x[0] - y[0])) {
|
|
980
|
+
out += code.slice(at, a) + t;
|
|
981
|
+
at = b;
|
|
982
|
+
}
|
|
983
|
+
return out + code.slice(at, e);
|
|
984
|
+
};
|
|
985
|
+
// every maximal JSX node under `node`, rewritten (a JSX node in expression position)
|
|
986
|
+
const subsIn = (node, subs = []) => {
|
|
987
|
+
if (!node || typeof node != 'object')
|
|
988
|
+
return subs;
|
|
989
|
+
if (node.type == 'JSXElement' || node.type == 'JSXFragment') {
|
|
990
|
+
subs.push([node.start, node.end, jsx(node)]);
|
|
991
|
+
return subs;
|
|
992
|
+
}
|
|
993
|
+
for (const k in node) {
|
|
994
|
+
if (k == 'start' || k == 'end' || k == 'loc' || k == 'range')
|
|
995
|
+
continue;
|
|
996
|
+
const v = node[k];
|
|
997
|
+
if (Array.isArray(v))
|
|
998
|
+
for (const c of v)
|
|
999
|
+
subsIn(c, subs);
|
|
1000
|
+
else if (v && typeof v == 'object' && typeof v.type == 'string')
|
|
1001
|
+
subsIn(v, subs);
|
|
1002
|
+
}
|
|
1003
|
+
return subs;
|
|
1004
|
+
};
|
|
1005
|
+
const expr = (e) => src(e.start, e.end, subsIn(e));
|
|
1006
|
+
const tag = (el) => el.openingElement.name.type == 'JSXIdentifier' ? el.openingElement.name.name : null;
|
|
1007
|
+
const compilable = (el) => {
|
|
1008
|
+
const t = tag(el);
|
|
1009
|
+
if (!t || !HTML.has(t))
|
|
1010
|
+
return false;
|
|
1011
|
+
for (const a of el.openingElement.attributes) {
|
|
1012
|
+
if (a.type != 'JSXAttribute' || a.name.type != 'JSXIdentifier')
|
|
1013
|
+
return false;
|
|
1014
|
+
if (BAIL_ATTR.test(a.name.name) && a.name.name != 'key' || ssr && SSR_BAIL_ATTR.test(a.name.name))
|
|
1015
|
+
return false;
|
|
1016
|
+
const v = a.value;
|
|
1017
|
+
if (v && v.type == 'Literal' && ENTITY.test(String(v.value)))
|
|
1018
|
+
return false;
|
|
1019
|
+
if (v && v.type != 'Literal' && !(v.type == 'JSXExpressionContainer' && v.expression.type != 'JSXEmptyExpression'))
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
for (const c of el.children)
|
|
1023
|
+
if (c.type == 'JSXText' && ENTITY.test(c.value) || c.type == 'JSXSpreadChild')
|
|
1024
|
+
return false;
|
|
1025
|
+
return true;
|
|
1026
|
+
};
|
|
1027
|
+
// no expression anywhere below (attributes, children): its HTML never changes (CV-1)
|
|
1028
|
+
const isStatic = (el) => compilable(el)
|
|
1029
|
+
&& el.openingElement.attributes.every((a) => !a.value || a.value.type == 'Literal')
|
|
1030
|
+
&& el.children.every((c) => c.type == 'JSXText' || c.type == 'JSXElement' && isStatic(c));
|
|
1031
|
+
// a JSX node in expression position: compiled, or JSX whose children may compile
|
|
1032
|
+
// (`vnodes`: inside a static SSR subtree's vnode factory, every element is a client one)
|
|
1033
|
+
let vnodes = 0;
|
|
1034
|
+
const jsx = (node) => node.type == 'JSXElement' && compilable(node)
|
|
1035
|
+
? (ssr && !vnodes ? ssrRoot(node) : blocks && !vnodes && roots.has(node) && blockable(node) ? block(node) : element(node)) : jsxSource(node);
|
|
1036
|
+
const jsxSource = (node) => {
|
|
1037
|
+
const subs = [];
|
|
1038
|
+
const open = node.openingElement;
|
|
1039
|
+
if (open)
|
|
1040
|
+
for (const a of open.attributes) {
|
|
1041
|
+
const v = a.type == 'JSXSpreadAttribute' ? a.argument : a.value?.type == 'JSXExpressionContainer' ? a.value.expression : null;
|
|
1042
|
+
if (v)
|
|
1043
|
+
subsIn(v, subs);
|
|
1044
|
+
}
|
|
1045
|
+
for (const c of node.children) {
|
|
1046
|
+
if (c.type == 'JSXElement')
|
|
1047
|
+
subs.push([c.start, c.end, compilable(c) ? '{' + jsx(c) + '}' : jsxSource(c)]);
|
|
1048
|
+
else if (c.type == 'JSXFragment')
|
|
1049
|
+
subs.push([c.start, c.end, jsxSource(c)]);
|
|
1050
|
+
else if (c.type == 'JSXExpressionContainer')
|
|
1051
|
+
subsIn(c.expression, subs);
|
|
1052
|
+
}
|
|
1053
|
+
return src(node.start, node.end, subs);
|
|
1054
|
+
};
|
|
1055
|
+
// the element's data expression and key (a static one hoisted; see __cv in src/pragma/index.ts)
|
|
1056
|
+
const dataOf = (el) => {
|
|
1057
|
+
let key = 'undefined', dynamic = false;
|
|
1058
|
+
const names = [], vals = [];
|
|
1059
|
+
for (const a of el.openingElement.attributes) {
|
|
1060
|
+
const name = a.name.name, v = a.value;
|
|
1061
|
+
const val = !v ? 'true' : v.type == 'Literal' ? JSON.stringify(v.value) : (dynamic = name != 'key' || dynamic, '(' + expr(v.expression) + ')');
|
|
1062
|
+
if (name == 'key')
|
|
1063
|
+
key = val;
|
|
1064
|
+
// a duplicate keeps its first place and takes the last value, as in an object literal
|
|
1065
|
+
else {
|
|
1066
|
+
const i = names.indexOf(name);
|
|
1067
|
+
if (i >= 0)
|
|
1068
|
+
vals[i] = val;
|
|
1069
|
+
else
|
|
1070
|
+
names.push(name), vals.push(val);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
// static: made once at load by the pragma's sanitizeData; dynamic: the keys routed once at
|
|
1074
|
+
// load by the pragma's route(), the data made per render from the values (__cv.d)
|
|
1075
|
+
const data = !dynamic ? hoist('__cv.sd({' + names.map((n, i) => JSON.stringify(n) + ': ' + vals[i]).join(', ') + '})')
|
|
1076
|
+
: `__cv.d(${hoist('__cv.r(' + JSON.stringify(names) + ')')}, [${vals.join(', ')}])`;
|
|
1077
|
+
return [data, key, dynamic];
|
|
1078
|
+
};
|
|
1079
|
+
const childArg = (kids) => kids.length == 0 ? 'undefined' : kids.length == 1 ? kids[0] : '[' + kids.join(', ') + ']';
|
|
1080
|
+
const element = (el) => {
|
|
1081
|
+
compiled++;
|
|
1082
|
+
const [data, key] = dataOf(el);
|
|
1083
|
+
const c = childArg(children(el.children, false));
|
|
1084
|
+
return `__cv.e(${JSON.stringify(tag(el))}, ${data}, ${c}${key == 'undefined' ? '' : ', ' + key})`;
|
|
1085
|
+
};
|
|
1086
|
+
// CV-1: the outermost compiled element in server code: a vnode with HTML parts inside
|
|
1087
|
+
const ssrRoot = (el) => {
|
|
1088
|
+
compiled++;
|
|
1089
|
+
const [data, key] = dataOf(el);
|
|
1090
|
+
return `__cvs.r(${JSON.stringify(tag(el))}, ${data}, ${childArg(children(el.children, true))}${key == 'undefined' ? '' : ', ' + key})`;
|
|
1091
|
+
};
|
|
1092
|
+
// CV-1: a compiled element inside one: a static one's HTML made once (per escaping mode), else
|
|
1093
|
+
// its open tag (static: once) and its children's parts
|
|
1094
|
+
const ssrInner = (el) => {
|
|
1095
|
+
compiled++;
|
|
1096
|
+
if (isStatic(el)) {
|
|
1097
|
+
vnodes++;
|
|
1098
|
+
const v = element(el);
|
|
1099
|
+
vnodes--;
|
|
1100
|
+
return `__cvs.s(${hoist('__cvs.S(() => ' + v + ')')})`;
|
|
1101
|
+
}
|
|
1102
|
+
const [data, , dynamic] = dataOf(el), t = JSON.stringify(tag(el));
|
|
1103
|
+
const open = dynamic ? `__cvs.od(${t}, ${data})` : hoist(`__cvs.O(${t}, ${data})`);
|
|
1104
|
+
return `__cvs.f(${open}, ${childArg(children(el.children, true))})`;
|
|
1105
|
+
};
|
|
1106
|
+
// CV-1: JSX in a direct value position of a compiled element's child expression (the expression,
|
|
1107
|
+
// a ?: branch, either side of && || ??, an array element, a .map() arrow's expression body) can
|
|
1108
|
+
// only become that element's child, so a compilable one is an HTML chunk; anything else (JSX in a
|
|
1109
|
+
// variable, a call's argument, a component prop) stays a vnode
|
|
1110
|
+
// (client builds with blocks: a .map() row there is a block, `row`: inside a .map() body)
|
|
1111
|
+
const direct = (n, subs, row = false) => {
|
|
1112
|
+
if (!n)
|
|
1113
|
+
return;
|
|
1114
|
+
const t = n.type;
|
|
1115
|
+
if (t == 'JSXElement' && compilable(n))
|
|
1116
|
+
subs.push([n.start, n.end, ssr ? ssrInner(n) : row && blockable(n) ? block(n) : jsx(n)]);
|
|
1117
|
+
else if (t == 'ParenthesizedExpression')
|
|
1118
|
+
direct(n.expression, subs, row);
|
|
1119
|
+
else if (t == 'ConditionalExpression') {
|
|
1120
|
+
subsIn(n.test, subs);
|
|
1121
|
+
direct(n.consequent, subs, row);
|
|
1122
|
+
direct(n.alternate, subs, row);
|
|
1123
|
+
}
|
|
1124
|
+
else if (t == 'LogicalExpression') {
|
|
1125
|
+
direct(n.left, subs, row);
|
|
1126
|
+
direct(n.right, subs, row);
|
|
1127
|
+
}
|
|
1128
|
+
else if (t == 'ArrayExpression') {
|
|
1129
|
+
for (const e of n.elements)
|
|
1130
|
+
e && (e.type == 'SpreadElement' ? subsIn(e, subs) : direct(e, subs, row));
|
|
1131
|
+
}
|
|
1132
|
+
else if (t == 'CallExpression' && n.callee.type == 'MemberExpression' && !n.callee.computed && n.callee.property.name == 'map'
|
|
1133
|
+
&& n.arguments.length == 1 && n.arguments[0].type == 'ArrowFunctionExpression' && n.arguments[0].expression) {
|
|
1134
|
+
subsIn(n.callee, subs);
|
|
1135
|
+
for (const p of n.arguments[0].params)
|
|
1136
|
+
subsIn(p, subs);
|
|
1137
|
+
direct(n.arguments[0].body, subs, true);
|
|
1138
|
+
}
|
|
1139
|
+
else
|
|
1140
|
+
subsIn(n, subs);
|
|
1141
|
+
};
|
|
1142
|
+
// CV-3: a block: compilable all the way down, at least two elements, children only text, text
|
|
1143
|
+
// holes (an expression with no JSX in it; not `children` / `slots`, which are vnodes) and such
|
|
1144
|
+
// elements
|
|
1145
|
+
const SLOTISH = /^(children|slots)$|\.(children|slots)$/;
|
|
1146
|
+
const hasJsx = (n) => {
|
|
1147
|
+
if (!n || typeof n != 'object')
|
|
1148
|
+
return false;
|
|
1149
|
+
if (n.type == 'JSXElement' || n.type == 'JSXFragment')
|
|
1150
|
+
return true;
|
|
1151
|
+
for (const k in n) {
|
|
1152
|
+
if (k == 'start' || k == 'end' || k == 'loc')
|
|
1153
|
+
continue;
|
|
1154
|
+
const v = n[k];
|
|
1155
|
+
if (Array.isArray(v) ? v.some(hasJsx) : v && typeof v == 'object' && typeof v.type == 'string' && hasJsx(v))
|
|
1156
|
+
return true;
|
|
1157
|
+
}
|
|
1158
|
+
return false;
|
|
1159
|
+
};
|
|
1160
|
+
const blockable = (el, top = true) => {
|
|
1161
|
+
if (!compilable(el))
|
|
1162
|
+
return false;
|
|
1163
|
+
let n = 0;
|
|
1164
|
+
const ok = (e) => {
|
|
1165
|
+
if (!compilable(e))
|
|
1166
|
+
return false;
|
|
1167
|
+
n++;
|
|
1168
|
+
for (const c of e.children) {
|
|
1169
|
+
if (c.type == 'JSXElement') {
|
|
1170
|
+
if (!ok(c))
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1173
|
+
else if (c.type == 'JSXExpressionContainer') {
|
|
1174
|
+
if (c.expression.type == 'JSXEmptyExpression')
|
|
1175
|
+
continue;
|
|
1176
|
+
if (hasJsx(c.expression) || SLOTISH.test(code.slice(c.expression.start, c.expression.end).trim()))
|
|
1177
|
+
return false;
|
|
1178
|
+
}
|
|
1179
|
+
else if (c.type != 'JSXText')
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1182
|
+
return true;
|
|
1183
|
+
};
|
|
1184
|
+
return ok(el) && (!top || n >= 2);
|
|
1185
|
+
};
|
|
1186
|
+
const block = (el) => {
|
|
1187
|
+
compiled++;
|
|
1188
|
+
nblocks++;
|
|
1189
|
+
const vals = [], kinds = [];
|
|
1190
|
+
const hole = (k, v) => (kinds.push(k), vals.push(v), `h(${vals.length - 1})`);
|
|
1191
|
+
const [rootData, key] = dataOf(el);
|
|
1192
|
+
hole('r', rootData);
|
|
1193
|
+
const bel = (e, root) => {
|
|
1194
|
+
let d;
|
|
1195
|
+
if (root)
|
|
1196
|
+
d = 'h(0)';
|
|
1197
|
+
else {
|
|
1198
|
+
const [data, , dynamic] = dataOf(e);
|
|
1199
|
+
d = dynamic ? hole('d', data) : data;
|
|
1200
|
+
}
|
|
1201
|
+
const kids = [];
|
|
1202
|
+
for (const c of e.children) {
|
|
1203
|
+
if (c.type == 'JSXText') {
|
|
1204
|
+
const t = cleanText(c.value);
|
|
1205
|
+
if (t)
|
|
1206
|
+
kids.push(JSON.stringify(t));
|
|
1207
|
+
}
|
|
1208
|
+
else if (c.type == 'JSXExpressionContainer') {
|
|
1209
|
+
if (c.expression.type != 'JSXEmptyExpression')
|
|
1210
|
+
kids.push(hole('t', '(' + expr(c.expression) + ')'));
|
|
1211
|
+
}
|
|
1212
|
+
else
|
|
1213
|
+
kids.push(bel(c, false));
|
|
1214
|
+
}
|
|
1215
|
+
return `__cv.e(${JSON.stringify(tag(e))}, ${d}, ${childArg(kids)})`;
|
|
1216
|
+
};
|
|
1217
|
+
const body = bel(el, true);
|
|
1218
|
+
const def = hoist(`__cvb.b((h) => ${body}, ${JSON.stringify(kinds.join(''))})`);
|
|
1219
|
+
return `__cvb.B(${def}, [${vals.join(', ')}]${key == 'undefined' ? '' : ', ' + key})`;
|
|
1220
|
+
};
|
|
1221
|
+
const hoist = (obj) => {
|
|
1222
|
+
const i = hoisted.indexOf(obj);
|
|
1223
|
+
if (i >= 0)
|
|
1224
|
+
return '$cv' + i;
|
|
1225
|
+
hoisted.push(obj);
|
|
1226
|
+
return '$cv' + (hoisted.length - 1);
|
|
1227
|
+
};
|
|
1228
|
+
// the children as the JSX transform passes them: JSX text cleaned as the JSX spec does (lines
|
|
1229
|
+
// trimmed, empty lines dropped, joined by a space), empty expressions dropped
|
|
1230
|
+
const children = (list, html) => {
|
|
1231
|
+
const out = [];
|
|
1232
|
+
for (const c of list) {
|
|
1233
|
+
if (c.type == 'JSXText') {
|
|
1234
|
+
const t = cleanText(c.value);
|
|
1235
|
+
if (t)
|
|
1236
|
+
out.push(JSON.stringify(t));
|
|
1237
|
+
}
|
|
1238
|
+
else if (c.type == 'JSXExpressionContainer') {
|
|
1239
|
+
const e = c.expression;
|
|
1240
|
+
if (e.type == 'JSXEmptyExpression')
|
|
1241
|
+
continue;
|
|
1242
|
+
if ((html || blocks) && !vnodes) {
|
|
1243
|
+
const subs = [];
|
|
1244
|
+
direct(e, subs);
|
|
1245
|
+
out.push('(' + src(e.start, e.end, subs) + ')');
|
|
1246
|
+
}
|
|
1247
|
+
else
|
|
1248
|
+
out.push('(' + expr(e) + ')');
|
|
1249
|
+
}
|
|
1250
|
+
else if (html && !vnodes && c.type == 'JSXElement' && compilable(c))
|
|
1251
|
+
out.push(ssrInner(c));
|
|
1252
|
+
else
|
|
1253
|
+
out.push(jsx(c));
|
|
1254
|
+
}
|
|
1255
|
+
return out;
|
|
1256
|
+
};
|
|
1257
|
+
const subs = subsIn(ast);
|
|
1258
|
+
if (!compiled)
|
|
1259
|
+
return null;
|
|
1260
|
+
const ms = new MagicString(code);
|
|
1261
|
+
for (const [a, b, t] of subs)
|
|
1262
|
+
ms.overwrite(a, b, t);
|
|
1263
|
+
ms.prepend(`import { __cv${ssr ? ', __cvs' : ''}${nblocks ? ', __cvb' : ''} } from 'sygnal';\n` + hoisted.map((h, i) => `const $cv${i} = ${h};\n`).join(''));
|
|
1264
|
+
return { code: ms.toString(), map: ms.generateMap({ hires: 'boundary', source: id, includeContent: true }) };
|
|
1265
|
+
}
|
|
1266
|
+
// JSX text as oxc's JSX transform cleans it (the transform the plugin configures): split on line
|
|
1267
|
+
// breaks, trim spaces and tabs at each line's edges (not the first line's start nor the last's
|
|
1268
|
+
// end), drop empty lines, join with ' '; tabs inside a line stay (Babel would make them spaces)
|
|
1269
|
+
function cleanText(value) {
|
|
1270
|
+
const lines = value.split(/\r\n|\n|\r/);
|
|
1271
|
+
let last = 0;
|
|
1272
|
+
for (let i = 0; i < lines.length; i++)
|
|
1273
|
+
if (/[^ \t]/.test(lines[i]))
|
|
1274
|
+
last = i;
|
|
1275
|
+
let str = '';
|
|
1276
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1277
|
+
let line = lines[i];
|
|
1278
|
+
if (i !== 0)
|
|
1279
|
+
line = line.replace(/^[ \t]+/, '');
|
|
1280
|
+
if (i !== lines.length - 1)
|
|
1281
|
+
line = line.replace(/[ \t]+$/, '');
|
|
1282
|
+
if (line) {
|
|
1283
|
+
if (i !== last)
|
|
1284
|
+
line += ' ';
|
|
1285
|
+
str += line;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return str;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/*
|
|
1292
|
+
* 'sygnal/api' (dev preview; index.ts adds contractFetch: this module imports no diagnostics, so
|
|
1293
|
+
* sygnal/vite can bundle it without a second copy of their state; research: dev-plans/research/fullstack-contracts/): typed clients
|
|
1294
|
+
* for an API whose contract the server already has. A client turns a call into plain data the
|
|
1295
|
+
* existing drivers understand, so effects stay data and replies come back as actions:
|
|
1296
|
+
*
|
|
1297
|
+
* - a GET endpoint → a FetchRequest (`resources`, the query cache, retry, `latest`), tagged
|
|
1298
|
+
* `api:<id>` so `invalidates: [api.x]` / `{ invalidate: [api.x] }`-style tags can name it;
|
|
1299
|
+
* - any other method → a FetchRequest with a JSON body (reply actions);
|
|
1300
|
+
* - a stream (an oRPC event iterator, or `.sse(path)`) → an SseConnection for `connections`.
|
|
1301
|
+
*
|
|
1302
|
+
* Adapters (one per contract source), all over plain JSON on the wire:
|
|
1303
|
+
* - orpcClient(contract, options): an oRPC contract router served by oRPC's OpenAPIHandler
|
|
1304
|
+
* (compact input: path params from `{name}` / `{+name}`, the rest in the query for GET with
|
|
1305
|
+
* bracket notation, else the JSON body). The RPCHandler protocol ({ json, meta }) is not
|
|
1306
|
+
* supported: its rich serialisation is what the network-calls research ruled out.
|
|
1307
|
+
* - openApiClient<paths>(options): openapi-typescript's `paths` (types only); input is
|
|
1308
|
+
* openapi-fetch's `{ params: { path, query }, body }`; query arrays repeat the key (form,
|
|
1309
|
+
* explode), objects use deepObject brackets.
|
|
1310
|
+
* - honoClient<ExtractSchema<typeof app>>(options): Hono routes (types only); input is hc's
|
|
1311
|
+
* `{ param, query, json }`.
|
|
1312
|
+
*
|
|
1313
|
+
* Each endpoint is a function `(input?, options?) => request` with `id`, `kind`, `method`,
|
|
1314
|
+
* `path` and `match(request)` (renderComponent's t.respond / t.fail take an endpoint as target).
|
|
1315
|
+
* Requests carry `endpoint: id` (an app key: not sent). `options` are the request's own fields
|
|
1316
|
+
* (ok, error, key, latest, cache, retry, ...); `invalidates` takes endpoints or tags.
|
|
1317
|
+
*
|
|
1318
|
+
* Version skew (D: contract hash): contractHash(contract) is a stable hash of the contract (an
|
|
1319
|
+
* oRPC router: ids, routes, JSON Schemas of input/output, error codes; anything else: its
|
|
1320
|
+
* stable JSON). The app sends it in a header of its choice (`headers` option); on the server,
|
|
1321
|
+
* contractSkew(request, { hash, header }) answers a different one with 409 CONTRACT_SKEW (the
|
|
1322
|
+
* oRPC error shape). isContractSkew(failure) tells a reducer; contractFetch() wraps fetch for the
|
|
1323
|
+
* drivers and reports SYG690 once (and calls onSkew) when a reply says the contract changed.
|
|
1324
|
+
*/
|
|
1325
|
+
// ── oRPC (OpenAPIHandler) ──────────────────────────────────────────────
|
|
1326
|
+
const isProcedure = (x) => !!x && typeof x == 'object' && '~orpc' in x && x['~orpc'] && 'route' in x['~orpc'];
|
|
1327
|
+
/** the yields schema of an oRPC eventIterator() output schema, or undefined */
|
|
1328
|
+
const eventYields = (schema) => {
|
|
1329
|
+
const std = schema?.['~standard'];
|
|
1330
|
+
if (!std)
|
|
1331
|
+
return undefined;
|
|
1332
|
+
const s = Object.getOwnPropertySymbols(std).find(s => s.description === 'ORPC_EVENT_ITERATOR_DETAILS');
|
|
1333
|
+
return s ? std[s].yields ?? {} : undefined;
|
|
1334
|
+
};
|
|
1335
|
+
/** every procedure of an oRPC contract router: [keys, procedure] */
|
|
1336
|
+
const procedures = (router, keys = [], out = [], seen = new Set()) => {
|
|
1337
|
+
if (isProcedure(router))
|
|
1338
|
+
out.push([keys, router]);
|
|
1339
|
+
// a router is plain nested objects: not a schema (a contract module may export those too), no cycles
|
|
1340
|
+
else if (router && typeof router == 'object' && !router['~standard'] && !seen.has(router)) {
|
|
1341
|
+
seen.add(router);
|
|
1342
|
+
Object.keys(router).forEach(k => procedures(router[k], [...keys, k], out, seen));
|
|
1343
|
+
}
|
|
1344
|
+
return out;
|
|
1345
|
+
};
|
|
1346
|
+
const orpcRoute = (keys, p) => {
|
|
1347
|
+
const def = p['~orpc'];
|
|
1348
|
+
const method = String(def.route.method || 'POST').toUpperCase();
|
|
1349
|
+
const path = def.route.path || '/' + keys.map(encodeURIComponent).join('/');
|
|
1350
|
+
// a manifest (a contract without its schemas, or a server function) says `stream` and `kind`
|
|
1351
|
+
const yields = def.stream ? def.outputSchema || {} : eventYields(def.outputSchema);
|
|
1352
|
+
const kind = yields ? 'stream' : def.kind || (method == 'GET' ? 'query' : 'mutation');
|
|
1353
|
+
return { id: keys.join('.'), def, method, path, yields, kind };
|
|
1354
|
+
};
|
|
1355
|
+
// ── contract hash and skew ─────────────────────────────────────────────
|
|
1356
|
+
const stable = (v) => {
|
|
1357
|
+
if (Array.isArray(v))
|
|
1358
|
+
return '[' + v.map(stable).join(',') + ']';
|
|
1359
|
+
if (v && typeof v == 'object')
|
|
1360
|
+
return '{' + Object.keys(v).sort().map(k => JSON.stringify(k) + ':' + stable(v[k])).join(',') + '}';
|
|
1361
|
+
return JSON.stringify(v) ?? 'null';
|
|
1362
|
+
};
|
|
1363
|
+
// cyrb53: a fast, well-distributed 53-bit string hash (not cryptographic: this only detects change)
|
|
1364
|
+
const cyrb53 = (s) => {
|
|
1365
|
+
let h1 = 0xdeadbeef, h2 = 0x41c6ce57;
|
|
1366
|
+
for (let i = 0; i < s.length; i++) {
|
|
1367
|
+
const c = s.charCodeAt(i);
|
|
1368
|
+
h1 = Math.imul(h1 ^ c, 2654435761);
|
|
1369
|
+
h2 = Math.imul(h2 ^ c, 1597334677);
|
|
1370
|
+
}
|
|
1371
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
1372
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
1373
|
+
return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36);
|
|
1374
|
+
};
|
|
1375
|
+
/** a schema's JSON Schema (Standard JSON Schema), else its vendor: what the hash can see of it */
|
|
1376
|
+
const schemaShape = (s, io) => {
|
|
1377
|
+
const std = s?.['~standard'];
|
|
1378
|
+
if (!std)
|
|
1379
|
+
return null;
|
|
1380
|
+
try {
|
|
1381
|
+
if (std.jsonSchema?.[io])
|
|
1382
|
+
return std.jsonSchema[io]({ target: 'draft-2020-12', libraryOptions: { unrepresentable: 'any' } });
|
|
1383
|
+
}
|
|
1384
|
+
catch (_) { /* a schema it can't represent: the vendor below */ }
|
|
1385
|
+
return std.vendor || 'schema';
|
|
1386
|
+
};
|
|
1387
|
+
function contractHash(contract) {
|
|
1388
|
+
// a manifest made at build time (sygnal/vite's api.contracts) carries its hash
|
|
1389
|
+
if (contract && typeof contract['~hash'] == 'string')
|
|
1390
|
+
return contract['~hash'];
|
|
1391
|
+
const ps = procedures(contract);
|
|
1392
|
+
if (!ps.length)
|
|
1393
|
+
return cyrb53(stable(contract));
|
|
1394
|
+
const shape = ps.map(([keys, p]) => {
|
|
1395
|
+
const r = orpcRoute(keys, p);
|
|
1396
|
+
return [r.id, r.method, r.path, r.kind, schemaShape(r.def.inputSchema, 'input'),
|
|
1397
|
+
r.yields ? schemaShape(r.yields, 'output') : schemaShape(r.def.outputSchema, 'output'),
|
|
1398
|
+
Object.keys(r.def.errorMap || {}).sort()];
|
|
1399
|
+
}).sort((a, b) => (a[0] < b[0] ? -1 : 1));
|
|
1400
|
+
return cyrb53(stable(shape));
|
|
1401
|
+
}
|
|
1402
|
+
// ── build time: a contract without its schemas ─────────────────────────
|
|
1403
|
+
/**
|
|
1404
|
+
* sygnal/vite's `api.contracts`: what a client needs of a contract (routes, kinds, error codes and
|
|
1405
|
+
* statuses) and its hash, so the client bundle has no schema library. Plain JSON (`~hash` aside).
|
|
1406
|
+
*/
|
|
1407
|
+
function contractManifest(contract) {
|
|
1408
|
+
const count = procedures(contract).length;
|
|
1409
|
+
if (!count)
|
|
1410
|
+
return { manifest: undefined, hash: '', count };
|
|
1411
|
+
const walk = (node, keys) => {
|
|
1412
|
+
if (isProcedure(node)) {
|
|
1413
|
+
const r = orpcRoute(keys, node);
|
|
1414
|
+
const errorMap = {};
|
|
1415
|
+
Object.keys(r.def.errorMap || {}).forEach(c => { errorMap[c] = { status: r.def.errorMap[c]?.status }; });
|
|
1416
|
+
const route = { method: r.method, path: r.path };
|
|
1417
|
+
if (r.def.route.inputStructure)
|
|
1418
|
+
route.inputStructure = r.def.route.inputStructure;
|
|
1419
|
+
return { '~orpc': { route, kind: r.kind, stream: r.kind == 'stream', manifest: true, errorMap } };
|
|
1420
|
+
}
|
|
1421
|
+
const out = {};
|
|
1422
|
+
Object.keys(node || {}).forEach(k => { if (procedures(node[k]).length)
|
|
1423
|
+
out[k] = walk(node[k], [...keys, k]); });
|
|
1424
|
+
return out;
|
|
1425
|
+
};
|
|
1426
|
+
return { manifest: walk(contract, []), hash: contractHash(contract), count };
|
|
1427
|
+
}
|
|
1428
|
+
// ── function-first: *.server.ts modules ────────────────────────────────
|
|
1429
|
+
/*
|
|
1430
|
+
* A `*.server.ts` file exports query(input, fn), mutation(input, fn) and stream(input, fn). Its
|
|
1431
|
+
* path and export names are the contract: `src/quotes.server.ts` → `export const list` is
|
|
1432
|
+
* `quotes.list`, POST /quotes/list (a stream: GET, read as server-sent events). sygnal/vite
|
|
1433
|
+
* (`api: { functions: true }`) replaces the file in the client by stubs that build the requests,
|
|
1434
|
+
* so nothing of it but its export names, kinds and error codes reaches the bundle; on the
|
|
1435
|
+
* server, serverFunctions(import.meta.glob(...), { os }) makes the oRPC router to mount.
|
|
1436
|
+
* Input is always validated (a Standard Schema, or none: then only an empty input passes), the
|
|
1437
|
+
* wire is plain JSON (oRPC's OpenAPIHandler), and only module exports are functions (no closures
|
|
1438
|
+
* over component scope, no ids made from a closure).
|
|
1439
|
+
*/
|
|
1440
|
+
const FN = '~sygnal';
|
|
1441
|
+
/** `src/admin/users.server.ts` (or `/src/...`, `../src/...`) → `admin/users` */
|
|
1442
|
+
const fnNamespace = (file, dir = 'src') => {
|
|
1443
|
+
let f = file.split('?')[0].replace(/\\/g, '/').replace(/^(\.{1,2}\/)+|^\/+/, '');
|
|
1444
|
+
const d = dir.replace(/^\.?\/+|\/+$/g, '');
|
|
1445
|
+
if (d && f.startsWith(d + '/'))
|
|
1446
|
+
f = f.slice(d.length + 1);
|
|
1447
|
+
return f.replace(/\.server\.[cm]?[jt]sx?$/, '');
|
|
1448
|
+
};
|
|
1449
|
+
const fnSpec = (id, kind, errors, schemas) => ({ '~orpc': {
|
|
1450
|
+
route: { method: kind == 'stream' ? 'GET' : 'POST', path: '/' + id.split('.').map(encodeURIComponent).join('/') },
|
|
1451
|
+
kind, stream: kind == 'stream', manifest: !schemas, errorMap: errors,
|
|
1452
|
+
inputSchema: schemas?.input, outputSchema: schemas?.output,
|
|
1453
|
+
} });
|
|
1454
|
+
/** each file's server functions: its namespace, its own contract tree (for the hash), [name, meta] */
|
|
1455
|
+
function fnFiles(modules, dir = 'src') {
|
|
1456
|
+
return Object.keys(modules).sort().map(file => {
|
|
1457
|
+
const ns = fnNamespace(file, dir);
|
|
1458
|
+
const keys = ns.split('/');
|
|
1459
|
+
const tree = {};
|
|
1460
|
+
let node = tree;
|
|
1461
|
+
keys.forEach(k => { node = node[k] = {}; });
|
|
1462
|
+
const fns = [];
|
|
1463
|
+
Object.keys(modules[file] || {}).forEach(name => {
|
|
1464
|
+
const meta = modules[file][name]?.[FN];
|
|
1465
|
+
if (!meta)
|
|
1466
|
+
return;
|
|
1467
|
+
const id = [...keys, name].join('.');
|
|
1468
|
+
node[name] = Object.assign(fnSpec(id, meta.kind, meta.errors, meta), { [FN]: meta });
|
|
1469
|
+
fns.push([name, meta]);
|
|
1470
|
+
});
|
|
1471
|
+
return { file, ns, keys, tree, fns, hash: contractHash(tree) };
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/**
|
|
1476
|
+
* sygnal/vite's `api` option (sygnal/api, dev preview; research: dev-plans/research/fullstack-contracts/):
|
|
1477
|
+
*
|
|
1478
|
+
* - `handler`: in dev, requests under `prefix` go to that module's default export, a Fetch handler
|
|
1479
|
+
* (Request → Response): the same module a production server mounts. The plugin only bridges
|
|
1480
|
+
* Node's request to it; the server is the app's own (oRPC), never one of Sygnal's.
|
|
1481
|
+
* - `contracts`: in client code (dev and build, never Vitest) each listed contract module is
|
|
1482
|
+
* replaced by what orpcClient needs of it (routes, kinds, error codes and statuses) with its
|
|
1483
|
+
* contractHash computed at build time, so the bundle has no schema library. The module is
|
|
1484
|
+
* evaluated on the server side (the dev server's SSR loader; for `vite build` a temporary one).
|
|
1485
|
+
* An export that is no contract is left out, so a client import of a schema from it fails
|
|
1486
|
+
* loudly (keep the module out of `contracts` to validate on the client).
|
|
1487
|
+
* - `functions`: `*.server.ts` files under `dir` (function-first). In client code a file becomes
|
|
1488
|
+
* stubs: `export const list = __serverFunction('quotes.list', { kind, errors }, client)`, plus
|
|
1489
|
+
* the file's contract hash in the stub header. Nothing else of the file (its imports, secrets,
|
|
1490
|
+
* handler bodies) is in the client graph. In server code and under Vitest the file keeps its
|
|
1491
|
+
* code and each export gets its id appended, so tests can call it as an endpoint.
|
|
1492
|
+
*/
|
|
1493
|
+
// @ts-ignore
|
|
1494
|
+
const SERVER_FILE = /\.server\.[cm]?[jt]sx?$/;
|
|
1495
|
+
// set while the temporary build-time server loads the config: its own sygnal() then adds no api plugin
|
|
1496
|
+
const NESTED = '__SYGNAL_API_EVAL__';
|
|
1497
|
+
const g = globalThis;
|
|
1498
|
+
const nodeProcess$1 = g.process;
|
|
1499
|
+
function apiPlugin(o) {
|
|
1500
|
+
if (g[NESTED])
|
|
1501
|
+
return null;
|
|
1502
|
+
const prefix = (o.prefix ?? '/api').replace(/\/+$/, '');
|
|
1503
|
+
const fo = o.functions === true ? {} : o.functions || null;
|
|
1504
|
+
const dir = (fo && fo.dir) || 'src';
|
|
1505
|
+
const header = (fo && fo.header) || 'x-api-functions';
|
|
1506
|
+
let root = nodeProcess$1.cwd();
|
|
1507
|
+
let isVitest = false;
|
|
1508
|
+
let config;
|
|
1509
|
+
let dev;
|
|
1510
|
+
let temp;
|
|
1511
|
+
const rel = (file) => path.relative(root, file).split(path.sep).join('/');
|
|
1512
|
+
const contractFiles = () => new Set((o.contracts || []).map((f) => path.resolve(root, f)));
|
|
1513
|
+
const isFnFile = (file) => {
|
|
1514
|
+
if (!fo || !SERVER_FILE.test(file) || file.includes('/node_modules/'))
|
|
1515
|
+
return false;
|
|
1516
|
+
const r = rel(file);
|
|
1517
|
+
return !r.startsWith('..') && (r + '/').startsWith(dir.replace(/^\.?\/+|\/+$/g, '') + '/');
|
|
1518
|
+
};
|
|
1519
|
+
// evaluates a module on the server side: the dev server's SSR loader, or (vite build) a temporary one
|
|
1520
|
+
async function load(file) {
|
|
1521
|
+
if (dev)
|
|
1522
|
+
return dev.ssrLoadModule(file);
|
|
1523
|
+
temp ?? (temp = (async () => {
|
|
1524
|
+
const vite = await import(/* @vite-ignore */ projectVite(root));
|
|
1525
|
+
g[NESTED] = true;
|
|
1526
|
+
try {
|
|
1527
|
+
return await vite.createServer({
|
|
1528
|
+
root, mode: config?.mode, configFile: config?.configFile ?? false, logLevel: 'warn', appType: 'custom',
|
|
1529
|
+
server: { middlewareMode: true, hmr: false, ws: false, watch: null },
|
|
1530
|
+
optimizeDeps: { noDiscovery: true, include: [] },
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
finally {
|
|
1534
|
+
delete g[NESTED];
|
|
1535
|
+
}
|
|
1536
|
+
})());
|
|
1537
|
+
return (await temp).ssrLoadModule(file);
|
|
1538
|
+
}
|
|
1539
|
+
const exportLine = (name, value) => (name === 'default' ? `export default ${value};` : `export const ${name} = ${value};`);
|
|
1540
|
+
return {
|
|
1541
|
+
name: 'vite-plugin-sygnal:api',
|
|
1542
|
+
config() {
|
|
1543
|
+
isVitest = !!nodeProcess$1?.env?.VITEST;
|
|
1544
|
+
},
|
|
1545
|
+
configResolved(c) {
|
|
1546
|
+
config = c;
|
|
1547
|
+
root = c.root;
|
|
1548
|
+
},
|
|
1549
|
+
configureServer(server) {
|
|
1550
|
+
dev = server;
|
|
1551
|
+
if (!o.handler || isVitest)
|
|
1552
|
+
return;
|
|
1553
|
+
const handlerFile = path.resolve(root, o.handler);
|
|
1554
|
+
server.middlewares.use(async (req, res, next) => {
|
|
1555
|
+
const url = req.url || '';
|
|
1556
|
+
if (!(url === prefix || url.startsWith(prefix + '/') || url.startsWith(prefix + '?')))
|
|
1557
|
+
return next();
|
|
1558
|
+
try {
|
|
1559
|
+
const mod = await server.ssrLoadModule(handlerFile);
|
|
1560
|
+
const handle = typeof mod.default === 'function' ? mod.default : mod.default?.fetch ?? mod.fetch;
|
|
1561
|
+
if (typeof handle !== 'function')
|
|
1562
|
+
throw new Error(`[sygnal] api.handler (${o.handler}) must export default a Fetch handler: (request: Request) => Response`);
|
|
1563
|
+
const ctl = new AbortController();
|
|
1564
|
+
res.on('close', () => ctl.abort());
|
|
1565
|
+
const body = req.method === 'GET' || req.method === 'HEAD' ? undefined : Readable.toWeb(req);
|
|
1566
|
+
const request = new Request(new URL(url, 'http://' + (req.headers.host || 'localhost')), {
|
|
1567
|
+
method: req.method, headers: req.headers, body, signal: ctl.signal, ...(body && { duplex: 'half' }),
|
|
1568
|
+
});
|
|
1569
|
+
const response = await handle(request);
|
|
1570
|
+
res.writeHead(response.status, Object.fromEntries(response.headers));
|
|
1571
|
+
if (!response.body)
|
|
1572
|
+
return void res.end();
|
|
1573
|
+
Readable.fromWeb(response.body).pipe(res);
|
|
1574
|
+
}
|
|
1575
|
+
catch (e) {
|
|
1576
|
+
next(e);
|
|
1577
|
+
}
|
|
1578
|
+
});
|
|
1579
|
+
},
|
|
1580
|
+
async transform(code, id, opts) {
|
|
1581
|
+
const file = id.split('?')[0];
|
|
1582
|
+
if (id.startsWith('\0'))
|
|
1583
|
+
return null;
|
|
1584
|
+
const client = !opts?.ssr && !isVitest;
|
|
1585
|
+
if (client && o.contracts && contractFiles().has(file)) {
|
|
1586
|
+
const mod = await load(file);
|
|
1587
|
+
const out = [];
|
|
1588
|
+
Object.keys(mod).forEach((name) => {
|
|
1589
|
+
const { manifest, hash, count } = contractManifest(mod[name]);
|
|
1590
|
+
if (!count)
|
|
1591
|
+
return;
|
|
1592
|
+
out.push(exportLine(name, `Object.defineProperty(${JSON.stringify(manifest)}, '~hash', { value: ${JSON.stringify(hash)} })`));
|
|
1593
|
+
});
|
|
1594
|
+
return { code: `// ${rel(file)}: routes and hash only (sygnal/vite api.contracts)\n${out.join('\n')}\n`, map: { mappings: '' } };
|
|
1595
|
+
}
|
|
1596
|
+
if (!isFnFile(file))
|
|
1597
|
+
return null;
|
|
1598
|
+
if (client) {
|
|
1599
|
+
const [f] = fnFiles({ [rel(file)]: await load(file) }, dir);
|
|
1600
|
+
const opts = JSON.stringify({ prefix, headers: { [header]: `${f.ns}:${f.hash}` } });
|
|
1601
|
+
const lines = f.fns.map(([name, meta]) => {
|
|
1602
|
+
const errors = {};
|
|
1603
|
+
Object.keys(meta.errors || {}).forEach((c) => { errors[c] = { status: meta.errors[c]?.status }; });
|
|
1604
|
+
return exportLine(name, `__serverFunction(${JSON.stringify(f.keys.concat(name).join('.'))}, ${JSON.stringify({ kind: meta.kind, errors })}, __o)`);
|
|
1605
|
+
});
|
|
1606
|
+
return {
|
|
1607
|
+
code: `// ${rel(file)}: client stubs (sygnal/vite api.functions)\nimport { __serverFunction } from 'sygnal/api';\nconst __o = ${opts};\n${lines.join('\n')}\n`,
|
|
1608
|
+
map: { mappings: '' },
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
// server code and tests: the module as written, each export given its id
|
|
1612
|
+
const ns = fnNamespace(rel(file), dir);
|
|
1613
|
+
const named = exportedLocals(this.parse(code));
|
|
1614
|
+
if (!named.length)
|
|
1615
|
+
return null;
|
|
1616
|
+
const keys = ns.split('/');
|
|
1617
|
+
const tail = named.map(([exp, local]) => `__sygnalFn(${JSON.stringify(keys.concat(exp).join('.'))}, ${local}, ${JSON.stringify({ prefix })});`);
|
|
1618
|
+
return { code: `${code}\nimport { __serverFunction as __sygnalFn } from 'sygnal/api';\n${tail.join('\n')}\n`, map: null };
|
|
1619
|
+
},
|
|
1620
|
+
async closeBundle() {
|
|
1621
|
+
if (temp) {
|
|
1622
|
+
const s = await temp;
|
|
1623
|
+
temp = undefined;
|
|
1624
|
+
await s.close();
|
|
1625
|
+
}
|
|
1626
|
+
},
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
/** [exported name, local binding] of a module's own named exports (declarations and `export { a as b }`) */
|
|
1630
|
+
function exportedLocals(ast) {
|
|
1631
|
+
const out = [];
|
|
1632
|
+
for (const node of ast.body || []) {
|
|
1633
|
+
if (node.type !== 'ExportNamedDeclaration' || node.source)
|
|
1634
|
+
continue;
|
|
1635
|
+
const d = node.declaration;
|
|
1636
|
+
if (d?.type === 'VariableDeclaration')
|
|
1637
|
+
d.declarations.forEach((x) => x.id?.type === 'Identifier' && out.push([x.id.name, x.id.name]));
|
|
1638
|
+
else if (d?.id?.name)
|
|
1639
|
+
out.push([d.id.name, d.id.name]);
|
|
1640
|
+
for (const s of node.specifiers || []) {
|
|
1641
|
+
const exp = s.exported?.name ?? s.exported?.value;
|
|
1642
|
+
if (s.local?.name && exp)
|
|
1643
|
+
out.push([exp, s.local.name]);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
return out;
|
|
1647
|
+
}
|
|
1648
|
+
/** the project's own Vite (not one next to sygnal), as an ESM URL */
|
|
1649
|
+
function projectVite(root) {
|
|
1650
|
+
const req = createRequire(path.join(root, 'package.json'));
|
|
1651
|
+
const pj = req.resolve('vite/package.json');
|
|
1652
|
+
const pkg = JSON.parse(fs.readFileSync(pj, 'utf8'));
|
|
1653
|
+
const dot = typeof pkg.exports === 'string' ? pkg.exports : pkg.exports?.['.'];
|
|
1654
|
+
let entry = typeof dot === 'string' ? dot : dot?.import ?? pkg.module ?? pkg.main;
|
|
1655
|
+
if (entry && typeof entry === 'object')
|
|
1656
|
+
entry = entry.default;
|
|
1657
|
+
return pathToFileURL(path.join(path.dirname(pj), entry)).href;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/*
|
|
1661
|
+
* The QuickJS WASM module: `quickjs-emscripten-core` + `@jitl/quickjs-wasmfile-release-sync`,
|
|
1662
|
+
* optional peer dependencies (D209), loaded on first use. Stock packages only (README §9 Q6):
|
|
1663
|
+
* a quickjs-ng variant can be passed as `variant`.
|
|
1664
|
+
*/
|
|
1665
|
+
// Node built-ins by a name bundlers don't follow (a browser bundle never loads them)
|
|
1666
|
+
const nodeImport = (name) => import(/* @vite-ignore */ /* webpackIgnore: true */ name);
|
|
1667
|
+
|
|
1668
|
+
/*
|
|
1669
|
+
* The guest kernel, built from the app's installed packages (README §7 option (a), §9 Q1; D209:
|
|
1670
|
+
* Sygnal's npm package never ships xstream or snabbdom prebuilt into it). sygnal/vite resolves
|
|
1671
|
+
* `sygnal/sandbox/kernel?raw` to this function's output (the project's own Vite), and
|
|
1672
|
+
* `buildSandboxKernel()` (sygnal/sandbox/build) does it for other toolchains (Vite, esbuild or
|
|
1673
|
+
* Rollup, whichever the project has).
|
|
1674
|
+
*
|
|
1675
|
+
* The output is one script: the platform prelude (timers, console, microtasks on `__host`,
|
|
1676
|
+
* which the core's modules read when they load), then dist/sandbox-kernel.esm.js bundled as an
|
|
1677
|
+
* IIFE with the installed sygnal (dist/index.esm.js and dist/host.esm.js of the same package: one
|
|
1678
|
+
* core), xstream and snabbdom. `process.env.NODE_ENV` is 'production'; xstream's `globalthis`
|
|
1679
|
+
* dependency is Sygnal's shim (as sygnal/vite does, G-099).
|
|
1680
|
+
*/
|
|
1681
|
+
// The platform a bare QuickJS context lacks. A plain script, so it runs before the bundle's modules.
|
|
1682
|
+
const PLATFORM = `var global = globalThis;
|
|
1683
|
+
(function () {
|
|
1684
|
+
var H = globalThis.__host, G = globalThis, timers = new Map(), intervals = new Map(), seq = 0;
|
|
1685
|
+
G.__sandboxTimers = timers;
|
|
1686
|
+
G.queueMicrotask = G.queueMicrotask || function (f) { Promise.resolve().then(f) };
|
|
1687
|
+
G.setTimeout = function (f, ms) { var id = ++seq; timers.set(id, f); H.timer(id, +ms || 0); return id };
|
|
1688
|
+
G.clearTimeout = function (id) { if (timers.delete(id)) H.clearTimer(id) };
|
|
1689
|
+
// the core's macrotask hop (runtime.ts macro()): its own channel, so a component's timers can't starve it
|
|
1690
|
+
G.setImmediate = function (f) { var id = ++seq; timers.set(id, f); H.timer(id, -1); return id };
|
|
1691
|
+
G.clearImmediate = G.clearTimeout;
|
|
1692
|
+
G.setInterval = function (f, ms) { var box = {}; var tick = function () { box.id = G.setTimeout(tick, ms); f() }; box.id = G.setTimeout(tick, ms); var id = ++seq; intervals.set(id, box); return id };
|
|
1693
|
+
G.clearInterval = function (id) { var b = intervals.get(id); if (b) { intervals.delete(id); G.clearTimeout(b.id) } };
|
|
1694
|
+
G.requestAnimationFrame = function (f) { return G.setTimeout(function () { f(Date.now()) }, 16) };
|
|
1695
|
+
G.cancelAnimationFrame = G.clearTimeout;
|
|
1696
|
+
var fmt = function (a) { return a.map(function (x) { if (typeof x == 'string') return x; if (x instanceof Error) return x.name + ': ' + x.message; try { return JSON.stringify(x) } catch (e) { return String(x) } }).join(' ') };
|
|
1697
|
+
G.console = {
|
|
1698
|
+
log: function () { H.log('log', fmt([].slice.call(arguments))) }, info: function () { H.log('log', fmt([].slice.call(arguments))) },
|
|
1699
|
+
warn: function () { H.log('warn', fmt([].slice.call(arguments))) }, error: function () { H.log('error', fmt([].slice.call(arguments))) },
|
|
1700
|
+
debug: function () {},
|
|
1701
|
+
};
|
|
1702
|
+
})();
|
|
1703
|
+
`;
|
|
1704
|
+
/** The guest kernel script for createSandbox / <Sandboxed> (`kernel`), built from the installed packages */
|
|
1705
|
+
async function buildSandboxKernel(opts = {}) {
|
|
1706
|
+
const [path, { createRequire }, { pathToFileURL }, fs] = await Promise.all([nodeImport('node:path'), nodeImport('node:module'), nodeImport('node:url'), nodeImport('node:fs')]);
|
|
1707
|
+
const root = path.resolve(opts.root || globalThis.process.cwd());
|
|
1708
|
+
const fromRoot = createRequire(path.join(root, 'package.json'));
|
|
1709
|
+
// the app's sygnal (else this one): its dist/ holds the kernel entry, the core and the kit
|
|
1710
|
+
let main;
|
|
1711
|
+
try {
|
|
1712
|
+
main = fromRoot.resolve('sygnal');
|
|
1713
|
+
}
|
|
1714
|
+
catch {
|
|
1715
|
+
main = createRequire(import.meta.url).resolve('sygnal');
|
|
1716
|
+
}
|
|
1717
|
+
const dist = path.dirname(main);
|
|
1718
|
+
const entry = path.join(dist, 'sandbox-kernel.esm.js');
|
|
1719
|
+
const files = {
|
|
1720
|
+
sygnal: path.join(dist, 'index.esm.js'),
|
|
1721
|
+
'sygnal/host': path.join(dist, 'host.esm.js'),
|
|
1722
|
+
globalthis: path.join(dist, 'shims/globalthis.cjs'),
|
|
1723
|
+
};
|
|
1724
|
+
for (const f of [entry, ...Object.values(files)])
|
|
1725
|
+
if (!fs.existsSync(f))
|
|
1726
|
+
throw new Error(`buildSandboxKernel: ${f} is missing (an incomplete sygnal install?)`);
|
|
1727
|
+
const minify = opts.minify !== false;
|
|
1728
|
+
const define = { 'process.env.NODE_ENV': '"production"' };
|
|
1729
|
+
const has = (name) => { try {
|
|
1730
|
+
fromRoot.resolve(name);
|
|
1731
|
+
return true;
|
|
1732
|
+
}
|
|
1733
|
+
catch {
|
|
1734
|
+
return false;
|
|
1735
|
+
} };
|
|
1736
|
+
const want = opts.bundler || 'auto';
|
|
1737
|
+
const bundler = want !== 'auto' ? want : has('vite') ? 'vite' : has('esbuild') ? 'esbuild' : has('rollup') ? 'rollup' : null;
|
|
1738
|
+
if (!bundler)
|
|
1739
|
+
throw new Error('buildSandboxKernel: needs vite, esbuild or rollup installed in ' + root);
|
|
1740
|
+
const load = (name) => import(/* @vite-ignore */ pathToFileURL(resolveEsm(fromRoot, fs, path, name)).href);
|
|
1741
|
+
let code;
|
|
1742
|
+
if (bundler === 'esbuild') {
|
|
1743
|
+
const esbuild = await load('esbuild');
|
|
1744
|
+
const r = await (esbuild.build || esbuild.default.build)({
|
|
1745
|
+
entryPoints: [entry], bundle: true, write: false, format: 'iife', platform: 'browser', target: 'es2020',
|
|
1746
|
+
minify, define, alias: files, logLevel: 'silent', legalComments: 'none',
|
|
1747
|
+
});
|
|
1748
|
+
code = r.outputFiles[0].text;
|
|
1749
|
+
}
|
|
1750
|
+
else if (bundler === 'rollup') {
|
|
1751
|
+
const { rollup } = await load('rollup');
|
|
1752
|
+
const plugins = [{
|
|
1753
|
+
name: 'sygnal-kernel', resolveId: (id) => files[id] || null,
|
|
1754
|
+
transform: (c) => (c.includes('process.env.NODE_ENV') ? c.split('process.env.NODE_ENV').join(define['process.env.NODE_ENV']) : null),
|
|
1755
|
+
}];
|
|
1756
|
+
const req = async (n) => { const m = await load(n); return m.default || m; };
|
|
1757
|
+
if (has('@rollup/plugin-node-resolve'))
|
|
1758
|
+
plugins.push((await load('@rollup/plugin-node-resolve')).nodeResolve({ browser: true }));
|
|
1759
|
+
if (has('@rollup/plugin-commonjs'))
|
|
1760
|
+
plugins.push((await req('@rollup/plugin-commonjs'))());
|
|
1761
|
+
if (minify && has('@rollup/plugin-terser'))
|
|
1762
|
+
plugins.push((await req('@rollup/plugin-terser'))({ ecma: 2020 }));
|
|
1763
|
+
const b = await rollup({ input: entry, plugins, onwarn: () => { } });
|
|
1764
|
+
code = (await b.generate({ format: 'iife', name: '__sygnalKernel' })).output[0].code;
|
|
1765
|
+
await b.close();
|
|
1766
|
+
}
|
|
1767
|
+
else {
|
|
1768
|
+
const vite = await load('vite');
|
|
1769
|
+
const out = await vite.build({
|
|
1770
|
+
configFile: false, root, logLevel: 'silent', mode: 'production', define,
|
|
1771
|
+
resolve: { alias: Object.entries(files).map(([find, replacement]) => ({ find: new RegExp(`^${find.replace('/', '\\/')}$`), replacement })) },
|
|
1772
|
+
build: { write: false, minify, target: 'es2020', lib: { entry, formats: ['iife'], name: '__sygnalKernel', fileName: () => 'kernel.js' }, rollupOptions: { output: { inlineDynamicImports: true } } },
|
|
1773
|
+
});
|
|
1774
|
+
const o = (Array.isArray(out) ? out[0] : out).output;
|
|
1775
|
+
code = o.find((c) => c.type === 'chunk').code;
|
|
1776
|
+
}
|
|
1777
|
+
return PLATFORM + code;
|
|
1778
|
+
}
|
|
1779
|
+
// a package's ESM entry, as resolved from the app (import() needs a file URL, not a bare name, to resolve from there)
|
|
1780
|
+
function resolveEsm(req, fs, path, name) {
|
|
1781
|
+
try {
|
|
1782
|
+
const pj = req.resolve(`${name}/package.json`);
|
|
1783
|
+
const pkg = JSON.parse(fs.readFileSync(pj, 'utf8'));
|
|
1784
|
+
const dot = typeof pkg.exports == 'string' ? pkg.exports : pkg.exports?.['.'];
|
|
1785
|
+
let e = typeof dot == 'string' ? dot : dot?.import ?? pkg.module ?? pkg.main ?? 'index.js';
|
|
1786
|
+
if (e && typeof e == 'object')
|
|
1787
|
+
e = e.default;
|
|
1788
|
+
return path.join(path.dirname(pj), e);
|
|
1789
|
+
}
|
|
1790
|
+
catch {
|
|
1791
|
+
return req.resolve(name);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
/**
|
|
1796
|
+
* sygnal/sandbox's guest kernel in sygnal/vite (README §7 (a), D209): `import kernel from
|
|
1797
|
+
* 'sygnal/sandbox/kernel?raw'` (or without `?raw`) resolves to a module whose default export is
|
|
1798
|
+
* the kernel script, built once per server or build with the project's own Vite from the
|
|
1799
|
+
* installed sygnal, xstream and snabbdom (src/sandbox/build.ts). Nothing is shipped prebuilt.
|
|
1800
|
+
*/
|
|
1801
|
+
const SANDBOX_KERNEL = /^sygnal\/sandbox\/kernel(\?raw)?$/;
|
|
1802
|
+
const SANDBOX_KERNEL_ID = '\0sygnal-sandbox-kernel';
|
|
1803
|
+
const built = new Map();
|
|
1804
|
+
async function sandboxKernelModule(root) {
|
|
1805
|
+
if (!built.has(root))
|
|
1806
|
+
built.set(root, buildSandboxKernel({ root, bundler: 'vite' }));
|
|
1807
|
+
return `// sygnal/sandbox's guest kernel, built from the installed packages (sygnal/vite)\nexport default ${JSON.stringify(await built.get(root))};\n`;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
892
1810
|
/**
|
|
893
1811
|
* Sygnal Vite Plugin
|
|
894
1812
|
*
|
|
@@ -1111,7 +2029,41 @@ function sygnal(options = {}) {
|
|
|
1111
2029
|
let devSnippet = devtoolsImport + (devOn ? flags + devImports : '');
|
|
1112
2030
|
const devInject = devOn || devtoolsOn;
|
|
1113
2031
|
let astroClient;
|
|
1114
|
-
|
|
2032
|
+
// CV-2: its own plugin, before Vite's JSX transform (this plugin's transform runs after it)
|
|
2033
|
+
let warnedCompile = false;
|
|
2034
|
+
const co = options.compile === true ? {} : options.compile || null;
|
|
2035
|
+
const compileOn = (k) => !!co && co[k] !== false;
|
|
2036
|
+
// (its own Vitest flag: it may be used without the main plugin, e.g. added to a config that already
|
|
2037
|
+
// has sygnal())
|
|
2038
|
+
let cVitest = false;
|
|
2039
|
+
const compilePlugin = co && {
|
|
2040
|
+
name: 'vite-plugin-sygnal:compile',
|
|
2041
|
+
enforce: 'pre',
|
|
2042
|
+
config() {
|
|
2043
|
+
cVitest = !!nodeProcess?.env?.VITEST;
|
|
2044
|
+
},
|
|
2045
|
+
transform(code, id, opts) {
|
|
2046
|
+
const isVitest = cVitest;
|
|
2047
|
+
const file = id.split('?')[0];
|
|
2048
|
+
if (!/\.[jt]sx$/.test(file) || file.includes('/node_modules/') || id.startsWith('\0'))
|
|
2049
|
+
return null;
|
|
2050
|
+
if (isVitest && !compileOn('test'))
|
|
2051
|
+
return null;
|
|
2052
|
+
// server code renders to strings (never under Vitest: renderComponent's mock DOM needs vnodes)
|
|
2053
|
+
const ssr = !!opts?.ssr && !isVitest && compileOn('ssr');
|
|
2054
|
+
if (!ssr && !compileOn('views'))
|
|
2055
|
+
return null;
|
|
2056
|
+
if (viteMajor(this, root) < 8) {
|
|
2057
|
+
if (!warnedCompile) {
|
|
2058
|
+
warnedCompile = true;
|
|
2059
|
+
this.warn('[sygnal] `compile` needs Vite 8; views are not compiled');
|
|
2060
|
+
}
|
|
2061
|
+
return null;
|
|
2062
|
+
}
|
|
2063
|
+
return compileViews(code, file, (c, o) => this.parse(c, o), { ssr, blocks: !ssr && compileOn('blocks') });
|
|
2064
|
+
},
|
|
2065
|
+
};
|
|
2066
|
+
const main = {
|
|
1115
2067
|
name: 'vite-plugin-sygnal',
|
|
1116
2068
|
config(config, env) {
|
|
1117
2069
|
isServe = env.command === 'serve';
|
|
@@ -1188,7 +2140,9 @@ function sygnal(options = {}) {
|
|
|
1188
2140
|
const include = result.optimizeDeps?.include || [];
|
|
1189
2141
|
result.optimizeDeps = {
|
|
1190
2142
|
...result.optimizeDeps,
|
|
1191
|
-
|
|
2143
|
+
// sygnal/sandbox: its worker script is found by `new URL(..., import.meta.url)`, which
|
|
2144
|
+
// only works served unbundled
|
|
2145
|
+
exclude: [VIKE_CLIENT, 'sygnal/sandbox'],
|
|
1192
2146
|
...(sygnalInNodeModules(root) ? { include: [...include, 'sygnal'] } : {}),
|
|
1193
2147
|
};
|
|
1194
2148
|
}
|
|
@@ -1219,6 +2173,9 @@ function sygnal(options = {}) {
|
|
|
1219
2173
|
resolveId: {
|
|
1220
2174
|
order: 'pre',
|
|
1221
2175
|
async handler(source, importer, opts) {
|
|
2176
|
+
// sygnal/sandbox: the guest kernel, built from the installed packages (every mode)
|
|
2177
|
+
if (SANDBOX_KERNEL.test(source))
|
|
2178
|
+
return SANDBOX_KERNEL_ID;
|
|
1222
2179
|
if (!isServe || isVitest)
|
|
1223
2180
|
return null;
|
|
1224
2181
|
if (source === DEV_CLIENT)
|
|
@@ -1245,6 +2202,8 @@ function sygnal(options = {}) {
|
|
|
1245
2202
|
},
|
|
1246
2203
|
},
|
|
1247
2204
|
load(id) {
|
|
2205
|
+
if (id === SANDBOX_KERNEL_ID)
|
|
2206
|
+
return sandboxKernelModule(root);
|
|
1248
2207
|
if (id === DEV_CLIENT_ID)
|
|
1249
2208
|
return devClientModule();
|
|
1250
2209
|
if (id === MCP_CLIENT_ID && mcpOn)
|
|
@@ -1313,6 +2272,9 @@ function sygnal(options = {}) {
|
|
|
1313
2272
|
});
|
|
1314
2273
|
},
|
|
1315
2274
|
};
|
|
2275
|
+
const api = options.api ? apiPlugin(options.api) : null;
|
|
2276
|
+
const plugins = [compilePlugin, main, api].filter(Boolean);
|
|
2277
|
+
return plugins.length > 1 ? plugins : main;
|
|
1316
2278
|
function devTransform(code, id, opts) {
|
|
1317
2279
|
if (!isServe)
|
|
1318
2280
|
return null;
|
|
@@ -1471,10 +2433,12 @@ const ADAPTER_PEERS = {
|
|
|
1471
2433
|
'ui/menu': ['@zag-js/vanilla', '@zag-js/menu'],
|
|
1472
2434
|
'ui/select': ['@zag-js/vanilla', '@zag-js/select'],
|
|
1473
2435
|
'ui/combobox': ['@zag-js/vanilla', '@zag-js/combobox'],
|
|
2436
|
+
// sygnal/sandbox: QuickJS and its WASM variant (stock packages, README §9 Q6)
|
|
2437
|
+
sandbox: ['quickjs-emscripten-core', '@jitl/quickjs-wasmfile-release-sync'],
|
|
1474
2438
|
};
|
|
1475
2439
|
// G-440: a dynamic import's specifier may be a template literal without substitutions
|
|
1476
|
-
const ADAPTER_RE = /(?:\bfrom|\bimport)\s*\(?\s*(['"`])sygnal\/(react|zag|ui\/(?:menu|select|combobox))\1/g;
|
|
1477
|
-
const ADAPTER_ID = /^sygnal\/(react|zag|ui\/(?:menu|select|combobox))$/;
|
|
2440
|
+
const ADAPTER_RE = /(?:\bfrom|\bimport)\s*\(?\s*(['"`])sygnal\/(react|zag|sandbox|ui\/(?:menu|select|combobox))\1/g;
|
|
2441
|
+
const ADAPTER_ID = /^sygnal\/(react|zag|sandbox|ui\/(?:menu|select|combobox))$/;
|
|
1478
2442
|
/**
|
|
1479
2443
|
* The adapter entries an app module imports (statically, dynamically or in a re-export), or null.
|
|
1480
2444
|
* G-415: real imports only. The module's AST from the plugin context's parse() (Vite and Rollup
|