@rettangoli/fe 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -5
- package/package.json +5 -2
- package/src/cli/build.js +12 -6
- package/src/cli/vitePlugin.js +27 -4
- package/src/cli/watch.js +15 -3
- package/src/web/componentUpdateHook.js +285 -21
- package/src/web/createWebComponentClass.js +5 -1
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ rtgl fe watch # Start dev server
|
|
|
38
38
|
- [Jempl](https://github.com/yuusoft-org/jempl) - Template engine
|
|
39
39
|
|
|
40
40
|
**Build & Development:**
|
|
41
|
-
- [Vite](https://vite.dev/) -
|
|
41
|
+
- [Vite 8](https://vite.dev/) - Rolldown-powered production bundling and dev server
|
|
42
42
|
|
|
43
43
|
**Browser Native:**
|
|
44
44
|
- Web Components - Component encapsulation
|
|
@@ -47,7 +47,7 @@ rtgl fe watch # Start dev server
|
|
|
47
47
|
|
|
48
48
|
### Prerequisites
|
|
49
49
|
|
|
50
|
-
- Node.js
|
|
50
|
+
- Node.js 20.19+, Node.js 22.12+, or Bun
|
|
51
51
|
- A `rettangoli.config.yaml` file in your project root
|
|
52
52
|
|
|
53
53
|
### Setup
|
|
@@ -93,10 +93,12 @@ src/
|
|
|
93
93
|
|
|
94
94
|
`@rettangoli/fe` uses Vite directly through the Node API, behind the existing FE CLI commands.
|
|
95
95
|
|
|
96
|
-
- `rtgl fe build` uses `vite.build()` with a virtual entry module generated from configured component files.
|
|
97
|
-
- `rtgl fe watch` uses `vite.createServer()
|
|
96
|
+
- `rtgl fe build` uses Vite 8's Rolldown-powered `vite.build()` with a virtual entry module generated from configured component files.
|
|
97
|
+
- `rtgl fe watch` uses `vite.createServer()`, warms the virtual entry during startup, and serves the configured `outfile` path via middleware.
|
|
98
|
+
- Watch projects can configure `fe.publicDir` to serve static assets from the project root without Vite transformations.
|
|
98
99
|
- FE runtime source is generated in memory (virtual module), so no temporary generated JS files are required.
|
|
99
100
|
- Contract validation, YAML parsing, and template parsing still run before code generation.
|
|
101
|
+
- Component directories, setup files, and locale files are registered explicitly so watch mode also sees sources outside the served static root.
|
|
100
102
|
|
|
101
103
|
Current Vite features used by FE:
|
|
102
104
|
|
|
@@ -106,7 +108,7 @@ Current Vite features used by FE:
|
|
|
106
108
|
- `resolveId` + `load` for the FE virtual entry (`virtual:rettangoli-fe-entry`).
|
|
107
109
|
- `handleHotUpdate` for FE file change detection.
|
|
108
110
|
- `configureServer` for full-page reload and serving the configured output entry URL.
|
|
109
|
-
-
|
|
111
|
+
- Rolldown output control through Vite (`entryFileNames`, `chunkFileNames`, `assetFileNames`) to preserve CLI `outfile` behavior.
|
|
110
112
|
|
|
111
113
|
Notes:
|
|
112
114
|
|
|
@@ -124,6 +126,7 @@ fe:
|
|
|
124
126
|
- "./src/pages"
|
|
125
127
|
setup: "setup.js"
|
|
126
128
|
outfile: "./dist/bundle.js"
|
|
129
|
+
publicDir: "./static"
|
|
127
130
|
i18n:
|
|
128
131
|
dir: "./src/i18n"
|
|
129
132
|
defaultLocale: "en"
|
|
@@ -135,6 +138,10 @@ fe:
|
|
|
135
138
|
outputDir: "./vt/specs/examples"
|
|
136
139
|
```
|
|
137
140
|
|
|
141
|
+
`publicDir` is optional and only affects `rtgl fe watch`. Its files are served
|
|
142
|
+
at `/` without Vite transformations. This is useful for extensionless or other
|
|
143
|
+
binary assets that must retain their source filenames.
|
|
144
|
+
|
|
138
145
|
## Setup Contract
|
|
139
146
|
|
|
140
147
|
`setup.js` should export `deps` only.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rettangoli/fe",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Frontend framework for building reactive web components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
"directory": "packages/rettangoli-fe"
|
|
23
23
|
},
|
|
24
24
|
"license": "MIT",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
27
|
+
},
|
|
25
28
|
"exports": {
|
|
26
29
|
".": "./src/index.js",
|
|
27
30
|
"./cli": "./src/cli/index.js",
|
|
@@ -36,7 +39,7 @@
|
|
|
36
39
|
"jempl": "1.0.1",
|
|
37
40
|
"js-yaml": "^4.1.0",
|
|
38
41
|
"snabbdom": "^3.6.2",
|
|
39
|
-
"vite": "^
|
|
42
|
+
"vite": "^8.1.4"
|
|
40
43
|
},
|
|
41
44
|
"scripts": {
|
|
42
45
|
"dev": "node ../rettangoli-cli/cli.js fe watch",
|
package/src/cli/build.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
2
3
|
|
|
3
4
|
import { build as viteBuild } from "vite";
|
|
4
5
|
|
|
@@ -9,8 +10,6 @@ import {
|
|
|
9
10
|
import { emitI18nAssets, loadI18nBuildContext } from "./i18nBuild.js";
|
|
10
11
|
|
|
11
12
|
const buildRettangoliFrontend = async (options = {}) => {
|
|
12
|
-
console.log("running build with options", options);
|
|
13
|
-
|
|
14
13
|
const {
|
|
15
14
|
cwd = process.cwd(),
|
|
16
15
|
dirs = ["./example"],
|
|
@@ -29,9 +28,14 @@ const buildRettangoliFrontend = async (options = {}) => {
|
|
|
29
28
|
i18n,
|
|
30
29
|
errorPrefix: "[Build]",
|
|
31
30
|
});
|
|
31
|
+
const startedAt = performance.now();
|
|
32
|
+
|
|
33
|
+
console.log(`[Build] Building ${resolvedOutfile}...`);
|
|
32
34
|
|
|
33
35
|
await viteBuild({
|
|
34
36
|
configFile: false,
|
|
37
|
+
clearScreen: false,
|
|
38
|
+
logLevel: "warn",
|
|
35
39
|
root: cwd,
|
|
36
40
|
plugins: [
|
|
37
41
|
createRettangoliFeVitePlugin({
|
|
@@ -45,10 +49,11 @@ const buildRettangoliFrontend = async (options = {}) => {
|
|
|
45
49
|
build: {
|
|
46
50
|
outDir: relativeOutDir,
|
|
47
51
|
emptyOutDir: false,
|
|
48
|
-
minify: development ? false : "
|
|
49
|
-
sourcemap:
|
|
52
|
+
minify: development ? false : "oxc",
|
|
53
|
+
sourcemap: false,
|
|
50
54
|
target: "esnext",
|
|
51
|
-
|
|
55
|
+
reportCompressedSize: false,
|
|
56
|
+
rolldownOptions: {
|
|
52
57
|
input: RETTANGOLI_FE_VIRTUAL_ENTRY_ID,
|
|
53
58
|
output: {
|
|
54
59
|
format: "es",
|
|
@@ -62,7 +67,8 @@ const buildRettangoliFrontend = async (options = {}) => {
|
|
|
62
67
|
|
|
63
68
|
emitI18nAssets({ outDir, i18nContext });
|
|
64
69
|
|
|
65
|
-
|
|
70
|
+
const durationMs = Math.round(performance.now() - startedAt);
|
|
71
|
+
console.log(`[Build] Complete in ${durationMs}ms.`);
|
|
66
72
|
};
|
|
67
73
|
|
|
68
74
|
export default buildRettangoliFrontend;
|
package/src/cli/vitePlugin.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
+
import { getAllFiles } from "../commonBuild.js";
|
|
3
4
|
import { isSupportedComponentFile } from "./contracts.js";
|
|
4
5
|
import { generateFrontendEntrySource } from "./frontendEntrySource.js";
|
|
5
6
|
import {
|
|
@@ -55,6 +56,24 @@ export const createRettangoliFeVitePlugin = ({
|
|
|
55
56
|
let currentCommand = "build";
|
|
56
57
|
let devServer = null;
|
|
57
58
|
|
|
59
|
+
const getComponentFilePaths = () => {
|
|
60
|
+
return getAllFiles(resolvedDirs)
|
|
61
|
+
.filter((filePath) => isSupportedComponentFile(filePath));
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const getEntryWatchPaths = ({ includeDirectories = false } = {}) => {
|
|
65
|
+
const i18nWatchPaths = includeDirectories
|
|
66
|
+
? getI18nWatchPaths({ i18nContext })
|
|
67
|
+
: Object.values(i18nContext.localeFiles || {});
|
|
68
|
+
|
|
69
|
+
return [
|
|
70
|
+
...(includeDirectories ? resolvedDirs : []),
|
|
71
|
+
resolvedSetup,
|
|
72
|
+
...getComponentFilePaths(),
|
|
73
|
+
...i18nWatchPaths,
|
|
74
|
+
];
|
|
75
|
+
};
|
|
76
|
+
|
|
58
77
|
const isTrackedFilePath = (value) => {
|
|
59
78
|
const filePath = path.resolve(value);
|
|
60
79
|
if (filePath === resolvedSetup) {
|
|
@@ -110,6 +129,13 @@ export const createRettangoliFeVitePlugin = ({
|
|
|
110
129
|
if (id !== RESOLVED_VIRTUAL_ENTRY_ID) {
|
|
111
130
|
return null;
|
|
112
131
|
}
|
|
132
|
+
|
|
133
|
+
for (const filePath of getEntryWatchPaths()) {
|
|
134
|
+
if (typeof this.addWatchFile === "function") {
|
|
135
|
+
this.addWatchFile(filePath);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
113
139
|
return generateFrontendEntrySource({
|
|
114
140
|
cwd,
|
|
115
141
|
dirs,
|
|
@@ -121,10 +147,7 @@ export const createRettangoliFeVitePlugin = ({
|
|
|
121
147
|
},
|
|
122
148
|
configureServer(server) {
|
|
123
149
|
devServer = server;
|
|
124
|
-
|
|
125
|
-
if (i18nWatchPaths.length > 0) {
|
|
126
|
-
server.watcher.add(i18nWatchPaths);
|
|
127
|
-
}
|
|
150
|
+
server.watcher.add(getEntryWatchPaths({ includeDirectories: true }));
|
|
128
151
|
|
|
129
152
|
const serveI18nAsset = (req, res, next) => {
|
|
130
153
|
if (!i18nContext.enabled || !normalizedPublicEntryPath) {
|
package/src/cli/watch.js
CHANGED
|
@@ -2,7 +2,10 @@ import path from "node:path";
|
|
|
2
2
|
|
|
3
3
|
import { createServer } from "vite";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
RETTANGOLI_FE_VIRTUAL_ENTRY_ID,
|
|
7
|
+
createRettangoliFeVitePlugin,
|
|
8
|
+
} from "./vitePlugin.js";
|
|
6
9
|
|
|
7
10
|
const toPosixPath = (value) => value.split(path.sep).join("/");
|
|
8
11
|
|
|
@@ -40,15 +43,23 @@ export const createWatchServer = async (options = {}) => {
|
|
|
40
43
|
outfile = "./vt/static/main.js",
|
|
41
44
|
setup = "setup.js",
|
|
42
45
|
i18n = null,
|
|
46
|
+
publicDir,
|
|
43
47
|
} = options;
|
|
44
48
|
|
|
45
49
|
const { root, publicEntryPath } = resolveServeContext({ cwd, outfile });
|
|
50
|
+
const resolvedPublicDir =
|
|
51
|
+
typeof publicDir === "string" && publicDir.length > 0
|
|
52
|
+
? path.resolve(cwd, publicDir)
|
|
53
|
+
: publicDir;
|
|
46
54
|
|
|
47
55
|
const server = await createServer({
|
|
56
|
+
clearScreen: false,
|
|
48
57
|
configFile: false,
|
|
58
|
+
publicDir: resolvedPublicDir,
|
|
49
59
|
root,
|
|
50
60
|
server: {
|
|
51
61
|
port,
|
|
62
|
+
strictPort: true,
|
|
52
63
|
host: "0.0.0.0",
|
|
53
64
|
allowedHosts: true,
|
|
54
65
|
},
|
|
@@ -75,12 +86,13 @@ const startWatching = async (options = {}) => {
|
|
|
75
86
|
} = options;
|
|
76
87
|
const { root, publicEntryPath } = resolveServeContext({ cwd, outfile });
|
|
77
88
|
|
|
78
|
-
console.log(
|
|
79
|
-
console.log(
|
|
89
|
+
console.log(`[Watch] Root: ${root}`);
|
|
90
|
+
console.log(`[Watch] Entry: ${publicEntryPath}`);
|
|
80
91
|
|
|
81
92
|
try {
|
|
82
93
|
const server = await createWatchServer(options);
|
|
83
94
|
await server.listen();
|
|
95
|
+
await server.transformRequest(RETTANGOLI_FE_VIRTUAL_ENTRY_ID);
|
|
84
96
|
server.printUrls();
|
|
85
97
|
if (enableCliShortcuts) {
|
|
86
98
|
server.bindCLIShortcuts({ print: true });
|
|
@@ -1,42 +1,306 @@
|
|
|
1
1
|
import { scheduleFrame } from "./scheduler.js";
|
|
2
2
|
|
|
3
|
+
export const RETTANGOLI_COMPONENT_MARKER = Symbol.for(
|
|
4
|
+
"@rettangoli/fe/component",
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
const propsSnapshots = new WeakMap();
|
|
8
|
+
const pendingUpdates = new WeakMap();
|
|
9
|
+
|
|
10
|
+
const createReferenceSnapshot = (value) => ({
|
|
11
|
+
value,
|
|
12
|
+
hasStructuralSnapshot: false,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const hasUnsupportedToJSONOrPrototypeCycle = (value) => {
|
|
16
|
+
const visited = new Set();
|
|
17
|
+
let current = value;
|
|
18
|
+
|
|
19
|
+
while (current !== null) {
|
|
20
|
+
if (visited.has(current)) return true;
|
|
21
|
+
visited.add(current);
|
|
22
|
+
|
|
23
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, "toJSON");
|
|
24
|
+
if (descriptor) {
|
|
25
|
+
if (!("value" in descriptor)) return true;
|
|
26
|
+
return typeof descriptor.value === "function";
|
|
27
|
+
}
|
|
28
|
+
current = Object.getPrototypeOf(current);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return false;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const isJsonData = (value, ancestors = new Set()) => {
|
|
35
|
+
if (value === null) return true;
|
|
36
|
+
|
|
37
|
+
const valueType = typeof value;
|
|
38
|
+
if (valueType === "string" || valueType === "boolean") return true;
|
|
39
|
+
if (valueType === "number") {
|
|
40
|
+
return Number.isFinite(value) && !Object.is(value, -0);
|
|
41
|
+
}
|
|
42
|
+
if (valueType !== "object") return false;
|
|
43
|
+
|
|
44
|
+
if (ancestors.has(value)) return false;
|
|
45
|
+
|
|
46
|
+
const isArray = Array.isArray(value);
|
|
47
|
+
const prototype = Object.getPrototypeOf(value);
|
|
48
|
+
if (
|
|
49
|
+
(isArray && prototype !== Array.prototype) ||
|
|
50
|
+
(!isArray && prototype !== Object.prototype && prototype !== null)
|
|
51
|
+
) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (hasUnsupportedToJSONOrPrototypeCycle(value)) return false;
|
|
56
|
+
|
|
57
|
+
ancestors.add(value);
|
|
58
|
+
try {
|
|
59
|
+
if (isArray) {
|
|
60
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
61
|
+
if (ownKeys.length !== value.length + 1 || !ownKeys.includes("length")) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
66
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
67
|
+
value,
|
|
68
|
+
String(index),
|
|
69
|
+
);
|
|
70
|
+
if (
|
|
71
|
+
!descriptor ||
|
|
72
|
+
!("value" in descriptor) ||
|
|
73
|
+
!descriptor.enumerable ||
|
|
74
|
+
!isJsonData(descriptor.value, ancestors)
|
|
75
|
+
) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
84
|
+
if (typeof key !== "string") return false;
|
|
85
|
+
|
|
86
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
87
|
+
if (
|
|
88
|
+
!descriptor ||
|
|
89
|
+
!("value" in descriptor) ||
|
|
90
|
+
!descriptor.enumerable ||
|
|
91
|
+
!isJsonData(descriptor.value, ancestors)
|
|
92
|
+
) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return true;
|
|
98
|
+
} finally {
|
|
99
|
+
ancestors.delete(value);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const createPropSnapshot = (value) => {
|
|
104
|
+
if (value === null || typeof value !== "object") {
|
|
105
|
+
return createReferenceSnapshot(value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
if (!isJsonData(value)) {
|
|
110
|
+
return createReferenceSnapshot(value);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Keep the live value for identity and a separate old value only for
|
|
114
|
+
// detecting and reporting same-reference JSON-data mutations.
|
|
115
|
+
const serialized = JSON.stringify(value);
|
|
116
|
+
if (serialized === undefined || typeof structuredClone !== "function") {
|
|
117
|
+
return createReferenceSnapshot(value);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
value,
|
|
121
|
+
hasStructuralSnapshot: true,
|
|
122
|
+
serialized,
|
|
123
|
+
oldValue: structuredClone(value),
|
|
124
|
+
};
|
|
125
|
+
} catch {
|
|
126
|
+
return createReferenceSnapshot(value);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const createPropsSnapshot = (props = {}) => {
|
|
131
|
+
const entries = new Map();
|
|
132
|
+
Object.keys(props).forEach((key) => {
|
|
133
|
+
entries.set(key, createPropSnapshot(props[key]));
|
|
134
|
+
});
|
|
135
|
+
return entries;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const propChanged = (previousEntry, nextEntry) => {
|
|
139
|
+
if (!previousEntry || !nextEntry) return true;
|
|
140
|
+
if (previousEntry.hasStructuralSnapshot !== nextEntry.hasStructuralSnapshot) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
if (previousEntry.hasStructuralSnapshot) {
|
|
144
|
+
return previousEntry.serialized !== nextEntry.serialized;
|
|
145
|
+
}
|
|
146
|
+
return !Object.is(previousEntry.value, nextEntry.value);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const getChangedPropKeys = (previousSnapshot, nextSnapshot) => {
|
|
150
|
+
const keys = new Set([...previousSnapshot.keys(), ...nextSnapshot.keys()]);
|
|
151
|
+
return [...keys].filter((key) =>
|
|
152
|
+
propChanged(previousSnapshot.get(key), nextSnapshot.get(key)),
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const defineProp = (target, key, value) => {
|
|
157
|
+
Object.defineProperty(target, key, {
|
|
158
|
+
configurable: true,
|
|
159
|
+
enumerable: true,
|
|
160
|
+
value,
|
|
161
|
+
writable: true,
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const hasDriftedFromSnapshot = (entry) => {
|
|
166
|
+
if (!entry.hasStructuralSnapshot) return false;
|
|
167
|
+
try {
|
|
168
|
+
if (!isJsonData(entry.value)) return true;
|
|
169
|
+
return JSON.stringify(entry.value) !== entry.serialized;
|
|
170
|
+
} catch {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const createOldProps = (previousSnapshot) => {
|
|
176
|
+
const oldProps = {};
|
|
177
|
+
previousSnapshot.forEach((entry, key) => {
|
|
178
|
+
defineProp(
|
|
179
|
+
oldProps,
|
|
180
|
+
key,
|
|
181
|
+
hasDriftedFromSnapshot(entry) ? entry.oldValue : entry.value,
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
return oldProps;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const isRettangoliComponent = (element) => {
|
|
188
|
+
try {
|
|
189
|
+
if (element?.[RETTANGOLI_COMPONENT_MARKER] === true) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Compatibility for separately bundled components built before the brand
|
|
194
|
+
// existed. These relationships are installed together by the FE runtime.
|
|
195
|
+
return (
|
|
196
|
+
typeof element?.elementName === "string" &&
|
|
197
|
+
typeof element.render === "function" &&
|
|
198
|
+
typeof element.patch === "function" &&
|
|
199
|
+
typeof element._snabbdomH === "function" &&
|
|
200
|
+
element.deps?.render === element.render &&
|
|
201
|
+
element.deps?.store === element.store &&
|
|
202
|
+
element.deps?.props === element.props
|
|
203
|
+
);
|
|
204
|
+
} catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const storePropsSnapshot = (element, props = {}) => {
|
|
210
|
+
const snapshot = createPropsSnapshot(props);
|
|
211
|
+
propsSnapshots.set(element, snapshot);
|
|
212
|
+
return snapshot;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const runPendingUpdate = (element, pendingUpdate) => {
|
|
216
|
+
if (pendingUpdates.get(element) !== pendingUpdate) return;
|
|
217
|
+
|
|
218
|
+
pendingUpdates.delete(element);
|
|
219
|
+
|
|
220
|
+
// Prop values remain live references between the parent render and this
|
|
221
|
+
// frame. Refresh the latest snapshot so the stored baseline matches the
|
|
222
|
+
// value the child is about to render.
|
|
223
|
+
const nextSnapshot = createPropsSnapshot(pendingUpdate.newProps);
|
|
224
|
+
propsSnapshots.set(element, nextSnapshot);
|
|
225
|
+
const changedPropKeys = getChangedPropKeys(
|
|
226
|
+
pendingUpdate.previousSnapshot,
|
|
227
|
+
nextSnapshot,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (changedPropKeys.length === 0) {
|
|
231
|
+
element.removeAttribute("isDirty");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const previousProps = createOldProps(pendingUpdate.previousSnapshot);
|
|
236
|
+
|
|
237
|
+
element.render();
|
|
238
|
+
element.removeAttribute("isDirty");
|
|
239
|
+
|
|
240
|
+
if (element.handlers && element.handlers.handleOnUpdate) {
|
|
241
|
+
const deps = {
|
|
242
|
+
...element.deps,
|
|
243
|
+
store: element.store,
|
|
244
|
+
render: element.render.bind(element),
|
|
245
|
+
handlers: element.handlers,
|
|
246
|
+
dispatchEvent: element.dispatchEvent.bind(element),
|
|
247
|
+
refs: element.refIds || {},
|
|
248
|
+
};
|
|
249
|
+
element.handlers.handleOnUpdate(deps, {
|
|
250
|
+
oldProps: previousProps,
|
|
251
|
+
newProps: pendingUpdate.newProps,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
3
256
|
export const createWebComponentUpdateHook = ({
|
|
4
257
|
scheduleFrameFn = scheduleFrame,
|
|
5
258
|
} = {}) => {
|
|
6
259
|
return {
|
|
260
|
+
insert: (vnode) => {
|
|
261
|
+
const element = vnode.elm;
|
|
262
|
+
if (!isRettangoliComponent(element)) return;
|
|
263
|
+
storePropsSnapshot(element, vnode.data?.props || {});
|
|
264
|
+
},
|
|
7
265
|
update: (oldVnode, vnode) => {
|
|
266
|
+
const element = vnode.elm;
|
|
267
|
+
if (
|
|
268
|
+
!isRettangoliComponent(element) ||
|
|
269
|
+
typeof element.render !== "function"
|
|
270
|
+
) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
8
274
|
const oldProps = oldVnode.data?.props || {};
|
|
9
275
|
const newProps = vnode.data?.props || {};
|
|
276
|
+
const pendingUpdate = pendingUpdates.get(element);
|
|
10
277
|
|
|
11
|
-
|
|
12
|
-
|
|
278
|
+
if (pendingUpdate) {
|
|
279
|
+
pendingUpdate.newProps = newProps;
|
|
13
280
|
return;
|
|
14
281
|
}
|
|
15
282
|
|
|
16
|
-
const
|
|
17
|
-
|
|
283
|
+
const previousSnapshot =
|
|
284
|
+
propsSnapshots.get(element) || createPropsSnapshot(oldProps);
|
|
285
|
+
const nextSnapshot = createPropsSnapshot(newProps);
|
|
286
|
+
const changedPropKeys = getChangedPropKeys(
|
|
287
|
+
previousSnapshot,
|
|
288
|
+
nextSnapshot,
|
|
289
|
+
);
|
|
290
|
+
if (changedPropKeys.length === 0) {
|
|
291
|
+
propsSnapshots.set(element, nextSnapshot);
|
|
18
292
|
return;
|
|
19
293
|
}
|
|
20
294
|
|
|
295
|
+
const nextPendingUpdate = {
|
|
296
|
+
previousSnapshot,
|
|
297
|
+
newProps,
|
|
298
|
+
};
|
|
299
|
+
pendingUpdates.set(element, nextPendingUpdate);
|
|
300
|
+
|
|
21
301
|
element.setAttribute("isDirty", "true");
|
|
22
302
|
scheduleFrameFn(() => {
|
|
23
|
-
element
|
|
24
|
-
element.removeAttribute("isDirty");
|
|
25
|
-
|
|
26
|
-
if (element.handlers && element.handlers.handleOnUpdate) {
|
|
27
|
-
const deps = {
|
|
28
|
-
...(element.deps),
|
|
29
|
-
store: element.store,
|
|
30
|
-
render: element.render.bind(element),
|
|
31
|
-
handlers: element.handlers,
|
|
32
|
-
dispatchEvent: element.dispatchEvent.bind(element),
|
|
33
|
-
refs: element.refIds || {},
|
|
34
|
-
};
|
|
35
|
-
element.handlers.handleOnUpdate(deps, {
|
|
36
|
-
oldProps,
|
|
37
|
-
newProps,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
303
|
+
runPendingUpdate(element, nextPendingUpdate);
|
|
40
304
|
});
|
|
41
305
|
},
|
|
42
306
|
};
|
|
@@ -15,7 +15,10 @@ import {
|
|
|
15
15
|
buildObservedAttributes,
|
|
16
16
|
} from "../core/runtime/componentRuntime.js";
|
|
17
17
|
import { initializeComponentDom } from "./componentDom.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
createWebComponentUpdateHook,
|
|
20
|
+
RETTANGOLI_COMPONENT_MARKER,
|
|
21
|
+
} from "./componentUpdateHook.js";
|
|
19
22
|
import { scheduleFrame } from "./scheduler.js";
|
|
20
23
|
|
|
21
24
|
export const createWebComponentClass = ({
|
|
@@ -34,6 +37,7 @@ export const createWebComponentClass = ({
|
|
|
34
37
|
deps,
|
|
35
38
|
}) => {
|
|
36
39
|
class BaseComponent extends HTMLElement {
|
|
40
|
+
[RETTANGOLI_COMPONENT_MARKER] = true;
|
|
37
41
|
elementName;
|
|
38
42
|
styles;
|
|
39
43
|
_snabbdomH;
|