alien deepsignals + LDO

refactor
Niko PLP 24 hours ago
parent 6755978c03
commit ead925409c
  1. 4
      pnpm-workspace.yaml
  2. 42
      sdk/ng-sdk-js/examples/multi-framework-signals/.gitignore
  3. 4
      sdk/ng-sdk-js/examples/multi-framework-signals/README.md
  4. 11
      sdk/ng-sdk-js/examples/multi-framework-signals/astro.config.mjs
  5. 11452
      sdk/ng-sdk-js/examples/multi-framework-signals/package-lock.json
  6. 48
      sdk/ng-sdk-js/examples/multi-framework-signals/package.json
  7. 79
      sdk/ng-sdk-js/examples/multi-framework-signals/playwright.config.ts
  8. 43
      sdk/ng-sdk-js/examples/multi-framework-signals/src/app/components/Highlight.astro
  9. 7
      sdk/ng-sdk-js/examples/multi-framework-signals/src/app/components/ReactRoot.tsx
  10. 5
      sdk/ng-sdk-js/examples/multi-framework-signals/src/app/components/SvelteRoot.svelte
  11. 9
      sdk/ng-sdk-js/examples/multi-framework-signals/src/app/components/VueRoot.vue
  12. 21
      sdk/ng-sdk-js/examples/multi-framework-signals/src/app/layouts/Layout.astro
  13. 24
      sdk/ng-sdk-js/examples/multi-framework-signals/src/app/pages/index.astro
  14. 3
      sdk/ng-sdk-js/examples/multi-framework-signals/src/app/store/state.ts
  15. 152
      sdk/ng-sdk-js/examples/multi-framework-signals/src/frontends/react/HelloWorld.tsx
  16. 122
      sdk/ng-sdk-js/examples/multi-framework-signals/src/frontends/svelte/HelloWorld.svelte
  17. 177
      sdk/ng-sdk-js/examples/multi-framework-signals/src/frontends/tests/reactiveCrossFramework.spec.ts
  18. 43
      sdk/ng-sdk-js/examples/multi-framework-signals/src/frontends/utils/flattenObject.ts
  19. 126
      sdk/ng-sdk-js/examples/multi-framework-signals/src/frontends/vue/HelloWorld.vue
  20. 218
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/tests/applyDiff.test.ts
  21. 56
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/tests/updatesWithWasm.test.ts
  22. 255
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/shapeHandler.ts
  23. 10
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/shapeManager.ts
  24. 14
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/README.md
  25. 149
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildConstruct.ts
  26. 152
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildSelect.ts
  27. 125
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/common.ts
  28. 19
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/types.ts
  29. 22
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/updateShape.ts
  30. 77
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/catShape.schema.compact.ts
  31. 9
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/catShape.shapeTypes.compact.ts
  32. 48
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/catShape.typings.ts
  33. 70
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/personShape.schema.compact.ts
  34. 9
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/personShape.shapeTypes.compact.ts
  35. 44
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/personShape.typings.ts
  36. 121
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/testShape.schema.compact.ts
  37. 9
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/testShape.shapeTypes.compact.ts
  38. 73
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/testShape.typings.ts
  39. 15
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/shex/catShape.shex
  40. 14
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/shex/personShape.shex
  41. 21
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/shex/testShape.shex
  42. 5
      sdk/ng-sdk-js/examples/multi-framework-signals/svelte.config.js
  43. 10
      sdk/ng-sdk-js/examples/multi-framework-signals/tsconfig.json
  44. 24
      sdk/ng-sdk-js/ng-alien-deepsignals/.gitignore
  45. 168
      sdk/ng-sdk-js/ng-alien-deepsignals/README.md
  46. 53
      sdk/ng-sdk-js/ng-alien-deepsignals/package.json
  47. 2200
      sdk/ng-sdk-js/ng-alien-deepsignals/pnpm-lock.yaml
  48. 5
      sdk/ng-sdk-js/ng-alien-deepsignals/src/contents.ts
  49. 163
      sdk/ng-sdk-js/ng-alien-deepsignals/src/core.ts
  50. 833
      sdk/ng-sdk-js/ng-alien-deepsignals/src/deepSignal.ts
  51. 17
      sdk/ng-sdk-js/ng-alien-deepsignals/src/index.ts
  52. 81
      sdk/ng-sdk-js/ng-alien-deepsignals/src/test/core.test.ts
  53. 1119
      sdk/ng-sdk-js/ng-alien-deepsignals/src/test/index.test.ts
  54. 47
      sdk/ng-sdk-js/ng-alien-deepsignals/src/test/patchOptimized.test.ts
  55. 148
      sdk/ng-sdk-js/ng-alien-deepsignals/src/test/tier3.test.ts
  56. 91
      sdk/ng-sdk-js/ng-alien-deepsignals/src/test/watch.test.ts
  57. 357
      sdk/ng-sdk-js/ng-alien-deepsignals/src/test/watchPatches.test.ts
  58. 41
      sdk/ng-sdk-js/ng-alien-deepsignals/src/utils.ts
  59. 188
      sdk/ng-sdk-js/ng-alien-deepsignals/src/watch.ts
  60. 32
      sdk/ng-sdk-js/ng-alien-deepsignals/src/watchEffect.ts
  61. 25
      sdk/ng-sdk-js/ng-alien-deepsignals/tsconfig.json
  62. 14
      sdk/ng-sdk-js/ng-alien-deepsignals/tsup.config.ts
  63. 38
      sdk/ng-sdk-js/ng-ldo-compact/.eslintrc
  64. 22
      sdk/ng-sdk-js/ng-ldo-compact/.gitignore
  65. 4
      sdk/ng-sdk-js/ng-ldo-compact/.vscode/settings.json
  66. 21
      sdk/ng-sdk-js/ng-ldo-compact/LICENSE.txt
  67. 30
      sdk/ng-sdk-js/ng-ldo-compact/Readme.md
  68. 30
      sdk/ng-sdk-js/ng-ldo-compact/jest.config.js
  69. 31
      sdk/ng-sdk-js/ng-ldo-compact/jest.esm.config.js
  70. 4
      sdk/ng-sdk-js/ng-ldo-compact/lerna.json
  71. 48778
      sdk/ng-sdk-js/ng-ldo-compact/package-lock.json
  72. 39
      sdk/ng-sdk-js/ng-ldo-compact/package.json
  73. 3
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/.eslintrc
  74. 2
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/.gitignore
  75. 21
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/LICENSE.txt
  76. 83
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/README.md
  77. 11
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/example-init-placeholder/package.json
  78. 1
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/example-init-placeholder/src/index.ts
  79. 6
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/jest.config.cjs
  80. 67
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/package.json
  81. 96
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/src/build.ts
  82. 119
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/src/create.ts
  83. 107
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/src/generateReadme.ts
  84. 58
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/src/index.ts
  85. 78
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/src/init.ts
  86. 8
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/src/templates/context.ejs
  87. 19
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/src/templates/defaultShapes/foafProfile.ejs
  88. 15
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/src/templates/readme/main.ejs
  89. 2
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/src/templates/readme/projectIndex.ejs
  90. 27
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/src/templates/readme/shape.ejs
  91. 8
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/src/templates/schema.compact.ejs
  92. 8
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/src/templates/schema.ejs
  93. 14
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/src/templates/shapeTypes.compact.ejs
  94. 16
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/src/templates/shapeTypes.ejs
  95. 18
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/src/templates/typings.ejs
  96. 78
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/src/util/forAllShapes.ts
  97. 33
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/src/util/modifyPackageJson.ts
  98. 5
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/test/trivial.test.ts
  99. 9
      sdk/ng-sdk-js/ng-ldo-compact/packages/cli/tsconfig.cjs.json
  100. 3
      sdk/ng-sdk-js/ng-ldo-compact/packages/connected-nextgraph/.eslintrc
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,5 +1,9 @@
packages:
- sdk/ng-sdk-js/pkg
- sdk/ng-sdk-js/ng-signals
- sdk/ng-sdk-js/ng-alien-deepsignals
- sdk/ng-sdk-js/ng-ldo-compact
- sdk/ng-sdk-js/examples/multi-framework-signals
onlyBuiltDependencies:
- '@parcel/watcher'
- '@swc/core'

@ -0,0 +1,42 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
.astro
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

@ -0,0 +1,4 @@
# Multi-Framework Signal Proxies
Thanks for providing the basic multi-framework template:
https://github.com/aleksadencic/multi-framework-app

@ -0,0 +1,11 @@
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import vue from "@astrojs/vue";
import svelte from "@astrojs/svelte";
// https://astro.build/config
export default defineConfig({
integrations: [react(), vue(), svelte()],
srcDir: "./src/app",
});

File diff suppressed because it is too large Load Diff

@ -0,0 +1,48 @@
{
"name": "multi-framework-signals",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"test": "vitest",
"test:e2e": "playwright test"
},
"dependencies": {
"ng-signals": "0.1.0",
"@astrojs/react": "4.3.0",
"@astrojs/svelte": "7.1.0",
"@astrojs/vue": "^5.1.0",
"@gn8/alien-signals-react": "^0.1.1",
"@gn8/alien-signals-solid": "^0.1.1",
"@gn8/alien-signals-svelte": "^0.1.1",
"@gn8/alien-signals-vue": "^0.1.1",
"@ldo/ldo": "^1.0.0-alpha.32",
"@types/react": "19.1.10",
"@types/react-dom": "19.1.7",
"@types/shexj": "^2.1.7",
"ng-alien-deepsignals": "^0.1.0",
"alien-signals": "^2.0.7",
"astro": "5.13.2",
"install": "^0.13.0",
"npm": "^11.5.2",
"prettier-eslint": "^16.4.2",
"react": "19.1.1",
"react-dom": "19.1.1",
"svelte": "5.38.2",
"vue": "3.5.19"
},
"devDependencies": {
"@ldo/traverser-shexj": "^1.0.0-alpha.28",
"@playwright/test": "^1.55.0",
"@types/node": "24.3.0",
"@types/react": "19.1.10",
"@types/react-dom": "19.1.7",
"vite": "7.1.3",
"vitest": "^3.2.4"
}
}

@ -0,0 +1,79 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./src/frontends/tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:4321",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
// {
// name: "chromium",
// use: { ...devices["Desktop Chrome"] },
// },
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: "npm run dev",
url: "http://localhost:4321",
reuseExistingServer: !process.env.CI,
},
});

@ -0,0 +1,43 @@
---
const { react, svelte, vue, astro } = Astro.props;
const frameworkName = Object.keys(Astro.props)[0];
---
<div class:list={["box", { react, svelte, vue, astro }]}>
<p class="title">{frameworkName}</p>
<div class="wrap">
<slot />
</div>
</div>
<style>
.box {
border: 2px solid rgb(88, 88, 81);
border-radius: 1em;
padding: 1em;
margin-top: 1em;
margin-left: auto;
margin-right: auto;
min-width: 60%;
}
.wrap {
padding: 10px;
}
.title {
background: var(--highlightColor);
padding: 0 10px;
}
.vue {
background-color: rgb(255, 212, 255);
}
.react {
background-color: rgb(189, 216, 255);
}
.svelte {
background-color: rgb(255, 216, 189);
}
</style>

@ -0,0 +1,7 @@
import { HelloWorldReact } from "src/frontends/react/HelloWorld";
const Root = () => {
return <HelloWorldReact />;
};
export default Root;

@ -0,0 +1,5 @@
<script lang="ts">
import HelloWorld from "src/frontends/svelte/HelloWorld.svelte";
</script>
<HelloWorld />

@ -0,0 +1,9 @@
<script setup lang="ts">
import HelloWorld from 'src/frontends/vue/HelloWorld.vue';
//
</script>
<template>
<HelloWorld></HelloWorld>
</template>

@ -0,0 +1,21 @@
---
import Highlight from "../components/Highlight.astro";
---
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Multi-Framework Signal Experiments</title>
</head>
<body>
<Highlight astro>
<header class="container">Multi-Framework Signal Experiments</header>
<main class="container">
<slot />
</main>
</Highlight>
</body>
</html>

@ -0,0 +1,24 @@
---
import Layout from "../layouts/Layout.astro";
import Highlight from "../components/Highlight.astro";
import VueRoot from "../components/VueRoot.vue";
import ReactRoot from "../components/ReactRoot";
import SvelteRoot from "../components/SvelteRoot.svelte";
const title = "Multi-framework app";
---
<Layout title={title}>
<Highlight vue>
<VueRoot client:only />
</Highlight>
<Highlight react>
<ReactRoot client:only="react" />
</Highlight>
<Highlight svelte>
<SvelteRoot client:only />
</Highlight>
</Layout>

@ -0,0 +1,3 @@
import { atom } from "nanostores";
export const selectedFilters = atom<string[]>([]);

@ -0,0 +1,152 @@
import React from "react";
import useShape from "ng-signals/frontendAdapters/react/useShape";
import flattenObject from "../utils/flattenObject";
import { TestObjectShapeType } from "src/shapes/ldo/testShape.shapeTypes";
export function HelloWorldReact() {
const state = useShape(TestObjectShapeType);
// @ts-expect-error
window.reactState = state;
if (!state) return <>Loading state</>;
// Create a table from the state object: One column for keys, one for values, one with an input to change the value.
return (
<div>
<p>Rendered in React</p>
<button
onClick={() => {
state.boolValue = !state.boolValue;
state.numValue += 2;
}}
>
click me to change multiple props
</button>
<table border={1} cellPadding={5}>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
<th>Edit</th>
</tr>
</thead>
<tbody>
{(() => {
const setNestedValue = (obj: any, path: string, value: any) => {
const keys = path.split(".");
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
};
const getNestedValue = (obj: any, path: string) => {
return path
.split(".")
.reduce((current, key) => current[key], obj);
};
return flattenObject(state).map(([key, value]) => (
<tr key={key}>
<td>{key}</td>
<td>
{value instanceof Set
? Array.from(value).join(", ")
: Array.isArray(value)
? `[${value.join(", ")}]`
: JSON.stringify(value)}
</td>
<td>
{typeof value === "string" ? (
<input
type="text"
value={value}
onChange={(e) => {
setNestedValue(state, key, e.target.value);
}}
/>
) : typeof value === "number" ? (
<input
type="number"
value={value}
onChange={(e) => {
setNestedValue(state, key, Number(e.target.value));
}}
/>
) : typeof value === "boolean" ? (
<input
type="checkbox"
checked={value}
onChange={(e) => {
setNestedValue(state, key, e.target.checked);
}}
/>
) : Array.isArray(value) ? (
<div>
<button
onClick={() => {
const currentArray = getNestedValue(state, key);
setNestedValue(state, key, [
...currentArray,
currentArray.length + 1,
]);
}}
>
Add
</button>
<button
onClick={() => {
const currentArray = getNestedValue(state, key);
if (currentArray.length > 0) {
setNestedValue(
state,
key,
currentArray.slice(0, -1)
);
}
}}
>
Remove
</button>
</div>
) : value instanceof Set ? (
<div>
<button
onClick={() => {
const currentSet = getNestedValue(state, key);
currentSet.add(`item${currentSet.size + 1}`);
}}
>
Add
</button>
<button
onClick={() => {
const currentSet = getNestedValue(state, key);
const lastItem = Array.from(currentSet).pop();
if (lastItem) {
currentSet.delete(lastItem);
}
}}
>
Remove
</button>
</div>
) : (
"N/A"
)}
</td>
</tr>
));
})()}
</tbody>
</table>
</div>
);
}

@ -0,0 +1,122 @@
<script lang="ts">
import { TestObjectShapeType } from "src/shapes/ldo/testShape.shapeTypes";
import useShape from "ng-signals/frontendAdapters/svelte/useShape.svelte";
import flattenObject from "../utils/flattenObject";
const shapeObject = useShape(TestObjectShapeType);
function getNestedValue(obj: any, path: string) {
return path
.split(".")
.reduce((cur, k) => (cur == null ? cur : cur[k]), obj);
}
function setNestedValue(obj: any, path: string, value: any) {
const keys = path.split(".");
let cur = obj;
for (let i = 0; i < keys.length - 1; i++) {
cur = cur[keys[i]];
if (cur == null) return;
}
cur[keys[keys.length - 1]] = value;
}
const flatEntries = $derived(
$shapeObject ? flattenObject($shapeObject as any) : []
);
$effect(() => {
(window as any).svelteState = $shapeObject;
});
</script>
{#if $shapeObject}
<div>
<p>Rendered in Svelte</p>
<table border="1" cellpadding="5">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
<th>Edit</th>
</tr>
</thead>
<tbody>
{#each flatEntries as [key, value] (key)}
<tr>
<td style="white-space:nowrap;">{key}</td>
<td>
{#if value instanceof Set}
{Array.from(value).join(", ")}
{:else if Array.isArray(value)}
[{value.join(", ")}]
{:else}
{JSON.stringify(value)}
{/if}
</td>
<td>
{#if typeof value === "string"}
<input
type="text"
{value}
oninput={(e: any) =>
setNestedValue($shapeObject, key, e.target.value)}
/>
{:else if typeof value === "number"}
<input
type="number"
{value}
oninput={(e: any) =>
setNestedValue($shapeObject, key, Number(e.target.value))}
/>
{:else if typeof value === "boolean"}
<input
type="checkbox"
checked={value}
onchange={(e: any) =>
setNestedValue($shapeObject, key, e.target.checked)}
/>
{:else if Array.isArray(value)}
<div style="display:flex; gap:.5rem;">
<button
onclick={() => {
const cur = getNestedValue($shapeObject, key) || [];
setNestedValue($shapeObject, key, [
...cur,
cur.length + 1,
]);
}}>Add</button
>
<button
onclick={() => {
const cur = getNestedValue($shapeObject, key) || [];
if (cur.length)
setNestedValue($shapeObject, key, cur.slice(0, -1));
}}>Remove</button
>
</div>
{:else if value instanceof Set}
<div style="display:flex; gap:.5rem;">
<button
onclick={() => {
const cur: Set<any> = getNestedValue($shapeObject, key);
cur.add(`item${cur.size + 1}`);
}}>Add</button
>
<button
onclick={() => {
const cur: Set<any> = getNestedValue($shapeObject, key);
const last = Array.from(cur).pop();
if (last !== undefined) cur.delete(last);
}}>Remove</button
>
</div>
{:else}
N/A
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{:else}
<p>Loading state</p>
{/if}

@ -0,0 +1,177 @@
import { test, expect } from "@playwright/test";
const mockTestObject = {
type: "TestObject",
stringValue: "string",
numValue: 42,
boolValue: true,
nullValue: null,
arrayValue: [1, 2, 3],
objectValue: {
nestedString: "nested",
nestedNum: 7,
nestedArray: [10, 12],
},
setValue: new Set(["v1", "v2", "v3"]),
};
test("components load", async ({ page }) => {
await page.goto("/");
await page.waitForSelector(".vue astro-island");
await expect(page.locator(".vue .title")).toHaveText("vue");
await expect(page.locator(".react .title")).toHaveText("react");
await expect(page.locator(".svelte .title")).toHaveText("svelte");
});
// TODO: Test without signal pooling.
test.describe("cross framework propagation", () => {
const frameworks = ["vue", "react", "svelte"] as const;
const isPlainObject = (v: unknown): v is Record<string, unknown> =>
typeof v === "object" &&
v !== null &&
!Array.isArray(v) &&
!(v instanceof Set);
function changedValue(original: unknown) {
if (typeof original === "string") return original + "_changed";
if (typeof original === "number") return original + 10;
if (typeof original === "boolean") return !original;
if (Array.isArray(original)) return [...original, {}, {}];
if (original instanceof Set) new Set(original).add("_changed");
return original;
}
async function mutateCell(
row: ReturnType<(typeof test)["info"] extends any ? any : never>,
original: unknown
) {
if (typeof original === "string") {
const input = row.locator("input[type='text']");
await input.fill(String(changedValue(original)));
await input.blur();
} else if (typeof original === "number") {
const input = row.locator("input[type='number']");
await input.fill(String(changedValue(original)));
await input.blur();
} else if (typeof original === "boolean") {
const input = row.locator("input[type='checkbox']");
await input.setChecked(Boolean(changedValue(original)));
} else if (Array.isArray(original)) {
const addButton = row.locator("button", { hasText: "Add" });
await addButton.click();
await addButton.click();
}
}
async function assertCell(
row: ReturnType<(typeof test)["info"] extends any ? any : never>,
original: unknown,
meta: { framework: string; key: string }
) {
const { framework, key } = meta;
const expected = changedValue(original);
const cell = row.locator("td").nth(1);
if (typeof original === "string") {
const input = row.locator("input[type='text']");
await expect(
input,
`Text value mismatch (${framework}:${key})`
).toHaveValue(String(expected));
await expect(
cell,
`Rendered text mismatch (${framework}:${key})`
).toContainText(String(expected));
} else if (typeof original === "number") {
const input = row.locator("input[type='number']");
await expect(
input,
`Number value mismatch (${framework}:${key})`
).toHaveValue(String(expected));
await expect(
cell,
`Rendered number mismatch (${framework}:${key})`
).toContainText(String(expected));
} else if (typeof original === "boolean") {
const input = row.locator("input[type='checkbox']");
await expect(
input,
`Checkbox state mismatch (${framework}:${key})`
).toBeChecked({
checked: Boolean(expected),
});
await expect(
cell,
`Rendered boolean mismatch (${framework}:${key})`
).toContainText(String(expected));
} else if (Array.isArray(original)) {
const expectedLength = (original as unknown[]).length + 2;
await expect(
cell,
`Array length mismatch (${framework}:${key}) expected ${expectedLength}`
).toContainText(String(expectedLength));
}
}
for (const source of frameworks) {
for (const target of frameworks) {
if (source === target) continue;
test(`${source} edits propagate to ${target}`, async ({ page }) => {
await page.goto("/");
await page.waitForSelector(".vue astro-island");
// Mutate in source
await test.step(`Mutate values in ${source}`, async () => {
for (const [key, value] of Object.entries(mockTestObject)) {
if (isPlainObject(value)) {
for (const [k2, v2] of Object.entries(value)) {
const fullKey = `${key}.${k2}`;
const row = page.locator(`.${source} tr`, { hasText: fullKey });
await mutateCell(row, v2);
}
} else {
const row = page.locator(`.${source} tr`, { hasText: key });
await mutateCell(row, value);
}
}
});
// Assert in target
await test.step(`Assert propagation into ${target}`, async () => {
for (const [key, value] of Object.entries(mockTestObject)) {
if (isPlainObject(value)) {
for (const [k2, v2] of Object.entries(value)) {
const fullKey = `${key}.${k2}`;
const row = page.locator(`.${target} tr`, { hasText: fullKey });
await assertCell(row, v2, { framework: target, key: fullKey });
}
} else {
const row = page.locator(`.${target} tr`, { hasText: key });
await assertCell(row, value, { framework: target, key });
}
}
});
// Optional: also ensure source reflects its own changes (helps isolate failures)
await test.step(`Validate mutated source ${source}`, async () => {
for (const [key, value] of Object.entries(mockTestObject)) {
if (isPlainObject(value)) {
for (const [k2, v2] of Object.entries(value)) {
const fullKey = `${key}.${k2}`;
const row = page.locator(`.${source} tr`, { hasText: fullKey });
await assertCell(row, v2, { framework: source, key: fullKey });
}
} else {
const row = page.locator(`.${source} tr`, { hasText: key });
await assertCell(row, value, { framework: source, key });
}
}
});
});
}
}
});

@ -0,0 +1,43 @@
interface FlattenOptions {
/** Maximum depth to traverse (default: 8). */
maxDepth?: number;
/** Skip keys that start with a dollar sign (deepSignal meta). Default: true */
skipDollarKeys?: boolean;
}
const isPlainObject = (v: any) =>
Object.prototype.toString.call(v) === "[object Object]";
const flattenObject = (
obj: any,
prefix = "",
options: FlattenOptions = {},
seen = new Set<any>(),
depth = 0
): Array<[string, any, string, any]> => {
const { maxDepth = 8, skipDollarKeys = true } = options;
const result: Array<[string, any, string, any]> = [];
if (!obj || typeof obj !== "object") return result;
if (seen.has(obj)) return result; // cycle detected
seen.add(obj);
if (depth > maxDepth) return result;
for (const [key, value] of Object.entries(obj)) {
if (skipDollarKeys && key.startsWith("$")) continue;
const fullKey = prefix ? `${prefix}.${key}` : key;
if (
value &&
typeof value === "object" &&
!Array.isArray(value) &&
!(value instanceof Set) &&
isPlainObject(value)
) {
result.push(...flattenObject(value, fullKey, options, seen, depth + 1));
} else {
result.push([fullKey, value, key, obj]);
}
}
return result;
};
export default flattenObject;

@ -0,0 +1,126 @@
<script setup lang="ts">
import { computed } from 'vue';
import useShape from 'ng-signals/frontendAdapters/vue/useShape';
import flattenObject from '../utils/flattenObject';
import { TestObjectShapeType } from 'src/shapes/ldo/testShape.shapeTypes';
// Acquire deep signal object (proxy) for a shape; scope second arg left empty string for parity
const shapeObj = useShape(TestObjectShapeType);
// Expose for devtools exploration
// @ts-ignore
window.vueState = shapeObj;
const flatEntries = computed(() => flattenObject(shapeObj));
</script>
<template>
<div class="vue">
<p>Rendered in Vue</p>
<template v-if="shapeObj && shapeObj.type">
<!-- Direct property access -->
<input type="text" v-model="shapeObj.type" />
<input type="text" v-model="shapeObj.objectValue.nestedString" />
<!-- Property access through object recursion -->
<table border="1" cellpadding="5" style="margin-top:1rem; max-width:100%; font-size:0.9rem;">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
<th>Edit</th>
</tr>
</thead>
<tbody>
<tr v-for="([path, value, key, parent]) in flatEntries" :key="path">
<!-- Key-->
<td style="white-space:nowrap;">{{ path }}</td>
<!-- Value -->
<td>
<template v-if="value instanceof Set">
{{ Array.from(value).join(', ') }}
</template>
<template v-else-if="Array.isArray(value)">
[{{ value.join(', ') }}]
</template>
<template v-else>
{{ JSON.stringify(value) }}
</template>
</td>
<!-- Edit -->
<td>
<!-- String editing -->
<template v-if="typeof value === 'string'">
<template v-if="path.indexOf('.') === -1">
<input type="text" v-model="(shapeObj)[key]" />
</template>
<template v-else>
<input type="text" v-bind:value="(parent)[key]"
v-on:input="(e) => { (parent)[key] = (e.target as any).value; }" />
</template>
</template>
<!-- Number editing -->
<template v-else-if="typeof value === 'number'">
<template v-if="path.indexOf('.') === -1">
<input type="number" v-model="(shapeObj)[key]" />
</template>
<template v-else>
<input type="number" v-bind:value="(parent)[key]"
v-on:input="(e) => { (parent)[key] = +(e.target as any).value; }" />
</template>
</template>
<!-- Boolean editing -->
<template v-else-if="typeof value === 'boolean'">
<template v-if="path.indexOf('.') === -1">
<input type="checkbox" v-model="(shapeObj)[key]" />
</template>
<template v-else>
<input type="checkbox" v-bind:value="value"
v-on:input="(e) => { (parent)[key] = (e.target as any).value; }" />
</template>
</template>
<!-- Array editing -->
<template v-else-if="Array.isArray(value)">
<template v-if="path.indexOf('.') === -1">
<div style="display:flex; gap:.5rem;">
<button @click="() => { parent[key] = [...value, value.length + 1] }">Add</button>
<button @click="() => { parent[key] = value.slice(1) }">Remove</button>
</div>
</template>
<template v-else>
<div style="display:flex; gap:.5rem;">
<button @click="() => { parent[key] = [...value, value.length + 1] }">Add</button>
<button @click="() => { parent[key] = value.slice(1) }">Remove</button>
</div>
</template>
</template>
<!-- Set editing -->
<template v-else-if="value instanceof Set">
<div style="display:flex; gap:.5rem;">
<button @click="() => { value.add(`item${value.size + 1}`); }">Add</button>
<button
@click="() => { const last = Array.from(value).pop(); if (last !== undefined) value.delete(last); }">Remove</button>
</div>
</template>
<template v-else>
N/A
</template>
</td>
</tr>
</tbody>
</table>
</template>
<template v-else>
<p>Loading state</p>
</template>
</div>
</template>

@ -0,0 +1,218 @@
import { describe, test, expect } from "vitest";
import {
applyDiff,
applyDiffToDeepSignal,
} from "ng-signals/connector/applyDiff";
import type { Patch } from "ng-signals/connector/applyDiff";
/**
* Build a patch path string from segments (auto-prefix /)
*/
function p(...segs: (string | number)[]) {
return "/" + segs.map(String).join("/");
}
describe("applyDiff - set operations (primitives)", () => {
test("add single primitive into new set", () => {
const state: any = {};
const diff: Patch[] = [
{ op: "add", type: "set", path: p("tags"), value: "a" },
];
applyDiff(state, diff);
expect(state.tags).toBeInstanceOf(Set);
expect([...state.tags]).toEqual(["a"]);
});
test("add multiple primitives into new set", () => {
const state: any = {};
const diff: Patch[] = [
{ op: "add", type: "set", path: p("nums"), value: [1, 2, 3] },
];
applyDiff(state, diff);
expect([...state.nums]).toEqual([1, 2, 3]);
});
test("add primitives merging into existing set", () => {
const state: any = { nums: new Set([1]) };
const diff: Patch[] = [
{ op: "add", type: "set", path: p("nums"), value: [2, 3] },
];
applyDiff(state, diff);
expect([...state.nums].sort()).toEqual([1, 2, 3]);
});
test("remove single primitive from set", () => {
const state: any = { tags: new Set(["a", "b"]) };
const diff: Patch[] = [
{ op: "remove", type: "set", path: p("tags"), value: "a" },
];
applyDiff(state, diff);
expect([...state.tags]).toEqual(["b"]);
});
test("remove multiple primitives from set", () => {
const state: any = { nums: new Set([1, 2, 3, 4]) };
const diff: Patch[] = [
{ op: "remove", type: "set", path: p("nums"), value: [2, 4] },
];
applyDiff(state, diff);
expect([...state.nums].sort()).toEqual([1, 3]);
});
});
describe("applyDiff - set operations (object sets)", () => {
test("add object entries to new object-set", () => {
const state: any = {};
const diff: Patch[] = [
{
op: "add",
type: "set",
path: p("users"),
value: { u1: { id: "u1", n: 1 }, u2: { id: "u2", n: 2 } },
},
];
applyDiff(state, diff);
expect(state.users.u1).toEqual({ id: "u1", n: 1 });
expect(state.users.u2).toEqual({ id: "u2", n: 2 });
});
test("merge object entries into existing object-set", () => {
const state: any = { users: { u1: { id: "u1", n: 1 } } };
const diff: Patch[] = [
{
op: "add",
type: "set",
path: p("users"),
value: { u2: { id: "u2", n: 2 } },
},
];
applyDiff(state, diff);
expect(Object.keys(state.users).sort()).toEqual(["u1", "u2"]);
});
test("remove object entries from object-set", () => {
const state: any = { users: { u1: {}, u2: {}, u3: {} } };
const diff: Patch[] = [
{ op: "remove", type: "set", path: p("users"), value: ["u1", "u3"] },
];
applyDiff(state, diff);
expect(Object.keys(state.users)).toEqual(["u2"]);
});
test("adding primitives to existing object-set replaces with Set", () => {
const state: any = { mixed: { a: {}, b: {} } };
const diff: Patch[] = [
{ op: "add", type: "set", path: p("mixed"), value: [1, 2] },
];
applyDiff(state, diff);
expect(state.mixed).toBeInstanceOf(Set);
expect([...state.mixed]).toEqual([1, 2]);
});
});
describe("applyDiff - object & literal operations", () => {
test("add object (create empty object)", () => {
const state: any = {};
const diff: Patch[] = [{ op: "add", path: p("address"), type: "object" }];
applyDiff(state, diff);
expect(state.address).toEqual({});
});
test("add nested object path with ensurePathExists", () => {
const state: any = {};
const diff: Patch[] = [
{ op: "add", path: p("a", "b", "c"), type: "object" },
];
applyDiff(state, diff, true);
expect(state.a.b.c).toEqual({});
});
test("add primitive value", () => {
const state: any = { address: {} };
const diff: Patch[] = [
{ op: "add", path: p("address", "street"), value: "1st" },
];
applyDiff(state, diff);
expect(state.address.street).toBe("1st");
});
test("overwrite primitive value", () => {
const state: any = { address: { street: "old" } };
const diff: Patch[] = [
{ op: "add", path: p("address", "street"), value: "new" },
];
applyDiff(state, diff);
expect(state.address.street).toBe("new");
});
test("remove primitive", () => {
const state: any = { address: { street: "1st", country: "Greece" } };
const diff: Patch[] = [{ op: "remove", path: p("address", "street") }];
applyDiff(state, diff);
expect(state.address.street).toBeUndefined();
expect(state.address.country).toBe("Greece");
});
test("remove object branch", () => {
const state: any = { address: { street: "1st" }, other: 1 };
const diff: Patch[] = [{ op: "remove", path: p("address") }];
applyDiff(state, diff);
expect(state.address).toBeUndefined();
expect(state.other).toBe(1);
});
});
describe("applyDiff - multiple mixed patches in a single diff", () => {
test("sequence of mixed set/object/literal add & remove", () => {
const state: any = {
users: { u1: { id: "u1" } },
tags: new Set(["old"]),
};
const diff: Patch[] = [
{ op: "add", type: "set", path: p("users"), value: { u2: { id: "u2" } } },
{ op: "add", path: p("profile"), type: "object" },
{ op: "add", path: p("profile", "name"), value: "Alice" },
{ op: "add", type: "set", path: p("tags"), value: ["new"] },
{ op: "remove", type: "set", path: p("tags"), value: "old" },
];
applyDiff(state, diff);
expect(Object.keys(state.users).sort()).toEqual(["u1", "u2"]);
expect(state.profile.name).toBe("Alice");
expect([...state.tags]).toEqual(["new"]);
});
test("complex nested path creation and mutations with ensurePathExists", () => {
const state: any = {};
const diff: Patch[] = [
{ op: "add", path: p("a", "b"), type: "object" },
{ op: "add", path: p("a", "b", "c"), value: 1 },
{ op: "add", type: "set", path: p("a", "nums"), value: [1, 2, 3] },
{ op: "remove", type: "set", path: p("a", "nums"), value: 2 },
{ op: "add", path: p("a", "b", "d"), value: 2 },
{ op: "remove", path: p("a", "b", "c") },
];
applyDiff(state, diff, true);
expect(state.a.b.c).toBeUndefined();
expect(state.a.b.d).toBe(2);
expect(state.a.nums).toBeInstanceOf(Set);
expect([...state.a.nums].sort()).toEqual([1, 3]);
});
});
describe("applyDiff - ignored / invalid scenarios", () => {
test("skip patch with non-leading slash path", () => {
const state: any = {};
const diff: Patch[] = [{ op: "add", path: "address/street", value: "x" }];
applyDiff(state, diff);
expect(state).toEqual({});
});
test("missing parent without ensurePathExists -> patch skipped and no mutation", () => {
const state: any = {};
const diff: Patch[] = [{ op: "add", path: p("a", "b", "c"), value: 1 }];
applyDiff(state, diff, false);
expect(state).toEqual({});
});
});
describe("applyDiff - ignored / invalid scenarios", () => {
test("skip patch with non-leading slash path", () => {
const state: any = {};
const diff: Patch[] = [{ op: "add", path: "address/street", value: "x" }];
applyDiff(state, diff);
expect(state).toEqual({});
});
test("missing parent without ensurePathExists -> patch skipped and no mutation", () => {
const state: any = {};
const diff: Patch[] = [{ op: "add", path: p("a", "b", "c"), value: 1 }];
applyDiff(state, diff, false);
expect(state).toEqual({});
});
});

@ -0,0 +1,56 @@
import { describe, expect, test } from "vitest";
import { createSignalObjectForShape } from "ng-signals/connector/createSignalObjectForShape.ts";
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
describe("Signal modification and propagation to backend with or without signal pooling", () => {
for (const withPooling of [true, false]) {
test(`shape object notification comes back to others ${
withPooling ? "with" : "without"
} signal pooling`, async () => {
const object1 = createSignalObjectForShape(
"TestShape",
undefined,
withPooling
);
const object2 = createSignalObjectForShape(
"TestShape",
undefined,
withPooling
);
const object3 = createSignalObjectForShape(
"Shape2",
undefined,
withPooling
);
const object4 = createSignalObjectForShape(
"Shape2",
undefined,
withPooling
);
await wait(10);
// Update object 1 and expect object 2 to update as well.
// @ts-expect-error
object1.name = "Updated name from object1";
await wait(10);
// @ts-expect-error
expect(object2.name).toBe("Updated name from object1");
// Expect object of different shape not to have changed.
// @ts-expect-error
expect(object3.name).toBe("Niko's cat");
// Update object 4 and expect object 3 with same shape to have updated.
// @ts-expect-error
object4.name = "Updated name from object4";
await wait(10);
// @ts-expect-error
expect(object3!.name).toBe("Updated name from object4");
});
}
});

@ -0,0 +1,255 @@
import * as shapeManager from "./shapeManager";
import type { WasmConnection, Diff, Scope } from "./types";
import type { CompactShapeType, LdoCompactBase } from "@ldo/ldo";
import type { Person } from "src/shapes/ldo/personShape.typings";
import type { Cat } from "src/shapes/ldo/catShape.typings";
import type { TestObject } from "src/shapes/ldo/testShape.typings";
import updateShape from "./updateShape";
// Messages exchanged over the BroadcastChannel("shape-manager")
interface WasmMessage {
type:
| "Request"
| "InitialResponse"
| "FrontendUpdate"
| "BackendUpdate"
| "Stop";
connectionId: string;
diff?: Diff;
schema?: CompactShapeType<any>["schema"];
initialData?: LdoCompactBase;
}
export const mockTestObject = {
id: "ex:mock-id-1",
type: "TestObject",
stringValue: "string",
numValue: 42,
boolValue: true,
arrayValue: [1, 2, 3],
objectValue: {
id: "urn:obj-1",
nestedString: "nested",
nestedNum: 7,
nestedArray: [10, 12],
},
anotherObject: {
"id:1": {
id: "id:1",
prop1: "prop1 value",
prop2: 100,
},
"id:2": {
id: "id:1",
prop1: "prop2 value",
prop2: 200,
},
},
} satisfies TestObject;
const mockShapeObject1 = {
id: "ex:person-1",
type: "Person",
name: "Bob",
address: {
id: "urn:person-home-1",
street: "First street",
houseNumber: "15",
},
hasChildren: true,
numberOfHouses: 0,
} satisfies Person;
const mockShapeObject2 = {
id: "ex:cat-1",
type: "Cat",
name: "Niko's cat",
age: 12,
numberOfHomes: 3,
address: {
id: "Nikos-cat-home",
street: "Niko's street",
houseNumber: "15",
floor: 0,
},
} satisfies Cat;
// Single BroadcastChannel for wasm-land side
const communicationChannel = new BroadcastChannel("shape-manager");
function getInitialObjectByShapeId<T extends LdoCompactBase>(
shapeId?: string,
): T {
if (shapeId?.includes("TestObject")) return mockTestObject as unknown as T;
if (shapeId?.includes("Person")) return mockShapeObject1 as unknown as T;
if (shapeId?.includes("Cat")) return mockShapeObject2 as unknown as T;
console.warn(
"BACKEND: requestShape for unknown shape, returning empty object.",
shapeId,
);
return {} as T;
}
// Register handler for messages coming from js-land
communicationChannel.addEventListener(
"message",
(event: MessageEvent<WasmMessage>) => {
console.log("BACKEND: Received message", event.data);
const { type, connectionId, schema } = event.data;
if (type === "Request") {
const shapeId = schema?.shapes?.[0]?.id;
const initialData = getInitialObjectByShapeId(shapeId);
// Store connection. We store the shapeId string to allow equality across connections.
shapeManager.connections.set(connectionId, {
id: connectionId,
// Cast to any to satisfy WasmConnection type, comparison in updateShape uses ==
shape: (shapeId ?? "__unknown__") as any,
state: initialData,
callback: (diff: Diff, conId: WasmConnection["id"]) => {
// Notify js-land about backend updates
const msg: WasmMessage = {
type: "BackendUpdate",
connectionId: conId,
diff,
};
communicationChannel.postMessage(msg);
},
});
const msg: WasmMessage = {
type: "InitialResponse",
connectionId,
initialData,
};
communicationChannel.postMessage(msg);
return;
}
if (type === "Stop") {
shapeManager.connections.delete(connectionId);
return;
}
if (type === "FrontendUpdate" && event.data.diff) {
updateShape(connectionId, event.data.diff);
return;
}
console.warn("BACKEND: Unknown message type or missing diff", event.data);
},
);
// Keep the original function for compatibility with any direct callers.
let connectionIdCounter = 1;
export default async function requestShape<T extends LdoCompactBase>(
shape: CompactShapeType<T>,
_scope: Scope | undefined,
callback: (diff: Diff, connectionId: WasmConnection["id"]) => void,
): Promise<{ connectionId: string; shapeObject: T }> {
const connectionId = `connection-${connectionIdCounter++}-${shape.schema.shapes?.[0]?.id}`;
const shapeId = shape.schema.shapes?.[0]?.id;
const shapeObject = getInitialObjectByShapeId<T>(shapeId);
shapeManager.connections.set(connectionId, {
id: connectionId,
shape: (shapeId ?? "__unknown__") as any,
state: shapeObject,
callback,
});
return { connectionId, shapeObject };
}
const getObjectsForShapeType = <T extends LdoCompactBase>(
shape: CompactShapeType<T>,
scope: string = "",
): T[] => {
// Procedure
// - Get all triples for the scope
// - Parse the schema (all shapes and anonymous shapes required for the shape type).
// - Group triples by subject
// - For the shapeType in the schema, match all required predicates
// - For predicates pointing to nested objects
// - recurse
// Repeat procedure for all matched subjects with optional predicates
const quads: [
string,
string,
number | string | boolean,
string | undefined,
][] = [];
// The URI of the shape to find matches for.
const schemaId = shape.shape;
// ShexJ shape object
const rootShapeDecl = shape.schema.shapes?.find(
(shape) => shape.id === schemaId,
);
if (!rootShapeDecl)
throw new Error(`Could not find shape id ${schemaId} in shape schema`);
if (rootShapeDecl.shapeExpr.type !== "Shape")
throw new Error("Expected shapeExpr.type to be Shape");
const shapeExpression = rootShapeDecl.shapeExpr.expression;
// If shape is a reference...
if (typeof shapeExpression === "string") {
// TODO: Recurse
return [];
}
const requiredPredicates = [];
const optionalPredicates = [];
if (shapeExpression?.type === "EachOf") {
const predicates = shapeExpression.expressions.map((constraint) => {
if (typeof constraint === "string") {
// Cannot parse constraint refs
return;
} else if (constraint.type === "TripleConstraint") {
requiredPredicates.push({
predicate: constraint.predicate,
});
} else {
// EachOf or OneOf possible?
}
});
} else if (shapeExpression?.type === "OneOf") {
// Does not occur AFAIK.
} else if (shapeExpression?.type === "TripleConstraint") {
// Does not occur AFAIK.
}
return [];
};
interface ShapeConstraintTracked {
subject: string;
childOf?: ShapeConstraintTracked;
predicates: [
{
displayName: string;
uri: string;
type: "number" | "string" | "boolean" | "nested" | "literal";
literalValue?: number | string | boolean | number[] | string[];
nested?: ShapeConstraintTracked;
min: number;
max: number;
currentCount: number;
},
];
}
// Group by subject, check predicates of root level
// For all subjects of root level,
// - recurse
// Construct matching subjects
// for each optional and non-optional predicate
// - fill objects and record
// - build tracked object (keeping reference counts to check if the object is still valid)

@ -0,0 +1,10 @@
import type { Diff, ObjectState, WasmConnection } from "./types";
const connections: Map<WasmConnection["id"], WasmConnection> = new Map();
/** Mock function to apply diffs. Just uses a copy of the diff as the new object. */
export function applyDiff(currentState: ObjectState, diff: Diff): ObjectState {
return JSON.parse(JSON.stringify(diff));
}
export { connections };

@ -0,0 +1,14 @@
# SPARQL builders
Utilities to build SPARQL SELECT and CONSTRUCT queries from a ShapeConstraint structure.
Exports:
- buildSelectQuery(shape, options)
- buildConstructQuery(shape, options)
Options:
- prefixes: Record<prefix, IRI>
- graph: named graph IRI or CURIE
- includeOptionalForMinZero: wrap min=0 predicates in OPTIONAL (default true)

@ -0,0 +1,149 @@
import type {
BuildContext,
PredicateConstraint,
ShapeConstraint,
SparqlBuildOptions,
} from "./common";
import {
predicateToSparql,
prefixesToText,
toIriOrCurie,
uniqueVar,
valuesBlock,
varToken,
} from "./common";
/**
* Build a SPARQL CONSTRUCT query from a ShapeConstraint definition.
* The WHERE mirrors the graph template. Optional predicates (min=0) are wrapped in OPTIONAL in WHERE
* but still appear in the CONSTRUCT template so that matched triples are constructed.
*/
export function buildConstructQuery(
shape: ShapeConstraint,
options?: SparqlBuildOptions,
): string {
const ctx: BuildContext = { usedVars: new Set<string>() };
const prefixes = prefixesToText(options?.prefixes);
const subject = toIriOrCurie(shape.subject);
const templateLines: string[] = [];
const whereLines: string[] = [];
const postFilters: string[] = [];
const valuesBlocks: string[] = [];
const rootVar =
subject.startsWith("?") || subject.startsWith("$")
? subject
: uniqueVar(ctx, "s");
if (!subject.startsWith("?") && !subject.startsWith("$")) {
valuesBlocks.push(valuesBlock(rootVar, [subject] as any));
}
const predicates = Array.isArray(shape.predicates)
? shape.predicates
: [...shape.predicates];
for (const pred of predicates) {
addConstructPattern(
ctx,
pred,
rootVar,
templateLines,
whereLines,
postFilters,
valuesBlocks,
options,
);
}
const graphWrap = (body: string) =>
options?.graph
? `GRAPH ${toIriOrCurie(options.graph)} {\n${body}\n}`
: body;
const where = [
...valuesBlocks,
graphWrap(whereLines.join("\n")),
...postFilters,
]
.filter(Boolean)
.join("\n");
const template = templateLines.join("\n");
return [prefixes, `CONSTRUCT {`, template, `} WHERE {`, where, `}`].join(
"\n",
);
}
function addConstructPattern(
ctx: BuildContext,
pred: PredicateConstraint,
subjectVar: string,
template: string[],
where: string[],
postFilters: string[],
valuesBlocks: string[],
options?: SparqlBuildOptions,
) {
const p = predicateToSparql(pred.uri);
const objVar = uniqueVar(ctx, pred.displayName || "o");
const objTerm =
pred.type === "nested" &&
pred.nested?.subject &&
!pred.nested.subject.match(/^\?|^\$/)
? toIriOrCurie(pred.nested.subject)
: objVar;
const triple = `${subjectVar} ${p} ${objTerm} .`;
const isOptional =
(pred.min ?? 0) === 0 && (options?.includeOptionalForMinZero ?? true);
if (pred.type === "nested" && pred.nested) {
template.push(triple);
const nestedBody: string[] = [triple];
const nestedPreds = Array.isArray(pred.nested.predicates)
? pred.nested.predicates
: [...pred.nested.predicates];
for (const n of nestedPreds) {
addConstructPattern(
ctx,
n,
objTerm,
template,
nestedBody,
postFilters,
valuesBlocks,
options,
);
}
const block = nestedBody.join("\n");
where.push(isOptional ? `OPTIONAL {\n${block}\n}` : block);
return;
}
// Non-nested
template.push(triple);
const blockLines: string[] = [triple];
if (pred.type === "literal" && pred.literalValue !== undefined) {
if (Array.isArray(pred.literalValue)) {
valuesBlocks.push(valuesBlock(objVar, pred.literalValue as any[]));
} else {
const lit =
typeof pred.literalValue === "string" ||
typeof pred.literalValue === "number" ||
typeof pred.literalValue === "boolean"
? pred.literalValue
: String(pred.literalValue);
postFilters.push(
`FILTER(${objVar} = ${typeof lit === "string" ? `"${String(lit).replace(/"/g, '\\"')}"` : lit})`,
);
}
}
const block = blockLines.join("\n");
where.push(isOptional ? `OPTIONAL {\n${block}\n}` : block);
}
export default buildConstructQuery;

@ -0,0 +1,152 @@
import type {
BuildContext,
PredicateConstraint,
ShapeConstraint,
SparqlBuildOptions,
} from "./common";
import {
predicateToSparql,
prefixesToText,
toIriOrCurie,
uniqueVar,
valuesBlock,
varToken,
} from "./common";
/**
* Build a SPARQL SELECT query from a ShapeConstraint definition.
* The query matches the shape subject and constraints; optional predicates (min=0) are wrapped in OPTIONAL.
*/
export function buildSelectQuery(
shape: ShapeConstraint,
options?: SparqlBuildOptions,
): string {
const ctx: BuildContext = { usedVars: new Set<string>() };
const prefixes = prefixesToText(options?.prefixes);
const subject = toIriOrCurie(shape.subject);
const selectVars: string[] = [];
const whereLines: string[] = [];
const postFilters: string[] = [];
const valuesBlocks: string[] = [];
// ensure a consistent root variable when subject is a variable
const rootVar =
subject.startsWith("?") || subject.startsWith("$")
? subject
: uniqueVar(ctx, "s");
if (!subject.startsWith("?") && !subject.startsWith("$")) {
// bind fixed subject via VALUES for portability
valuesBlocks.push(valuesBlock(rootVar, [subject] as any));
}
const predicates = Array.isArray(shape.predicates)
? shape.predicates
: [...shape.predicates];
for (const pred of predicates) {
addPredicatePattern(
ctx,
pred,
rootVar,
whereLines,
selectVars,
postFilters,
valuesBlocks,
options,
);
}
const graphWrap = (body: string) =>
options?.graph
? `GRAPH ${toIriOrCurie(options.graph)} {\n${body}\n}`
: body;
const where = [
...valuesBlocks,
graphWrap(whereLines.join("\n")),
...postFilters,
]
.filter(Boolean)
.join("\n");
const select = selectVars.length ? selectVars.join(" ") : "*";
return [prefixes, `SELECT ${select} WHERE {`, where, `}`].join("\n");
}
function addPredicatePattern(
ctx: BuildContext,
pred: PredicateConstraint,
subjectVar: string,
where: string[],
selectVars: string[],
postFilters: string[],
valuesBlocks: string[],
options?: SparqlBuildOptions,
) {
const p = predicateToSparql(pred.uri);
const objVar = uniqueVar(ctx, pred.displayName || "o");
const objTerm =
pred.type === "nested" &&
pred.nested?.subject &&
!pred.nested.subject.match(/^\?|^\$/)
? toIriOrCurie(pred.nested.subject)
: objVar;
const triple = `${subjectVar} ${p} ${objTerm} .`;
const isOptional =
(pred.min ?? 0) === 0 && (options?.includeOptionalForMinZero ?? true);
if (pred.type === "nested" && pred.nested) {
// For nested, we select the nested object var and then recurse
if (objTerm === objVar) selectVars.push(objVar);
const nestedBody: string[] = [triple];
const nestedPreds = Array.isArray(pred.nested.predicates)
? pred.nested.predicates
: [...pred.nested.predicates];
for (const n of nestedPreds) {
addPredicatePattern(
ctx,
n,
objTerm,
nestedBody,
selectVars,
postFilters,
valuesBlocks,
options,
);
}
const block = nestedBody.join("\n");
where.push(isOptional ? `OPTIONAL {\n${block}\n}` : block);
return;
}
// Non-nested: literals or IRIs
selectVars.push(objVar);
const blockLines: string[] = [triple];
if (pred.type === "literal" && pred.literalValue !== undefined) {
if (Array.isArray(pred.literalValue)) {
// VALUES block for IN-like matching
valuesBlocks.push(valuesBlock(objVar, pred.literalValue as any[]));
} else {
// simple equality filter
const lit =
typeof pred.literalValue === "string" ||
typeof pred.literalValue === "number" ||
typeof pred.literalValue === "boolean"
? pred.literalValue
: String(pred.literalValue);
postFilters.push(
`FILTER(${objVar} = ${typeof lit === "string" ? `"${String(lit).replace(/"/g, '\\"')}"` : lit})`,
);
}
}
const block = blockLines.join("\n");
where.push(isOptional ? `OPTIONAL {\n${block}\n}` : block);
}
export default buildSelectQuery;

@ -0,0 +1,125 @@
/**
* Shared helpers and types to build SPARQL queries from ShapeConstraint
*/
export type LiteralKind =
| "number"
| "string"
| "boolean"
| "nested"
| "literal";
export interface PredicateConstraint {
displayName: string;
uri: string;
type: LiteralKind;
literalValue?: number | string | boolean | number[] | string[];
nested?: ShapeConstraint;
min: number;
max: number;
currentCount: number;
}
export interface ShapeConstraint {
subject: string;
// In upstream code this is typed as a 1-length tuple; we normalize to an array here
predicates: PredicateConstraint[] | [PredicateConstraint];
}
export interface SparqlBuildOptions {
prefixes?: Record<string, string>;
graph?: string; // IRI of the named graph to query, if any
includeOptionalForMinZero?: boolean; // default true
}
export const defaultPrefixes: Record<string, string> = {
xsd: "http://www.w3.org/2001/XMLSchema#",
rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
};
export function prefixesToText(prefixes?: Record<string, string>): string {
const all = { ...defaultPrefixes, ...(prefixes ?? {}) };
return Object.entries(all)
.map(([p, iri]) => `PREFIX ${p}: <${iri}>`)
.join("\n");
}
export function toIriOrCurie(term: string): string {
// variable
if (term.startsWith("?") || term.startsWith("$")) return term;
// blank node
if (term.startsWith("_:")) return term;
// full IRI
if (term.includes("://")) return `<${term}>`;
// fallback: assume CURIE or already-angled
if (term.startsWith("<") && term.endsWith(">")) return term;
return term; // CURIE, caller must ensure prefix provided
}
export function predicateToSparql(uri: string): string {
// Allow CURIEs or IRIs
return toIriOrCurie(uri);
}
export function safeVarName(name: string): string {
const base = name
.replace(/[^a-zA-Z0-9_]/g, "_")
.replace(/^([0-9])/, "_$1")
.slice(0, 60);
return base || "v";
}
export function varToken(name: string): string {
const n = name.startsWith("?") || name.startsWith("$") ? name.slice(1) : name;
return `?${safeVarName(n)}`;
}
export function formatLiteral(value: string | number | boolean): string {
if (typeof value === "number") return String(value);
if (typeof value === "boolean") return value ? "true" : "false";
// default string literal
const escaped = value.replace(/"/g, '\\"');
return `"${escaped}"`;
}
export function formatTermForValues(value: string | number | boolean): string {
if (typeof value === "number" || typeof value === "boolean")
return formatLiteral(value);
// strings: detect IRI or CURIE and keep raw; otherwise quote
const v = value.trim();
const looksLikeIri = v.startsWith("<") && v.endsWith(">");
const looksLikeHttp = v.includes("://");
const looksLikeCurie =
/^[A-Za-z_][A-Za-z0-9_-]*:.+$/u.test(v) && !looksLikeHttp;
if (looksLikeIri || looksLikeHttp || looksLikeCurie) {
return looksLikeHttp ? `<${v}>` : v;
}
return formatLiteral(v);
}
export function valuesBlock(
varName: string,
values: Array<string | number | boolean>,
): string {
const rendered = values.map(formatTermForValues).join(" ");
return `VALUES ${varName} { ${rendered} }`;
}
export interface BuildContext {
// Tracks used variable names to avoid collisions
usedVars: Set<string>;
}
export function uniqueVar(ctx: BuildContext, base: string): string {
let candidate = varToken(base);
if (!ctx.usedVars.has(candidate)) {
ctx.usedVars.add(candidate);
return candidate;
}
let i = 2;
while (ctx.usedVars.has(`${candidate}_${i}`)) i++;
const unique = `${candidate}_${i}`;
ctx.usedVars.add(unique);
return unique;
}

@ -0,0 +1,19 @@
import type { CompactShapeType, LdoCompactBase } from "@ldo/ldo";
import type { Patch } from "ng-signals/connector/applyDiff";
import type { Shape } from "ng-signals/types";
/** The Scope of a shape request */
export type Scope = string | string[];
/** The diff format used to communicate updates between wasm-land and js-land. */
export type Diff = Patch[];
export type ObjectState = object;
/** A connection established between wasm-land and js-land for subscription of a shape. */
export type WasmConnection<T extends LdoCompactBase = LdoCompactBase> = {
id: string;
shape: CompactShapeType<T>;
state: ObjectState;
callback: (diff: Diff, connectionId: WasmConnection["id"]) => void;
};

@ -0,0 +1,22 @@
import * as shapeManager from "./shapeManager";
import type { WasmConnection, Diff } from "./types";
export default async function updateShape(
connectionId: WasmConnection["id"],
diff: Diff,
) {
const connection = shapeManager.connections.get(connectionId);
if (!connection) throw new Error("No Connection found.");
console.log("BACKEND: Received update request from ", connectionId);
const newState = shapeManager.applyDiff(connection.state, diff);
connection.state = newState;
shapeManager.connections.forEach((con) => {
// if (con.shape == connection.shape) {
// con.state = newState;
// con.callback(diff, con.id);
// }
});
}

@ -0,0 +1,77 @@
import type { CompactSchema } from "@ldo/ldo";
/**
* =============================================================================
* catShapeSchema: Compact Schema for catShape
* =============================================================================
*/
export const catShapeSchema: CompactSchema = {
"http://example.org/Cat": {
iri: "http://example.org/Cat",
predicates: [
{
type: "literal",
literalValue: ["Cat"],
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
readablePredicate: "type",
},
{
type: "string",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/name",
readablePredicate: "name",
},
{
type: "number",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/age",
readablePredicate: "age",
},
{
type: "number",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/numberOfHomes",
readablePredicate: "numberOfHomes",
},
{
type: "nested",
nestedSchema: "http://example.org/Cat::http://example.org/address",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/address",
readablePredicate: "address",
},
],
},
"http://example.org/Cat::http://example.org/address": {
iri: "http://example.org/Cat::http://example.org/address",
predicates: [
{
type: "string",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/street",
readablePredicate: "street",
},
{
type: "string",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/houseNumber",
readablePredicate: "houseNumber",
},
{
type: "number",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/floor",
readablePredicate: "floor",
},
],
},
};

@ -0,0 +1,9 @@
import type { CompactShapeType } from "@ldo/ldo";
import { catShapeSchema } from "./catShape.schema";
import type { Cat } from "./catShape.typings";
// Compact ShapeTypes for catShape
export const CatShapeType: CompactShapeType<Cat> = {
schema: catShapeSchema,
shape: "http://example.org/Cat",
};

@ -0,0 +1,48 @@
export type IRI = string;
/**
* =============================================================================
* Typescript Typings for catShape
* =============================================================================
*/
/**
* Cat Type
*/
export interface Cat {
id: IRI;
/**
* Original IRI: http://www.w3.org/1999/02/22-rdf-syntax-ns#type
*/
type: "Cat";
/**
* Original IRI: http://example.org/name
*/
name: string;
/**
* Original IRI: http://example.org/age
*/
age: number;
/**
* Original IRI: http://example.org/numberOfHomes
*/
numberOfHomes: number;
/**
* Original IRI: http://example.org/address
*/
address: {
id: IRI;
/**
* Original IRI: http://example.org/street
*/
street: string;
/**
* Original IRI: http://example.org/houseNumber
*/
houseNumber: string;
/**
* Original IRI: http://example.org/floor
*/
floor: number;
};
}

@ -0,0 +1,70 @@
import type { CompactSchema } from "@ldo/ldo";
/**
* =============================================================================
* personShapeSchema: Compact Schema for personShape
* =============================================================================
*/
export const personShapeSchema: CompactSchema = {
"http://example.org/Person": {
iri: "http://example.org/Person",
predicates: [
{
type: "literal",
literalValue: ["Person"],
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
readablePredicate: "type",
},
{
type: "string",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/name",
readablePredicate: "name",
},
{
type: "nested",
nestedSchema: "http://example.org/Person::http://example.org/address",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/address",
readablePredicate: "address",
},
{
type: "boolean",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/hasChildren",
readablePredicate: "hasChildren",
},
{
type: "number",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/numberOfHouses",
readablePredicate: "numberOfHouses",
},
],
},
"http://example.org/Person::http://example.org/address": {
iri: "http://example.org/Person::http://example.org/address",
predicates: [
{
type: "string",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/street",
readablePredicate: "street",
},
{
type: "string",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/houseNumber",
readablePredicate: "houseNumber",
},
],
},
};

@ -0,0 +1,9 @@
import type { CompactShapeType } from "@ldo/ldo";
import { personShapeSchema } from "./personShape.schema";
import type { Person } from "./personShape.typings";
// Compact ShapeTypes for personShape
export const PersonShapeType: CompactShapeType<Person> = {
schema: personShapeSchema,
shape: "http://example.org/Person",
};

@ -0,0 +1,44 @@
export type IRI = string;
/**
* =============================================================================
* Typescript Typings for personShape
* =============================================================================
*/
/**
* Person Type
*/
export interface Person {
id: IRI;
/**
* Original IRI: http://www.w3.org/1999/02/22-rdf-syntax-ns#type
*/
type: "Person";
/**
* Original IRI: http://example.org/name
*/
name: string;
/**
* Original IRI: http://example.org/address
*/
address: {
id: IRI;
/**
* Original IRI: http://example.org/street
*/
street: string;
/**
* Original IRI: http://example.org/houseNumber
*/
houseNumber: string;
};
/**
* Original IRI: http://example.org/hasChildren
*/
hasChildren: boolean;
/**
* Original IRI: http://example.org/numberOfHouses
*/
numberOfHouses: number;
}

@ -0,0 +1,121 @@
import type { CompactSchema } from "@ldo/ldo";
/**
* =============================================================================
* testShapeSchema: Compact Schema for testShape
* =============================================================================
*/
export const testShapeSchema: CompactSchema = {
"http://example.org/TestObject": {
iri: "http://example.org/TestObject",
predicates: [
{
type: "literal",
literalValue: ["TestObject"],
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
readablePredicate: "type",
extra: true,
},
{
type: "string",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/stringValue",
readablePredicate: "stringValue",
},
{
type: "number",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/numValue",
readablePredicate: "numValue",
},
{
type: "boolean",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/boolValue",
readablePredicate: "boolValue",
},
{
type: "number",
maxCardinality: -1,
minCardinality: 0,
predicateUri: "http://example.org/arrayValue",
readablePredicate: "arrayValue",
},
{
type: "nested",
nestedSchema:
"http://example.org/TestObject::http://example.org/objectValue",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/objectValue",
readablePredicate: "objectValue",
},
{
type: "nested",
nestedSchema:
"http://example.org/TestObject::http://example.org/anotherObject",
maxCardinality: -1,
minCardinality: 0,
predicateUri: "http://example.org/anotherObject",
readablePredicate: "anotherObject",
},
{
type: "string",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/numOrStr",
readablePredicate: "numOrStr",
},
],
},
"http://example.org/TestObject::http://example.org/objectValue": {
iri: "http://example.org/TestObject::http://example.org/objectValue",
predicates: [
{
type: "string",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/nestedString",
readablePredicate: "nestedString",
},
{
type: "number",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/nestedNum",
readablePredicate: "nestedNum",
},
{
type: "number",
maxCardinality: -1,
minCardinality: 0,
predicateUri: "http://example.org/nestedArray",
readablePredicate: "nestedArray",
},
],
},
"http://example.org/TestObject::http://example.org/anotherObject": {
iri: "http://example.org/TestObject::http://example.org/anotherObject",
predicates: [
{
type: "string",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/prop1",
readablePredicate: "prop1",
},
{
type: "number",
maxCardinality: 1,
minCardinality: 1,
predicateUri: "http://example.org/prop2",
readablePredicate: "prop2",
},
],
},
};

@ -0,0 +1,9 @@
import type { CompactShapeType } from "@ldo/ldo";
import { testShapeSchema } from "./testShape.schema";
import type { TestObject } from "./testShape.typings";
// Compact ShapeTypes for testShape
export const TestObjectShapeType: CompactShapeType<TestObject> = {
schema: testShapeSchema,
shape: "http://example.org/TestObject",
};

@ -0,0 +1,73 @@
export type IRI = string;
/**
* =============================================================================
* Typescript Typings for testShape
* =============================================================================
*/
/**
* TestObject Type
*/
export interface TestObject {
id: IRI;
/**
* Original IRI: http://www.w3.org/1999/02/22-rdf-syntax-ns#type
*/
type: "TestObject";
/**
* Original IRI: http://example.org/stringValue
*/
stringValue: string;
/**
* Original IRI: http://example.org/numValue
*/
numValue: number;
/**
* Original IRI: http://example.org/boolValue
*/
boolValue: boolean;
/**
* Original IRI: http://example.org/arrayValue
*/
arrayValue?: Set<number>;
/**
* Original IRI: http://example.org/objectValue
*/
objectValue: {
id: IRI;
/**
* Original IRI: http://example.org/nestedString
*/
nestedString: string;
/**
* Original IRI: http://example.org/nestedNum
*/
nestedNum: number;
/**
* Original IRI: http://example.org/nestedArray
*/
nestedArray?: Set<number>;
};
/**
* Original IRI: http://example.org/anotherObject
*/
anotherObject?: Record<
IRI,
{
id: IRI;
/**
* Original IRI: http://example.org/prop1
*/
prop1: string;
/**
* Original IRI: http://example.org/prop2
*/
prop2: number;
}
>;
/**
* Original IRI: http://example.org/numOrStr
*/
numOrStr: string;
}

@ -0,0 +1,15 @@
PREFIX ex: <http://example.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
ex:Cat {
a ["Cat"] ;
ex:name xsd:string ;
ex:age xsd:integer ;
ex:numberOfHomes xsd:integer ;
ex:address {
ex:street xsd:string ;
ex:houseNumber xsd:string ;
ex:floor xsd:integer
}
}

@ -0,0 +1,14 @@
PREFIX ex: <http://example.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
ex:Person {
a ["Person"] ;
ex:name xsd:string ;
ex:address {
ex:street xsd:string ;
ex:houseNumber xsd:string
} ;
ex:hasChildren xsd:boolean ;
ex:numberOfHouses xsd:integer ;
}

@ -0,0 +1,21 @@
PREFIX ex: <http://example.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
ex:TestObject EXTRA a {
a ["TestObject"] ;
ex:stringValue xsd:string ;
ex:numValue xsd:integer ;
ex:boolValue xsd:boolean ;
ex:arrayValue xsd:integer* ;
ex:objectValue {
ex:nestedString xsd:string ;
ex:nestedNum xsd:integer ;
ex:nestedArray xsd:integer* ;
} ;
ex:anotherObject {
ex:prop1 xsd:string;
ex:prop2 xsd:integer ;
} * ;
ex:numOrStr xsd:string;
# TODO: ShapeOr -- | ex:numOrStr xsd:integer
}

@ -0,0 +1,5 @@
import { vitePreprocess } from "@astrojs/svelte";
export default {
preprocess: vitePreprocess(),
};

@ -0,0 +1,10 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react",
"baseUrl": "."
},
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.sln
*.sw?
coverage

@ -0,0 +1,168 @@
# alien-deepsignals
Deep structural reactivity for plain objects / arrays / Sets built on top of `alien-signals`.
Core idea: wrap a data tree in a `Proxy` that lazily creates per-property signals the first time you read them. Accessing a property returns the plain value; accessing `$prop` returns the underlying signal function. Deep mutations emit compact batched patch objects you can observe with `watch()`.
## Features
* Lazy: signals & child proxies created only when touched.
* Deep: nested objects, arrays, Sets proxied on demand.
* [ ] TODO: Methods might not be proxied (e.g. array.push)?
* Per-property signals: fine‑grained invalidation without traversal on each change.
* Patch stream: microtask‑batched granular mutations (paths + op) for syncing external stores / framework adapters.
* Getter => computed: property getters become derived (readonly) signals automatically.
* `$` accessors: TypeScript exposes `$prop` for each non‑function key plus `$` / `$length` for arrays.
* Sets: structural `add/delete/clear` emit patches; object entries get synthetic stable ids (prefers `id` / `@id` fields or auto‑generated blank IDs).
* Shallow escape hatch: wrap sub-objects with `shallow(obj)` to track only reference replacement.
## Install
```bash
pnpm add alien-deepsignals
# or
npm i alien-deepsignals
```
## Quick start
```ts
import { deepSignal } from 'alien-deepsignals'
const state = deepSignal({
count: 0,
user: { name: 'Ada' },
items: [{ id: 'i1', qty: 1 }],
settings: new Set(['dark'])
})
state.count++ // mutate normally
state.user.name = 'Grace' // nested write
state.items.push({ id: 'i2', qty: 2 })
state.settings.add('beta')
// Direct signal access
state.$count!.set(5) // update via signal
console.log(state.$count!()) // read via signal function
```
## Watching patches
`watch(root, cb, options?)` observes a deepSignal root and invokes your callback with microtask‑batched mutation patches plus snapshots.
```ts
import { watch } from 'alien-deepsignals'
const stop = watch(state, ({ patches, oldValue, newValue }) => {
for (const p of patches) {
console.log(p.op, p.path.join('.'), 'value' in p ? p.value : p.type)
}
})
state.user.name = 'Lin'
state.items[0].qty = 3
await Promise.resolve() // flush microtask
stop()
```
### Callback event shape
```ts
type WatchPatchEvent<T> = {
patches: DeepPatch[] // empty only on immediate
oldValue: T | undefined // deep-cloned snapshot before batch
newValue: T // live proxy (already mutated)
registerCleanup(fn): void // register disposer for next batch/stop
stopListening(): void // unsubscribe
}
```
### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `immediate` | boolean | false | Fire once right away with `patches: []`. |
| `once` | boolean | false | Auto stop after first callback (immediate counts). |
`observe()` is an alias of `watch()`.
## DeepPatch format
```ts
type DeepPatch = {
root: symbol // stable id per deepSignal root
path: (string | number)[] // root-relative segments
} & (
| { op: 'add'; type: 'object' } // assigned object/array/Set entry object
| { op: 'add'; value: string | number | boolean } // primitive write
| { op: 'remove' } // deletion
| { op: 'add'; type: 'set'; value: [] } // Set.clear()
| { op: 'add'; type: 'set'; value: (string|number|boolean)[] | { [id: string]: object } } // (reserved)
)
```
Notes:
* `type:'object'` omits value to avoid deep cloning; read from `newValue` if needed.
* `Set.add(entry)` emits object vs primitive form depending on entry type; path ends with synthetic id.
* `Set.clear()` emits one structural patch and suppresses per‑entry removals in same batch.
## Sets & synthetic ids
Object entries inside Sets need a stable key. Priority:
1. `entry.id`
2. `entry['@id']`
3. Custom via `setSetEntrySyntheticId(entry, 'myId')` before `add`
4. Auto `_bN` blank id
Helpers:
```ts
import { addWithId, setSetEntrySyntheticId } from 'alien-deepsignals'
setSetEntrySyntheticId(obj, 'custom')
state.settings.add(obj)
addWithId(state.settings as any, { x:1 }, 'x1')
```
## Shallow
Skip deep proxying of a subtree (only reference replacement tracked):
```ts
import { shallow } from 'alien-deepsignals'
state.config = shallow({ huge: { blob: true } })
```
## TypeScript ergonomics
`DeepSignal<T>` exposes both plain properties and optional `$prop` signal accessors (excluded for function members). Arrays add `$` (index signal map) and `$length`.
```ts
const state = deepSignal({ count: 0, user: { name: 'A' } })
state.count++ // ok
state.$count!.set(9) // write via signal
const n: number = state.$count!() // typed number
```
## API surface
| Function | Description |
|----------|-------------|
| `deepSignal(obj)` | Create (or reuse) reactive deep proxy. |
| `watch(root, cb, opts?)` | Observe batched deep mutations. |
| `observe(root, cb, opts?)` | Alias of `watch`. |
| `peek(obj,key)` | Untracked property read. |
| `shallow(obj)` | Mark object to skip deep proxying. |
| `isDeepSignal(val)` | Runtime predicate. |
| `isShallow(val)` | Was value marked shallow. |
| `setSetEntrySyntheticId(obj,id)` | Assign custom Set entry id. |
| `addWithId(set, entry, id)` | Insert with desired synthetic id. |
| `subscribeDeepMutations(root, cb)` | Low-level patch stream (used by watch). |
## Credits
Inspired by [deepsignal](https://github.com/luisherranz/deepsignal) – thanks to @luisherranz. Re-imagined with patch batching & Set support.
## License
MIT

@ -0,0 +1,53 @@
{
"name": "@nextgraph-monorepo/ng-alien-deepsignals",
"version": "0.1.0",
"private": false,
"author": "CCherry07",
"license": "MIT",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"description": "AlienDeepSignals 🧶 -alien signals, but using regular JavaScript objects",
"scripts": {
"test": "vitest --coverage",
"dev": "tsup --watch src",
"build": "tsup",
"release": "bumpp && npm run build && npm publish --registry=https://registry.npmjs.org/"
},
"dependencies": {
"alien-signals": "^2.0.7"
},
"devDependencies": {
"@types/node": "^22.10.9",
"@vitest/coverage-v8": "3.0.2",
"bumpp": "^9.9.2",
"tsup": "^8.3.5",
"typescript": "^5.4.3",
"vitest": "^3.0.2"
},
"repository": {
"type": "git",
"url": "git+https://github.com/CCherry07/alien-deepsignals.git"
},
"keywords": [
"signal",
"signals",
"deepsignals",
"alien-signals",
"alien-deepsignals"
],
"bugs": {
"url": "https://github.com/CCherry07/alien-deepsignals/issues"
},
"homepage": "https://github.com/CCherry07/alien-deepsignals#readme",
"packageManager": "pnpm@9.14.2+sha512.6e2baf77d06b9362294152c851c4f278ede37ab1eba3a55fda317a4a17b209f4dbb973fb250a77abc463a341fcb1f17f17cfa24091c4eb319cda0d9b84278387"
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,5 @@
export enum ReactiveFlags {
IS_SIGNAL = '__v_isSignal',
SKIP = "__v_skip",
IS_SHALLOW = "__v_isShallow",
}

@ -0,0 +1,163 @@
/** Lightweight façade adding ergonomic helpers (.value/.peek/.get/.set) to native alien-signals function signals. */
// Native re-exports for advanced usage.
export {
signal as _rawSignal,
computed as _rawComputed,
effect,
startBatch,
endBatch,
getCurrentSub,
setCurrentSub,
} from "alien-signals";
import {
signal as alienSignal,
computed as alienComputed,
effect as alienEffect,
startBatch as alienStartBatch,
endBatch as alienEndBatch,
} from "alien-signals";
import { ReactiveFlags as ReactiveFlags_ } from "./contents";
import { isFunction } from "./utils";
// Nominal constructor removal: we no longer expose classes; signals are plain tagged functions.
/** Internal shape of a tagged writable signal after adding ergonomic helpers. */
type TaggedSignal<T> = ReturnType<typeof alienSignal<T>> & {
/** Tracking read / write via property syntax */
value: T;
/** Non-tracking read */
peek(): T;
/** Alias for tracking read */
get(): T;
/** Write helper */
set(v: T): void;
};
/**
* Decorate a native signal function with legacy helpers & identity.
*/
function tagSignal(fn: any): TaggedSignal<any> {
Object.defineProperty(fn, ReactiveFlags_.IS_SIGNAL, { value: true });
Object.defineProperty(fn, "value", {
get: () => fn(),
set: (v) => fn(v),
});
// Add peek to mirror old API (non-tracking read)
if (!fn.peek) Object.defineProperty(fn, "peek", { value: () => fn() });
if (!fn.get) Object.defineProperty(fn, "get", { value: () => fn() });
if (!fn.set) Object.defineProperty(fn, "set", { value: (v: any) => fn(v) });
return fn;
}
/**
* Decorate a native computed function similarly (readonly value accessor).
*/
function tagComputed(fn: any) {
Object.defineProperty(fn, ReactiveFlags_.IS_SIGNAL, { value: true });
Object.defineProperty(fn, "value", { get: () => fn() });
if (!fn.peek) Object.defineProperty(fn, "peek", { value: () => fn() });
if (!fn.get) Object.defineProperty(fn, "get", { value: () => fn() });
return fn;
}
/**
* Create a new writable function-form signal enhanced with `.value`, `.peek()`, `.get()`, `.set()`.
*
* @example
* const count = signal(0);
* count(); // 0 (track)
* count(1); // write
* count.value; // 1 (track)
* count.peek(); // 1 (non-tracking)
*/
export const signal = <T>(v?: T) => tagSignal(alienSignal(v));
/**
* Create a lazy computed (readonly) signal derived from other signals.
* The returned function is tagged with `.value` and `.peek()` for convenience.
*/
export const computed = <T>(getter: () => T) =>
tagComputed(alienComputed(getter));
/** Union allowing a plain value or a writable signal wrapping that value. */
export type MaybeSignal<T = any> = T | ReturnType<typeof signal>;
/** Union allowing value, writable signal, computed signal or plain getter function. */
export type MaybeSignalOrGetter<T = any> =
| MaybeSignal<T>
| ReturnType<typeof computed>
| (() => T);
/** Runtime guard that an unknown value is one of our tagged signals/computeds. */
export const isSignal = (s: any): boolean =>
typeof s === "function" && !!s && !!s[ReactiveFlags_.IS_SIGNAL];
/**
* Minimal Effect wrapper for legacy watch implementation.
* Provides: active, dirty, scheduler hook, run() & stop().
*/
/**
* Minimal Effect wrapper mimicking the legacy interface used by the watch implementation.
*
* Each instance wraps a native alien `effect`, setting `dirty=true` on invalidation and invoking
* the provided scheduler callback. Consumers may manually `run()` the getter (marks clean) or `stop()`
* to dispose the underlying reactive subscription.
*/
export class Effect {
public active = true;
public dirty = true;
public scheduler: (immediateFirstRun?: boolean) => void = () => {};
private _runner: any;
constructor(private _getter: () => any) {
const self = this;
this._runner = alienEffect(function wrapped() {
self.dirty = true;
self._getter();
self.scheduler();
});
}
run() {
this.dirty = false;
return this._getter();
}
stop() {
if (this.active) {
this._runner();
this.active = false;
}
}
}
/** Resolve a plain value, a signal/computed or a getter function to its current value. */
// Lightweight direct resolver (inlined former toValue/unSignal logic)
/**
* Resolve a possibly reactive input to its current value.
* Accepts: plain value, writable signal, computed signal, or getter function.
* Signals & getters are invoked once; plain values are returned directly.
*/
export function toValue<T>(src: MaybeSignalOrGetter<T>): T {
return isFunction(src)
? (src as any)()
: isSignal(src)
? (src as any)()
: (src as any);
}
/**
* Execute multiple signal writes in a single batched update frame.
* All downstream computed/effect re-evaluations are deferred until the function exits.
*
* IMPORTANT: The callback MUST be synchronous. If it returns a Promise the batch will
* still end immediately after scheduling, possibly causing mid-async flushes.
*
* @example
* batch(() => {
* count(count() + 1);
* other(other() + 2);
* }); // effects observing both run only once
*/
export function batch<T>(fn: () => T): T {
alienStartBatch();
try {
return fn();
} finally {
alienEndBatch();
}
}

@ -0,0 +1,833 @@
import { ReactiveFlags } from "./contents";
import { computed, signal, isSignal } from "./core";
/**
* deepSignal: wrap an object / array / Set graph in lazy per-property signals plus an optional deep patch stream.
* - `$prop` returns a signal; plain prop returns its current value.
* - Getter props become computed signals.
* - Arrays expose `$` (index signals) & `$length`; Sets emit structural entry patches with synthetic ids.
* - subscribeDeepMutations(root, cb) batches set/delete ops per microtask (DeepPatch[]).
* - shallow(obj) skips deep proxying of a subtree.
*/
/** A batched deep mutation (set/add/remove) from a deepSignal root. */
export type DeepPatch = {
/** Unique identifier for the deep signal root which produced this patch. */
root: symbol;
/** Property path (array indices, object keys, synthetic Set entry ids) from the root to the mutated location. */
path: (string | number)[];
} & (
| DeepSetAddPatch
| DeepSetRemovePatch
| DeepObjectAddPatch
| DeepRemovePatch
| DeepLiteralAddPatch
);
export interface DeepSetAddPatch {
/** Mutation kind applied at the resolved `path`. */
op: "add";
type: "set";
/** New value for `set` mutations (omitted for `delete`). */
value: (number | string | boolean)[] | { [id: string]: object };
}
export interface DeepSetRemovePatch {
/** Mutation kind applied at the resolved `path`. */
op: "remove";
type: "set";
/** The value to be removed from the set. Either a literal or the key (id) of an object. */
value: string | number | boolean;
}
export interface DeepObjectAddPatch {
/** Mutation kind applied at the resolved `path`. */
op: "add";
type: "object";
}
export interface DeepRemovePatch {
/** Mutation kind applied at the resolved `path`. */
op: "remove";
}
export interface DeepLiteralAddPatch {
/** Mutation kind applied at the resolved `path` */
op: "add";
/** The literal value to be added at the resolved `path` */
value: string | number | boolean;
}
/** Callback signature for subscribeDeepMutations. */
export type DeepPatchSubscriber = (patches: DeepPatch[]) => void;
/** Minimal per-proxy metadata for path reconstruction. */
interface ProxyMeta {
/** Parent proxy in the object graph (undefined for root). */
parent?: object;
/** Key within the parent pointing to this proxy (undefined for root). */
key?: string | number;
/** Stable root id symbol shared by the entire deepSignal tree. */
root: symbol;
}
// Proxy -> metadata
const proxyMeta = new WeakMap<object, ProxyMeta>();
// Root symbol -> subscribers
const mutationSubscribers = new Map<symbol, Set<DeepPatchSubscriber>>();
// Pending patches grouped per root (flushed once per microtask)
let pendingPatches: Map<symbol, DeepPatch[]> | null = null;
let microtaskScheduled = false;
/** Sentinel symbol; get concrete root id via getDeepSignalRootId(proxy). */
export const DEEP_SIGNAL_ROOT_ID = Symbol("alienDeepSignalRootId");
function buildPath(
startProxy: object,
leafKey: string | number
): (string | number)[] {
const path: (string | number)[] = [leafKey];
let cur: object | undefined = startProxy;
while (cur) {
const meta = proxyMeta.get(cur);
if (!meta) break; // Defensive: metadata should always exist.
if (meta.key === undefined) break; // Reached root (no key recorded).
path.unshift(meta.key);
cur = meta.parent;
}
return path;
}
function queuePatch(patch: DeepPatch) {
if (!pendingPatches) pendingPatches = new Map();
const root = patch.root;
let list = pendingPatches.get(root);
if (!list) {
list = [];
pendingPatches.set(root, list);
}
list.push(patch);
if (!microtaskScheduled) {
microtaskScheduled = true;
queueMicrotask(() => {
microtaskScheduled = false;
const groups = pendingPatches;
pendingPatches = null;
if (!groups) return;
for (const [rootId, patches] of groups) {
if (!patches.length) continue;
const subs = mutationSubscribers.get(rootId);
if (subs) subs.forEach((cb) => cb(patches));
}
});
}
}
/** Subscribe to microtask-batched deep patches for a root (returns unsubscribe). */
export function subscribeDeepMutations(
root: object | symbol,
sub: DeepPatchSubscriber
): () => void {
const rootId = typeof root === "symbol" ? root : getDeepSignalRootId(root);
if (!rootId)
throw new Error(
"subscribeDeepMutations() expects a deepSignal root proxy or root id symbol"
);
let set = mutationSubscribers.get(rootId);
if (!set) {
set = new Set();
mutationSubscribers.set(rootId, set);
}
set.add(sub);
return () => {
const bucket = mutationSubscribers.get(rootId);
if (!bucket) return;
bucket.delete(sub);
if (bucket.size === 0) mutationSubscribers.delete(rootId);
};
}
/** Return the stable root symbol for any deepSignal proxy (undefined if not one). */
export function getDeepSignalRootId(obj: any): symbol | undefined {
return proxyMeta.get(obj)?.root;
}
// Proxy -> Map of property name -> signal function
/** Proxy -> Map<propertyName, signalFn> (lazy). */
const proxyToSignals = new WeakMap();
// Raw object/array/Set -> stable proxy
const objToProxy = new WeakMap();
// Raw array -> `$` meta proxy with index signals
const arrayToArrayOfSignals = new WeakMap();
// Objects already proxied or marked shallow
const ignore = new WeakSet();
// Object -> signal counter for enumeration invalidation
const objToIterable = new WeakMap();
const rg = /^\$/;
const descriptor = Object.getOwnPropertyDescriptor;
let peeking = false;
// Deep array interface refining callback parameter types.
type DeepArray<T> = Array<T> & {
map: <U>(
callbackfn: (
value: DeepSignal<T>,
index: number,
array: DeepSignalArray<T[]>
) => U,
thisArg?: any
) => U[];
forEach: (
callbackfn: (
value: DeepSignal<T>,
index: number,
array: DeepSignalArray<T[]>
) => void,
thisArg?: any
) => void;
concat(...items: ConcatArray<T>[]): DeepSignalArray<T[]>;
concat(...items: (T | ConcatArray<T>)[]): DeepSignalArray<T[]>;
reverse(): DeepSignalArray<T[]>;
shift(): DeepSignal<T> | undefined;
slice(start?: number, end?: number): DeepSignalArray<T[]>;
splice(start: number, deleteCount?: number): DeepSignalArray<T[]>;
splice(
start: number,
deleteCount: number,
...items: T[]
): DeepSignalArray<T[]>;
filter<S extends T>(
predicate: (
value: DeepSignal<T>,
index: number,
array: DeepSignalArray<T[]>
) => value is DeepSignal<S>,
thisArg?: any
): DeepSignalArray<S[]>;
filter(
predicate: (
value: DeepSignal<T>,
index: number,
array: DeepSignalArray<T[]>
) => unknown,
thisArg?: any
): DeepSignalArray<T[]>;
reduce(
callbackfn: (
previousValue: DeepSignal<T>,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => T
): DeepSignal<T>;
reduce(
callbackfn: (
previousValue: DeepSignal<T>,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => DeepSignal<T>,
initialValue: T
): DeepSignal<T>;
reduce<U>(
callbackfn: (
previousValue: U,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => U,
initialValue: U
): U;
reduceRight(
callbackfn: (
previousValue: DeepSignal<T>,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => T
): DeepSignal<T>;
reduceRight(
callbackfn: (
previousValue: DeepSignal<T>,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => DeepSignal<T>,
initialValue: T
): DeepSignal<T>;
reduceRight<U>(
callbackfn: (
previousValue: U,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => U,
initialValue: U
): U;
};
// Synthetic ids for Set entry objects (stable key for patches)
let __blankNodeCounter = 0;
const setObjectIds = new WeakMap<object, string>();
const assignBlankNodeId = (obj: any) => {
if (setObjectIds.has(obj)) return setObjectIds.get(obj)!;
const id = `_b${++__blankNodeCounter}`;
setObjectIds.set(obj, id);
return id;
};
/** Assign (or override) synthetic id before Set.add(). */
export function setSetEntrySyntheticId(obj: object, id: string | number) {
setObjectIds.set(obj, String(id));
}
const getSetEntryKey = (val: any): string | number => {
if (val && typeof val === "object") {
if (setObjectIds.has(val)) return setObjectIds.get(val)!;
if (
typeof (val as any).id === "string" ||
typeof (val as any).id === "number"
)
return (val as any).id;
if (
typeof (val as any)["@id"] === "string" ||
typeof (val as any)["@id"] === "number"
)
return (val as any)["@id"];
return assignBlankNodeId(val);
}
return val as any;
};
/** Add entry with synthetic id; returns proxied object if applicable. */
export function addWithId<T extends object>(
set: Set<T>,
entry: T,
id: string | number
): DeepSignal<T>;
export function addWithId<T>(set: Set<T>, entry: T, id: string | number): T;
export function addWithId(set: Set<any>, entry: any, id: string | number) {
if (entry && typeof entry === "object") setSetEntrySyntheticId(entry, id);
(set as any).add(entry);
if (entry && typeof entry === "object" && objToProxy.has(entry))
return objToProxy.get(entry);
return entry;
}
/** Is value a deepSignal-managed proxy? */
export const isDeepSignal = (source: any) => {
return proxyToSignals.has(source);
};
/** Was value explicitly marked shallow? */
export const isShallow = (source: any) => {
return ignore.has(source);
};
/** Create (or reuse) a deep reactive proxy for an object / array / Set. */
export const deepSignal = <T extends object>(obj: T): DeepSignal<T> => {
if (!shouldProxy(obj)) throw new Error("This object can't be observed.");
if (!objToProxy.has(obj)) {
// Create a unique root id symbol to identify this deep signal tree in patches.
const rootId = Symbol("deepSignalRoot");
const proxy = createProxy(obj, objectHandlers, rootId) as DeepSignal<T>;
const meta = proxyMeta.get(proxy)!;
meta.parent = undefined; // root has no parent
meta.key = undefined; // root not addressed by a key
meta.root = rootId; // ensure root id stored (explicit)
// Pre-register an empty signals map so isDeepSignal() is true before any property access.
if (!proxyToSignals.has(proxy)) proxyToSignals.set(proxy, new Map());
objToProxy.set(obj, proxy);
}
return objToProxy.get(obj);
};
/** Read property without tracking (untracked read). */
export const peek = <
T extends DeepSignalObject<object>,
K extends keyof RevertDeepSignalObject<T>
>(
obj: T,
key: K
): RevertDeepSignal<RevertDeepSignalObject<T>[K]> => {
peeking = true;
const value = obj[key];
try {
peeking = false;
} catch (e) {}
return value as RevertDeepSignal<RevertDeepSignalObject<T>[K]>;
};
const shallowFlag = Symbol(ReactiveFlags.IS_SHALLOW);
/** Mark object to skip deep proxying (only reference changes tracked). */
export function shallow<T extends object>(obj: T): Shallow<T> {
ignore.add(obj);
return obj as Shallow<T>;
}
const createProxy = (
target: object,
handlers: ProxyHandler<object>,
rootId?: symbol
) => {
const proxy = new Proxy(target, handlers);
ignore.add(proxy);
// Initialize proxy metadata if not present. Root proxies provide a stable root id.
if (!proxyMeta.has(proxy)) {
proxyMeta.set(proxy, { root: rootId || Symbol("deepSignalDetachedRoot") });
}
return proxy;
};
// Set-specific access & structural patch emission.
function getFromSet(
raw: Set<any>,
key: string | symbol,
receiver: object
): any {
const meta = proxyMeta.get(receiver);
// Helper to proxy a single entry (object) & assign synthetic id if needed.
const ensureEntryProxy = (entry: any) => {
if (
entry &&
typeof entry === "object" &&
shouldProxy(entry) &&
!objToProxy.has(entry)
) {
const synthetic = getSetEntryKey(entry);
const childProxy = createProxy(entry, objectHandlers, meta!.root);
const childMeta = proxyMeta.get(childProxy)!;
childMeta.parent = receiver;
childMeta.key = synthetic;
objToProxy.set(entry, childProxy);
return childProxy;
}
if (objToProxy.has(entry)) return objToProxy.get(entry);
return entry;
};
// Pre-pass to ensure any existing non-proxied object entries are proxied (enables deep patches after iteration)
if (meta) raw.forEach(ensureEntryProxy);
if (key === "add" || key === "delete" || key === "clear") {
const fn: Function = (raw as any)[key];
return function (this: any, ...args: any[]) {
const sizeBefore = raw.size;
const result = fn.apply(raw, args);
if (raw.size !== sizeBefore) {
const metaNow = proxyMeta.get(receiver);
if (
metaNow &&
metaNow.parent !== undefined &&
metaNow.key !== undefined
) {
const containerPath = buildPath(metaNow.parent, metaNow.key);
if (key === "add") {
const entry = args[0];
let synthetic = getSetEntryKey(entry);
if (entry && typeof entry === "object") {
for (const existing of raw.values()) {
if (existing === entry) continue;
if (getSetEntryKey(existing) === synthetic) {
synthetic = assignBlankNodeId(entry);
break;
}
}
}
let entryVal = entry;
if (
entryVal &&
typeof entryVal === "object" &&
shouldProxy(entryVal) &&
!objToProxy.has(entryVal)
) {
const childProxy = createProxy(
entryVal,
objectHandlers,
metaNow.root
);
const childMeta = proxyMeta.get(childProxy)!;
childMeta.parent = receiver;
childMeta.key = synthetic;
objToProxy.set(entryVal, childProxy);
entryVal = childProxy;
}
// Set entry add: emit object vs literal variant.
if (entryVal && typeof entryVal === "object") {
queuePatch({
root: metaNow.root,
path: [...containerPath, synthetic],
op: "add",
type: "object",
});
} else {
queuePatch({
root: metaNow.root,
path: [...containerPath, synthetic],
op: "add",
value: entryVal,
});
}
} else if (key === "delete") {
const entry = args[0];
const synthetic = getSetEntryKey(entry);
queuePatch({
root: metaNow.root,
path: [...containerPath, synthetic],
op: "remove",
});
} else if (key === "clear") {
// Structural clear: remove prior entry-level patches for this Set this tick.
if (pendingPatches) {
const group = pendingPatches.get(metaNow.root);
if (group && group.length) {
for (let i = group.length - 1; i >= 0; i--) {
const p = group[i];
if (
p.path.length === containerPath.length + 1 &&
containerPath.every((seg, idx) => p.path[idx] === seg)
) {
group.splice(i, 1);
}
}
}
}
queuePatch({
root: metaNow.root,
path: containerPath,
op: "add",
type: "set",
value: [],
});
}
}
}
return result;
};
}
const makeIterator = (pair: boolean) => {
return function thisIter(this: any) {
const iterable = raw.values();
return {
[Symbol.iterator]() {
return {
next() {
const n = iterable.next();
if (n.done) return n;
const entry = ensureEntryProxy(n.value);
return { value: pair ? [entry, entry] : entry, done: false };
},
};
},
} as Iterable<any>;
};
};
if (key === "values" || key === "keys") return makeIterator(false);
if (key === "entries") return makeIterator(true);
if (key === "forEach") {
return function thisForEach(this: any, cb: any, thisArg?: any) {
raw.forEach((entry: any) => {
cb.call(thisArg, ensureEntryProxy(entry), ensureEntryProxy(entry), raw);
});
};
}
// Properly handle native iteration (for..of, Array.from, spread) by binding to the raw Set.
if (key === Symbol.iterator) {
// Return a function whose `this` is the raw Set (avoids brand check failure on the proxy).
return function (this: any) {
// Use raw.values() so we can still ensure child entries are proxied lazily.
const iterable = raw.values();
return {
[Symbol.iterator]() {
return this;
},
next() {
const n = iterable.next();
if (n.done) return n;
const entry = ensureEntryProxy(n.value);
return { value: entry, done: false };
},
} as Iterator<any>;
};
}
if (key === Symbol.iterator.toString()) {
// string form access of iterator symbol; pass through (rare path)
}
const val = (raw as any)[key];
if (typeof val === "function") return val.bind(raw);
return val;
}
const throwOnMutation = () => {
throw new Error(
"Don't mutate the signals directly (use the underlying property/value instead)."
);
};
// Does target define a getter for key?
function hasGetter(target: any, key: any) {
return typeof descriptor(target, key)?.get === "function";
}
// Lazily allocate / fetch signal map for a proxy receiver.
function getSignals(receiver: object) {
if (!proxyToSignals.has(receiver)) proxyToSignals.set(receiver, new Map());
return proxyToSignals.get(receiver)!;
}
// Wrap & link child object/array/Set if needed.
function ensureChildProxy(value: any, parent: object, key: string | number) {
if (!shouldProxy(value)) return value;
if (!objToProxy.has(value)) {
const parentMeta = proxyMeta.get(parent)!;
const childProxy = createProxy(value, objectHandlers, parentMeta.root);
const childMeta = proxyMeta.get(childProxy)!;
childMeta.parent = parent;
childMeta.key = key as string;
objToProxy.set(value, childProxy);
}
return objToProxy.get(value);
}
// Normalize raw property key (handles $-prefix & array meta) -> { key, returnSignal }
function normalizeKey(
target: any,
fullKey: string,
isArrayMeta: boolean,
receiver: object
) {
let returnSignal = isArrayMeta || fullKey[0] === "$";
if (!isArrayMeta && Array.isArray(target) && returnSignal) {
if (fullKey === "$") {
// Provide $ meta proxy for array index signals
if (!arrayToArrayOfSignals.has(target)) {
arrayToArrayOfSignals.set(
target,
createProxy(target, arrayHandlers, proxyMeta.get(receiver)?.root)
);
}
return { shortCircuit: arrayToArrayOfSignals.get(target) };
}
returnSignal = fullKey === "$length";
}
const key = returnSignal ? fullKey.replace(rg, "") : fullKey;
return { key, returnSignal } as any;
}
// Create computed signal for getter property if needed.
function ensureComputed(
signals: Map<any, any>,
target: any,
key: any,
receiver: any
) {
if (!signals.has(key) && hasGetter(target, key)) {
signals.set(
key,
computed(() => Reflect.get(target, key, receiver))
);
}
}
// Unified get trap factory (object / array meta variant)
const get =
(isArrayMeta: boolean) =>
(target: object, fullKey: string, receiver: object): unknown => {
if (peeking) return Reflect.get(target, fullKey, receiver);
// Set handling delegated completely.
if (target instanceof Set) {
return getFromSet(target as Set<any>, fullKey as any, receiver);
}
const norm = normalizeKey(target, fullKey, isArrayMeta, receiver);
if ((norm as any).shortCircuit) return (norm as any).shortCircuit; // returned meta proxy
const { key, returnSignal } = norm as {
key: string;
returnSignal: boolean;
};
// Symbol fast-path
if (typeof key === "symbol" && wellKnownSymbols.has(key))
return Reflect.get(target, key, receiver);
const signals = getSignals(receiver);
ensureComputed(signals, target, key, receiver);
if (!signals.has(key)) {
let value = Reflect.get(target, key, receiver);
if (returnSignal && typeof value === "function") return; // user asked for signal wrapper of function => ignore
value = ensureChildProxy(value, receiver, key);
signals.set(key, signal(value));
}
const sig = signals.get(key);
return returnSignal ? sig : sig();
};
// Standard object / array handlers
const objectHandlers = {
get: get(false),
set(target: object, fullKey: string, val: any, receiver: object): boolean {
// Respect original getter/setter semantics
if (typeof descriptor(target, fullKey)?.set === "function")
return Reflect.set(target, fullKey, val, receiver);
if (!proxyToSignals.has(receiver)) proxyToSignals.set(receiver, new Map());
const signals = proxyToSignals.get(receiver);
if (fullKey[0] === "$") {
if (!isSignal(val)) throwOnMutation();
const key = fullKey.replace(rg, "");
signals.set(key, val);
return Reflect.set(target, key, val.peek(), receiver);
} else {
let internal = val;
if (shouldProxy(val)) {
if (!objToProxy.has(val)) {
// Link newly wrapped child to its parent for path reconstruction.
// In some edge cases parent metadata might not yet be initialized (e.g.,
// if a proxied structure was reconstructed in a way that bypassed the
// original deepSignal root path). Fall back to creating/assigning it.
let parentMeta = proxyMeta.get(receiver);
if (!parentMeta) {
// Assign a root id (new symbol) so downstream patches remain groupable.
const created: ProxyMeta = {
root: Symbol("deepSignalRootAuto"),
} as ProxyMeta;
proxyMeta.set(receiver, created);
parentMeta = created;
}
const childProxy = createProxy(val, objectHandlers, parentMeta!.root);
const childMeta = proxyMeta.get(childProxy)!;
childMeta.parent = receiver;
childMeta.key = fullKey;
objToProxy.set(val, childProxy);
}
internal = objToProxy.get(val);
}
const isNew = !(fullKey in target);
const result = Reflect.set(target, fullKey, val, receiver);
if (!signals.has(fullKey)) {
// First write after structure change -> create signal.
signals.set(fullKey, signal(internal));
} else {
// Subsequent writes -> update underlying signal.
signals.get(fullKey).set(internal);
}
if (isNew && objToIterable.has(target)) objToIterable.get(target).value++;
if (Array.isArray(target) && signals.has("length"))
signals.get("length").set(target.length);
// Emit patch (after mutation) so subscribers get final value snapshot.
const meta = proxyMeta.get(receiver);
if (meta) {
// Object/Array/Set assignment at property path.
if (val && typeof val === "object") {
queuePatch({
root: meta.root,
path: buildPath(receiver, fullKey),
op: "add",
type: "object",
});
} else {
queuePatch({
root: meta.root,
path: buildPath(receiver, fullKey),
op: "add",
value: val,
});
}
}
return result;
}
},
deleteProperty(target: object, key: string): boolean {
if (key[0] === "$") throwOnMutation();
const signals = proxyToSignals.get(objToProxy.get(target));
const result = Reflect.deleteProperty(target, key);
if (signals && signals.has(key)) signals.get(key).value = undefined;
objToIterable.has(target) && objToIterable.get(target).value++;
// Emit deletion patch
const receiverProxy = objToProxy.get(target);
const meta = receiverProxy && proxyMeta.get(receiverProxy);
if (meta) {
queuePatch({
root: meta.root,
path: buildPath(receiverProxy, key),
op: "remove",
});
}
return result;
},
ownKeys(target: object): (string | symbol)[] {
if (!objToIterable.has(target)) objToIterable.set(target, signal(0));
(objToIterable as any)._ = objToIterable.get(target).get();
return Reflect.ownKeys(target);
},
};
// Array `$` meta proxy handlers (index signals only)
const arrayHandlers = {
get: get(true),
set: throwOnMutation,
deleteProperty: throwOnMutation,
};
const wellKnownSymbols = new Set(
Object.getOwnPropertyNames(Symbol)
.map((key) => Symbol[key as WellKnownSymbols])
.filter((value) => typeof value === "symbol")
);
// Supported constructors (Map intentionally excluded for now)
const supported = new Set([Object, Array, Set]);
const shouldProxy = (val: any): boolean => {
if (typeof val !== "object" || val === null) return false;
return supported.has(val.constructor) && !ignore.has(val);
};
/** TYPES **/ // Structural deep reactive view of an input type.
export type DeepSignal<T> = T extends Function
? T
: T extends { [shallowFlag]: true }
? T
: T extends Array<unknown>
? DeepSignalArray<T>
: T extends object
? DeepSignalObject<T>
: T;
/** Recursive mapped type converting an object graph into its deepSignal proxy shape. */
export type DeepSignalObject<T extends object> = {
[P in keyof T & string as `$${P}`]?: T[P] extends Function
? never
: ReturnType<typeof signal<T[P]>>;
} & {
[P in keyof T]: DeepSignal<T[P]>;
};
/** Extract element type from an array. */
type ArrayType<T> = T extends Array<infer I> ? I : T;
/** DeepSignal-enhanced array type (numeric indices & `$` meta accessors). */
type DeepSignalArray<T> = DeepArray<ArrayType<T>> & {
[key: number]: DeepSignal<ArrayType<T>>;
$?: { [key: number]: ReturnType<typeof signal<ArrayType<T>>> };
$length?: ReturnType<typeof signal<number>>;
};
/** Marker utility type for objects passed through without deep proxying. */
export type Shallow<T extends object> = T & { [shallowFlag]: true };
/** Framework adapter hook returning a DeepSignal proxy. */
export declare const useDeepSignal: <T extends object>(obj: T) => DeepSignal<T>;
// @ts-ignore
// Strip `$`-prefixed synthetic signal accessors from key union.
type FilterSignals<K> = K extends `$${string}` ? never : K;
/** Reverse of DeepSignalObject: remove signal accessors to obtain original object shape. */
type RevertDeepSignalObject<T> = Pick<T, FilterSignals<keyof T>>;
/** Reverse of DeepSignalArray: omit meta accessors. */
type RevertDeepSignalArray<T> = Omit<T, "$" | "$length">;
/** Inverse mapped type removing deepSignal wrapper affordances. */
export type RevertDeepSignal<T> = T extends Array<unknown>
? RevertDeepSignalArray<T>
: T extends object
? RevertDeepSignalObject<T>
: T;
/** Subset of ECMAScript well-known symbols we explicitly pass through without proxy wrapping. */
type WellKnownSymbols =
| "asyncIterator"
| "hasInstance"
| "isConcatSpreadable"
| "iterator"
| "match"
| "matchAll"
| "replace"
| "search"
| "species"
| "split"
| "toPrimitive"
| "toStringTag"
| "unscopables";

@ -0,0 +1,17 @@
export * from "./core";
export * from "./deepSignal";
export * from "./watch";
export * from "./watchEffect";
export {
isArray,
isDate,
isFunction,
isMap,
isObject,
isPlainObject,
isPromise,
isRegExp,
isSet,
isString,
isSymbol,
} from "./utils";

@ -0,0 +1,81 @@
import { describe, it, expect } from "vitest";
import { signal, computed, isSignal, Effect, toValue } from "../core";
import { deepSignal } from "../deepSignal";
describe("core.ts coverage", () => {
it("signal tagging helpers (.value/.peek/.get/.set)", () => {
const s: any = signal(1);
expect(isSignal(s)).toBe(true);
expect(s.value).toBe(1);
expect(s.peek()).toBe(1);
expect(s.get()).toBe(1);
s.set(2);
expect(s.value).toBe(2);
s.value = 3;
expect(s.peek()).toBe(3);
});
it("computed tagging helpers (.value/.peek/.get)", () => {
const s: any = signal(2);
const c: any = computed(() => s.value * 2);
expect(isSignal(c)).toBe(true);
expect(c.value).toBe(4);
expect(c.peek()).toBe(4);
expect(c.get()).toBe(4);
s.value = 3;
expect(c.value).toBe(6);
});
it("toValue resolves function, signal and plain value", () => {
const s: any = signal(5);
const fn = () => 10;
expect(toValue(fn)).toBe(10);
expect(toValue(s)).toBe(5);
expect(toValue(42)).toBe(42);
});
it("Effect wrapper run/stop behavior", () => {
let runs = 0;
const eff = new Effect(() => {
runs++;
});
// Constructing Effect registers alienEffect and schedules first run immediately when dependency accessed (none here), run() executes getter
eff.run();
// Construction may trigger an initial scheduler pass; ensure at least 1
expect(runs).toBeGreaterThanOrEqual(1);
// Add scheduler side effect and dependency in second effect
const dep = signal(0);
const eff2 = new Effect(() => {
dep();
runs++;
});
const base = runs;
dep.set(1); // triggers wrapped effect, increments runs again
expect(runs).toBeGreaterThan(base);
eff2.stop();
const prev = runs;
dep.set(2); // no further increment after stop
expect(runs).toBe(prev);
// stopping already stopped effect has no effect
eff2.stop();
expect(runs).toBe(prev);
});
});
describe("deepSignal.ts extra branches", () => {
it("access well-known symbol property returns raw value and not a signal", () => {
const tag = Symbol.toStringTag;
const ds = deepSignal({ [tag]: "Custom", x: 1 }) as any;
const val = ds[tag];
expect(val).toBe("Custom");
});
it("access Set Symbol.iterator.toString() key path (skip branch)", () => {
const ds = deepSignal({ set: new Set([1]) }) as any;
const iterKey = Symbol.iterator.toString(); // 'Symbol(Symbol.iterator)'
// Accessing this string property triggers skip branch (no special handling needed)
const maybe = ds.set[iterKey];
// underlying Set likely has undefined for that string key
expect(maybe).toBeUndefined();
});
});

File diff suppressed because it is too large Load Diff

@ -0,0 +1,47 @@
import { describe, it, expect, beforeEach } from "vitest";
import { deepSignal } from "../deepSignal";
import { watch } from "../watch";
// Goal: demonstrate that patchOptimized deep watch performs fewer traversals
// than standard deep watch for the same batch of nested mutations.
// We use the exported __traverseCount instrumentation to measure how many
// times traverse() executes under each strategy.
describe("watch patch-only simplified performance placeholder", () => {
let store: any;
const build = (breadth = 3, depth = 3) => {
const make = (d: number): any => {
if (d === 0) return { v: 0 };
const obj: any = {};
for (let i = 0; i < breadth; i++) obj["k" + i] = make(d - 1);
return obj;
};
return make(depth);
};
beforeEach(() => {
store = deepSignal(build());
});
function mutateAll(breadth = 3, depth = 3) {
const visit = (node: any, d: number) => {
if (d === 0) {
node.v++;
return;
}
for (let i = 0; i < breadth; i++) visit(node["k" + i], d - 1);
};
visit(store, depth);
}
it("receives a single batch of patches after deep mutations", async () => {
let batches = 0;
const { stopListening: stop } = watch(store, ({ patches }) => {
if (patches.length) batches++;
});
mutateAll();
await Promise.resolve();
expect(batches).toBe(1);
stop();
});
});

@ -0,0 +1,148 @@
import { describe, it, expect } from "vitest";
import { deepSignal, getDeepSignalRootId, peek } from "../deepSignal";
import {
watch,
__traverseCount,
__resetTraverseCount,
traverse,
} from "../watch";
import { effect } from "../core";
describe("watch advanced", () => {
it("basic patch watcher fires on deep mutations", async () => {
const st = deepSignal({ a: { b: { c: 1 } } });
let batches: number = 0;
watch(st, ({ patches }) => {
if (patches.length) batches++;
});
st.a.b.c = 2;
st.a.b = { c: 3 } as any;
await Promise.resolve();
expect(batches).toBeGreaterThan(0);
});
// multi-source value mode removed; patch-only now - skip equivalent
// getter source value mode removed in patch-only watcher
it("watch once option still stops after first batch", async () => {
const st = deepSignal({ a: 1 });
let count = 0;
watch(
st,
() => {
count++;
},
{ once: true, immediate: true }
);
st.a = 2;
st.a = 3;
await Promise.resolve();
expect(count).toBe(1);
});
// observe value mode removed; observe is alias of watch
});
describe("patches & root ids", () => {
it("root ids are unique", () => {
const a = deepSignal({});
const b = deepSignal({});
expect(getDeepSignalRootId(a)).not.toBe(getDeepSignalRootId(b));
});
// legacy watchPatches API removed; patch mode only valid for deepSignal roots
it("watch throws on non-deepSignal input", () => {
expect(() => watch({} as any, () => {})).toThrow();
});
it("Map unsupported does not emit patches", async () => {
const m = new Map<string, number>();
const st = deepSignal({ m });
const patches: any[] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch)
);
m.set("a", 1);
await Promise.resolve();
await Promise.resolve();
expect(patches.length).toBe(0);
stop();
});
});
describe("tier3: Set iteration variants", () => {
it("entries() iteration proxies nested mutation", async () => {
const st = deepSignal({ s: new Set<any>() });
st.s.add({ id: "eEnt", inner: { v: 1 } });
const paths: string[] = [];
const { stopListening: stop } = watch(st, ({ patches }) =>
paths.push(...patches.map((pp: any) => pp.path.join(".")))
);
for (const [val] of st.s.entries()) {
(val as any).inner.v;
} // ensure proxy
for (const [val] of st.s.entries()) {
(val as any).inner.v = 2;
}
await Promise.resolve();
await Promise.resolve();
expect(paths.some((p) => p.endsWith("eEnt.inner.v"))).toBe(true);
stop();
});
it("forEach iteration proxies nested mutation", async () => {
const st = deepSignal({ s: new Set<any>() });
st.s.add({ id: "fe1", data: { n: 1 } });
const { stopListening: stop } = watch(st, () => {});
st.s.forEach((e) => (e as any).data.n); // access
st.s.forEach((e) => {
(e as any).data.n = 2;
});
await Promise.resolve();
await Promise.resolve();
stop();
});
it("keys() iteration returns proxies", async () => {
const st = deepSignal({ s: new Set<any>() });
st.s.add({ id: "k1", foo: { x: 1 } });
const { stopListening: stop } = watch(st, () => {});
for (const e of st.s.keys()) {
(e as any).foo.x = 2;
}
await Promise.resolve();
await Promise.resolve();
stop();
});
});
describe("tier3: peek behavior", () => {
it("peek does not create reactive dependency on property", async () => {
const st = deepSignal({ a: 1 });
let runs = 0;
effect(() => {
runs++;
peek(st, "a");
});
expect(runs).toBe(1);
st.a = 2;
// Flush microtasks
await Promise.resolve();
await Promise.resolve();
expect(runs).toBe(1); // no rerun
});
});
describe("tier3: traverse helper direct calls (symbols & sets)", () => {
it("traverse counts and respects depth param", () => {
__resetTraverseCount();
const obj: any = { a: { b: { c: 1 } } };
traverse(obj, 1);
const shallowCount = __traverseCount;
__resetTraverseCount();
traverse(obj, 3);
const deepCount = __traverseCount;
expect(deepCount).toBeGreaterThan(shallowCount);
});
});

@ -0,0 +1,91 @@
import { describe, expect, it } from "vitest";
import { deepSignal } from "../deepSignal";
import { watch } from "../watch";
import { watchEffect } from "../watchEffect";
describe("watch", () => {
it("watch immediate", () => {
const store = deepSignal({
userinfo: {
name: "tom",
},
});
let val!: string;
watch(
store,
({ newValue }) => {
val = newValue.userinfo.name;
},
{ immediate: true }
);
expect(val).toEqual("tom");
});
it("watch deep", () => {
const store = deepSignal({
userinfo: {
name: "tom",
},
});
let val!: string;
watch(
store,
({ newValue }) => {
val = newValue.userinfo.name;
},
{ immediate: true }
);
let value2!: string;
watch(
store,
({ newValue }) => {
value2 = newValue.userinfo.name;
},
{ immediate: true }
);
expect(val).toEqual("tom");
store.userinfo.name = "jon";
// patch delivery async (microtask)
return Promise.resolve().then(() => {
expect(val).toEqual("jon");
// With refactored watch using native effect, shallow watcher now also updates root reference
expect(value2).toEqual("jon");
});
});
it("watch once", () => {
const store = deepSignal({
userinfo: {
name: "tom",
},
});
let val!: string;
watch(
store,
({ newValue }) => {
val = newValue.userinfo.name;
},
{ immediate: true, once: true }
);
expect(val).toEqual("tom");
store.userinfo.name = "jon";
// once watcher shouldn't update after first run
expect(val).toEqual("tom");
});
it("watch effect", () => {
const store = deepSignal({
userinfo: {
name: "tom",
},
});
let x = undefined;
watchEffect(() => {
x = store.userinfo.name;
});
expect(x).toEqual("tom");
store.userinfo.name = "jon";
expect(x).toEqual("jon");
});
});

@ -0,0 +1,357 @@
import { describe, it, expect } from "vitest";
import {
deepSignal,
setSetEntrySyntheticId,
addWithId,
DeepPatch,
} from "../deepSignal";
import { watch, observe } from "../watch";
describe("watch (patch mode)", () => {
it("emits set patches with correct paths and batching", async () => {
const state = deepSignal({ a: { b: 1 }, arr: [1, { x: 2 }] });
const received: DeepPatch[][] = [];
const { stopListening: stop } = watch(state, ({ patches }) => {
received.push(patches);
});
state.a.b = 2;
(state.arr[1] as any).x = 3;
state.arr.push(5);
await Promise.resolve();
expect(received.length).toBe(1);
const batch = received[0];
const paths = batch.map((p) => p.path.join(".")).sort();
expect(paths).toContain("a.b");
expect(paths).toContain("arr.1.x");
expect(paths).toContain("arr.2");
const addOps = batch.filter((p) => p.op === "add").length;
expect(addOps).toBe(batch.length);
stop();
});
it("emits delete patches without value", async () => {
const state = deepSignal<{ a: { b?: number }; c?: number }>({
a: { b: 1 },
c: 2,
});
const out: DeepPatch[][] = [];
const { stopListening: stop } = watch(state, ({ patches }) =>
out.push(patches)
);
delete state.a.b;
delete state.c;
await Promise.resolve();
expect(out.length).toBe(1);
const [batch] = out;
const deletePatches = batch.filter((p) => p.op === "remove");
const delPaths = deletePatches.map((p) => p.path.join(".")).sort();
expect(delPaths).toEqual(["a.b", "c"]);
deletePatches.forEach((p: any) => expect(p.value).toBeUndefined());
stop();
});
it("observe patch mode mirrors watch patch mode", async () => {
const state = deepSignal({ a: 1 });
const wp: DeepPatch[][] = [];
const ob: DeepPatch[][] = [];
const { stopListening: stop1 } = watch(state, ({ patches }) =>
wp.push(patches)
);
const { stopListening: stop2 } = observe(state, ({ patches }) =>
ob.push(patches)
);
state.a = 2;
await Promise.resolve();
expect(wp.length).toBe(1);
expect(ob.length).toBe(1);
expect(wp[0][0].path.join(".")).toBe("a");
stop1();
stop2();
});
it("filters out patches from other roots", async () => {
const a = deepSignal({ x: 1 });
const b = deepSignal({ y: 2 });
const out: DeepPatch[][] = [];
const { stopListening: stop } = watch(a, ({ patches }) =>
out.push(patches)
);
b.y = 3;
a.x = 2;
await Promise.resolve();
expect(out.length).toBe(1);
expect(out[0][0].path.join(".")).toBe("x");
stop();
});
it("emits patches for Set structural mutations (add/delete)", async () => {
const state = deepSignal<{ s: Set<number> }>({ s: new Set([1, 2]) });
const batches: DeepPatch[][] = [];
const { stopListening: stop } = watch(state, ({ patches }) =>
batches.push(patches)
);
state.s.add(3);
state.s.delete(1);
await Promise.resolve();
expect(batches.length >= 1).toBe(true);
const allPaths = batches.flatMap((b) => b.map((p) => p.path.join(".")));
expect(allPaths.some((p) => p.startsWith("s."))).toBe(true);
stop();
});
it("emits patches for nested objects added after initialization", async () => {
const state = deepSignal<{ root: any }>({ root: {} });
const patches: DeepPatch[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch)
);
state.root.child = { level: { value: 1 } };
state.root.child.level.value = 2;
await Promise.resolve();
const flat = patches.flat().map((p) => p.path.join("."));
expect(flat).toContain("root.child");
expect(flat).toContain("root.child.level.value");
stop();
});
it("emits structural patches for sets of sets", async () => {
const innerA = new Set<any>([{ id: "node1", x: 1 }]);
const s = new Set<any>([innerA]);
const state = deepSignal<{ graph: Set<any> }>({ graph: s });
const batches: DeepPatch[][] = [];
const { stopListening: stop } = watch(state, ({ patches }) =>
batches.push(patches)
);
const innerB = new Set<any>([{ id: "node2", x: 5 }]);
state.graph.add(innerB);
([...innerA][0] as any).x = 2;
await Promise.resolve();
const pathStrings = batches.flat().map((p) => p.path.join("."));
expect(pathStrings.some((p) => p.startsWith("graph."))).toBe(true);
stop();
});
it("tracks deep nested object mutation inside a Set entry after iteration", async () => {
const rawEntry = { id: "n1", data: { val: 1 } };
const st = deepSignal({ bag: new Set<any>([rawEntry]) });
const collected: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) =>
collected.push(patches)
);
let proxied: any;
for (const e of st.bag.values()) {
proxied = e;
e.data.val;
}
proxied.data.val = 2;
await Promise.resolve();
const flat = collected.flat().map((p: DeepPatch) => p.path.join("."));
expect(flat.some((p: string) => p.endsWith("n1.data.val"))).toBe(true);
stop();
});
it("allows custom synthetic id for Set entry", async () => {
const node = { name: "x" };
const state = deepSignal({ s: new Set<any>() });
const collected2: DeepPatch[][] = [];
const { stopListening: stop } = watch(state, ({ patches }) =>
collected2.push(patches)
);
addWithId(state.s as any, node, "custom123");
await Promise.resolve();
const flat = collected2.flat().map((p: DeepPatch) => p.path.join("."));
expect(flat.some((p: string) => p === "s.custom123")).toBe(true);
stop();
});
describe("Set", () => {
it("emits single structural patch on Set.clear()", async () => {
const st = deepSignal({ s: new Set<any>() });
addWithId(st.s as any, { id: "a", x: 1 }, "a");
addWithId(st.s as any, { id: "b", x: 2 }, "b");
const batches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches)
);
st.s.clear();
await Promise.resolve();
const all = batches.flat().map((p) => p.path.join("."));
expect(all).toEqual(["s"]);
stop();
});
it("emits delete patch for object entry", async () => {
const st = deepSignal({ s: new Set<any>() });
const obj = { id: "n1", x: 1 };
const patches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch)
);
st.s.add(obj);
st.s.delete(obj);
await Promise.resolve();
const all = patches
.flat()
.filter((p) => p.op === "remove")
.map((p) => p.path.join("."));
expect(all).toContain("s.n1");
stop();
});
it("does not emit patch for duplicate add", async () => {
const st = deepSignal({ s: new Set<number>([1]) });
const patches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch)
);
st.s.add(1);
await Promise.resolve();
expect(patches.length).toBe(0);
stop();
});
it("does not emit patch deleting non-existent entry", async () => {
const st = deepSignal({ s: new Set<number>([1]) });
const patches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch)
);
st.s.delete(2);
await Promise.resolve();
expect(patches.length).toBe(0);
stop();
});
it("addWithId primitive returns primitive and emits patch with primitive key", async () => {
const st = deepSignal({ s: new Set<any>() });
const patches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch)
);
const ret = addWithId(st.s as any, 5, "ignored");
expect(ret).toBe(5);
await Promise.resolve();
const paths = patches.flat().map((p) => p.path.join("."));
expect(paths).toContain("s.5");
stop();
});
it("setSetEntrySyntheticId applies custom id without helper", async () => {
const st = deepSignal({ s: new Set<any>() });
const obj = { name: "x" };
setSetEntrySyntheticId(obj, "customX");
const patches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch)
);
st.s.add(obj);
await Promise.resolve();
const paths = patches.flat().map((p) => p.path.join("."));
expect(paths).toContain("s.customX");
stop();
});
it("values/entries/forEach proxy nested mutation", async () => {
const st = deepSignal({ s: new Set<any>() });
const entry = addWithId(st.s as any, { id: "e1", inner: { v: 1 } }, "e1");
const batches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches)
);
for (const e of st.s.values()) {
e.inner.v;
}
entry.inner.v = 2;
await Promise.resolve();
const vPaths = batches.flat().map((p) => p.path.join("."));
expect(vPaths.some((p) => p.endsWith("e1.inner.v"))).toBe(true);
stop();
});
it("raw reference mutation produces no deep patch while proxied does", async () => {
const raw = { id: "id1", data: { x: 1 } };
const st = deepSignal({ s: new Set<any>([raw]) });
const batches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches)
);
raw.data.x = 2;
await Promise.resolve();
const afterRaw = batches.flat().map((p) => p.path.join("."));
expect(afterRaw.some((p) => p.endsWith("id1.data.x"))).toBe(false);
let proxied: any;
for (const e of st.s.values()) proxied = e;
proxied.data.x = 3;
await Promise.resolve();
const afterProxied = batches.flat().map((p) => p.path.join("."));
expect(afterProxied.some((p) => p.endsWith("id1.data.x"))).toBe(true);
stop();
});
it("synthetic id collision assigns unique blank node id", async () => {
const st = deepSignal({ s: new Set<any>() });
const a1 = { id: "dup", v: 1 };
const a2 = { id: "dup", v: 2 };
const patches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch)
);
st.s.add(a1);
st.s.add(a2);
await Promise.resolve();
const keys = patches
.flat()
.filter((p) => p.op === "add")
.map((p) => p.path.slice(-1)[0]);
expect(new Set(keys).size).toBe(2);
stop();
});
it("allows Array.from() and spread on Set without brand errors and tracks nested mutation", async () => {
const st = deepSignal({
s: new Set<any>([{ id: "eIter", inner: { v: 1 } }]),
});
// Regression: previously 'values method called on incompatible Proxy' was thrown here.
const arr = Array.from(st.s);
expect(arr.length).toBe(1);
expect(arr[0].inner.v).toBe(1);
const spread = [...st.s];
expect(spread[0].inner.v).toBe(1);
const batches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches)
);
spread[0].inner.v = 2; // mutate nested field of iterated (proxied) entry
await Promise.resolve();
const flat = batches.flat().map((p) => p.path.join("."));
expect(flat.some((p) => p.endsWith("eIter.inner.v"))).toBe(true);
stop();
});
});
describe("Arrays & mixed batch", () => {
it("emits patches for splice/unshift/shift in single batch", async () => {
const st = deepSignal({ arr: [1, 2, 3] });
const batches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches)
);
st.arr.splice(1, 1, 99, 100);
st.arr.unshift(0);
st.arr.shift();
await Promise.resolve();
const paths = batches.flat().map((p) => p.path.join("."));
expect(paths.some((p) => p.startsWith("arr."))).toBe(true);
stop();
});
it("mixed object/array/Set mutations batch together", async () => {
const st = deepSignal({ o: { a: 1 }, arr: [1], s: new Set<any>() });
const batches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches)
);
st.o.a = 2;
st.arr.push(2);
addWithId(st.s as any, { id: "z", v: 1 }, "z");
await Promise.resolve();
expect(batches.length).toBe(1);
const paths = batches[0].map((p) => p.path.join("."));
expect(paths).toContain("o.a");
expect(paths).toContain("arr.1");
expect(paths.some((p) => p.startsWith("s."))).toBe(true);
stop();
});
});
});

@ -0,0 +1,41 @@
export const objectToString: typeof Object.prototype.toString =
Object.prototype.toString
export const toTypeString = (value: unknown): string =>
objectToString.call(value)
const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (
val: object,
key: string | symbol,
): key is keyof typeof val => hasOwnProperty.call(val, key)
export const isArray: typeof Array.isArray = Array.isArray
export const isMap = (val: unknown): val is Map<any, any> =>
toTypeString(val) === '[object Map]'
export const isSet = (val: unknown): val is Set<any> =>
toTypeString(val) === '[object Set]'
export const isDate = (val: unknown): val is Date =>
toTypeString(val) === '[object Date]'
export const isRegExp = (val: unknown): val is RegExp =>
toTypeString(val) === '[object RegExp]'
export const isFunction = (val: unknown): val is Function =>
typeof val === 'function'
export const isString = (val: unknown): val is string => typeof val === 'string'
export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol'
export const isObject = (val: unknown): val is Record<any, any> =>
val !== null && typeof val === 'object'
export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
return (
(isObject(val) || isFunction(val)) &&
isFunction((val as any).then) &&
isFunction((val as any).catch)
)
}
export const isPlainObject = (val: unknown): val is object =>
toTypeString(val) === '[object Object]'
export const hasChanged = (value: any, oldValue: any): boolean =>
!Object.is(value, oldValue)
export function NOOP() {}

@ -0,0 +1,188 @@
import { isObject, isPlainObject, isSet, isMap, isArray } from "./utils";
import { isSignal } from "./core";
import {
isDeepSignal,
subscribeDeepMutations,
getDeepSignalRootId,
DeepPatch,
} from "./deepSignal";
import { ReactiveFlags } from "./contents";
/** Function provided to register a disposer (runs before next callback or on stop). */
export type RegisterCleanup = (cleanupFn: () => void) => void;
/** Signature for watchEffect style sources receiving the cleanup registrar. */
export type WatchEffect = (registerCleanup: RegisterCleanup) => void;
/** Options for {@link watch}. */
export interface WatchOptions {
/** Trigger the callback immediately with the current value (default: false). */
immediate?: boolean;
/** Auto-stop the watcher after the first callback run that delivers patches (or immediate call if no patches). */
once?: boolean;
/** Allow legacy/unknown options (ignored) to avoid hard breaks while migrating. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[legacy: string]: any;
}
export interface WatchPatchEvent<Root = any> {
/** Patch batch that triggered this callback (may be empty for immediate). */
patches: DeepPatch[];
/** Previous snapshot (deep-cloned) of the root value before these patches. Undefined on first call. */
oldValue: Root | undefined;
/** Current root value (live proxy). */
newValue: Root;
}
export type WatchPatchCallback<Root = any> = (
event: WatchPatchEvent<Root>
) => any;
// Internal helper kept for external compatibility.
export const remove = <T>(arr: T[], el: T): void => {
const i = arr.indexOf(el);
if (i > -1) arr.splice(i, 1);
};
/** Observe patch batches on a deep signal root. */
export function watch<Root = any>(
source: Root,
callback: WatchPatchCallback<Root>,
options: WatchOptions = {}
) {
if (!isDeepSignal(source)) {
throw new Error(
"watch() now only supports deepSignal roots (patch mode only)"
);
}
const { immediate, once } = options;
const rootId = getDeepSignalRootId(source as any)!;
let active = true;
let cleanup: (() => void) | undefined;
const registerCleanup: RegisterCleanup = (fn) => {
cleanup = fn;
};
const runCleanup = () => {
if (cleanup) {
try {
cleanup();
} catch {
/* ignore */
} finally {
cleanup = undefined;
}
}
};
// Deep clone snapshot helper (JSON clone sufficient for typical reactive plain data)
const clone = (v: any) => {
try {
return JSON.parse(JSON.stringify(v));
} catch {
return undefined as any;
}
};
let lastSnapshot: Root | undefined = clone(source);
const stopListening = () => {
if (!active) return;
active = false;
runCleanup();
unsubscribe && unsubscribe();
};
const deliver = (patches: DeepPatch[]) => {
if (!active) return;
runCleanup();
const prev = lastSnapshot;
const next = source as any as Root; // live proxy
try {
callback({
patches,
oldValue: prev,
newValue: next,
});
} finally {
if (active) lastSnapshot = clone(next);
if (once) stopListening();
}
};
const unsubscribe = subscribeDeepMutations(rootId, (patches) => {
if (!patches.length) return; // ignore empty batches
deliver(patches);
});
if (immediate) {
// Immediate call with empty patch list (snapshot only)
deliver([]);
}
return {
/** Stop listening to future patch batches; idempotent. */
stopListening,
/** Register a cleanup callback run before the next invocation / stop. */
registerCleanup,
};
}
// observe alias
export function observe(
source: any,
cb: WatchPatchCallback,
options?: WatchOptions
) {
return watch(source, cb, options);
}
// Instrumentation counter for performance tests (number of traverse invocations)
/** Instrumentation counter tracking total `traverse()` invocations (used in tests). */
export let __traverseCount = 0; // retained for external tooling/tests although watch no longer uses traversal
/** Reset the traversal instrumentation counter back to 0. */
export function __resetTraverseCount() {
__traverseCount = 0;
}
/**
* Recursively touch (read) nested properties/entries/values of a reactive structure for dependency collection.
* Depth-limited; protects against cycles via `seen` set; respects ReactiveFlags.SKIP opt-out.
*/
export function traverse(
value: unknown,
depth: number = Infinity,
seen?: Set<unknown>
): unknown {
__traverseCount++;
if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
return value;
}
seen = seen || new Set();
if (seen.has(value)) {
return value;
}
seen.add(value);
depth--;
if (isSignal(value)) {
traverse((value as any)(), depth, seen);
} else if (isArray(value)) {
for (let i = 0; i < value.length; i++) {
traverse(value[i], depth, seen);
}
} else if (isSet(value) || isMap(value)) {
value.forEach((v: any) => {
traverse(v, depth, seen);
});
} else if (isPlainObject(value)) {
for (const key in value) {
traverse(value[key], depth, seen);
}
for (const key of Object.getOwnPropertySymbols(value)) {
if (Object.prototype.propertyIsEnumerable.call(value, key)) {
traverse(value[key as any], depth, seen);
}
}
}
return value;
}

@ -0,0 +1,32 @@
import { effect as coreEffect } from "./core";
/** Run a reactive function and re-run on its dependencies; supports cleanup. */
export function watchEffect(
fn: (registerCleanup?: (cleanup: () => void) => void) => void
) {
let cleanup: (() => void) | undefined;
const registerCleanup = (cb: () => void) => {
cleanup = cb;
};
const stop = coreEffect(() => {
if (cleanup) {
try {
cleanup();
} catch {
/* ignore */
} finally {
cleanup = undefined;
}
}
fn(registerCleanup);
});
return () => {
if (cleanup) {
try {
cleanup();
} catch {
/* ignore */
}
}
stop();
};
}

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": [
"ES2020"
],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": false,
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
},
"include": [
"src"
]
}

@ -0,0 +1,14 @@
import type { Options } from 'tsup'
export const tsup: Options = {
entry: [
'src/index.ts',
],
format: ['esm', 'cjs'],
dts: true,
splitting: true,
clean: true,
shims: false,
minify: false,
external: ['alien-signals'],
}

@ -0,0 +1,38 @@
{
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"prettier",
"react"
],
"extends": [
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:prettier/recommended"
],
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
]
}
}

@ -0,0 +1,22 @@
node_modules/
lerna-debug.log
npm-debug.log
packages/*/dist
.idea
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
coverage/
docs/
.nx

@ -0,0 +1,4 @@
{
"typescript.preferences.importModuleSpecifierEnding": "js",
"javascript.preferences.importModuleSpecifierEnding": "js"
}

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Jackson Morgan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,30 @@
# LDO Monorepo
This is a monorepo that contains all libraries associated with Linked Data Objects (LDO).
## Documentation
Full documentation can be found at [ldo.js.org](https://ldo.js.org).
## Libraries
The LDO monorepo contains the following
- [@ldo/cli](./packages/cli/)
- [@ldo/dataset](./packages/dataset/)
- [@ldo/jsonld-dataset-proxy](./packages/jsonld-dataset-proxy/)
- [@ldo/ldo](./packages/ldo/)
- [@ldo/rdf-utils](./packages/rdf-utils/)
- [@ldo/schema-converter-shex](./packages/schema-converter-shex/)
- [@ldo/solid](./packages/solid/)
- [@ldo/solid-react](./packages/solid-react/)
- [@ldo/solid-type-index](./packages/solid-type-index/)
- [@ldo/subscribable-dataset](./packages/subscribable-dataset/)
- [@ldo/traverser-shexj](./packages/traverser-shexj/)
- [@ldo/type-traverser](./packages/type-traverser/)
## Sponsorship
This project was made possible by a grant from NGI Zero Entrust via nlnet. Learn more on the [NLnet project page](https://nlnet.nl/project/SolidUsableApps/).
[<img src="https://nlnet.nl/logo/banner.png" alt="nlnet foundation logo" width="300" />](https://nlnet.nl/)
[<img src="https://nlnet.nl/image/logos/NGI0Entrust_tag.svg" alt="NGI Zero Entrust Logo" width="300" />](https://nlnet.nl/)
## Liscense
MIT

@ -0,0 +1,30 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require("path");
const monorepoRoot = path.resolve(__dirname);
module.exports = {
preset: "ts-jest/presets/js-with-ts",
testEnvironment: "node",
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1",
"^@ldo/([^/]+)$": `${monorepoRoot}/packages/$1/src/index.ts`,
"^@ldo/([^/]+)/(.*)$": `${monorepoRoot}/packages/$1/$2`,
},
coveragePathIgnorePatterns: [
"/node_modules/",
"/dist/",
"/coverage/",
"/test/",
],
transform: {
"^.+\\.ts$": [
"ts-jest",
{
tsconfig: "<rootDir>/tsconfig.cjs.json",
},
],
},
testPathIgnorePatterns: ["/node_modules/", "/dist/"],
transformIgnorePatterns: ["/node_modules/", "/dist/"],
modulePathIgnorePatterns: ["/dist/"],
};

@ -0,0 +1,31 @@
/* eslint-disable @typescript-eslint/no-var-requires */
// jest.esm.config.js
const path = require("path");
const monorepoRoot = path.resolve(__dirname);
module.exports = {
preset: "ts-jest/presets/default-esm",
testEnvironment: "node",
extensionsToTreatAsEsm: [".ts"],
transform: {
"^.+\\.ts$": [
"ts-jest",
{
useESM: true,
tsconfig: "<rootDir>/tsconfig.esm.json",
},
],
},
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1",
},
transformIgnorePatterns: ["/node_modules/", "/dist/"],
modulePathIgnorePatterns: ["/dist/"],
testPathIgnorePatterns: ["/node_modules/", "/dist/"],
coveragePathIgnorePatterns: [
"/node_modules/",
"/dist/",
"/coverage/",
"/test/",
],
};

@ -0,0 +1,4 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "1.0.0-alpha.33"
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,39 @@
{
"name": "@nextgraph-monorepo/ng-ldo-compact",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"test": "lerna run test",
"build": "lerna run build",
"lint": "lerna run lint",
"clean": "lerna clean --yes && lerna run remove-dist && rimraf node_modules",
"publish": "lerna publish --no-private"
},
"devDependencies": {
"@babel/preset-env": "^7.26.9",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^27.5.2",
"@types/node": "^20.5.7",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"@vitejs/plugin-react": "^4.4.1",
"@vitest/coverage-istanbul": "^3.2.3",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-react": "^7.33.2",
"jest": "^29.7.0",
"jsdom": "^26.1.0",
"lerna": "^7.2.0",
"prettier": "3.0.3",
"ts-jest": "^29.3.0",
"typescript": "^5.2.2",
"vitest": "^3.1.3"
},
"dependencies": {
"prettier-eslint": "^16.4.2"
}
}

@ -0,0 +1,3 @@
{
"extends": ["../../.eslintrc"]
}

@ -0,0 +1,2 @@
example-create
example-init

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Jackson Morgan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,83 @@
# @ldo/cli
The `@ldo/cli` is a command line interface for initializing LDO and building ShapeTypes.
## Setup
### Automatic Setup
To setup LDO, `cd` into your typescript project and run `npx @ldo/cli init`.
```bash
cd my-typescript-project
npx @ldo/cli init
```
<details>
<summary>
Manual Setup
</summary>
The following is handled by the __automatic setup__:
Install the LDO dependencies.
```bash
npm install @ldo/ldo
npm install @ldo/cli --save-dev
```
Create a folder to store your ShEx shapes:
```bash
mkdir shapes
```
Create a script to build ShEx shapes and convert them into Linked Data Objects. You can put this script in `package.json`
```json
{
...
scripts: {
...
"build:ldo": "ldo build --input ./shapes --output ./ldo"
...
}
...
}
```
</details>
## Generating a ShapeType
@ldo/cli generates shape types using the `*.shex` files in the "input" folder. If you followed the instructions above, run the following command:
```bash
npm run build:ldo
```
This will generate five files:
- `./ldo/foafProfile.shapeTypes.ts` <-- This is the important file
- `./ldo/foafProfile.typings.ts`
- `./ldo/foafProfile.schema.ts`
- `./ldo/foafProfile.context.ts`
## Creating a new project to distribure shapes
Sometimes, you might want to distribute shapes to others. The easiest way to do that is to deploy them to NPM. The LDO CLI has an easy-to-use command for generating a standalone project just for your shapes.
```bash
npx @ldo/cli create ./my-project
```
This script will generate a project with a place to put your shapes. Running `npm publish` will build the shapes and push to project to NPM for you.
## API Details
- [`init` command](https://ldo.js.org/latest/api/cli/init/)
- [`build` command](https://ldo.js.org/latest/api/cli/build/)
- [`create` command](https://ldo.js.org/latest/api/cli/create/)
## Sponsorship
This project was made possible by a grant from NGI Zero Entrust via nlnet. Learn more on the [NLnet project page](https://nlnet.nl/project/SolidUsableApps/).
[<img src="https://nlnet.nl/logo/banner.png" alt="nlnet foundation logo" width="300" />](https://nlnet.nl/)
[<img src="https://nlnet.nl/image/logos/NGI0Entrust_tag.svg" alt="NGI Zero Entrust Logo" width="300" />](https://nlnet.nl/)
## Liscense
MIT

@ -0,0 +1,11 @@
{
"name": "example-init",
"version": "1.0.0",
"description": "",
"keywords": [
""
],
"author": "",
"license": "MIT",
"main": "./index.js"
}

@ -0,0 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const sharedConfig = require("../../jest.config.js");
module.exports = {
...sharedConfig,
rootDir: "./",
};

@ -0,0 +1,67 @@
{
"name": "@ldo/cli",
"version": "1.0.0-alpha.32",
"description": "A Command Line Interface for Linked Data Objects",
"main": "./dist/index.js",
"type": "module",
"bin": {
"ldo": "./dist/index.js"
},
"scripts": {
"build": "npm run remove-dist && npm run build:ts && npm run copy-files && npm run update-permission",
"build:ts": "tsc --project tsconfig.cjs.json",
"remove-dist": "rimraf dist/",
"copy-files": "copyfiles -u 1 \"./src/**/*.ejs\" dist/",
"update-permission": "chmod +x ./dist/index.js",
"test": "jest --coverage",
"prepublishOnly": " npm run build",
"lint": "eslint src/** --fix --no-error-on-unmatched-pattern",
"test:init": "rm -rf ./example-init && cp -R ./example-init-placeholder ./example-init && cd ./example-init && ../dist/index.js init",
"test:create": "rm -rf ./example-create && ./dist/index.js create ./example-create"
},
"repository": {
"type": "git",
"url": "git+https://github.com/o-development/ldo.git"
},
"author": "Jackson Morgan",
"license": "MIT",
"bugs": {
"url": "https://github.com/o-development/ldo/issues"
},
"homepage": "https://github.com/o-development/ldo/tree/main/packages/cli#readme",
"devDependencies": {
"@types/child-process-promise": "^2.2.2",
"@types/ejs": "^3.1.1",
"@types/fs-extra": "^9.0.13",
"@types/jsonld": "^1.5.15",
"@types/prompts": "^2.4.9",
"@types/shexj": "^2.1.4",
"copyfiles": "^2.4.1",
"rimraf": "^3.0.2"
},
"dependencies": {
"@jeswr/shacl2shex": "^1.1.0",
"@ldo/ldo": "^1.0.0-alpha.32",
"@ldo/schema-converter-shex": "^1.0.0-alpha.32",
"@shexjs/parser": "^1.0.0-alpha.24",
"child-process-promise": "^2.2.1",
"commander": "^9.3.0",
"ejs": "^3.1.8",
"fs-extra": "^10.1.0",
"loading-cli": "^1.1.0",
"prettier": "^3.0.3",
"prompts": "^2.4.2",
"rdf-dereference-store": "^1.4.0",
"rdf-namespaces": "^1.13.1",
"ts-morph": "^24.0.0",
"type-fest": "^2.19.0"
},
"files": [
"dist",
"src"
],
"publishConfig": {
"access": "public"
},
"gitHead": "840910c56ec3f61416f031cc76771a5673af6757"
}

@ -0,0 +1,96 @@
import fs from "fs-extra";
import path from "path";
import type { Schema } from "@ldo/traverser-shexj";
import parser from "@shexjs/parser";
import schemaConverterShex from "@ldo/schema-converter-shex";
import { renderFile } from "ejs";
import prettier from "prettier";
import loading from "loading-cli";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { forAllShapes } from "./util/forAllShapes.js";
import { annotateReadablePredicates } from "@ldo/schema-converter-shex";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const __dirname = dirname(fileURLToPath(import.meta.url));
interface BuildOptions {
input: string;
output: string;
format?: "ldo" | "compact";
}
export async function build(options: BuildOptions) {
const load = loading("Preparing Environment");
load.start();
// Prepare new folder by clearing/and/or creating it
if (fs.existsSync(options.output)) {
await fs.promises.rm(options.output, { recursive: true });
}
await fs.promises.mkdir(options.output);
const format = options.format || "ldo";
const fileTemplates: string[] = [];
if (format === "compact") {
// Pre-annotate schema with readablePredicate to unify naming across outputs
fileTemplates.push("schema.compact", "typings", "shapeTypes.compact");
} else {
fileTemplates.push("schema", "typings", "shapeTypes", "context");
}
load.text = "Generating LDO Documents";
await forAllShapes(options.input, async (fileName, shexC) => {
// Convert to ShexJ
let schema: Schema;
try {
// @ts-expect-error ...
schema = parser.construct("https://ldo.js.org/").parse(shexC);
} catch (err) {
const errMessage =
err instanceof Error
? err.message
: typeof err === "string"
? err
: "Unknown Error";
console.error(`Error processing ${fileName}: ${errMessage}`);
return;
}
// Add readable predicates to schema as the single source of truth.
if (format === "compact") {
// @ts-expect-error ...
annotateReadablePredicates(schema);
}
const [typings, context, compactSchema] = await schemaConverterShex(
schema,
{
format,
},
);
await Promise.all(
fileTemplates.map(async (templateName) => {
const finalContent = await renderFile(
path.join(__dirname, "./templates", `${templateName}.ejs`),
{
typings: typings.typings,
fileName,
schema: JSON.stringify(schema, null, 2),
context: JSON.stringify(context, null, 2),
compactSchema: JSON.stringify(compactSchema, null, 2),
format,
},
);
await fs.promises.writeFile(
path.join(options.output, `${fileName}.${templateName}.ts`),
await prettier.format(finalContent, { parser: "typescript" }),
);
}),
);
});
load.stop();
}

@ -0,0 +1,119 @@
import { init } from "./init.js";
import {
modifyPackageJson,
savePackageJson,
} from "./util/modifyPackageJson.js";
import { generateReadme } from "./generateReadme.js";
import path from "path";
import prompts from "prompts";
import type { PackageJson } from "type-fest";
import loading from "loading-cli";
import { promises as fs } from "fs";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const __dirname = dirname(fileURLToPath(import.meta.url));
export async function create(directory: string) {
// Init the NPM Package
const responses = await prompts([
{
type: "text",
name: "name",
message: "Package name:",
initial: path.basename(directory),
},
{
type: "text",
name: "version",
message: "Version:",
initial: "1.0.0",
},
{
type: "text",
name: "description",
message: "Description:",
},
{
type: "list",
name: "keywords",
message: "Keywords (comma separated):",
separator: ",",
},
{
type: "text",
name: "author",
message: "Author:",
},
{
type: "text",
name: "license",
message: "License:",
initial: "MIT",
},
{
type: "text",
name: "repository",
message: "Git repository (optional):",
},
]);
const load = loading("Generating package.json");
const packageJson: PackageJson = {
name: responses.name,
version: responses.version,
description: responses.description,
keywords: responses.keywords,
author: responses.author,
license: responses.license,
main: "./index.js",
};
if (responses.repository) {
packageJson.repository = {
type: "git",
url: responses.repository,
};
packageJson.bugs = {
url: `${responses.repository.replace(/\.git$/, "")}/issues`,
};
packageJson.homepage = `${responses.repository.replace(
/\.git$/,
"",
)}#readme`;
}
await savePackageJson(directory, packageJson);
// Init LDO
load.text = "Initializing LDO";
await init(directory);
// Add prepublish script
await modifyPackageJson(directory, async (packageJson) => {
if (!packageJson.scripts) packageJson.scripts = {};
packageJson.scripts.prepublish =
"npm run build:ldo && npm run generate-readme";
packageJson.scripts[
"generate-readme"
] = `ldo generate-readme --project ./ --shapes ./.shapes --ldo ./.ldo`;
return packageJson;
});
// Generate ReadMe
load.text = "Generating README";
await generateReadme({
project: directory,
shapes: path.join(directory, ".shapes"),
ldo: path.join(directory, ".ldo"),
});
// Create .gitignore
load.text = "Create .gitignore";
await fs.writeFile(path.join(directory, ".gitignore"), "node_modules");
load.stop();
}

@ -0,0 +1,107 @@
import { getPackageJson } from "./util/modifyPackageJson.js";
import { forAllShapes } from "./util/forAllShapes.js";
import { promises as fs } from "fs";
import path from "path";
import { Project } from "ts-morph";
import { renderFile } from "ejs";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const __dirname = dirname(fileURLToPath(import.meta.url));
interface GenerateReadmeOptions {
project: string;
shapes: string;
ldo: string;
}
interface ReadmeEjsOptions {
projectName: string;
projectDescription: string;
shapes: {
name: string;
types: {
typeName: string;
shapeTypeName: string;
}[];
shex: string;
typescript: string;
}[];
}
export async function generateReadme(options: GenerateReadmeOptions) {
const packageJson = await getPackageJson(options.project);
const projectName = packageJson.name!;
const projectDescription = packageJson.description!;
const shapes: ReadmeEjsOptions["shapes"] = [];
await forAllShapes(options.shapes, async (fileName, shexC) => {
const typeFilePath = path.join(options.ldo, `${fileName}.typings.ts`);
const typesRaw = await fs.readFile(typeFilePath, "utf8");
const shape: ReadmeEjsOptions["shapes"][0] = {
name: fileName,
shex: shexC,
typescript: typesRaw,
types: [],
};
listInterfaces(typeFilePath).forEach((interfaceName) => {
shape.types.push({
typeName: interfaceName,
shapeTypeName: `${interfaceName}ShapeType`,
});
});
shapes.push(shape);
});
const readmeEjsOptions: ReadmeEjsOptions = {
projectName,
projectDescription,
shapes,
};
// Save Readme
const finalContent = await renderFile(
path.join(__dirname, "./templates/readme/", "main.ejs"),
readmeEjsOptions,
);
// Save readme to document
await fs.writeFile(path.join(options.project, "README.md"), finalContent);
await generateIndex({ project: options.project });
}
/**
* Helper Function that lists all the interfaces in a typescript file
*/
function listInterfaces(filePath: string): string[] {
const project = new Project();
const sourceFile = project.addSourceFileAtPath(filePath);
// Get all interfaces in the file
const interfaces = sourceFile.getInterfaces().map((iface) => iface.getName());
return interfaces;
}
/**
* Generate Index
*/
interface GenerateIndexOptions {
project: string;
}
export async function generateIndex(options: GenerateIndexOptions) {
const ldoDir = await fs.readdir(path.join(options.project, "./.ldo"), {
withFileTypes: true,
});
const indexText = await renderFile(
path.join(__dirname, "./templates/readme/projectIndex.ejs"),
{ fileNames: ldoDir.map((file) => file.name) },
);
await fs.writeFile(path.join(options.project, "index.js"), indexText);
}

@ -0,0 +1,58 @@
#!/usr/bin/env node
import { program } from "commander";
import { build } from "./build.js";
import { init } from "./init.js";
import { create } from "./create.js";
import { generateReadme } from "./generateReadme.js";
program
.name("LDO-CLI")
.description("CLI to some JavaScript string utilities")
.version("3.0.1");
program
.command("build")
.description("Build contents of a shex folder into Shape Types")
.option("-i, --input <inputPath>", "Provide the input path", "./.shapes")
.option("-o, --output <outputPath>", "Provide the output path", "./.ldo")
.option(
"-f, --format <format>",
'Typings format: "compact" (default) or "ldo"',
"compact"
)
.action(build);
program
.command("init")
.argument("[directory]", "A parent directory for ldo files")
.description("Initializes a project for LDO.")
.action(init);
program
.command("create")
.argument("<directory>", "The package's directory")
.description("Creates a standalone package for shapes to publish to NPM.")
.action(create);
program
.command("generate-readme")
.description("Create a ReadMe from the shapes and generated code.")
.requiredOption(
"-p, --project <projectPath>",
"Provide the path to the root project",
"./"
)
.requiredOption(
"-s, --shapes <shapesPath>",
"Provide the path to the shapes folder",
"./.shapes"
)
.requiredOption(
"-s, --ldo <ldoPath>",
"Provide the path to the ldo folder",
"./.ldo"
)
.action(generateReadme);
program.parse();

@ -0,0 +1,78 @@
import { exec } from "child-process-promise";
import fs from "fs-extra";
import path from "path";
import { renderFile } from "ejs";
import { modifyPackageJson } from "./util/modifyPackageJson.js";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const __dirname = dirname(fileURLToPath(import.meta.url));
const DEFAULT_SHAPES_FOLDER = "./.shapes";
const DEFAULT_LDO_FOLDER = "./.ldo";
const POTENTIAL_PARENT_DIRECTORIES = ["src", "lib", "bin"];
export async function init(directory?: string) {
// Find folder to save to
const projectDirectory = directory ?? "./";
// Get the parent directory for the ldo files
let parentDirectory = projectDirectory;
parentDirectory = "./";
const allDirectories = (
await fs.promises.readdir("./", {
withFileTypes: true,
})
).filter((file) => file.isDirectory());
for (let i = 0; i < POTENTIAL_PARENT_DIRECTORIES.length; i++) {
if (
allDirectories.some((dir) => dir.name === POTENTIAL_PARENT_DIRECTORIES[i])
) {
parentDirectory = POTENTIAL_PARENT_DIRECTORIES[i];
break;
}
}
// Install dependencies
await exec(`cd ${projectDirectory} && npm install @ldo/ldo --save`);
await exec(
`cd ${projectDirectory} && npm install @ldo/cli @types/shexj @types/jsonld --save-dev`,
);
// Create "shapes" folder
const shapesFolderPath = path.join(parentDirectory, DEFAULT_SHAPES_FOLDER);
await fs.promises.mkdir(shapesFolderPath);
const defaultShapePaths = await fs.promises.readdir(
path.join(__dirname, "./templates/defaultShapes"),
);
await Promise.all(
defaultShapePaths.map(async (shapePath) => {
const shapeContent = await renderFile(
path.join(__dirname, "./templates/defaultShapes", shapePath),
{},
);
await fs.promises.writeFile(
path.join(shapesFolderPath, `${path.parse(shapePath).name}.shex`),
shapeContent,
);
}),
);
// Add build script
await modifyPackageJson("./", async (packageJson) => {
if (!packageJson.scripts) {
packageJson.scripts = {};
}
const ldoFolder = path.join(parentDirectory, DEFAULT_LDO_FOLDER);
packageJson.scripts["build:ldo"] = `ldo build --input ${path.relative(
projectDirectory,
shapesFolderPath,
)} --output ${path.relative(projectDirectory, ldoFolder)}`;
return packageJson;
});
// Build LDO
await exec(`cd ${projectDirectory} && npm run build:ldo`);
}

@ -0,0 +1,8 @@
import { LdoJsonldContext } from "@ldo/ldo";
/**
* =============================================================================
* <%- fileName %>Context: JSONLD Context for <%- fileName %>
* =============================================================================
*/
export const <%- fileName %>Context: LdoJsonldContext = <%- context %>;

@ -0,0 +1,19 @@
# This shape is provided by default as an example
# You can create your own shape to fit your needs using ShEx (https://shex.io)
# Also check out https://shaperepo.com for examples of more shapes.
PREFIX ex: <https://example.com/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
ex:FoafProfile EXTRA a {
a [ foaf:Person ]
// rdfs:comment "Defines the node as a Person (from foaf)" ;
foaf:name xsd:string ?
// rdfs:comment "Define a person's name." ;
foaf:img xsd:string ?
// rdfs:comment "Photo link but in string form" ;
foaf:knows @ex:FoafProfile *
// rdfs:comment "A list of WebIds for all the people this user knows." ;
}

@ -0,0 +1,15 @@
# <%= projectName %>
<%- projectDescription %>
This project includes shapes and generated files for [LDO](https://ldo.js.org).
## Installation
```bash
npm i <%= projectName %>
```
<% shapes.forEach(function(shape) { %>
<%- include('shape', { shape: shape, projectName: projectName }) %>
<% }); %>

@ -0,0 +1,2 @@
<% fileNames.forEach((fileName) => { %>export * from "./.ldo/<%- fileName %>";
<% }); %>

@ -0,0 +1,27 @@
## <%= shape.name %>
### Usage with LDO
```typescript
import { createLdoDataset } from "@ldo/ldo";
import { <%= shape.types.map((type) => type.shapeTypeName).join(", ") %> } from "<%= projectName %>";
import type { <%= shape.types.map((type) => type.typeName).join(", ") %> } from "<%= projectName %>";
const ldoDataset = createLdoDataset();
<% shape.types.forEach(function(type, index) { %>
const example<%= index %>: <%= type.typeName %> = ldoDataset
.usingType(<%= type.shapeTypeName %>)
.fromSubject("http://example.com/example<%= index %>");
<% }); %>
```
### ShEx Typings
```shex
<%- shape.shex %>
```
### TypeScript Typings
```typescript
<%- shape.typescript %>
```

@ -0,0 +1,8 @@
import type { CompactSchema } from "@ldo/ldo";
/**
* =============================================================================
* <%- fileName %>Schema: Compact Schema for <%- fileName %>
* =============================================================================
*/
export const <%- fileName %>Schema: CompactSchema = <%- compactSchema %>;

@ -0,0 +1,8 @@
import type { Schema } from "shexj";
/**
* =============================================================================
* <%- fileName %>Schema: ShexJ Schema for <%- fileName %>
* =============================================================================
*/
export const <%- fileName %>Schema: Schema = <%- schema %>;

@ -0,0 +1,14 @@
import type { CompactShapeType } from "@ldo/ldo";
import { <%- fileName %>Schema } from "./<%- fileName %>.schema";
import type {
<% typings.forEach((typing)=> { if (!/Id$/.test(typing.dts.name)) { -%>
<%- typing.dts.name %>,
<% } }); -%>} from "./<%- fileName %>.typings";
// Compact ShapeTypes for <%- fileName %>
<% typings.forEach((typing)=> { if (!/Id$/.test(typing.dts.name)) { -%>
export const <%- typing.dts.name %>ShapeType: CompactShapeType<<%- typing.dts.name %>> = {
schema: <%- fileName %>Schema,
shape: "<%- typing.dts.shapeId %>",
};
<% } }); -%>

@ -0,0 +1,16 @@
import { ShapeType } from "@ldo/ldo";
import { <%- fileName %>Schema } from "./<%- fileName %>.schema";
import { <%- fileName %>Context } from "./<%- fileName %>.context";
import {
<% typings.forEach((typing)=> { if (!/Id$/.test(typing.dts.name)) { -%>
<%- typing.dts.name %>,
<% } }); -%>} from "./<%- fileName %>.typings";
// LDO ShapeTypes for <%- fileName %>
<% typings.forEach((typing)=> { if (!/Id$/.test(typing.dts.name)) { -%>
export const <%- typing.dts.name %>ShapeType: ShapeType<<%- typing.dts.name %>> = {
schema: <%- fileName %>Schema,
shape: "<%- typing.dts.shapeId %>",
context: <%- fileName %>Context,
};
<% } }); -%>

@ -0,0 +1,18 @@
<% if (format==='ldo' ) { -%>
import { LdoJsonldContext, LdSet } from "@ldo/ldo";
<% } else { -%>
export type IRI = string;
<% } -%>
/**
* =============================================================================
* Typescript Typings for <%- fileName %>
* =============================================================================
*/
<% typings.forEach((typing)=> { -%>
/**
* <%- typing.dts.name %> Type
*/
export <%- typing.typingString -%>
<% }); -%>

@ -0,0 +1,78 @@
import fs from "fs";
import path from "path";
import { shaclStoreToShexSchema, writeShexSchema } from "@jeswr/shacl2shex";
import { dereferenceToStore } from "rdf-dereference-store";
import type { Store } from "n3";
import { DataFactory as DF } from "n3";
import { rdf } from "rdf-namespaces";
function hasMatch(store: Store, predicate: string, object: string) {
for (const _ in store.match(
null,
DF.namedNode(predicate),
DF.namedNode(object),
DF.defaultGraph(),
)) {
return true;
}
return false;
}
export async function forAllShapes(
shapePath: string,
callback: (filename: string, shape: string) => Promise<void>,
): Promise<void> {
const shapeDir = await fs.promises.readdir(shapePath, {
withFileTypes: true,
});
// Filter out non-shex documents
const shexFiles = shapeDir.filter(
(file) => file.isFile() && file.name.endsWith(".shex"),
);
const shexPromise = Promise.all(
shexFiles.map(async (file) => {
const fileName = path.parse(file.name).name;
// Get the content of each document
const shexC = await fs.promises.readFile(
path.join(shapePath, file.name),
"utf8",
);
await callback(fileName, shexC);
}),
);
const shaclPromise = Promise.all(
shapeDir.map(async (file) => {
if (file.isFile()) {
let store: Awaited<ReturnType<typeof dereferenceToStore>>;
try {
store = await dereferenceToStore(path.join(shapePath, file.name), {
localFiles: true,
});
} catch (e) {
return;
}
// Make sure the RDF file contains a SHACL shape
if (
hasMatch(
store.store,
rdf.type,
"http://www.w3.org/ns/shacl#NodeShape",
) ||
hasMatch(
store.store,
rdf.type,
"http://www.w3.org/ns/shacl#PropertyShape",
)
) {
const shex = await writeShexSchema(
await shaclStoreToShexSchema(store.store),
store.prefixes,
);
await callback(path.parse(file.name).name, shex);
}
}
}),
);
await Promise.all([shexPromise, shaclPromise]);
}

@ -0,0 +1,33 @@
import type { PackageJson } from "type-fest";
import fs from "fs-extra";
import path from "path";
export async function getPackageJson(
projectFolder: string,
): Promise<PackageJson> {
return JSON.parse(
(
await fs.promises.readFile(path.join(projectFolder, "./package.json"))
).toString(),
);
}
export async function savePackageJson(
projectFolder: string,
packageJson: PackageJson,
): Promise<void> {
await fs.promises.mkdir(projectFolder, { recursive: true });
await fs.promises.writeFile(
path.join(projectFolder, "./package.json"),
JSON.stringify(packageJson, null, 2),
);
}
export async function modifyPackageJson(
projectFolder: string,
modifyCallback: (packageJson: PackageJson) => Promise<PackageJson>,
): Promise<void> {
const packageJson: PackageJson = await getPackageJson(projectFolder);
const newPackageJson = await modifyCallback(packageJson);
await savePackageJson(projectFolder, newPackageJson);
}

@ -0,0 +1,5 @@
describe("cli", () => {
it("trivial", () => {
expect(true).toBe(true);
});
});

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.esm.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "node",
"outDir": "./dist"
},
"include": ["./src"]
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save