@rettangoli/fe 1.0.1 โ 1.0.3
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 +27 -3
- package/package.json +2 -3
- package/src/cli/build.js +38 -170
- package/src/cli/contracts.js +1 -1
- package/src/cli/frontendEntrySource.js +178 -0
- package/src/cli/vitePlugin.js +158 -0
- package/src/cli/watch.js +72 -79
- package/src/core/runtime/componentOrchestrator.js +7 -0
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ A modern frontend framework that uses YAML for view definitions, web components
|
|
|
8
8
|
- **๐ YAML Views** - Declarative UI definitions that compile to virtual DOM
|
|
9
9
|
- **๐งฉ Web Components** - Standards-based component architecture
|
|
10
10
|
- **๐ Reactive State** - Immer-powered immutable state management
|
|
11
|
-
- **โก Fast Development** -
|
|
11
|
+
- **โก Fast Development** - Auto reload with Vite integration
|
|
12
12
|
- **๐ฏ Template System** - Jempl templating for dynamic content
|
|
13
13
|
- **๐งช Testing Ready** - Pure functions and dependency injection for easy testing
|
|
14
14
|
|
|
@@ -38,7 +38,7 @@ rtgl fe watch # Start dev server
|
|
|
38
38
|
- [RxJS](https://github.com/ReactiveX/rxjs) - Reactive programming
|
|
39
39
|
|
|
40
40
|
**Build & Development:**
|
|
41
|
-
- [
|
|
41
|
+
- [Vite](https://vite.dev/) - Dev server and production bundling
|
|
42
42
|
|
|
43
43
|
**Browser Native:**
|
|
44
44
|
- Web Components - Component encapsulation
|
|
@@ -78,7 +78,7 @@ node ../rettangoli-cli/cli.js fe watch
|
|
|
78
78
|
src/
|
|
79
79
|
โโโ cli/
|
|
80
80
|
โ โโโ build.js # Build component bundles
|
|
81
|
-
โ โโโ watch.js # Development server with
|
|
81
|
+
โ โโโ watch.js # Development server with auto reload
|
|
82
82
|
โ โโโ scaffold.js # Component scaffolding
|
|
83
83
|
โ โโโ examples.js # Generate examples for testing
|
|
84
84
|
โ โโโ blank/ # Component templates
|
|
@@ -89,6 +89,30 @@ src/
|
|
|
89
89
|
โโโ index.js # Main exports
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
## Vite Integration
|
|
93
|
+
|
|
94
|
+
`@rettangoli/fe` uses Vite directly through the Node API, behind the existing FE CLI commands.
|
|
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()` and serves the configured `outfile` path via middleware.
|
|
98
|
+
- FE runtime source is generated in memory (virtual module), so no temporary generated JS files are required.
|
|
99
|
+
- Contract validation, YAML parsing, and template parsing still run before code generation.
|
|
100
|
+
|
|
101
|
+
Current Vite features used by FE:
|
|
102
|
+
|
|
103
|
+
- Build API (`build`) for production bundles.
|
|
104
|
+
- Dev Server API (`createServer`) for watch mode serving.
|
|
105
|
+
- Custom plugin hooks:
|
|
106
|
+
- `resolveId` + `load` for the FE virtual entry (`virtual:rettangoli-fe-entry`).
|
|
107
|
+
- `handleHotUpdate` for FE file change detection.
|
|
108
|
+
- `configureServer` for full-page reload and serving the configured output entry URL.
|
|
109
|
+
- Rollup output control through Vite (`entryFileNames`, `chunkFileNames`, `assetFileNames`) to preserve CLI `outfile` behavior.
|
|
110
|
+
|
|
111
|
+
Notes:
|
|
112
|
+
|
|
113
|
+
- Watch mode currently performs full reloads (not component-level HMR).
|
|
114
|
+
- No dedicated CSS plugin pipeline is enabled in FE at this time.
|
|
115
|
+
|
|
92
116
|
## Configuration
|
|
93
117
|
|
|
94
118
|
Create a `rettangoli.config.yaml` file in your project root:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rettangoli/fe",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Frontend framework for building reactive web components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
"vitest": "^4.0.15"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"esbuild": "^0.25.5",
|
|
35
34
|
"immer": "^10.1.1",
|
|
36
35
|
"jempl": "0.3.2-rc2",
|
|
37
36
|
"js-yaml": "^4.1.0",
|
|
@@ -40,7 +39,7 @@
|
|
|
40
39
|
"vite": "^6.3.5"
|
|
41
40
|
},
|
|
42
41
|
"scripts": {
|
|
43
|
-
"dev": "node
|
|
42
|
+
"dev": "node ../rettangoli-cli/cli.js fe watch",
|
|
44
43
|
"test": "vitest run --reporter=verbose",
|
|
45
44
|
"test:puty": "vitest run puty.spec.js --reporter=verbose",
|
|
46
45
|
"test:vitest": "vitest run test/**/*.test.js --reporter=verbose"
|
package/src/cli/build.js
CHANGED
|
@@ -1,58 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
readFileSync,
|
|
3
|
-
writeFileSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
existsSync,
|
|
6
|
-
} from "node:fs";
|
|
7
|
-
|
|
8
|
-
import esbuild from "esbuild";
|
|
9
|
-
import { load as loadYaml } from "js-yaml";
|
|
10
|
-
import { parse } from 'jempl';
|
|
11
|
-
import { extractCategoryAndComponent } from '../commonBuild.js';
|
|
12
|
-
import { getAllFiles } from '../commonBuild.js';
|
|
13
|
-
import {
|
|
14
|
-
isSupportedComponentFile,
|
|
15
|
-
validateComponentEntries,
|
|
16
|
-
} from "./contracts.js";
|
|
17
1
|
import path from "node:path";
|
|
18
2
|
|
|
19
|
-
|
|
20
|
-
return word ? word[0].toUpperCase() + word.slice(1) : word;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const writeYamlModuleFile = (yamlObject, category, component, fileType, tempDir = path.resolve(process.cwd(), ".temp")) => {
|
|
24
|
-
const dir = path.join(tempDir, category);
|
|
25
|
-
if (!existsSync(dir)) {
|
|
26
|
-
mkdirSync(dir, { recursive: true });
|
|
27
|
-
}
|
|
28
|
-
writeFileSync(
|
|
29
|
-
path.join(dir, `${component}.${fileType}.js`),
|
|
30
|
-
`export default ${JSON.stringify(yamlObject)};`,
|
|
31
|
-
);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// Function to process view files - loads YAML and creates temporary JS file
|
|
35
|
-
export const writeViewFile = (view, category, component, tempDir = path.resolve(process.cwd(), ".temp")) => {
|
|
36
|
-
writeYamlModuleFile(view, category, component, "view", tempDir);
|
|
37
|
-
};
|
|
3
|
+
import { build as viteBuild } from "vite";
|
|
38
4
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
bundle: true,
|
|
44
|
-
minify: !development,
|
|
45
|
-
sourcemap: !!development,
|
|
46
|
-
outfile: outfile,
|
|
47
|
-
format: "esm",
|
|
48
|
-
loader: {
|
|
49
|
-
".wasm": "binary",
|
|
50
|
-
},
|
|
51
|
-
platform: "browser",
|
|
52
|
-
});
|
|
53
|
-
};
|
|
5
|
+
import {
|
|
6
|
+
RETTANGOLI_FE_VIRTUAL_ENTRY_ID,
|
|
7
|
+
createRettangoliFeVitePlugin,
|
|
8
|
+
} from "./vitePlugin.js";
|
|
54
9
|
|
|
55
|
-
const buildRettangoliFrontend = async (options) => {
|
|
10
|
+
const buildRettangoliFrontend = async (options = {}) => {
|
|
56
11
|
console.log("running build with options", options);
|
|
57
12
|
|
|
58
13
|
const {
|
|
@@ -60,130 +15,43 @@ const buildRettangoliFrontend = async (options) => {
|
|
|
60
15
|
dirs = ["./example"],
|
|
61
16
|
outfile = "./vt/static/main.js",
|
|
62
17
|
setup = "setup.js",
|
|
63
|
-
development = false
|
|
18
|
+
development = false,
|
|
64
19
|
} = options;
|
|
65
20
|
|
|
66
|
-
// Resolve all paths relative to cwd
|
|
67
|
-
const resolvedDirs = dirs.map(dir => path.resolve(cwd, dir));
|
|
68
|
-
const resolvedSetup = path.resolve(cwd, setup);
|
|
69
21
|
const resolvedOutfile = path.resolve(cwd, outfile);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (!categories.includes(category)) {
|
|
103
|
-
categories.push(category);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const componentContractEntry = {
|
|
108
|
-
category,
|
|
109
|
-
component,
|
|
110
|
-
fileType,
|
|
111
|
-
filePath,
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
if (["handlers", "store", "methods"].includes(fileType)) {
|
|
115
|
-
const relativePath = path.relative(tempDir, filePath).replaceAll(path.sep, "/");
|
|
116
|
-
output += `import * as ${component}${capitalize(
|
|
117
|
-
fileType,
|
|
118
|
-
)} from '${relativePath}';\n`;
|
|
119
|
-
|
|
120
|
-
replaceMap[count] = `${component}${capitalize(fileType)}`;
|
|
121
|
-
imports[category][component][fileType] = count;
|
|
122
|
-
count++;
|
|
123
|
-
} else if (["view", "constants", "schema"].includes(fileType)) {
|
|
124
|
-
const yamlObject = loadYaml(readFileSync(filePath, "utf8")) ?? {};
|
|
125
|
-
componentContractEntry.yamlObject = yamlObject;
|
|
126
|
-
if (fileType === "view") {
|
|
127
|
-
try {
|
|
128
|
-
yamlObject.template = parse(yamlObject.template);
|
|
129
|
-
} catch (error) {
|
|
130
|
-
console.error(`Error parsing template in file: ${filePath}`);
|
|
131
|
-
throw error;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (
|
|
135
|
-
fileType === "constants" &&
|
|
136
|
-
(yamlObject === null || typeof yamlObject !== "object" || Array.isArray(yamlObject))
|
|
137
|
-
) {
|
|
138
|
-
throw new Error(`[Build] ${filePath} must contain a YAML object at the root.`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
writeYamlModuleFile(yamlObject, category, component, fileType, tempDir);
|
|
142
|
-
output += `import ${component}${capitalize(
|
|
143
|
-
fileType,
|
|
144
|
-
)} from './${category}/${component}.${fileType}.js';\n`;
|
|
145
|
-
replaceMap[count] = `${component}${capitalize(fileType)}`;
|
|
146
|
-
|
|
147
|
-
imports[category][component][fileType] = count;
|
|
148
|
-
count++;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
componentContractEntries.push(componentContractEntry);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
validateComponentEntries({
|
|
155
|
-
entries: componentContractEntries,
|
|
156
|
-
errorPrefix: "[Build]",
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
const relativeSetup = path.relative(tempDir, resolvedSetup).replaceAll(path.sep, "/");
|
|
160
|
-
output += `
|
|
161
|
-
import { createComponent } from '@rettangoli/fe';
|
|
162
|
-
import { deps } from '${relativeSetup}';
|
|
163
|
-
const imports = ${JSON.stringify(imports, null, 2)};
|
|
164
|
-
|
|
165
|
-
Object.keys(imports).forEach(category => {
|
|
166
|
-
Object.keys(imports[category]).forEach(component => {
|
|
167
|
-
const componentConfig = imports[category][component];
|
|
168
|
-
const webComponent = createComponent({ ...componentConfig }, deps[category])
|
|
169
|
-
const elementName = componentConfig.schema?.componentName;
|
|
170
|
-
if (!elementName) {
|
|
171
|
-
throw new Error(\`[Build] Missing schema.componentName for \${category}/\${component}. Define it in .schema.yaml.\`);
|
|
172
|
-
}
|
|
173
|
-
customElements.define(elementName, webComponent);
|
|
174
|
-
})
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
`;
|
|
178
|
-
|
|
179
|
-
Object.keys(replaceMap).forEach((key) => {
|
|
180
|
-
output = output.replace(key, replaceMap[key]);
|
|
22
|
+
const outDir = path.dirname(resolvedOutfile);
|
|
23
|
+
const outFileName = path.basename(resolvedOutfile);
|
|
24
|
+
const relativeOutDir = path.relative(cwd, outDir) || ".";
|
|
25
|
+
|
|
26
|
+
await viteBuild({
|
|
27
|
+
configFile: false,
|
|
28
|
+
root: cwd,
|
|
29
|
+
plugins: [
|
|
30
|
+
createRettangoliFeVitePlugin({
|
|
31
|
+
cwd,
|
|
32
|
+
dirs,
|
|
33
|
+
setup,
|
|
34
|
+
errorPrefix: "[Build]",
|
|
35
|
+
}),
|
|
36
|
+
],
|
|
37
|
+
build: {
|
|
38
|
+
outDir: relativeOutDir,
|
|
39
|
+
emptyOutDir: false,
|
|
40
|
+
minify: development ? false : "esbuild",
|
|
41
|
+
sourcemap: !!development,
|
|
42
|
+
target: "esnext",
|
|
43
|
+
rollupOptions: {
|
|
44
|
+
input: RETTANGOLI_FE_VIRTUAL_ENTRY_ID,
|
|
45
|
+
output: {
|
|
46
|
+
format: "es",
|
|
47
|
+
entryFileNames: outFileName,
|
|
48
|
+
chunkFileNames: "chunks/[name]-[hash].js",
|
|
49
|
+
assetFileNames: "assets/[name]-[hash][extname]",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
181
53
|
});
|
|
182
54
|
|
|
183
|
-
writeFileSync(path.join(tempDir, "dynamicImport.js"), output);
|
|
184
|
-
|
|
185
|
-
await bundleFile({ outfile: resolvedOutfile, tempDir, development });
|
|
186
|
-
|
|
187
55
|
console.log(`Build complete. Output file: ${resolvedOutfile}`);
|
|
188
56
|
};
|
|
189
57
|
|
package/src/cli/contracts.js
CHANGED
|
@@ -59,7 +59,7 @@ export const validateComponentEntries = ({
|
|
|
59
59
|
const errors = validateComponentContractIndex(index);
|
|
60
60
|
if (errors.length > 0) {
|
|
61
61
|
throw new Error(
|
|
62
|
-
`${errorPrefix} Component contract validation failed:\n${
|
|
62
|
+
`${errorPrefix} Component contract validation failed:\n${formatContractErrorLines(errors).join("\n")}`,
|
|
63
63
|
);
|
|
64
64
|
}
|
|
65
65
|
return {
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { parse as parseTemplate } from "jempl";
|
|
5
|
+
import { load as loadYaml } from "js-yaml";
|
|
6
|
+
|
|
7
|
+
import { extractCategoryAndComponent, getAllFiles } from "../commonBuild.js";
|
|
8
|
+
import {
|
|
9
|
+
isSupportedComponentFile,
|
|
10
|
+
validateComponentEntries,
|
|
11
|
+
} from "./contracts.js";
|
|
12
|
+
|
|
13
|
+
const MODULE_FILE_TYPES = new Set(["handlers", "store", "methods"]);
|
|
14
|
+
const YAML_FILE_TYPES = new Set(["view", "constants", "schema"]);
|
|
15
|
+
|
|
16
|
+
const toPosixPath = (value) => value.split(path.sep).join("/");
|
|
17
|
+
|
|
18
|
+
const toImportPath = ({ absolutePath, command }) => {
|
|
19
|
+
const normalizedAbsolutePath = toPosixPath(path.resolve(absolutePath));
|
|
20
|
+
if (command === "serve") {
|
|
21
|
+
return normalizedAbsolutePath.startsWith("/")
|
|
22
|
+
? `/@fs${normalizedAbsolutePath}`
|
|
23
|
+
: `/@fs/${normalizedAbsolutePath}`;
|
|
24
|
+
}
|
|
25
|
+
return normalizedAbsolutePath;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const readYamlObject = (filePath) => {
|
|
29
|
+
const content = readFileSync(filePath, "utf8");
|
|
30
|
+
return loadYaml(content) ?? {};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const validateConstantsRoot = ({ filePath, yamlObject, errorPrefix }) => {
|
|
34
|
+
if (
|
|
35
|
+
yamlObject === null ||
|
|
36
|
+
typeof yamlObject !== "object" ||
|
|
37
|
+
Array.isArray(yamlObject)
|
|
38
|
+
) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`${errorPrefix} ${filePath} must contain a YAML object at the root.`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const createComponentImportLines = ({ componentMatrix, categories }) => {
|
|
46
|
+
const lines = ["const imports = {};"];
|
|
47
|
+
|
|
48
|
+
for (const category of categories) {
|
|
49
|
+
lines.push(`imports[${JSON.stringify(category)}] = {};`);
|
|
50
|
+
const components = Object.keys(componentMatrix[category]).sort();
|
|
51
|
+
for (const component of components) {
|
|
52
|
+
lines.push(
|
|
53
|
+
`imports[${JSON.stringify(category)}][${JSON.stringify(component)}] = {};`,
|
|
54
|
+
);
|
|
55
|
+
const componentConfig = componentMatrix[category][component];
|
|
56
|
+
const fileTypes = Object.keys(componentConfig).sort();
|
|
57
|
+
for (const fileType of fileTypes) {
|
|
58
|
+
lines.push(
|
|
59
|
+
`imports[${JSON.stringify(category)}][${JSON.stringify(component)}][${JSON.stringify(fileType)}] = ${componentConfig[fileType]};`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const generateFrontendEntrySource = ({
|
|
69
|
+
cwd = process.cwd(),
|
|
70
|
+
dirs = ["./example"],
|
|
71
|
+
setup = "setup.js",
|
|
72
|
+
command = "build",
|
|
73
|
+
errorPrefix = "[Build]",
|
|
74
|
+
} = {}) => {
|
|
75
|
+
const resolvedDirs = dirs.map((dir) => path.resolve(cwd, dir));
|
|
76
|
+
const resolvedSetup = path.resolve(cwd, setup);
|
|
77
|
+
const allFiles = getAllFiles(resolvedDirs)
|
|
78
|
+
.filter((filePath) => isSupportedComponentFile(filePath))
|
|
79
|
+
.sort((a, b) => a.localeCompare(b));
|
|
80
|
+
|
|
81
|
+
const componentMatrix = {};
|
|
82
|
+
const componentContractEntries = [];
|
|
83
|
+
const declarationLines = [];
|
|
84
|
+
let declarationIndex = 0;
|
|
85
|
+
|
|
86
|
+
for (const filePath of allFiles) {
|
|
87
|
+
const { category, component, fileType } =
|
|
88
|
+
extractCategoryAndComponent(filePath);
|
|
89
|
+
|
|
90
|
+
if (!componentMatrix[category]) {
|
|
91
|
+
componentMatrix[category] = {};
|
|
92
|
+
}
|
|
93
|
+
if (!componentMatrix[category][component]) {
|
|
94
|
+
componentMatrix[category][component] = {};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const declarationName = `__rtgl_${declarationIndex}_${fileType}`;
|
|
98
|
+
declarationIndex += 1;
|
|
99
|
+
|
|
100
|
+
const componentContractEntry = {
|
|
101
|
+
category,
|
|
102
|
+
component,
|
|
103
|
+
fileType,
|
|
104
|
+
filePath,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (MODULE_FILE_TYPES.has(fileType)) {
|
|
108
|
+
const importPath = toImportPath({ absolutePath: filePath, command });
|
|
109
|
+
declarationLines.push(
|
|
110
|
+
`import * as ${declarationName} from ${JSON.stringify(importPath)};`,
|
|
111
|
+
);
|
|
112
|
+
componentMatrix[category][component][fileType] = declarationName;
|
|
113
|
+
} else if (YAML_FILE_TYPES.has(fileType)) {
|
|
114
|
+
const yamlObject = readYamlObject(filePath);
|
|
115
|
+
|
|
116
|
+
if (fileType === "view") {
|
|
117
|
+
try {
|
|
118
|
+
yamlObject.template = parseTemplate(yamlObject.template);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`${errorPrefix} Error parsing template in file: ${filePath}\n${error.message}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (fileType === "constants") {
|
|
127
|
+
validateConstantsRoot({ filePath, yamlObject, errorPrefix });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (fileType === "view" || fileType === "schema") {
|
|
131
|
+
componentContractEntry.yamlObject = yamlObject;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
declarationLines.push(
|
|
135
|
+
`const ${declarationName} = ${JSON.stringify(yamlObject)};`,
|
|
136
|
+
);
|
|
137
|
+
componentMatrix[category][component][fileType] = declarationName;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
componentContractEntries.push(componentContractEntry);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
validateComponentEntries({
|
|
144
|
+
entries: componentContractEntries,
|
|
145
|
+
errorPrefix,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const setupImportPath = toImportPath({
|
|
149
|
+
absolutePath: resolvedSetup,
|
|
150
|
+
command,
|
|
151
|
+
});
|
|
152
|
+
const categoryLines = createComponentImportLines({
|
|
153
|
+
componentMatrix,
|
|
154
|
+
categories: Object.keys(componentMatrix).sort(),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return `
|
|
158
|
+
${declarationLines.join("\n")}
|
|
159
|
+
import { createComponent } from "@rettangoli/fe";
|
|
160
|
+
import { deps } from ${JSON.stringify(setupImportPath)};
|
|
161
|
+
|
|
162
|
+
${categoryLines}
|
|
163
|
+
|
|
164
|
+
Object.keys(imports).forEach((category) => {
|
|
165
|
+
Object.keys(imports[category]).forEach((component) => {
|
|
166
|
+
const componentConfig = imports[category][component];
|
|
167
|
+
const webComponent = createComponent({ ...componentConfig }, deps[category]);
|
|
168
|
+
const elementName = componentConfig.schema?.componentName;
|
|
169
|
+
if (!elementName) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
\`[Build] Missing schema.componentName for \${category}/\${component}. Define it in .schema.yaml.\`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
customElements.define(elementName, webComponent);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
`.trim();
|
|
178
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { isSupportedComponentFile } from "./contracts.js";
|
|
4
|
+
import { generateFrontendEntrySource } from "./frontendEntrySource.js";
|
|
5
|
+
|
|
6
|
+
export const RETTANGOLI_FE_VIRTUAL_ENTRY_ID = "virtual:rettangoli-fe-entry";
|
|
7
|
+
const RESOLVED_VIRTUAL_ENTRY_ID = `\0${RETTANGOLI_FE_VIRTUAL_ENTRY_ID}`;
|
|
8
|
+
|
|
9
|
+
const isWithinDirectory = ({ filePath, directoryPath }) => {
|
|
10
|
+
const relativePath = path.relative(directoryPath, filePath);
|
|
11
|
+
return (
|
|
12
|
+
relativePath === "" ||
|
|
13
|
+
(!relativePath.startsWith("..") && !path.isAbsolute(relativePath))
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const normalizePublicEntryPath = (value) => {
|
|
18
|
+
if (!value) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const normalized = value.replace(/\\/g, "/");
|
|
22
|
+
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const createRettangoliFeVitePlugin = ({
|
|
26
|
+
cwd = process.cwd(),
|
|
27
|
+
dirs = ["./example"],
|
|
28
|
+
setup = "setup.js",
|
|
29
|
+
errorPrefix = "[Build]",
|
|
30
|
+
publicEntryPath = null,
|
|
31
|
+
} = {}) => {
|
|
32
|
+
const resolvedDirs = dirs.map((directory) => path.resolve(cwd, directory));
|
|
33
|
+
const resolvedSetup = path.resolve(cwd, setup);
|
|
34
|
+
const normalizedPublicEntryPath = normalizePublicEntryPath(publicEntryPath);
|
|
35
|
+
|
|
36
|
+
let currentCommand = "build";
|
|
37
|
+
let devServer = null;
|
|
38
|
+
|
|
39
|
+
const isTrackedFilePath = (value) => {
|
|
40
|
+
const filePath = path.resolve(value);
|
|
41
|
+
if (filePath === resolvedSetup) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!isSupportedComponentFile(filePath)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return resolvedDirs.some((directoryPath) =>
|
|
50
|
+
isWithinDirectory({ filePath, directoryPath }),
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const invalidateVirtualEntry = () => {
|
|
55
|
+
if (!devServer) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const module = devServer.moduleGraph.getModuleById(
|
|
59
|
+
RESOLVED_VIRTUAL_ENTRY_ID,
|
|
60
|
+
);
|
|
61
|
+
if (module) {
|
|
62
|
+
devServer.moduleGraph.invalidateModule(module);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const triggerFullReload = () => {
|
|
67
|
+
if (!devServer) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
invalidateVirtualEntry();
|
|
71
|
+
devServer.ws.send({ type: "full-reload" });
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
name: "rettangoli-fe",
|
|
76
|
+
enforce: "pre",
|
|
77
|
+
configResolved(config) {
|
|
78
|
+
currentCommand = config.command;
|
|
79
|
+
},
|
|
80
|
+
resolveId(id) {
|
|
81
|
+
if (id === RETTANGOLI_FE_VIRTUAL_ENTRY_ID) {
|
|
82
|
+
return RESOLVED_VIRTUAL_ENTRY_ID;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
},
|
|
86
|
+
load(id) {
|
|
87
|
+
if (id !== RESOLVED_VIRTUAL_ENTRY_ID) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return generateFrontendEntrySource({
|
|
91
|
+
cwd,
|
|
92
|
+
dirs,
|
|
93
|
+
setup,
|
|
94
|
+
command: currentCommand,
|
|
95
|
+
errorPrefix,
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
configureServer(server) {
|
|
99
|
+
devServer = server;
|
|
100
|
+
|
|
101
|
+
const onAdd = (filePath) => {
|
|
102
|
+
if (isTrackedFilePath(filePath)) {
|
|
103
|
+
triggerFullReload();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const onUnlink = (filePath) => {
|
|
108
|
+
if (isTrackedFilePath(filePath)) {
|
|
109
|
+
triggerFullReload();
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
server.watcher.on("add", onAdd);
|
|
114
|
+
server.watcher.on("unlink", onUnlink);
|
|
115
|
+
|
|
116
|
+
if (normalizedPublicEntryPath) {
|
|
117
|
+
server.middlewares.use(async (req, res, next) => {
|
|
118
|
+
const reqPath = (req.url || "").split("?")[0];
|
|
119
|
+
if (reqPath !== normalizedPublicEntryPath) {
|
|
120
|
+
next();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const transformed = await server.transformRequest(
|
|
126
|
+
RETTANGOLI_FE_VIRTUAL_ENTRY_ID,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (!transformed) {
|
|
130
|
+
res.statusCode = 500;
|
|
131
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
132
|
+
res.end(
|
|
133
|
+
`Failed to transform ${RETTANGOLI_FE_VIRTUAL_ENTRY_ID}.`,
|
|
134
|
+
);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
res.statusCode = 200;
|
|
139
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
140
|
+
res.end(transformed.code);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
server.ssrFixStacktrace(error);
|
|
143
|
+
res.statusCode = 500;
|
|
144
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
145
|
+
res.end(error.stack || String(error));
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
handleHotUpdate(context) {
|
|
151
|
+
if (!isTrackedFilePath(context.file)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
triggerFullReload();
|
|
155
|
+
return [];
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
};
|
package/src/cli/watch.js
CHANGED
|
@@ -1,100 +1,93 @@
|
|
|
1
|
-
import { readFileSync, watch } from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
|
|
4
|
-
import {
|
|
5
|
-
import { createServer } from 'vite'
|
|
6
|
-
import { writeViewFile } from './build.js';
|
|
7
|
-
import buildRettangoliFrontend from './build.js';
|
|
8
|
-
import { extractCategoryAndComponent } from '../commonBuild.js';
|
|
3
|
+
import { createServer } from "vite";
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
let rebuildTimeout = null;
|
|
12
|
-
const DEBOUNCE_DELAY = 200; // 200ms delay
|
|
5
|
+
import { createRettangoliFeVitePlugin } from "./vitePlugin.js";
|
|
13
6
|
|
|
7
|
+
const toPosixPath = (value) => value.split(path.sep).join("/");
|
|
14
8
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
console.log(`Detected ${event} in ${directory}/${filename}`);
|
|
21
|
-
if (filename) {
|
|
22
|
-
try {
|
|
23
|
-
const changedFilePath = path.join(directory, filename);
|
|
24
|
-
if (filename.endsWith('.view.yaml')) {
|
|
25
|
-
const view = loadYaml(readFileSync(changedFilePath, "utf8"));
|
|
26
|
-
const { category, component } = extractCategoryAndComponent(changedFilePath);
|
|
27
|
-
await writeViewFile(view, category, component);
|
|
28
|
-
}
|
|
9
|
+
export const resolveServeContext = ({ cwd, outfile }) => {
|
|
10
|
+
const resolvedOutfile = path.resolve(cwd, outfile);
|
|
11
|
+
const relativeOutfile = path.relative(cwd, resolvedOutfile);
|
|
12
|
+
const parts = relativeOutfile.split(path.sep).filter(Boolean);
|
|
13
|
+
const staticIndex = parts.indexOf("static");
|
|
29
14
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
15
|
+
if (staticIndex >= 0) {
|
|
16
|
+
const rootParts = parts.slice(0, staticIndex);
|
|
17
|
+
const root = path.resolve(cwd, ...rootParts);
|
|
18
|
+
const publicEntryPath = `/${toPosixPath(parts.slice(staticIndex).join(path.sep))}`;
|
|
19
|
+
return {
|
|
20
|
+
root,
|
|
21
|
+
publicEntryPath,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const outDir = path.dirname(resolvedOutfile);
|
|
26
|
+
const root = path.dirname(outDir);
|
|
27
|
+
const publicEntryPath = `/${toPosixPath(path.relative(root, resolvedOutfile))}`;
|
|
34
28
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
return {
|
|
30
|
+
root,
|
|
31
|
+
publicEntryPath,
|
|
32
|
+
};
|
|
33
|
+
};
|
|
39
34
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
export const createWatchServer = async (options = {}) => {
|
|
36
|
+
const {
|
|
37
|
+
cwd = process.cwd(),
|
|
38
|
+
dirs = ["src"],
|
|
39
|
+
port = 3001,
|
|
40
|
+
outfile = "./vt/static/main.js",
|
|
41
|
+
setup = "setup.js",
|
|
42
|
+
} = options;
|
|
43
|
+
|
|
44
|
+
const { root, publicEntryPath } = resolveServeContext({ cwd, outfile });
|
|
45
|
+
|
|
46
|
+
const server = await createServer({
|
|
47
|
+
configFile: false,
|
|
48
|
+
root,
|
|
49
|
+
server: {
|
|
50
|
+
port,
|
|
51
|
+
host: "0.0.0.0",
|
|
52
|
+
allowedHosts: true,
|
|
45
53
|
},
|
|
46
|
-
|
|
54
|
+
plugins: [
|
|
55
|
+
createRettangoliFeVitePlugin({
|
|
56
|
+
cwd,
|
|
57
|
+
dirs,
|
|
58
|
+
setup,
|
|
59
|
+
errorPrefix: "[Watch]",
|
|
60
|
+
publicEntryPath,
|
|
61
|
+
}),
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return server;
|
|
47
66
|
};
|
|
48
67
|
|
|
49
|
-
async
|
|
50
|
-
const {
|
|
68
|
+
const startWatching = async (options = {}) => {
|
|
69
|
+
const {
|
|
70
|
+
cwd = process.cwd(),
|
|
71
|
+
outfile = "./vt/static/main.js",
|
|
72
|
+
enableCliShortcuts = !!process.stdin.isTTY,
|
|
73
|
+
} = options;
|
|
74
|
+
const { root, publicEntryPath } = resolveServeContext({ cwd, outfile });
|
|
75
|
+
|
|
76
|
+
console.log("watch root dir:", root);
|
|
77
|
+
console.log("watch entry path:", publicEntryPath);
|
|
51
78
|
|
|
52
|
-
// Extract the directory from outfile path
|
|
53
|
-
const outDir = path.dirname(outfile);
|
|
54
|
-
// Go up one level from the JS file directory to serve the site root
|
|
55
|
-
const root = path.dirname(outDir);
|
|
56
|
-
console.log('watch root dir:', root)
|
|
57
79
|
try {
|
|
58
|
-
const server = await
|
|
59
|
-
// any valid user config options, plus `mode` and `configFile`
|
|
60
|
-
// configFile: false,
|
|
61
|
-
root,
|
|
62
|
-
server: {
|
|
63
|
-
port,
|
|
64
|
-
host: '0.0.0.0',
|
|
65
|
-
allowedHosts: true
|
|
66
|
-
},
|
|
67
|
-
});
|
|
80
|
+
const server = await createWatchServer(options);
|
|
68
81
|
await server.listen();
|
|
69
|
-
|
|
70
82
|
server.printUrls();
|
|
71
|
-
|
|
83
|
+
if (enableCliShortcuts) {
|
|
84
|
+
server.bindCLIShortcuts({ print: true });
|
|
85
|
+
}
|
|
86
|
+
return server;
|
|
72
87
|
} catch (error) {
|
|
73
88
|
console.error("Error during Vite server startup:", error);
|
|
74
89
|
process.exit(1);
|
|
75
90
|
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const startWatching = async (options) => {
|
|
80
|
-
const { dirs = ['src'], port = 3001 } = options;
|
|
81
|
-
|
|
82
|
-
// Set development mode for all builds in watch mode
|
|
83
|
-
const watchOptions = {
|
|
84
|
-
development: true,
|
|
85
|
-
...options
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
// Do initial build with all directories
|
|
89
|
-
console.log('Starting initial build...');
|
|
90
|
-
await buildRettangoliFrontend(watchOptions);
|
|
91
|
-
console.log('Initial build complete');
|
|
92
|
-
|
|
93
|
-
dirs.forEach(dir => {
|
|
94
|
-
setupWatcher(dir, watchOptions);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
startViteServer({ port, outfile: options.outfile });
|
|
98
|
-
}
|
|
91
|
+
};
|
|
99
92
|
|
|
100
93
|
export default startWatching;
|
|
@@ -85,6 +85,13 @@ export const runAttributeChangedComponentLifecycle = ({
|
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// Browsers can invoke attributeChangedCallback before connectedCallback.
|
|
89
|
+
// In that phase the component has no render target and transformed
|
|
90
|
+
// handlers are not wired yet, so update handlers must not trigger render.
|
|
91
|
+
if (instance.isConnected === false || !instance.renderTarget) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
88
95
|
if (instance.handlers?.handleOnUpdate) {
|
|
89
96
|
const runtimeDeps = createRuntimeDepsForInstance({ instance });
|
|
90
97
|
const changes = buildOnUpdateChanges({
|