@rettangoli/fe 1.1.3 → 1.2.1
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 +14 -5
- package/package.json +5 -2
- package/src/cli/build.js +22 -6
- package/src/cli/check.js +6 -1
- package/src/cli/contracts.js +32 -6
- package/src/cli/frontendEntrySource.js +54 -2
- package/src/cli/i18nBuild.js +367 -0
- package/src/cli/vitePlugin.js +91 -0
- package/src/cli/watch.js +11 -3
- package/src/core/contracts/componentFiles.js +10 -1
- package/src/core/i18n/viewReferences.js +287 -0
- package/src/core/runtime/componentOrchestrator.js +1 -0
- package/src/core/runtime/events.js +7 -0
- package/src/core/runtime/globalListeners.js +2 -0
- package/src/core/runtime/i18n.js +155 -0
- package/src/core/runtime/lifecycle.js +14 -1
- package/src/core/runtime/store.js +11 -3
- package/src/index.js +2 -0
- package/src/parser.js +1 -0
- package/src/web/createWebComponentClass.js +28 -2
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ rtgl fe watch # Start dev server
|
|
|
26
26
|
- **[Schema System](./docs/schema.md)** - Component API and metadata
|
|
27
27
|
- **[Store Management](./docs/store.md)** - State patterns
|
|
28
28
|
- **[Event Handlers](./docs/handlers.md)** - Event handling
|
|
29
|
+
- **[Internationalization](./docs/i18n.md)** - Locale files, view usage, and locale switching
|
|
29
30
|
|
|
30
31
|
## Architecture
|
|
31
32
|
|
|
@@ -37,7 +38,7 @@ rtgl fe watch # Start dev server
|
|
|
37
38
|
- [Jempl](https://github.com/yuusoft-org/jempl) - Template engine
|
|
38
39
|
|
|
39
40
|
**Build & Development:**
|
|
40
|
-
- [Vite](https://vite.dev/) -
|
|
41
|
+
- [Vite 8](https://vite.dev/) - Rolldown-powered production bundling and dev server
|
|
41
42
|
|
|
42
43
|
**Browser Native:**
|
|
43
44
|
- Web Components - Component encapsulation
|
|
@@ -46,7 +47,7 @@ rtgl fe watch # Start dev server
|
|
|
46
47
|
|
|
47
48
|
### Prerequisites
|
|
48
49
|
|
|
49
|
-
- Node.js
|
|
50
|
+
- Node.js 20.19+, Node.js 22.12+, or Bun
|
|
50
51
|
- A `rettangoli.config.yaml` file in your project root
|
|
51
52
|
|
|
52
53
|
### Setup
|
|
@@ -92,10 +93,11 @@ src/
|
|
|
92
93
|
|
|
93
94
|
`@rettangoli/fe` uses Vite directly through the Node API, behind the existing FE CLI commands.
|
|
94
95
|
|
|
95
|
-
- `rtgl fe build` uses `vite.build()` with a virtual entry module generated from configured component files.
|
|
96
|
-
- `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.
|
|
97
98
|
- FE runtime source is generated in memory (virtual module), so no temporary generated JS files are required.
|
|
98
99
|
- Contract validation, YAML parsing, and template parsing still run before code generation.
|
|
100
|
+
- Component directories, setup files, and locale files are registered explicitly so watch mode also sees sources outside the served static root.
|
|
99
101
|
|
|
100
102
|
Current Vite features used by FE:
|
|
101
103
|
|
|
@@ -105,7 +107,7 @@ Current Vite features used by FE:
|
|
|
105
107
|
- `resolveId` + `load` for the FE virtual entry (`virtual:rettangoli-fe-entry`).
|
|
106
108
|
- `handleHotUpdate` for FE file change detection.
|
|
107
109
|
- `configureServer` for full-page reload and serving the configured output entry URL.
|
|
108
|
-
-
|
|
110
|
+
- Rolldown output control through Vite (`entryFileNames`, `chunkFileNames`, `assetFileNames`) to preserve CLI `outfile` behavior.
|
|
109
111
|
|
|
110
112
|
Notes:
|
|
111
113
|
|
|
@@ -123,6 +125,13 @@ fe:
|
|
|
123
125
|
- "./src/pages"
|
|
124
126
|
setup: "setup.js"
|
|
125
127
|
outfile: "./dist/bundle.js"
|
|
128
|
+
i18n:
|
|
129
|
+
dir: "./src/i18n"
|
|
130
|
+
defaultLocale: "en"
|
|
131
|
+
fallbackLocale: "en"
|
|
132
|
+
locales:
|
|
133
|
+
- "en"
|
|
134
|
+
- "vi"
|
|
126
135
|
examples:
|
|
127
136
|
outputDir: "./vt/specs/examples"
|
|
128
137
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rettangoli/fe",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
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
|
|
|
@@ -6,41 +7,53 @@ import {
|
|
|
6
7
|
RETTANGOLI_FE_VIRTUAL_ENTRY_ID,
|
|
7
8
|
createRettangoliFeVitePlugin,
|
|
8
9
|
} from "./vitePlugin.js";
|
|
10
|
+
import { emitI18nAssets, loadI18nBuildContext } from "./i18nBuild.js";
|
|
9
11
|
|
|
10
12
|
const buildRettangoliFrontend = async (options = {}) => {
|
|
11
|
-
console.log("running build with options", options);
|
|
12
|
-
|
|
13
13
|
const {
|
|
14
14
|
cwd = process.cwd(),
|
|
15
15
|
dirs = ["./example"],
|
|
16
16
|
outfile = "./vt/static/main.js",
|
|
17
17
|
setup = "setup.js",
|
|
18
18
|
development = false,
|
|
19
|
+
i18n = null,
|
|
19
20
|
} = options;
|
|
20
21
|
|
|
21
22
|
const resolvedOutfile = path.resolve(cwd, outfile);
|
|
22
23
|
const outDir = path.dirname(resolvedOutfile);
|
|
23
24
|
const outFileName = path.basename(resolvedOutfile);
|
|
24
25
|
const relativeOutDir = path.relative(cwd, outDir) || ".";
|
|
26
|
+
const i18nContext = loadI18nBuildContext({
|
|
27
|
+
cwd,
|
|
28
|
+
i18n,
|
|
29
|
+
errorPrefix: "[Build]",
|
|
30
|
+
});
|
|
31
|
+
const startedAt = performance.now();
|
|
32
|
+
|
|
33
|
+
console.log(`[Build] Building ${resolvedOutfile}...`);
|
|
25
34
|
|
|
26
35
|
await viteBuild({
|
|
27
36
|
configFile: false,
|
|
37
|
+
clearScreen: false,
|
|
38
|
+
logLevel: "warn",
|
|
28
39
|
root: cwd,
|
|
29
40
|
plugins: [
|
|
30
41
|
createRettangoliFeVitePlugin({
|
|
31
42
|
cwd,
|
|
32
43
|
dirs,
|
|
33
44
|
setup,
|
|
45
|
+
i18n,
|
|
34
46
|
errorPrefix: "[Build]",
|
|
35
47
|
}),
|
|
36
48
|
],
|
|
37
49
|
build: {
|
|
38
50
|
outDir: relativeOutDir,
|
|
39
51
|
emptyOutDir: false,
|
|
40
|
-
minify: development ? false : "
|
|
41
|
-
sourcemap:
|
|
52
|
+
minify: development ? false : "oxc",
|
|
53
|
+
sourcemap: false,
|
|
42
54
|
target: "esnext",
|
|
43
|
-
|
|
55
|
+
reportCompressedSize: false,
|
|
56
|
+
rolldownOptions: {
|
|
44
57
|
input: RETTANGOLI_FE_VIRTUAL_ENTRY_ID,
|
|
45
58
|
output: {
|
|
46
59
|
format: "es",
|
|
@@ -52,7 +65,10 @@ const buildRettangoliFrontend = async (options = {}) => {
|
|
|
52
65
|
},
|
|
53
66
|
});
|
|
54
67
|
|
|
55
|
-
|
|
68
|
+
emitI18nAssets({ outDir, i18nContext });
|
|
69
|
+
|
|
70
|
+
const durationMs = Math.round(performance.now() - startedAt);
|
|
71
|
+
console.log(`[Build] Complete in ${durationMs}ms.`);
|
|
56
72
|
};
|
|
57
73
|
|
|
58
74
|
export default buildRettangoliFrontend;
|
package/src/cli/check.js
CHANGED
|
@@ -9,11 +9,16 @@ const checkRettangoliFrontend = (options = {}) => {
|
|
|
9
9
|
cwd = process.cwd(),
|
|
10
10
|
dirs = ["./example"],
|
|
11
11
|
format = "text",
|
|
12
|
+
i18n = null,
|
|
12
13
|
} = options;
|
|
13
14
|
const outputFormat = format === "json" ? "json" : "text";
|
|
14
15
|
|
|
15
16
|
const resolvedDirs = dirs.map((dir) => path.resolve(cwd, dir));
|
|
16
|
-
const { errors, summary, index } = analyzeComponentDirs({
|
|
17
|
+
const { errors, summary, index } = analyzeComponentDirs({
|
|
18
|
+
dirs: resolvedDirs,
|
|
19
|
+
cwd,
|
|
20
|
+
i18n,
|
|
21
|
+
});
|
|
17
22
|
|
|
18
23
|
if (errors.length > 0) {
|
|
19
24
|
if (outputFormat === "json") {
|
package/src/cli/contracts.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
formatContractErrors as formatContractErrorLines,
|
|
7
7
|
validateComponentContractIndex,
|
|
8
8
|
} from "../core/contracts/componentFiles.js";
|
|
9
|
+
import { analyzeI18nBuildContext } from "./i18nBuild.js";
|
|
9
10
|
|
|
10
11
|
export const SUPPORTED_COMPONENT_FILE_SUFFIXES = Object.freeze([
|
|
11
12
|
".store.js",
|
|
@@ -54,9 +55,10 @@ export const collectComponentContractEntriesFromDirs = (dirs = []) => {
|
|
|
54
55
|
export const validateComponentEntries = ({
|
|
55
56
|
entries = [],
|
|
56
57
|
errorPrefix = "[Check]",
|
|
58
|
+
i18nContext = { enabled: false },
|
|
57
59
|
}) => {
|
|
58
60
|
const index = buildComponentContractIndex(entries);
|
|
59
|
-
const errors = validateComponentContractIndex(index);
|
|
61
|
+
const errors = validateComponentContractIndex(index, { i18nContext });
|
|
60
62
|
if (errors.length > 0) {
|
|
61
63
|
throw new Error(
|
|
62
64
|
`${errorPrefix} Component contract validation failed:\n${formatContractErrorLines(errors).join("\n")}`,
|
|
@@ -71,9 +73,14 @@ export const validateComponentEntries = ({
|
|
|
71
73
|
export const validateComponentDirs = ({
|
|
72
74
|
dirs = [],
|
|
73
75
|
errorPrefix = "[Check]",
|
|
76
|
+
i18nContext = { enabled: false },
|
|
74
77
|
}) => {
|
|
75
78
|
const entries = collectComponentContractEntriesFromDirs(dirs);
|
|
76
|
-
const validationResult = validateComponentEntries({
|
|
79
|
+
const validationResult = validateComponentEntries({
|
|
80
|
+
entries,
|
|
81
|
+
errorPrefix,
|
|
82
|
+
i18nContext,
|
|
83
|
+
});
|
|
77
84
|
return {
|
|
78
85
|
entries,
|
|
79
86
|
...validationResult,
|
|
@@ -131,9 +138,16 @@ export const formatContractFailureReport = ({
|
|
|
131
138
|
].join("\n");
|
|
132
139
|
};
|
|
133
140
|
|
|
134
|
-
export const analyzeComponentEntries = ({
|
|
141
|
+
export const analyzeComponentEntries = ({
|
|
142
|
+
entries = [],
|
|
143
|
+
i18nContext = { enabled: false },
|
|
144
|
+
i18nErrors = [],
|
|
145
|
+
}) => {
|
|
135
146
|
const index = buildComponentContractIndex(entries);
|
|
136
|
-
const errors =
|
|
147
|
+
const errors = [
|
|
148
|
+
...i18nErrors,
|
|
149
|
+
...validateComponentContractIndex(index, { i18nContext }),
|
|
150
|
+
];
|
|
137
151
|
const summary = summarizeContractErrors(errors);
|
|
138
152
|
return {
|
|
139
153
|
entries,
|
|
@@ -143,7 +157,19 @@ export const analyzeComponentEntries = ({ entries = [] }) => {
|
|
|
143
157
|
};
|
|
144
158
|
};
|
|
145
159
|
|
|
146
|
-
export const analyzeComponentDirs = ({
|
|
160
|
+
export const analyzeComponentDirs = ({
|
|
161
|
+
dirs = [],
|
|
162
|
+
cwd = process.cwd(),
|
|
163
|
+
i18n = null,
|
|
164
|
+
}) => {
|
|
147
165
|
const entries = collectComponentContractEntriesFromDirs(dirs);
|
|
148
|
-
|
|
166
|
+
const { context: i18nContext, errors: i18nErrors } = analyzeI18nBuildContext({
|
|
167
|
+
cwd,
|
|
168
|
+
i18n,
|
|
169
|
+
});
|
|
170
|
+
return analyzeComponentEntries({
|
|
171
|
+
entries,
|
|
172
|
+
i18nContext,
|
|
173
|
+
i18nErrors,
|
|
174
|
+
});
|
|
149
175
|
};
|
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
isSupportedComponentFile,
|
|
10
10
|
validateComponentEntries,
|
|
11
11
|
} from "./contracts.js";
|
|
12
|
+
import {
|
|
13
|
+
buildI18nAssets,
|
|
14
|
+
loadI18nBuildContext,
|
|
15
|
+
} from "./i18nBuild.js";
|
|
12
16
|
|
|
13
17
|
const MODULE_FILE_TYPES = new Set(["handlers", "store", "methods"]);
|
|
14
18
|
const YAML_FILE_TYPES = new Set(["view", "constants", "schema"]);
|
|
@@ -65,15 +69,52 @@ const createComponentImportLines = ({ componentMatrix, categories }) => {
|
|
|
65
69
|
return lines.join("\n");
|
|
66
70
|
};
|
|
67
71
|
|
|
72
|
+
const createI18nRuntimeSource = ({ i18nContext }) => {
|
|
73
|
+
if (!i18nContext.enabled) {
|
|
74
|
+
return "const __rtglFrameworkDeps = {};";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const assets = buildI18nAssets({ i18nContext });
|
|
78
|
+
const urlEntries = assets
|
|
79
|
+
.map((asset) => {
|
|
80
|
+
return ` ${JSON.stringify(asset.locale)}: new URL(/* @vite-ignore */ ${JSON.stringify(`./${asset.relativeFileName}`)}, import.meta.url).href,`;
|
|
81
|
+
})
|
|
82
|
+
.join("\n");
|
|
83
|
+
|
|
84
|
+
const initialCatalogs = {};
|
|
85
|
+
initialCatalogs[i18nContext.defaultLocale] =
|
|
86
|
+
i18nContext.catalogs[i18nContext.defaultLocale];
|
|
87
|
+
initialCatalogs[i18nContext.fallbackLocale] =
|
|
88
|
+
i18nContext.catalogs[i18nContext.fallbackLocale];
|
|
89
|
+
|
|
90
|
+
return `
|
|
91
|
+
const __rtglI18nRuntime = createI18nRuntime({
|
|
92
|
+
defaultLocale: ${JSON.stringify(i18nContext.defaultLocale)},
|
|
93
|
+
fallbackLocale: ${JSON.stringify(i18nContext.fallbackLocale)},
|
|
94
|
+
locales: ${JSON.stringify(i18nContext.locales)},
|
|
95
|
+
urls: {
|
|
96
|
+
${urlEntries}
|
|
97
|
+
},
|
|
98
|
+
initialCatalogs: ${JSON.stringify(initialCatalogs)},
|
|
99
|
+
});
|
|
100
|
+
await __rtglI18nRuntime.ready();
|
|
101
|
+
const __rtglFrameworkDeps = {
|
|
102
|
+
__rtglI18nRuntime,
|
|
103
|
+
locale: __rtglI18nRuntime.locale,
|
|
104
|
+
};`.trim();
|
|
105
|
+
};
|
|
106
|
+
|
|
68
107
|
export const generateFrontendEntrySource = ({
|
|
69
108
|
cwd = process.cwd(),
|
|
70
109
|
dirs = ["./example"],
|
|
71
110
|
setup = "setup.js",
|
|
72
111
|
command = "build",
|
|
112
|
+
i18n = null,
|
|
73
113
|
errorPrefix = "[Build]",
|
|
74
114
|
} = {}) => {
|
|
75
115
|
const resolvedDirs = dirs.map((dir) => path.resolve(cwd, dir));
|
|
76
116
|
const resolvedSetup = path.resolve(cwd, setup);
|
|
117
|
+
const i18nContext = loadI18nBuildContext({ cwd, i18n, errorPrefix });
|
|
77
118
|
const allFiles = getAllFiles(resolvedDirs)
|
|
78
119
|
.filter((filePath) => isSupportedComponentFile(filePath))
|
|
79
120
|
.sort((a, b) => a.localeCompare(b));
|
|
@@ -143,6 +184,7 @@ export const generateFrontendEntrySource = ({
|
|
|
143
184
|
validateComponentEntries({
|
|
144
185
|
entries: componentContractEntries,
|
|
145
186
|
errorPrefix,
|
|
187
|
+
i18nContext,
|
|
146
188
|
});
|
|
147
189
|
|
|
148
190
|
const setupImportPath = toImportPath({
|
|
@@ -153,18 +195,28 @@ export const generateFrontendEntrySource = ({
|
|
|
153
195
|
componentMatrix,
|
|
154
196
|
categories: Object.keys(componentMatrix).sort(),
|
|
155
197
|
});
|
|
198
|
+
const feImports = i18nContext.enabled
|
|
199
|
+
? "createComponent, createI18nRuntime"
|
|
200
|
+
: "createComponent";
|
|
201
|
+
const i18nRuntimeSource = createI18nRuntimeSource({ i18nContext });
|
|
156
202
|
|
|
157
203
|
return `
|
|
158
204
|
${declarationLines.join("\n")}
|
|
159
|
-
import {
|
|
205
|
+
import { ${feImports} } from "@rettangoli/fe";
|
|
160
206
|
import { deps } from ${JSON.stringify(setupImportPath)};
|
|
161
207
|
|
|
162
208
|
${categoryLines}
|
|
163
209
|
|
|
210
|
+
${i18nRuntimeSource}
|
|
211
|
+
|
|
164
212
|
Object.keys(imports).forEach((category) => {
|
|
165
213
|
Object.keys(imports[category]).forEach((component) => {
|
|
166
214
|
const componentConfig = imports[category][component];
|
|
167
|
-
const
|
|
215
|
+
const categoryDeps = {
|
|
216
|
+
...((deps && deps[category]) || {}),
|
|
217
|
+
...__rtglFrameworkDeps,
|
|
218
|
+
};
|
|
219
|
+
const webComponent = createComponent({ ...componentConfig }, categoryDeps);
|
|
168
220
|
const elementName = componentConfig.schema?.componentName;
|
|
169
221
|
if (!elementName) {
|
|
170
222
|
throw new Error(
|