@rettangoli/fe 1.0.0 โ†’ 1.0.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 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** - Hot reload with Vite integration
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
- - [ESBuild](https://esbuild.github.io/) - Fast bundling
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 hot reload
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.0",
3
+ "version": "1.0.2",
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 watch.js --watch",
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
- function capitalize(word) {
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
- export const bundleFile = async (options) => {
40
- const { outfile, tempDir, development = false } = options;
41
- await esbuild.build({
42
- entryPoints: [path.join(tempDir, "dynamicImport.js")],
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 tempDir = path.resolve(cwd, ".temp");
71
-
72
- // Ensure temp directory exists
73
- if (!existsSync(tempDir)) {
74
- mkdirSync(tempDir, { recursive: true });
75
- }
76
-
77
- const allFiles = getAllFiles(resolvedDirs).filter((filePath) => isSupportedComponentFile(filePath));
78
-
79
- let output = "";
80
-
81
- const categories = [];
82
- const imports = {};
83
- const componentContractEntries = [];
84
-
85
- // unique identifier needed for replacing
86
- let count = 10000000000;
87
-
88
- const replaceMap = {};
89
-
90
- for (const filePath of allFiles) {
91
- const { category, component, fileType } =
92
- extractCategoryAndComponent(filePath);
93
-
94
- if (!imports[category]) {
95
- imports[category] = {};
96
- }
97
-
98
- if (!imports[category][component]) {
99
- imports[category][component] = {};
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
 
@@ -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${formatContractErrors(errors).join("\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 { load as loadYaml } from "js-yaml";
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
- // Debounce mechanism to prevent excessive rebuilds
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 setupWatcher = (directory, options) => {
16
- watch(
17
- directory,
18
- { recursive: true },
19
- async (event, filename) => {
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
- // Debounce the rebuild
31
- if (rebuildTimeout) {
32
- clearTimeout(rebuildTimeout);
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
- rebuildTimeout = setTimeout(async () => {
36
- console.log('Triggering rebuild...');
37
- await buildRettangoliFrontend(options);
38
- }, DEBOUNCE_DELAY);
29
+ return {
30
+ root,
31
+ publicEntryPath,
32
+ };
33
+ };
39
34
 
40
- } catch (error) {
41
- console.error(`Error processing ${filename}:`, error);
42
- // Keep the watcher running
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 function startViteServer(options) {
50
- const { port = 3001, outfile = "./vt/static/main.js" } = options;
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 createServer({
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
- server.bindCLIShortcuts({ print: true });
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;