@rettangoli/fe 0.0.14 → 1.0.0-rc3

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
@@ -1,10 +1,10 @@
1
1
  # Rettangoli Frontend
2
2
 
3
- A modern frontend framework that uses YAML for view definitions, web components for composition, and Immer for state management. Build reactive applications with minimal complexity using just 3 types of files.
3
+ A modern frontend framework that uses YAML for view definitions, web components for composition, and Immer for state management. Build reactive applications with minimal complexity using 4 types of files.
4
4
 
5
5
  ## Features
6
6
 
7
- - **🗂️ Three-File Architecture** - `.view.yaml`, `.store.js`, `.handlers.js` files scale from single page to complex applications
7
+ - **🗂️ Four-File Architecture** - `.view.yaml`, `.store.js`, `.handlers.js`, `.schema.yaml` scale from single page to complex applications
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
@@ -23,6 +23,7 @@ rtgl fe watch # Start dev server
23
23
 
24
24
  - **[Developer Quickstart](./docs/overview.md)** - Complete introduction and examples
25
25
  - **[View System](./docs/view.md)** - Complete YAML syntax
26
+ - **[Schema System](./docs/schema.md)** - Component API and metadata
26
27
  - **[Store Management](./docs/store.md)** - State patterns
27
28
  - **[Event Handlers](./docs/handlers.md)** - Event handling
28
29
 
@@ -38,7 +39,6 @@ rtgl fe watch # Start dev server
38
39
 
39
40
  **Build & Development:**
40
41
  - [ESBuild](https://esbuild.github.io/) - Fast bundling
41
- - [Vite](https://vite.dev/) - Development server with hot reload
42
42
 
43
43
  **Browser Native:**
44
44
  - Web Components - Component encapsulation
@@ -60,7 +60,7 @@ bun install
60
60
  2. **Create project structure**:
61
61
  ```bash
62
62
  # Scaffold a new component
63
- node ../rettangoli-cli/cli.js fe scaffold --category components --name MyButton
63
+ node ../rettangoli-cli/cli.js fe scaffold --category components --component-name MyButton
64
64
  ```
65
65
 
66
66
  3. **Start development**:
@@ -83,7 +83,7 @@ src/
83
83
  │ ├── examples.js # Generate examples for testing
84
84
  │ └── blank/ # Component templates
85
85
  ├── createComponent.js # Component factory
86
- ├── createWebPatch.js # Virtual DOM patching
86
+ ├── createWebPatch.js # Internal virtual DOM patching
87
87
  ├── parser.js # YAML to JSON converter
88
88
  ├── common.js # Shared utilities
89
89
  └── index.js # Main exports
@@ -104,17 +104,102 @@ fe:
104
104
  outputDir: "./vt/specs/examples"
105
105
  ```
106
106
 
107
+ ## Setup Contract
108
+
109
+ `setup.js` should export `deps` only.
110
+ `createWebPatch`/`h` wiring is internalized by the framework.
111
+
112
+ ```js
113
+ const deps = {
114
+ components: {},
115
+ pages: {},
116
+ };
117
+
118
+ export { deps };
119
+ ```
120
+
121
+ ## Action Listeners
122
+
123
+ In `.view.yaml`, listeners can dispatch store actions directly with `action`.
124
+ This path auto-runs render after the action executes.
125
+
126
+ ```yaml
127
+ refs:
128
+ inputEmail:
129
+ eventListeners:
130
+ input:
131
+ action: setEmail
132
+ payload:
133
+ value: ${_event.target.value}
134
+ ```
135
+
136
+ Store action:
137
+
138
+ ```js
139
+ export const setEmail = ({ state }, { value }) => {
140
+ state.email = value;
141
+ };
142
+ ```
143
+
144
+ Runtime-injected action payload fields:
145
+ - `_event`
146
+ - `_action` (internal dispatch metadata)
147
+
107
148
  ## Testing
108
149
 
109
- ### View Components
150
+ ### Unit and Contract Tests
110
151
 
111
- Use visual testing with `rtgl vt`:
152
+ - **Puty contract tests** (`spec/`) — YAML-driven pure-function specs for view, store, schema, and handler contracts.
153
+ - **Vitest integration tests** (`test/`) — runtime behavior tests for component lifecycle, props, events, and DOM.
112
154
 
113
155
  ```bash
114
- rtgl vt generate
115
- rtgl vt report
156
+ bun run test # all tests
157
+ bun run test:puty # contract tests only
158
+ bun run test:vitest # integration tests only
116
159
  ```
117
160
 
161
+ ### End-to-End Testing
162
+
163
+ FE has two E2E suites in this package:
164
+
165
+ - `packages/rettangoli-fe/e2e/dashboard`
166
+ - `packages/rettangoli-fe/e2e/interactions`
167
+
168
+ Use this workflow:
169
+ 1. Build FE with the local repo CLI (`cli.js`) so it uses your current FE source.
170
+ 2. Run VT in Docker for stable Playwright runtime.
171
+
172
+ Docker image:
173
+
174
+ ```bash
175
+ IMAGE="han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc4"
176
+ ```
177
+
178
+ Dashboard suite:
179
+
180
+ ```bash
181
+ (cd packages/rettangoli-fe/e2e/dashboard && node ../../../rettangoli-cli/cli.js fe build)
182
+ docker run --rm -v "$(pwd):/workspace" -w /workspace/packages/rettangoli-fe/e2e/dashboard "$IMAGE" rtgl vt generate
183
+ docker run --rm -v "$(pwd):/workspace" -w /workspace/packages/rettangoli-fe/e2e/dashboard "$IMAGE" rtgl vt report
184
+ ```
185
+
186
+ Interactions suite:
187
+
188
+ ```bash
189
+ (cd packages/rettangoli-fe/e2e/interactions && node ../../../rettangoli-cli/cli.js fe build)
190
+ docker run --rm -v "$(pwd):/workspace" -w /workspace/packages/rettangoli-fe/e2e/interactions "$IMAGE" rtgl vt generate
191
+ docker run --rm -v "$(pwd):/workspace" -w /workspace/packages/rettangoli-fe/e2e/interactions "$IMAGE" rtgl vt report
192
+ ```
193
+
194
+ Accept intentional visual changes:
195
+
196
+ ```bash
197
+ docker run --rm -v "$(pwd):/workspace" -w /workspace/packages/rettangoli-fe/e2e/dashboard "$IMAGE" rtgl vt accept
198
+ docker run --rm -v "$(pwd):/workspace" -w /workspace/packages/rettangoli-fe/e2e/interactions "$IMAGE" rtgl vt accept
199
+ ```
200
+
201
+ VT specs live under each suite's `vt/specs/` directory.
202
+
118
203
  ## Examples
119
204
 
120
205
  For a complete working example, see the todos app in `examples/example1/`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/fe",
3
- "version": "0.0.14",
3
+ "version": "1.0.0-rc3",
4
4
  "description": "Frontend framework for building reactive web components",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -25,7 +25,10 @@
25
25
  ".": "./src/index.js",
26
26
  "./cli": "./src/cli/index.js"
27
27
  },
28
- "devDependencies": {},
28
+ "devDependencies": {
29
+ "puty": "^0.1.1",
30
+ "vitest": "^4.0.15"
31
+ },
29
32
  "dependencies": {
30
33
  "esbuild": "^0.25.5",
31
34
  "immer": "^10.1.1",
@@ -36,6 +39,9 @@
36
39
  "vite": "^6.3.5"
37
40
  },
38
41
  "scripts": {
39
- "dev": "node watch.js --watch"
42
+ "dev": "node watch.js --watch",
43
+ "test": "vitest run --reporter=verbose",
44
+ "test:puty": "vitest run puty.spec.js --reporter=verbose",
45
+ "test:vitest": "vitest run test/**/*.test.js --reporter=verbose"
40
46
  }
41
47
  }
@@ -1,8 +1,11 @@
1
+ export const handleBeforeMount = (deps, payload) => {
2
+ //
3
+ }
1
4
 
2
- export const handleOnMount = (deps, event) => {
5
+ export const handleAfterMount = async (deps, payload) => {
3
6
  //
4
7
  }
5
8
 
6
- export const handlerSomeEvent = (deps, event) => {
9
+ export const handleSomeEvent = (deps, payload) => {
7
10
  //
8
11
  }
@@ -0,0 +1,13 @@
1
+ componentName: custom-blank
2
+
3
+ description: Blank component scaffold
4
+
5
+ propsSchema:
6
+ type: object
7
+ properties: {}
8
+
9
+ events: []
10
+
11
+ methods:
12
+ type: object
13
+ properties: {}
@@ -1,16 +1,5 @@
1
- elementName: custom-blank
2
-
3
- viewDataSchema:
4
- type: object
5
-
6
- propsSchema:
7
- type: object
8
- properties: {}
9
-
10
1
  refs: {}
11
2
 
12
- events: {}
13
-
14
3
  template:
15
4
  - rtgl-view w=f h=f p=md:
16
5
  - rtgl-text s=h2: blank
package/src/cli/build.js CHANGED
@@ -10,24 +10,32 @@ import { load as loadYaml } from "js-yaml";
10
10
  import { parse } from 'jempl';
11
11
  import { extractCategoryAndComponent } from '../commonBuild.js';
12
12
  import { getAllFiles } from '../commonBuild.js';
13
+ import {
14
+ isSupportedComponentFile,
15
+ validateComponentEntries,
16
+ } from "./contracts.js";
13
17
  import path from "node:path";
14
18
 
15
19
  function capitalize(word) {
16
20
  return word ? word[0].toUpperCase() + word.slice(1) : word;
17
21
  }
18
22
 
19
- // Function to process view files - loads YAML and creates temporary JS file
20
- export const writeViewFile = (view, category, component, tempDir) => {
23
+ const writeYamlModuleFile = (yamlObject, category, component, fileType, tempDir = path.resolve(process.cwd(), ".temp")) => {
21
24
  const dir = path.join(tempDir, category);
22
25
  if (!existsSync(dir)) {
23
26
  mkdirSync(dir, { recursive: true });
24
27
  }
25
28
  writeFileSync(
26
- path.join(dir, `${component}.view.js`),
27
- `export default ${JSON.stringify(view)};`,
29
+ path.join(dir, `${component}.${fileType}.js`),
30
+ `export default ${JSON.stringify(yamlObject)};`,
28
31
  );
29
32
  };
30
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
+ };
38
+
31
39
  export const bundleFile = async (options) => {
32
40
  const { outfile, tempDir, development = false } = options;
33
41
  await esbuild.build({
@@ -66,18 +74,13 @@ const buildRettangoliFrontend = async (options) => {
66
74
  mkdirSync(tempDir, { recursive: true });
67
75
  }
68
76
 
69
- const allFiles = getAllFiles(resolvedDirs).filter((filePath) => {
70
- return (
71
- filePath.endsWith(".store.js") ||
72
- filePath.endsWith(".handlers.js") ||
73
- filePath.endsWith(".view.yaml")
74
- );
75
- });
77
+ const allFiles = getAllFiles(resolvedDirs).filter((filePath) => isSupportedComponentFile(filePath));
76
78
 
77
79
  let output = "";
78
80
 
79
81
  const categories = [];
80
82
  const imports = {};
83
+ const componentContractEntries = [];
81
84
 
82
85
  // unique identifier needed for replacing
83
86
  let count = 10000000000;
@@ -101,7 +104,14 @@ const buildRettangoliFrontend = async (options) => {
101
104
  }
102
105
 
103
106
 
104
- if (["handlers", "store"].includes(fileType)) {
107
+ const componentContractEntry = {
108
+ category,
109
+ component,
110
+ fileType,
111
+ filePath,
112
+ };
113
+
114
+ if (["handlers", "store", "methods"].includes(fileType)) {
105
115
  const relativePath = path.relative(tempDir, filePath).replaceAll(path.sep, "/");
106
116
  output += `import * as ${component}${capitalize(
107
117
  fileType,
@@ -110,35 +120,57 @@ const buildRettangoliFrontend = async (options) => {
110
120
  replaceMap[count] = `${component}${capitalize(fileType)}`;
111
121
  imports[category][component][fileType] = count;
112
122
  count++;
113
- } else if (["view"].includes(fileType)) {
114
- const view = loadYaml(readFileSync(filePath, "utf8"));
115
- try {
116
- view.template = parse(view.template);
117
- } catch (error) {
118
- console.error(`Error parsing template in file: ${filePath}`);
119
- throw error;
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
+ }
120
133
  }
121
- writeViewFile(view, category, component, tempDir);
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);
122
142
  output += `import ${component}${capitalize(
123
143
  fileType,
124
- )} from './${category}/${component}.view.js';\n`;
144
+ )} from './${category}/${component}.${fileType}.js';\n`;
125
145
  replaceMap[count] = `${component}${capitalize(fileType)}`;
126
146
 
127
147
  imports[category][component][fileType] = count;
128
148
  count++;
129
149
  }
150
+
151
+ componentContractEntries.push(componentContractEntry);
130
152
  }
131
153
 
154
+ validateComponentEntries({
155
+ entries: componentContractEntries,
156
+ errorPrefix: "[Build]",
157
+ });
158
+
132
159
  const relativeSetup = path.relative(tempDir, resolvedSetup).replaceAll(path.sep, "/");
133
160
  output += `
134
161
  import { createComponent } from '@rettangoli/fe';
135
- import { deps, patch, h } from '${relativeSetup}';
162
+ import { deps } from '${relativeSetup}';
136
163
  const imports = ${JSON.stringify(imports, null, 2)};
137
164
 
138
165
  Object.keys(imports).forEach(category => {
139
166
  Object.keys(imports[category]).forEach(component => {
140
- const webComponent = createComponent({ ...imports[category][component], patch, h }, deps[category])
141
- customElements.define(imports[category][component].view.elementName, webComponent);
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);
142
174
  })
143
175
  })
144
176
 
@@ -0,0 +1,53 @@
1
+ import path from "node:path";
2
+ import {
3
+ analyzeComponentDirs,
4
+ formatContractFailureReport,
5
+ } from "./contracts.js";
6
+
7
+ const checkRettangoliFrontend = (options = {}) => {
8
+ const {
9
+ cwd = process.cwd(),
10
+ dirs = ["./example"],
11
+ format = "text",
12
+ } = options;
13
+ const outputFormat = format === "json" ? "json" : "text";
14
+
15
+ const resolvedDirs = dirs.map((dir) => path.resolve(cwd, dir));
16
+ const { errors, summary, index } = analyzeComponentDirs({ dirs: resolvedDirs });
17
+
18
+ if (errors.length > 0) {
19
+ if (outputFormat === "json") {
20
+ console.log(JSON.stringify({
21
+ ok: false,
22
+ prefix: "[Check]",
23
+ summary,
24
+ errors,
25
+ }, null, 2));
26
+ } else {
27
+ console.error(formatContractFailureReport({
28
+ errorPrefix: "[Check]",
29
+ errors,
30
+ }));
31
+ }
32
+ process.exitCode = 1;
33
+ return;
34
+ }
35
+
36
+ const componentCount = Object.values(index).reduce((count, categoryComponents) => {
37
+ return count + Object.keys(categoryComponents).length;
38
+ }, 0);
39
+
40
+ if (outputFormat === "json") {
41
+ console.log(JSON.stringify({
42
+ ok: true,
43
+ prefix: "[Check]",
44
+ componentCount,
45
+ summary,
46
+ }, null, 2));
47
+ return;
48
+ }
49
+
50
+ console.log(`[Check] Component contracts passed for ${componentCount} component(s).`);
51
+ };
52
+
53
+ export default checkRettangoliFrontend;
@@ -0,0 +1,149 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { load as loadYaml } from "js-yaml";
3
+ import { extractCategoryAndComponent, getAllFiles } from "../commonBuild.js";
4
+ import {
5
+ buildComponentContractIndex,
6
+ formatContractErrors as formatContractErrorLines,
7
+ validateComponentContractIndex,
8
+ } from "../core/contracts/componentFiles.js";
9
+
10
+ export const SUPPORTED_COMPONENT_FILE_SUFFIXES = Object.freeze([
11
+ ".store.js",
12
+ ".handlers.js",
13
+ ".methods.js",
14
+ ".constants.yaml",
15
+ ".schema.yaml",
16
+ ".view.yaml",
17
+ ]);
18
+
19
+ export const isSupportedComponentFile = (filePath) => {
20
+ return SUPPORTED_COMPONENT_FILE_SUFFIXES.some((suffix) => filePath.endsWith(suffix));
21
+ };
22
+
23
+ export const collectComponentContractEntriesFromFiles = (allFiles = []) => {
24
+ return allFiles
25
+ .filter((filePath) => isSupportedComponentFile(filePath))
26
+ .map((filePath) => {
27
+ const { category, component, fileType } = extractCategoryAndComponent(filePath);
28
+ const entry = {
29
+ category,
30
+ component,
31
+ fileType,
32
+ filePath,
33
+ };
34
+
35
+ if (["view", "schema"].includes(fileType)) {
36
+ try {
37
+ entry.yamlObject = loadYaml(readFileSync(filePath, "utf8")) ?? {};
38
+ } catch (err) {
39
+ throw new Error(
40
+ `[Check] Failed to read or parse ${filePath}: ${err.message}`,
41
+ );
42
+ }
43
+ }
44
+
45
+ return entry;
46
+ });
47
+ };
48
+
49
+ export const collectComponentContractEntriesFromDirs = (dirs = []) => {
50
+ const allFiles = getAllFiles(dirs);
51
+ return collectComponentContractEntriesFromFiles(allFiles);
52
+ };
53
+
54
+ export const validateComponentEntries = ({
55
+ entries = [],
56
+ errorPrefix = "[Check]",
57
+ }) => {
58
+ const index = buildComponentContractIndex(entries);
59
+ const errors = validateComponentContractIndex(index);
60
+ if (errors.length > 0) {
61
+ throw new Error(
62
+ `${errorPrefix} Component contract validation failed:\n${formatContractErrors(errors).join("\n")}`,
63
+ );
64
+ }
65
+ return {
66
+ index,
67
+ errors,
68
+ };
69
+ };
70
+
71
+ export const validateComponentDirs = ({
72
+ dirs = [],
73
+ errorPrefix = "[Check]",
74
+ }) => {
75
+ const entries = collectComponentContractEntriesFromDirs(dirs);
76
+ const validationResult = validateComponentEntries({ entries, errorPrefix });
77
+ return {
78
+ entries,
79
+ ...validationResult,
80
+ };
81
+ };
82
+
83
+ export const summarizeContractErrors = (errors = []) => {
84
+ const byCode = {};
85
+ const byComponent = {};
86
+
87
+ errors.forEach((error) => {
88
+ const code = error?.code || "UNKNOWN";
89
+ byCode[code] = (byCode[code] || 0) + 1;
90
+
91
+ const componentLabelMatch = String(error?.message || "").match(/^([^:]+):\s/);
92
+ const componentLabel = componentLabelMatch ? componentLabelMatch[1] : "unknown";
93
+ byComponent[componentLabel] = (byComponent[componentLabel] || 0) + 1;
94
+ });
95
+
96
+ const byCodeSorted = Object.entries(byCode)
97
+ .map(([code, count]) => ({ code, count }))
98
+ .sort((a, b) => a.code.localeCompare(b.code));
99
+
100
+ const byComponentSorted = Object.entries(byComponent)
101
+ .map(([component, count]) => ({ component, count }))
102
+ .sort((a, b) => b.count - a.count || a.component.localeCompare(b.component));
103
+
104
+ return {
105
+ total: errors.length,
106
+ byCode: byCodeSorted,
107
+ byComponent: byComponentSorted,
108
+ };
109
+ };
110
+
111
+ export const formatContractFailureReport = ({
112
+ errorPrefix = "[Check]",
113
+ errors = [],
114
+ }) => {
115
+ const summary = summarizeContractErrors(errors);
116
+ const header = `${errorPrefix} Component contract validation failed: ${summary.total} issue(s)`;
117
+ const byCodeLines = summary.byCode.map(({ code, count }) => `- ${code}: ${count}`);
118
+ const byComponentLines = summary.byComponent.map(
119
+ ({ component, count }) => `- ${component}: ${count}`,
120
+ );
121
+ const detailLines = formatContractErrorLines(errors);
122
+
123
+ return [
124
+ header,
125
+ "By rule:",
126
+ ...(byCodeLines.length > 0 ? byCodeLines : ["- none"]),
127
+ "By component:",
128
+ ...(byComponentLines.length > 0 ? byComponentLines : ["- none"]),
129
+ "Details:",
130
+ ...(detailLines.length > 0 ? detailLines : ["- none"]),
131
+ ].join("\n");
132
+ };
133
+
134
+ export const analyzeComponentEntries = ({ entries = [] }) => {
135
+ const index = buildComponentContractIndex(entries);
136
+ const errors = validateComponentContractIndex(index);
137
+ const summary = summarizeContractErrors(errors);
138
+ return {
139
+ entries,
140
+ index,
141
+ errors,
142
+ summary,
143
+ };
144
+ };
145
+
146
+ export const analyzeComponentDirs = ({ dirs = [] }) => {
147
+ const entries = collectComponentContractEntriesFromDirs(dirs);
148
+ return analyzeComponentEntries({ entries });
149
+ };
package/src/cli/index.js CHANGED
@@ -1,11 +1,5 @@
1
- import build from './build.js';
2
- import scaffold from './scaffold.js';
3
- import watch from './watch.js';
4
- import examples from './examples.js';
5
-
6
- export {
7
- build,
8
- scaffold,
9
- watch,
10
- examples
11
- }
1
+ export { default as build } from "./build.js";
2
+ export { default as check } from "./check.js";
3
+ export { default as scaffold } from "./scaffold.js";
4
+ export { default as watch } from "./watch.js";
5
+ export { default as examples } from "./examples.js";
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { validateComponentDirs } from './contracts.js';
3
4
 
4
5
  const __dirname = path.dirname(new URL(import.meta.url).pathname);
5
6
 
@@ -37,6 +38,11 @@ const scaffoldPage = (options) => {
37
38
  }
38
39
  });
39
40
 
41
+ validateComponentDirs({
42
+ dirs: [path.resolve(targetDir)],
43
+ errorPrefix: "[Scaffold]",
44
+ });
45
+
40
46
  console.log(`Successfully scaffolded ${targetDir} from template`);
41
47
  }
42
48
 
package/src/cli/watch.js CHANGED
@@ -20,9 +20,10 @@ const setupWatcher = (directory, options) => {
20
20
  console.log(`Detected ${event} in ${directory}/${filename}`);
21
21
  if (filename) {
22
22
  try {
23
+ const changedFilePath = path.join(directory, filename);
23
24
  if (filename.endsWith('.view.yaml')) {
24
- const view = loadYaml(readFileSync(path.join(directory, filename), "utf8"));
25
- const { category, component } = extractCategoryAndComponent(filename);
25
+ const view = loadYaml(readFileSync(changedFilePath, "utf8"));
26
+ const { category, component } = extractCategoryAndComponent(changedFilePath);
26
27
  await writeViewFile(view, category, component);
27
28
  }
28
29