@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
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
import { load as loadYaml } from "js-yaml";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_OUTPUT_DIR = "i18n";
|
|
12
|
+
const LOCALE_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
13
|
+
|
|
14
|
+
const isPlainObject = (value) =>
|
|
15
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
|
|
17
|
+
const toPosixPath = (value) => value.split(path.sep).join("/");
|
|
18
|
+
|
|
19
|
+
const createI18nError = ({ code = "RTGL-I18N-004", message, filePath }) => ({
|
|
20
|
+
code,
|
|
21
|
+
message,
|
|
22
|
+
filePath: filePath || "",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const assertNoI18nErrors = ({ errors, errorPrefix }) => {
|
|
26
|
+
if (errors.length === 0) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const details = errors
|
|
31
|
+
.map((error) => `${error.code} ${error.message}${error.filePath ? ` [${error.filePath}]` : ""}`)
|
|
32
|
+
.join("\n");
|
|
33
|
+
throw new Error(`${errorPrefix} i18n validation failed:\n${details}`);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const validateLocaleName = ({ locale, errors, filePath }) => {
|
|
37
|
+
if (typeof locale !== "string" || !LOCALE_PATTERN.test(locale)) {
|
|
38
|
+
errors.push(createI18nError({
|
|
39
|
+
message: `invalid locale name ${JSON.stringify(locale)}. Locale names may contain letters, numbers, "_" and "-".`,
|
|
40
|
+
filePath,
|
|
41
|
+
}));
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const flattenCatalogKeys = (catalog = {}) => {
|
|
48
|
+
const keys = [];
|
|
49
|
+
Object.entries(catalog).forEach(([namespace, messages]) => {
|
|
50
|
+
Object.keys(messages || {}).forEach((key) => {
|
|
51
|
+
keys.push(`${namespace}.${key}`);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
return keys.sort();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const validateCatalog = ({ catalog, locale, filePath }) => {
|
|
58
|
+
const errors = [];
|
|
59
|
+
const normalized = {};
|
|
60
|
+
|
|
61
|
+
if (!isPlainObject(catalog)) {
|
|
62
|
+
errors.push(createI18nError({
|
|
63
|
+
message: `${locale}.yaml must contain a YAML object at the root.`,
|
|
64
|
+
filePath,
|
|
65
|
+
}));
|
|
66
|
+
return { catalog: normalized, errors };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Object.entries(catalog).forEach(([namespace, messages]) => {
|
|
70
|
+
if (!isPlainObject(messages)) {
|
|
71
|
+
errors.push(createI18nError({
|
|
72
|
+
message: `${locale}.yaml namespace "${namespace}" must be an object. Use namespace -> key -> content.`,
|
|
73
|
+
filePath,
|
|
74
|
+
}));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
normalized[namespace] = {};
|
|
79
|
+
Object.entries(messages).forEach(([key, content]) => {
|
|
80
|
+
if (isPlainObject(content) || Array.isArray(content)) {
|
|
81
|
+
errors.push(createI18nError({
|
|
82
|
+
message: `${locale}.yaml key "${namespace}.${key}" is nested too deeply. Use namespace -> key -> content only.`,
|
|
83
|
+
filePath,
|
|
84
|
+
}));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (typeof content !== "string") {
|
|
89
|
+
errors.push(createI18nError({
|
|
90
|
+
message: `${locale}.yaml key "${namespace}.${key}" must be a string.`,
|
|
91
|
+
filePath,
|
|
92
|
+
}));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
normalized[namespace][key] = content;
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return { catalog: normalized, errors };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const normalizeI18nConfig = ({ cwd, i18n }) => {
|
|
104
|
+
const errors = [];
|
|
105
|
+
|
|
106
|
+
if (!i18n) {
|
|
107
|
+
return {
|
|
108
|
+
config: { enabled: false },
|
|
109
|
+
errors,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!isPlainObject(i18n)) {
|
|
114
|
+
errors.push(createI18nError({
|
|
115
|
+
message: "fe.i18n must be an object.",
|
|
116
|
+
}));
|
|
117
|
+
return {
|
|
118
|
+
config: { enabled: false },
|
|
119
|
+
errors,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const dir = i18n.dir;
|
|
124
|
+
if (typeof dir !== "string" || dir.trim() === "") {
|
|
125
|
+
errors.push(createI18nError({
|
|
126
|
+
message: "fe.i18n.dir must be a non-empty string.",
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const defaultLocale = i18n.defaultLocale;
|
|
131
|
+
const fallbackLocale = i18n.fallbackLocale;
|
|
132
|
+
validateLocaleName({ locale: defaultLocale, errors });
|
|
133
|
+
validateLocaleName({ locale: fallbackLocale, errors });
|
|
134
|
+
|
|
135
|
+
if (!Array.isArray(i18n.locales) || i18n.locales.length === 0) {
|
|
136
|
+
errors.push(createI18nError({
|
|
137
|
+
message: "fe.i18n.locales must be a non-empty array.",
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const locales = [];
|
|
142
|
+
const seen = new Set();
|
|
143
|
+
if (Array.isArray(i18n.locales)) {
|
|
144
|
+
i18n.locales.forEach((locale) => {
|
|
145
|
+
if (!validateLocaleName({ locale, errors })) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (seen.has(locale)) {
|
|
149
|
+
errors.push(createI18nError({
|
|
150
|
+
message: `fe.i18n.locales contains duplicate locale "${locale}".`,
|
|
151
|
+
}));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
seen.add(locale);
|
|
155
|
+
locales.push(locale);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (defaultLocale && locales.length > 0 && !seen.has(defaultLocale)) {
|
|
160
|
+
errors.push(createI18nError({
|
|
161
|
+
message: `fe.i18n.defaultLocale "${defaultLocale}" must be listed in fe.i18n.locales.`,
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (fallbackLocale && locales.length > 0 && !seen.has(fallbackLocale)) {
|
|
166
|
+
errors.push(createI18nError({
|
|
167
|
+
message: `fe.i18n.fallbackLocale "${fallbackLocale}" must be listed in fe.i18n.locales.`,
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const outputDir = typeof i18n.outputDir === "string" && i18n.outputDir.trim()
|
|
172
|
+
? i18n.outputDir
|
|
173
|
+
: DEFAULT_OUTPUT_DIR;
|
|
174
|
+
const normalizedOutputDir = outputDir.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
175
|
+
if (
|
|
176
|
+
!normalizedOutputDir ||
|
|
177
|
+
normalizedOutputDir.split("/").includes("..") ||
|
|
178
|
+
path.posix.isAbsolute(normalizedOutputDir)
|
|
179
|
+
) {
|
|
180
|
+
errors.push(createI18nError({
|
|
181
|
+
message: "fe.i18n.outputDir must be a relative path when provided.",
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
config: {
|
|
187
|
+
enabled: errors.length === 0,
|
|
188
|
+
dir,
|
|
189
|
+
resolvedDir: dir ? path.resolve(cwd, dir) : null,
|
|
190
|
+
outputDir: normalizedOutputDir || DEFAULT_OUTPUT_DIR,
|
|
191
|
+
defaultLocale,
|
|
192
|
+
fallbackLocale,
|
|
193
|
+
locales,
|
|
194
|
+
},
|
|
195
|
+
errors,
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const analyzeI18nBuildContext = ({
|
|
200
|
+
cwd = process.cwd(),
|
|
201
|
+
i18n = null,
|
|
202
|
+
} = {}) => {
|
|
203
|
+
const { config, errors } = normalizeI18nConfig({ cwd, i18n });
|
|
204
|
+
|
|
205
|
+
if (!i18n || errors.length > 0) {
|
|
206
|
+
return {
|
|
207
|
+
context: { enabled: false },
|
|
208
|
+
errors,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!existsSync(config.resolvedDir)) {
|
|
213
|
+
errors.push(createI18nError({
|
|
214
|
+
message: `fe.i18n.dir does not exist: ${config.resolvedDir}`,
|
|
215
|
+
filePath: config.resolvedDir,
|
|
216
|
+
}));
|
|
217
|
+
return {
|
|
218
|
+
context: { enabled: false },
|
|
219
|
+
errors,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const localeFiles = {};
|
|
224
|
+
const catalogs = {};
|
|
225
|
+
|
|
226
|
+
config.locales.forEach((locale) => {
|
|
227
|
+
const filePath = path.join(config.resolvedDir, `${locale}.yaml`);
|
|
228
|
+
localeFiles[locale] = filePath;
|
|
229
|
+
|
|
230
|
+
if (!existsSync(filePath)) {
|
|
231
|
+
errors.push(createI18nError({
|
|
232
|
+
message: `missing i18n file for locale "${locale}". Expected ${filePath}.`,
|
|
233
|
+
filePath,
|
|
234
|
+
}));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let yamlObject;
|
|
239
|
+
try {
|
|
240
|
+
yamlObject = loadYaml(readFileSync(filePath, "utf8")) ?? {};
|
|
241
|
+
} catch (error) {
|
|
242
|
+
errors.push(createI18nError({
|
|
243
|
+
message: `failed to parse ${locale}.yaml: ${error.message}`,
|
|
244
|
+
filePath,
|
|
245
|
+
}));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const result = validateCatalog({ catalog: yamlObject, locale, filePath });
|
|
250
|
+
errors.push(...result.errors);
|
|
251
|
+
catalogs[locale] = result.catalog;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const defaultCatalog = catalogs[config.defaultLocale];
|
|
255
|
+
if (defaultCatalog) {
|
|
256
|
+
const defaultKeys = flattenCatalogKeys(defaultCatalog);
|
|
257
|
+
config.locales.forEach((locale) => {
|
|
258
|
+
if (locale === config.defaultLocale || !catalogs[locale]) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
defaultKeys.forEach((fullKey) => {
|
|
263
|
+
const [namespace, key] = fullKey.split(".");
|
|
264
|
+
if (
|
|
265
|
+
!Object.prototype.hasOwnProperty.call(catalogs[locale] || {}, namespace) ||
|
|
266
|
+
!Object.prototype.hasOwnProperty.call(catalogs[locale]?.[namespace] || {}, key)
|
|
267
|
+
) {
|
|
268
|
+
errors.push(createI18nError({
|
|
269
|
+
message: `${locale}.yaml is missing key "${fullKey}" from default locale "${config.defaultLocale}".`,
|
|
270
|
+
filePath: localeFiles[locale],
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const context = {
|
|
278
|
+
...config,
|
|
279
|
+
enabled: errors.length === 0,
|
|
280
|
+
localeFiles,
|
|
281
|
+
catalogs,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
context,
|
|
286
|
+
errors,
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export const loadI18nBuildContext = ({
|
|
291
|
+
cwd = process.cwd(),
|
|
292
|
+
i18n = null,
|
|
293
|
+
errorPrefix = "[Build]",
|
|
294
|
+
} = {}) => {
|
|
295
|
+
const { context, errors } = analyzeI18nBuildContext({ cwd, i18n });
|
|
296
|
+
assertNoI18nErrors({ errors, errorPrefix });
|
|
297
|
+
return context;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
export const buildI18nAssets = ({ i18nContext }) => {
|
|
301
|
+
if (!i18nContext?.enabled) {
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return i18nContext.locales.map((locale) => {
|
|
306
|
+
const relativeFileName = path.posix.join(
|
|
307
|
+
i18nContext.outputDir,
|
|
308
|
+
`${locale}.json`,
|
|
309
|
+
);
|
|
310
|
+
return {
|
|
311
|
+
locale,
|
|
312
|
+
relativeFileName,
|
|
313
|
+
sourcePath: i18nContext.localeFiles[locale],
|
|
314
|
+
catalog: i18nContext.catalogs[locale],
|
|
315
|
+
content: `${JSON.stringify(i18nContext.catalogs[locale], null, 2)}\n`,
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
export const emitI18nAssets = ({ outDir, i18nContext }) => {
|
|
321
|
+
const assets = buildI18nAssets({ i18nContext });
|
|
322
|
+
|
|
323
|
+
return assets.map((asset) => {
|
|
324
|
+
const outputPath = path.resolve(outDir, asset.relativeFileName);
|
|
325
|
+
mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
326
|
+
writeFileSync(outputPath, asset.content);
|
|
327
|
+
return {
|
|
328
|
+
...asset,
|
|
329
|
+
outputPath,
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
export const isI18nSourceFilePath = ({ filePath, i18nContext }) => {
|
|
335
|
+
if (!i18nContext?.enabled && !i18nContext?.resolvedDir) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const resolvedFilePath = path.resolve(filePath);
|
|
340
|
+
const extension = path.extname(resolvedFilePath);
|
|
341
|
+
if (extension !== ".yaml") {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const relativePath = path.relative(i18nContext.resolvedDir, resolvedFilePath);
|
|
346
|
+
return (
|
|
347
|
+
relativePath !== "" &&
|
|
348
|
+
!relativePath.startsWith("..") &&
|
|
349
|
+
!path.isAbsolute(relativePath)
|
|
350
|
+
);
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
export const getI18nPublicAssetPath = ({
|
|
354
|
+
publicEntryPath,
|
|
355
|
+
relativeFileName,
|
|
356
|
+
}) => {
|
|
357
|
+
const normalizedEntry = publicEntryPath
|
|
358
|
+
? publicEntryPath.replace(/\\/g, "/")
|
|
359
|
+
: "/main.js";
|
|
360
|
+
const entryPath = normalizedEntry.startsWith("/")
|
|
361
|
+
? normalizedEntry
|
|
362
|
+
: `/${normalizedEntry}`;
|
|
363
|
+
return path.posix.join(
|
|
364
|
+
path.posix.dirname(entryPath),
|
|
365
|
+
toPosixPath(relativeFileName),
|
|
366
|
+
);
|
|
367
|
+
};
|
package/src/cli/vitePlugin.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
+
import { getAllFiles } from "../commonBuild.js";
|
|
3
4
|
import { isSupportedComponentFile } from "./contracts.js";
|
|
4
5
|
import { generateFrontendEntrySource } from "./frontendEntrySource.js";
|
|
6
|
+
import {
|
|
7
|
+
buildI18nAssets,
|
|
8
|
+
getI18nPublicAssetPath,
|
|
9
|
+
isI18nSourceFilePath,
|
|
10
|
+
loadI18nBuildContext,
|
|
11
|
+
} from "./i18nBuild.js";
|
|
5
12
|
|
|
6
13
|
export const RETTANGOLI_FE_VIRTUAL_ENTRY_ID = "virtual:rettangoli-fe-entry";
|
|
7
14
|
const RESOLVED_VIRTUAL_ENTRY_ID = `\0${RETTANGOLI_FE_VIRTUAL_ENTRY_ID}`;
|
|
@@ -22,26 +29,61 @@ const normalizePublicEntryPath = (value) => {
|
|
|
22
29
|
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
23
30
|
};
|
|
24
31
|
|
|
32
|
+
const getI18nWatchPaths = ({ i18nContext }) => {
|
|
33
|
+
if (!i18nContext?.enabled) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return [
|
|
38
|
+
i18nContext.resolvedDir,
|
|
39
|
+
...Object.values(i18nContext.localeFiles || {}),
|
|
40
|
+
].filter(Boolean);
|
|
41
|
+
};
|
|
42
|
+
|
|
25
43
|
export const createRettangoliFeVitePlugin = ({
|
|
26
44
|
cwd = process.cwd(),
|
|
27
45
|
dirs = ["./example"],
|
|
28
46
|
setup = "setup.js",
|
|
47
|
+
i18n = null,
|
|
29
48
|
errorPrefix = "[Build]",
|
|
30
49
|
publicEntryPath = null,
|
|
31
50
|
} = {}) => {
|
|
32
51
|
const resolvedDirs = dirs.map((directory) => path.resolve(cwd, directory));
|
|
33
52
|
const resolvedSetup = path.resolve(cwd, setup);
|
|
34
53
|
const normalizedPublicEntryPath = normalizePublicEntryPath(publicEntryPath);
|
|
54
|
+
const i18nContext = loadI18nBuildContext({ cwd, i18n, errorPrefix });
|
|
35
55
|
|
|
36
56
|
let currentCommand = "build";
|
|
37
57
|
let devServer = null;
|
|
38
58
|
|
|
59
|
+
const getComponentFilePaths = () => {
|
|
60
|
+
return getAllFiles(resolvedDirs)
|
|
61
|
+
.filter((filePath) => isSupportedComponentFile(filePath));
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const getEntryWatchPaths = ({ includeDirectories = false } = {}) => {
|
|
65
|
+
const i18nWatchPaths = includeDirectories
|
|
66
|
+
? getI18nWatchPaths({ i18nContext })
|
|
67
|
+
: Object.values(i18nContext.localeFiles || {});
|
|
68
|
+
|
|
69
|
+
return [
|
|
70
|
+
...(includeDirectories ? resolvedDirs : []),
|
|
71
|
+
resolvedSetup,
|
|
72
|
+
...getComponentFilePaths(),
|
|
73
|
+
...i18nWatchPaths,
|
|
74
|
+
];
|
|
75
|
+
};
|
|
76
|
+
|
|
39
77
|
const isTrackedFilePath = (value) => {
|
|
40
78
|
const filePath = path.resolve(value);
|
|
41
79
|
if (filePath === resolvedSetup) {
|
|
42
80
|
return true;
|
|
43
81
|
}
|
|
44
82
|
|
|
83
|
+
if (isI18nSourceFilePath({ filePath, i18nContext })) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
45
87
|
if (!isSupportedComponentFile(filePath)) {
|
|
46
88
|
return false;
|
|
47
89
|
}
|
|
@@ -87,16 +129,64 @@ export const createRettangoliFeVitePlugin = ({
|
|
|
87
129
|
if (id !== RESOLVED_VIRTUAL_ENTRY_ID) {
|
|
88
130
|
return null;
|
|
89
131
|
}
|
|
132
|
+
|
|
133
|
+
for (const filePath of getEntryWatchPaths()) {
|
|
134
|
+
if (typeof this.addWatchFile === "function") {
|
|
135
|
+
this.addWatchFile(filePath);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
90
139
|
return generateFrontendEntrySource({
|
|
91
140
|
cwd,
|
|
92
141
|
dirs,
|
|
93
142
|
setup,
|
|
143
|
+
i18n,
|
|
94
144
|
command: currentCommand,
|
|
95
145
|
errorPrefix,
|
|
96
146
|
});
|
|
97
147
|
},
|
|
98
148
|
configureServer(server) {
|
|
99
149
|
devServer = server;
|
|
150
|
+
server.watcher.add(getEntryWatchPaths({ includeDirectories: true }));
|
|
151
|
+
|
|
152
|
+
const serveI18nAsset = (req, res, next) => {
|
|
153
|
+
if (!i18nContext.enabled || !normalizedPublicEntryPath) {
|
|
154
|
+
next();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const reqPath = (req.url || "").split("?")[0];
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const currentI18nContext = loadI18nBuildContext({
|
|
162
|
+
cwd,
|
|
163
|
+
i18n,
|
|
164
|
+
errorPrefix,
|
|
165
|
+
});
|
|
166
|
+
const asset = buildI18nAssets({
|
|
167
|
+
i18nContext: currentI18nContext,
|
|
168
|
+
}).find((candidate) => {
|
|
169
|
+
const publicPath = getI18nPublicAssetPath({
|
|
170
|
+
publicEntryPath: normalizedPublicEntryPath,
|
|
171
|
+
relativeFileName: candidate.relativeFileName,
|
|
172
|
+
});
|
|
173
|
+
return reqPath === publicPath;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (!asset) {
|
|
177
|
+
next();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
res.statusCode = 200;
|
|
182
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
183
|
+
res.end(asset.content);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
res.statusCode = 500;
|
|
186
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
187
|
+
res.end(error.stack || String(error));
|
|
188
|
+
}
|
|
189
|
+
};
|
|
100
190
|
|
|
101
191
|
const onAdd = (filePath) => {
|
|
102
192
|
if (isTrackedFilePath(filePath)) {
|
|
@@ -114,6 +204,7 @@ export const createRettangoliFeVitePlugin = ({
|
|
|
114
204
|
server.watcher.on("unlink", onUnlink);
|
|
115
205
|
|
|
116
206
|
if (normalizedPublicEntryPath) {
|
|
207
|
+
server.middlewares.use(serveI18nAsset);
|
|
117
208
|
server.middlewares.use(async (req, res, next) => {
|
|
118
209
|
const reqPath = (req.url || "").split("?")[0];
|
|
119
210
|
if (reqPath !== normalizedPublicEntryPath) {
|
package/src/cli/watch.js
CHANGED
|
@@ -2,7 +2,10 @@ import path from "node:path";
|
|
|
2
2
|
|
|
3
3
|
import { createServer } from "vite";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
RETTANGOLI_FE_VIRTUAL_ENTRY_ID,
|
|
7
|
+
createRettangoliFeVitePlugin,
|
|
8
|
+
} from "./vitePlugin.js";
|
|
6
9
|
|
|
7
10
|
const toPosixPath = (value) => value.split(path.sep).join("/");
|
|
8
11
|
|
|
@@ -39,15 +42,18 @@ export const createWatchServer = async (options = {}) => {
|
|
|
39
42
|
port = 3001,
|
|
40
43
|
outfile = "./vt/static/main.js",
|
|
41
44
|
setup = "setup.js",
|
|
45
|
+
i18n = null,
|
|
42
46
|
} = options;
|
|
43
47
|
|
|
44
48
|
const { root, publicEntryPath } = resolveServeContext({ cwd, outfile });
|
|
45
49
|
|
|
46
50
|
const server = await createServer({
|
|
51
|
+
clearScreen: false,
|
|
47
52
|
configFile: false,
|
|
48
53
|
root,
|
|
49
54
|
server: {
|
|
50
55
|
port,
|
|
56
|
+
strictPort: true,
|
|
51
57
|
host: "0.0.0.0",
|
|
52
58
|
allowedHosts: true,
|
|
53
59
|
},
|
|
@@ -56,6 +62,7 @@ export const createWatchServer = async (options = {}) => {
|
|
|
56
62
|
cwd,
|
|
57
63
|
dirs,
|
|
58
64
|
setup,
|
|
65
|
+
i18n,
|
|
59
66
|
errorPrefix: "[Watch]",
|
|
60
67
|
publicEntryPath,
|
|
61
68
|
}),
|
|
@@ -73,12 +80,13 @@ const startWatching = async (options = {}) => {
|
|
|
73
80
|
} = options;
|
|
74
81
|
const { root, publicEntryPath } = resolveServeContext({ cwd, outfile });
|
|
75
82
|
|
|
76
|
-
console.log(
|
|
77
|
-
console.log(
|
|
83
|
+
console.log(`[Watch] Root: ${root}`);
|
|
84
|
+
console.log(`[Watch] Entry: ${publicEntryPath}`);
|
|
78
85
|
|
|
79
86
|
try {
|
|
80
87
|
const server = await createWatchServer(options);
|
|
81
88
|
await server.listen();
|
|
89
|
+
await server.transformRequest(RETTANGOLI_FE_VIRTUAL_ENTRY_ID);
|
|
82
90
|
server.printUrls();
|
|
83
91
|
if (enableCliShortcuts) {
|
|
84
92
|
server.bindCLIShortcuts({ print: true });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { findUnsupportedTemplatePropertyBindingSyntax } from "../view/templatePropertyBindings.js";
|
|
2
|
+
import { validateViewI18nReferences } from "../i18n/viewReferences.js";
|
|
2
3
|
|
|
3
4
|
export const FORBIDDEN_VIEW_KEYS = Object.freeze([
|
|
4
5
|
"elementName",
|
|
@@ -51,7 +52,8 @@ export const buildComponentContractIndex = (entries = []) => {
|
|
|
51
52
|
return index;
|
|
52
53
|
};
|
|
53
54
|
|
|
54
|
-
export const validateComponentContractIndex = (index = {}) => {
|
|
55
|
+
export const validateComponentContractIndex = (index = {}, options = {}) => {
|
|
56
|
+
const { i18nContext = { enabled: false } } = options;
|
|
55
57
|
const errors = [];
|
|
56
58
|
|
|
57
59
|
Object.entries(index).forEach(([category, components]) => {
|
|
@@ -90,6 +92,13 @@ export const validateComponentContractIndex = (index = {}) => {
|
|
|
90
92
|
filePath: viewFilePath || representativeFile,
|
|
91
93
|
});
|
|
92
94
|
}
|
|
95
|
+
|
|
96
|
+
errors.push(...validateViewI18nReferences({
|
|
97
|
+
viewYaml,
|
|
98
|
+
componentLabel,
|
|
99
|
+
filePath: viewFilePath || representativeFile,
|
|
100
|
+
i18nContext,
|
|
101
|
+
}));
|
|
93
102
|
});
|
|
94
103
|
});
|
|
95
104
|
|