@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 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/) - Dev server and production bundling
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 18+ or Bun
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()` and serves the configured `outfile` path via middleware.
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
- - Rollup output control through Vite (`entryFileNames`, `chunkFileNames`, `assetFileNames`) to preserve CLI `outfile` behavior.
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",
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": "^6.3.5"
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 : "esbuild",
41
- sourcemap: !!development,
52
+ minify: development ? false : "oxc",
53
+ sourcemap: false,
42
54
  target: "esnext",
43
- rollupOptions: {
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
- console.log(`Build complete. Output file: ${resolvedOutfile}`);
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({ dirs: resolvedDirs });
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") {
@@ -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({ entries, errorPrefix });
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 = ({ entries = [] }) => {
141
+ export const analyzeComponentEntries = ({
142
+ entries = [],
143
+ i18nContext = { enabled: false },
144
+ i18nErrors = [],
145
+ }) => {
135
146
  const index = buildComponentContractIndex(entries);
136
- const errors = validateComponentContractIndex(index);
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 = ({ dirs = [] }) => {
160
+ export const analyzeComponentDirs = ({
161
+ dirs = [],
162
+ cwd = process.cwd(),
163
+ i18n = null,
164
+ }) => {
147
165
  const entries = collectComponentContractEntriesFromDirs(dirs);
148
- return analyzeComponentEntries({ entries });
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 { createComponent } from "@rettangoli/fe";
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 webComponent = createComponent({ ...componentConfig }, deps[category]);
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(