@ubean/build 0.2.2 → 0.3.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.
@@ -1,17 +1,19 @@
1
- import { a as createRoutingVirtualModule, i as createPagesVirtualModule, n as createLocalesVirtualModule, r as createMetaVirtualModule, t as createAppVirtualModule } from "./virtual-modules-BDgTqx3t.js";
2
- import { ubeanPlugin } from "./vite.js";
3
- import { useVirtualRegistry } from "@ubean/build-core";
1
+ import { a as useVirtualRegistry } from "./virtual-registry-BWkaQHXN.js";
2
+ import { a as createRoutingVirtualModule, i as createPagesVirtualModule, n as createLocalesVirtualModule, r as createMetaVirtualModule, t as createAppVirtualModule } from "./virtual-modules-D5WBA3u7.js";
3
+ import { a as ssrSingletonProdOptimizeExclude, o as ssrSingletonProdSsr } from "./ssr-singleton-CEQi_H6-.js";
4
+ import { n as localeVueParamFromI18n, r as serializeI18nConfig, t as ubeanPlugin } from "./vite-B3y8Qoan.js";
5
+ import { a as createVueAppEntryVirtualModule, i as createServerEntryVirtualModule, n as ubeanVite, o as createVuePagesVirtualModule, r as createClientEntryVirtualModule, t as VUE_PLUGIN_INCLUDE } from "./vue-CeSUQZ1J.js";
4
6
  import { join, relative, resolve } from "pathe";
5
- import { existsSync } from "node:fs";
6
- import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
7
+ import { resolveModules } from "@ubean/config";
7
8
  import { build } from "vite";
8
- import vue from "@vitejs/plugin-vue";
9
9
  import { getColorModeScript, resolveColorModeConfig } from "@ubean/client";
10
+ import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
11
+ import { existsSync } from "node:fs";
12
+ import vue from "@vitejs/plugin-vue";
10
13
  import { ubeanIslandsPlugin } from "@ubean/islands/vite";
11
- import { getLogger } from "@ubean/logger";
12
- import { resolveModules } from "@ubean/modules";
14
+ import { isEphemeralCachePreset, resolveProductionCacheStore } from "@ubean/preset";
15
+ import { getLogger } from "@ubean/shared/logger";
13
16
  import { findUserViteConfig } from "@ubean/shared/node";
14
- import { VUE_PLUGIN_INCLUDE, createClientEntryVirtualModule, createServerEntryVirtualModule, createVueAppEntryVirtualModule, createVuePagesVirtualModule, ubeanVite } from "@ubean/vite";
15
17
  //#region src/production.ts
16
18
  const logger = getLogger("build");
17
19
  function getOutDirs(cwd, outputDir) {
@@ -27,7 +29,7 @@ function getOutDirs(cwd, outputDir) {
27
29
  function toVitePath(p) {
28
30
  return p.replace(/\\/g, "/");
29
31
  }
30
- async function generateVirtualModulesToDisk(cwd, config, scanResult, virtualDir) {
32
+ async function generateVirtualModulesToDisk(cwd, config, scanResult, virtualDir, preset, contentSnapshot) {
31
33
  const mode = config.mode;
32
34
  const ssrEnabled = mode === "fullstack" && config.ssr.enabled || mode === "ssg";
33
35
  const hasPages = mode !== "backend";
@@ -62,12 +64,12 @@ async function generateVirtualModulesToDisk(cwd, config, scanResult, virtualDir)
62
64
  isDefault: l.isDefault
63
65
  })), cwd));
64
66
  registry.register(createMetaVirtualModule());
65
- registry.register(createVuePagesVirtualModule(scanResult.pages, scanResult.layouts, scanResult.notFoundPage, scanResult.loadingPage, scanResult.errorPage));
67
+ registry.register(createVuePagesVirtualModule(scanResult.pages, scanResult.layouts, scanResult.notFoundPage, scanResult.loadingPage, scanResult.errorPage, localeVueParamFromI18n(config.i18n)));
66
68
  registry.register(createVueAppEntryVirtualModule(scanResult.appEntry));
67
69
  registry.register(createClientEntryVirtualModule());
68
70
  }
69
71
  registry.register(createAppVirtualModule(scanResult.apiRoutes, scanResult.middlewares, hasPages ? scanResult.pages : [], viteSrcPrefix || "/"));
70
- registry.register(createLocalesVirtualModule(scanResult.locales, scanResult.defaultLocale, viteSrcPrefix || "/"));
72
+ registry.register(createLocalesVirtualModule(scanResult.locales, scanResult.defaultLocale, viteSrcPrefix || "/", serializeI18nConfig(config.i18n)));
71
73
  if (hasServer) registry.register(createServerEntryVirtualModule(scanResult.serverEntry));
72
74
  async function writeModule(id, filename, header = "// Auto-generated by ubean\n") {
73
75
  const mod = registry.getModules().find((m) => m.id === id);
@@ -90,6 +92,11 @@ async function generateVirtualModulesToDisk(cwd, config, scanResult, virtualDir)
90
92
  const routesGlob = JSON.stringify(`${viteSrcPrefix}/routes/**/*.{ts,js,mjs}`);
91
93
  const middlewareGlob = JSON.stringify(`${viteSrcPrefix}/middleware/**/*.{ts,js,mjs}`);
92
94
  const layoutsGlob = JSON.stringify(`${viteSrcPrefix}/layouts/**/*.{vue,ts}`);
95
+ const cronsDirs = (Array.isArray(config.dir.crons) ? config.dir.crons : [config.dir.crons || "crons"]).map((d) => d.replace(/\\/g, "/"));
96
+ const cronsGlob = JSON.stringify(cronsDirs.length === 1 ? `${viteSrcPrefix}/${cronsDirs[0]}/**/*.{ts,js,mjs}` : `${viteSrcPrefix}/{${cronsDirs.join(",")}}/**/*.{ts,js,mjs}`);
97
+ const seoGlob = JSON.stringify(`${viteSrcPrefix}/{sitemap,robots,manifest,opengraph-image,icon,apple-icon}.{ts,js,mjs,mts,cjs}`);
98
+ const enableInProcessCron = !isEphemeralCachePreset(preset.name || "standard");
99
+ const enableIpx = Boolean(config.image);
93
100
  const outputDir = config.build.outputDir || "dist";
94
101
  const publicDir = toVitePath(resolve(cwd, outputDir, "public"));
95
102
  const routesJson = JSON.stringify(scanResult.apiRoutes.map((r) => ({
@@ -175,23 +182,36 @@ const _pageRenderer = createVueRenderer({
175
182
  // --- SSR disabled (mode=${mode}, ssr=${config.ssr.enabled}) ---
176
183
  // pageRenderer is null; page requests fall back to a client-only HTML shell.
177
184
  const _pageRenderer = null;`;
185
+ const productionCache = resolveProductionCacheStore(preset.name || "standard", config.cache);
186
+ const contentEntries = Object.entries(contentSnapshot ?? {}).filter(([, docs]) => Array.isArray(docs));
187
+ const contentBootstrap = contentEntries.length > 0 ? `
188
+ import { configureContentRuntime, registerContent } from '@ubean/content';
189
+ configureContentRuntime();
190
+ ${contentEntries.map(([name, docs]) => `registerContent(${JSON.stringify(name)}, ${JSON.stringify(docs)});`).join("\n")}
191
+ ` : "";
192
+ const ipxImport = enableIpx ? `import { createIpxHonoHandler } from '@ubean/image';
193
+ ` : "";
194
+ const cronImport = enableInProcessCron ? `import { startCronScheduler } from '@ubean/server/cron';
195
+ ` : "";
178
196
  if (hasServer) {
179
197
  const serverEntry = `// Auto-generated server entry
180
198
  import { createUbeanApp, applyServerConfig } from 'ubean/runtime/app';
199
+ import 'ubean:locales';
181
200
  ${rendererImport}
182
201
  import { resolveAppConfig as _resolveAppConfig } from 'virtual:ubean-app';
183
202
  import { resolveServerConfig as _resolveServerConfig } from 'virtual:ubean-server';
184
- import { loadLocales } from 'ubean:locales';
185
203
  import { readFileSync } from 'node:fs';
186
204
  import { join, dirname } from 'node:path';
187
205
  import { fileURLToPath } from 'node:url';
188
-
206
+ ${contentBootstrap}${ipxImport}${cronImport}
189
207
  export { createUbeanApp };
190
208
 
191
209
  const routeModules = import.meta.glob(${routesGlob}, { eager: false });
192
210
  const middlewareModules = import.meta.glob(${middlewareGlob}, { eager: false });
193
211
  const pageModules = import.meta.glob(${pagesGlob}, { eager: false });
194
212
  const layoutModules = import.meta.glob(${layoutsGlob}, { eager: false });
213
+ const cronModules = import.meta.glob(${cronsGlob}, { eager: true });
214
+ const seoConventionModules = import.meta.glob(${seoGlob}, { eager: true });
195
215
 
196
216
  const _srcPrefix = ${JSON.stringify(viteSrcPrefix)};
197
217
  function normalizeKey(p) {
@@ -267,6 +287,14 @@ export async function createApp(options = {}) {
267
287
  streaming: ${JSON.stringify(config.ssr?.streaming ?? false)},
268
288
  notFoundPage: _notFoundPage || undefined,
269
289
  colorModeScript: ${JSON.stringify(colorModeScript)},
290
+ routeRules: ${JSON.stringify(config.routeRules || {})},
291
+ csrf: ${JSON.stringify(config.security === false ? false : config.security?.csrf ?? true)},
292
+ securityHeaders: ${JSON.stringify(config.security === false ? false : config.security?.headers ?? true)},
293
+ dataCache: ${JSON.stringify(config.dataCache ?? true)},
294
+ cache: ${JSON.stringify(productionCache)},
295
+ seoConventions: { srcDir: ${JSON.stringify(srcDirAbs)} },
296
+ seoConventionModules,
297
+ ipxHandler: ${enableIpx ? `createIpxHonoHandler({ rootDir: ${JSON.stringify(publicDir)}, staticDir: '.' })` : "undefined"},
270
298
  ...options
271
299
  });
272
300
 
@@ -274,9 +302,12 @@ export async function createApp(options = {}) {
274
302
  const _serverConfig = _resolveServerConfig('prod');
275
303
  await applyServerConfig(app, _serverConfig);
276
304
 
277
- await loadLocales();
278
305
  await app.init();
279
306
 
307
+ ${enableInProcessCron ? `if (Object.keys(cronModules).length > 0) {
308
+ startCronScheduler();
309
+ }` : ""}
310
+
280
311
  // Call onServerReady after init completes
281
312
  if (_serverConfig.onServerReady) {
282
313
  await _serverConfig.onServerReady(app);
@@ -434,7 +465,7 @@ export default {
434
465
  `.trim();
435
466
  }
436
467
  async function buildProduction(options) {
437
- const { cwd, config, preset, scanResult, minify = true, sourcemap = false } = options;
468
+ const { cwd, config, preset, scanResult, minify = true, sourcemap = false, contentSnapshot } = options;
438
469
  const outDirs = getOutDirs(cwd, config.build.outputDir);
439
470
  const srcDir = resolve(cwd, config.srcDir);
440
471
  const mode = config.mode;
@@ -449,7 +480,7 @@ async function buildProduction(options) {
449
480
  if (hasPages) await mkdir(outDirs.public, { recursive: true });
450
481
  if (hasServer) await mkdir(outDirs.server, { recursive: true });
451
482
  logger.info("Generating virtual modules...");
452
- await generateVirtualModulesToDisk(cwd, config, scanResult, outDirs.virtual);
483
+ await generateVirtualModulesToDisk(cwd, config, scanResult, outDirs.virtual, preset, contentSnapshot);
453
484
  const userViteConfig = findUserViteConfig(cwd);
454
485
  const builtinPlugins = [];
455
486
  if (hasPages) builtinPlugins.push(vue({
@@ -523,8 +554,7 @@ async function buildProduction(options) {
523
554
  },
524
555
  plugins: [...plugins],
525
556
  resolve: commonResolve,
526
- optimizeDeps: { exclude: [
527
- "ubean",
557
+ optimizeDeps: { exclude: ssrSingletonProdOptimizeExclude([
528
558
  "virtual:ubean-pages",
529
559
  "virtual:ubean-app",
530
560
  "virtual:ubean-server",
@@ -533,7 +563,7 @@ async function buildProduction(options) {
533
563
  "#ubean-app",
534
564
  "#ubean-server",
535
565
  "#ubean-client-entry"
536
- ] }
566
+ ]) }
537
567
  });
538
568
  const manifestPath = join(outDirs.public, ".vite", "manifest.json");
539
569
  if (existsSync(manifestPath)) try {
@@ -555,7 +585,7 @@ async function buildProduction(options) {
555
585
  mode: "production",
556
586
  ssr: {
557
587
  target: presetBuildConfig.target === "node18" ? "node" : "webworker",
558
- noExternal: ["ubean"]
588
+ ...ssrSingletonProdSsr()
559
589
  },
560
590
  build: {
561
591
  outDir: outDirs.server,
@@ -0,0 +1,113 @@
1
+ //#region src/registry.ts
2
+ const REGISTRY_KEY = "__ubean_module_registry__";
3
+ function getRegistry() {
4
+ const g = globalThis;
5
+ if (!g[REGISTRY_KEY]) g[REGISTRY_KEY] = {
6
+ componentResolvers: [],
7
+ cssImports: []
8
+ };
9
+ return g[REGISTRY_KEY];
10
+ }
11
+ /**
12
+ * Register a component resolver (e.g. `UiResolver()` from `@soybeanjs/ui/resolver`).
13
+ * The resolver will be merged into `unplugin-vue-components`'s `resolvers` array.
14
+ */
15
+ function registerComponentResolver(resolver) {
16
+ getRegistry().componentResolvers.push(resolver);
17
+ }
18
+ /**
19
+ * Get all registered component resolvers.
20
+ * Called by `ubeanVite` when constructing the `Components()` plugin.
21
+ */
22
+ function getComponentResolvers() {
23
+ return [...getRegistry().componentResolvers];
24
+ }
25
+ /**
26
+ * Register a CSS import path to be injected into the client entry.
27
+ * e.g. `registerCssImport('@soybeanjs/ui/styles.css')` makes the client
28
+ * entry module prepend `import '@soybeanjs/ui/styles.css'`.
29
+ */
30
+ function registerCssImport(cssPath) {
31
+ const reg = getRegistry();
32
+ if (!reg.cssImports.includes(cssPath)) reg.cssImports.push(cssPath);
33
+ }
34
+ /**
35
+ * Get all registered CSS import paths.
36
+ * Called by `createClientEntryVirtualModule` to prepend CSS imports.
37
+ */
38
+ function getCssImports() {
39
+ return [...getRegistry().cssImports];
40
+ }
41
+ /**
42
+ * Reset the registry (for testing).
43
+ * @internal
44
+ */
45
+ function resetModuleRegistry() {
46
+ const reg = getRegistry();
47
+ reg.componentResolvers.length = 0;
48
+ reg.cssImports.length = 0;
49
+ }
50
+ //#endregion
51
+ //#region src/ssr-singleton.ts
52
+ /**
53
+ * Single-copy runtime policy for Vite client / SSR graphs.
54
+ *
55
+ * `@ubean/i18n` (ALS + catalogs) and `@ubean/client` (locale runtime) must
56
+ * resolve to one module instance per process. Dev and production use the
57
+ * same package lists; only the SSR externalization differs:
58
+ *
59
+ * - Dev: Vite `ssrLoadModule` and the CLI-created app share Node's
60
+ * `@ubean/i18n` (external). `ubean` stays noExternal so virtual ids resolve.
61
+ * - Prod: `ubean` is bundled (`noExternal`). Do **not** also externalize
62
+ * `@ubean/i18n` — that would create a second copy beside the bundle.
63
+ */
64
+ const SSR_SINGLETON_PACKAGES = [
65
+ "ubean",
66
+ "@ubean/client",
67
+ "@ubean/i18n",
68
+ "vue-i18n",
69
+ "@intlify/core",
70
+ "@intlify/core-base"
71
+ ];
72
+ const SSR_SINGLETON_DEDUPE = [
73
+ "vue",
74
+ "vue-i18n",
75
+ "@ubean/client",
76
+ "@ubean/i18n"
77
+ ];
78
+ const SSR_SINGLETON_OPTIMIZE_EXCLUDE = [
79
+ "ubean",
80
+ "@ubean/client",
81
+ "@ubean/i18n"
82
+ ];
83
+ function ssrSingletonDevPolicy() {
84
+ return {
85
+ resolve: { dedupe: [...SSR_SINGLETON_DEDUPE] },
86
+ optimizeDeps: {
87
+ exclude: [...SSR_SINGLETON_OPTIMIZE_EXCLUDE],
88
+ include: ["vue-i18n"]
89
+ },
90
+ ssr: {
91
+ noExternal: [
92
+ "ubean",
93
+ "vue-i18n",
94
+ "@intlify/core",
95
+ "@intlify/core-base"
96
+ ],
97
+ external: ["@ubean/i18n"]
98
+ }
99
+ };
100
+ }
101
+ function ssrSingletonProdSsr() {
102
+ return { noExternal: [
103
+ "ubean",
104
+ "vue-i18n",
105
+ "@intlify/core",
106
+ "@intlify/core-base"
107
+ ] };
108
+ }
109
+ function ssrSingletonProdOptimizeExclude(extra = []) {
110
+ return [...SSR_SINGLETON_OPTIMIZE_EXCLUDE, ...extra];
111
+ }
112
+ //#endregion
113
+ export { ssrSingletonProdOptimizeExclude as a, getCssImports as c, resetModuleRegistry as d, ssrSingletonDevPolicy as i, registerComponentResolver as l, SSR_SINGLETON_OPTIMIZE_EXCLUDE as n, ssrSingletonProdSsr as o, SSR_SINGLETON_PACKAGES as r, getComponentResolvers as s, SSR_SINGLETON_DEDUPE as t, registerCssImport as u };
@@ -1,5 +1,111 @@
1
- import { defineVirtualModule } from "@ubean/build-core";
1
+ import { n as defineVirtualModule } from "./virtual-registry-BWkaQHXN.js";
2
2
  import { isAbsolute, relative } from "pathe";
3
+ //#region src/macros.ts
4
+ const MACRO_NAMES = ["definePage"];
5
+ function findBalancedCall(code, funcName, startSearch = 0) {
6
+ const pattern = new RegExp(`\\b${funcName}\\s*\\(`, "g");
7
+ pattern.lastIndex = startSearch;
8
+ const match = pattern.exec(code);
9
+ if (!match) return null;
10
+ const startIdx = match.index;
11
+ const parenStart = match.index + match[0].length;
12
+ let depth = 1;
13
+ let i = parenStart;
14
+ let inString = null;
15
+ let escaped = false;
16
+ let inTemplateExpr = 0;
17
+ while (i < code.length && depth > 0) {
18
+ const ch = code[i];
19
+ if (escaped) {
20
+ escaped = false;
21
+ i++;
22
+ continue;
23
+ }
24
+ if (ch === "\\") {
25
+ escaped = true;
26
+ i++;
27
+ continue;
28
+ }
29
+ if (inString) {
30
+ if (inString === "`" && ch === "$" && code[i + 1] === "{") {
31
+ inTemplateExpr++;
32
+ i += 2;
33
+ continue;
34
+ }
35
+ if (inString === "`" && ch === "}" && inTemplateExpr > 0) {
36
+ inTemplateExpr--;
37
+ i++;
38
+ continue;
39
+ }
40
+ if (ch === inString) inString = null;
41
+ i++;
42
+ continue;
43
+ }
44
+ if (ch === "\"" || ch === "'" || ch === "`") {
45
+ inString = ch;
46
+ i++;
47
+ continue;
48
+ }
49
+ if (ch === "/" && code[i + 1] === "/") {
50
+ while (i < code.length && code[i] !== "\n") i++;
51
+ continue;
52
+ }
53
+ if (ch === "/" && code[i + 1] === "*") {
54
+ i += 2;
55
+ while (i < code.length && !(code[i] === "*" && code[i + 1] === "/")) i++;
56
+ i += 2;
57
+ continue;
58
+ }
59
+ if (ch === "(" || ch === "{" || ch === "[") depth++;
60
+ else if (ch === ")" || ch === "}" || ch === "]") {
61
+ depth--;
62
+ if (depth === 0 && ch === ")") return {
63
+ start: startIdx,
64
+ end: i + 1
65
+ };
66
+ }
67
+ i++;
68
+ }
69
+ return null;
70
+ }
71
+ function stripStatement(code, callStart, callEnd) {
72
+ let start = callStart;
73
+ let end = callEnd;
74
+ while (start > 0 && /[ \t]/.test(code[start - 1])) start--;
75
+ const exportDefaultMatch = code.slice(0, start).match(/export\s+default\s*$/);
76
+ if (exportDefaultMatch) {
77
+ start -= exportDefaultMatch[0].length;
78
+ while (start > 0 && /[ \t\n\r]/.test(code[start - 1])) start--;
79
+ }
80
+ if (code[end] === ";") end++;
81
+ while (end < code.length && /[ \t]/.test(code[end])) end++;
82
+ return code.slice(0, start) + code.slice(end);
83
+ }
84
+ function stripMacros(code, macros = MACRO_NAMES) {
85
+ let result = code;
86
+ for (const macro of macros) {
87
+ let offset = 0;
88
+ while (true) {
89
+ const found = findBalancedCall(result, macro, offset);
90
+ if (!found) break;
91
+ result = stripStatement(result, found.start, found.end);
92
+ offset = found.start;
93
+ }
94
+ }
95
+ return result;
96
+ }
97
+ function transformMacros(code, id) {
98
+ if (!(id.includes("/src/pages/") || id.includes("/src/routes/") || id.includes("/src/middleware/"))) return null;
99
+ if (id.endsWith(".vue")) return transformVueMacros(code);
100
+ if (/\.(ts|js|mjs|mts|tsx|jsx)$/.test(id)) return stripMacros(code);
101
+ return null;
102
+ }
103
+ function transformVueMacros(code) {
104
+ return code.replace(/<script([^>]*)>([\s\S]*?)<\/script>/g, (_match, attrs, content) => {
105
+ return `<script${attrs}>${stripMacros(content)}<\/script>`;
106
+ });
107
+ }
108
+ //#endregion
3
109
  //#region src/virtual-modules.ts
4
110
  function toVitePath(p) {
5
111
  return p.replace(/\\/g, "/");
@@ -94,18 +200,20 @@ export default { routeLoaders, middlewareLoaders, pageLoaders, apiRoutes, middle
94
200
  `;
95
201
  });
96
202
  }
97
- function createLocalesVirtualModule(_locales, defaultLocale, srcDir = "src") {
203
+ function createLocalesVirtualModule(_locales, defaultLocale, srcDir = "src", i18nConfigJson) {
98
204
  return defineVirtualModule("ubean:locales", () => {
99
205
  const defaultCode = defaultLocale == null ? "null" : JSON.stringify(defaultLocale);
100
206
  const viteSrcDir = toVitePath(srcDir);
101
207
  const prefix = viteSrcDir === "/" || viteSrcDir === "" ? "" : viteSrcDir;
208
+ const localesGlob = JSON.stringify(`${prefix}/locales/**/*.{json,json5,yaml,yml,js,mjs,cjs,ts,mts,cts}`);
209
+ const srcPrefix = JSON.stringify(`${prefix || ""}/locales/`);
102
210
  return `
103
- import { defineLocale, setLocale, mergeLocale } from 'ubean/runtime/i18n';
211
+ export const i18nConfig = ${i18nConfigJson ?? "null"};
104
212
 
105
- const localeModules = import.meta.glob([${JSON.stringify(`${prefix}/locales/**/*.{json,json5,yaml,yml,js,mjs,cjs,ts,mts,cts}`)}], { eager: false });
213
+ const localeModules = import.meta.glob([${localesGlob}], { eager: false });
106
214
 
107
215
  function parseLocalePath(path) {
108
- const withoutPrefix = path.replace(${JSON.stringify(`${prefix || ""}/locales/`)}, '');
216
+ const withoutPrefix = path.replace(${srcPrefix}, '');
109
217
  const lastDot = withoutPrefix.lastIndexOf('.');
110
218
  const withoutExt = lastDot > 0 ? withoutPrefix.slice(0, lastDot) : withoutPrefix;
111
219
  const parts = withoutExt.split('/');
@@ -142,94 +250,93 @@ function setNestedValue(obj, path, value) {
142
250
  current[path[path.length - 1]] = value;
143
251
  }
144
252
 
145
- let loaded = false;
253
+ const cache = new Map();
146
254
 
147
- export async function loadLocales() {
148
- if (loaded) return;
149
- loaded = true;
255
+ function codesFromConfig() {
256
+ if (!i18nConfig || !i18nConfig.locales) return [];
257
+ return i18nConfig.locales.map(l => typeof l === 'string' ? l : l.code);
258
+ }
259
+
260
+ export const localeCodes = codesFromConfig();
261
+ const defaultCode = ${defaultCode};
262
+
263
+ let i18nRuntime = null;
264
+ if (import.meta.env.SSR) {
265
+ i18nRuntime = await import('ubean/runtime/i18n');
266
+ if (i18nConfig && i18nConfig.fallbackLocale) {
267
+ i18nRuntime.setFallbackLocale(i18nConfig.fallbackLocale);
268
+ } else if (defaultCode) {
269
+ i18nRuntime.setFallbackLocale(defaultCode);
270
+ }
271
+ }
150
272
 
151
- const localeData = new Map();
273
+ async function collectMessages(code) {
274
+ const localeData = { messages: {}, options: { isDefault: false, name: undefined, dir: 'ltr', language: undefined } };
275
+ const configLoc = i18nConfig && i18nConfig.locales
276
+ ? i18nConfig.locales.find(l => (typeof l === 'string' ? l : l.code) === code)
277
+ : undefined;
278
+ if (configLoc && typeof configLoc === 'object') {
279
+ localeData.options.name = configLoc.name;
280
+ localeData.options.dir = configLoc.dir || 'ltr';
281
+ localeData.options.language = configLoc.language;
282
+ localeData.options.isDefault = configLoc.code === (i18nConfig && i18nConfig.defaultLocale);
283
+ }
152
284
 
153
285
  for (const [path, loader] of Object.entries(localeModules)) {
286
+ const parsed = parseLocalePath(path);
287
+ if (parsed.code !== code) continue;
154
288
  try {
155
- const { code, namespace, isDefault } = parseLocalePath(path);
156
289
  const mod = await loader();
157
290
  const data = mod.default || mod;
158
291
  const hasMeta = typeof data.name === 'string' || data.dir === 'ltr' || data.dir === 'rtl' || typeof data.isDefault === 'boolean';
159
292
  const isWrapper = hasMeta && typeof data.messages === 'object' && data.messages !== null;
160
293
  const messages = isWrapper ? data.messages : data;
161
- const options = {
162
- name: isWrapper ? data.name : undefined,
163
- dir: isWrapper ? (data.dir || 'ltr') : 'ltr',
164
- isDefault: isDefault || (isWrapper ? data.isDefault : false)
165
- };
166
-
167
- if (!localeData.has(code)) {
168
- localeData.set(code, { messages: {}, options: { isDefault: options.isDefault || isDefault, name: options.name, dir: options.dir } });
294
+ if (isWrapper) {
295
+ if (data.name) localeData.options.name = data.name;
296
+ if (data.dir) localeData.options.dir = data.dir;
297
+ if (data.isDefault) localeData.options.isDefault = true;
169
298
  }
170
- const entry = localeData.get(code);
171
- if (options.name) entry.options.name = options.name;
172
- if (options.dir) entry.options.dir = options.dir;
173
- if (options.isDefault) entry.options.isDefault = true;
174
-
175
- if (namespace) {
176
- setNestedValue(entry.messages, namespace.split('.'), messages);
299
+ if (parsed.isDefault) localeData.options.isDefault = true;
300
+ if (parsed.namespace) {
301
+ setNestedValue(localeData.messages, parsed.namespace.split('.'), messages);
177
302
  } else {
178
- Object.assign(entry.messages, messages);
303
+ Object.assign(localeData.messages, messages);
179
304
  }
180
305
  } catch (e) {
181
306
  console.warn('[ubean] Failed to load locale:', path, e);
182
307
  }
183
308
  }
309
+ return localeData;
310
+ }
184
311
 
185
- for (const [code, { messages, options }] of localeData.entries()) {
186
- defineLocale({
187
- code,
188
- messages,
189
- name: options.name,
190
- dir: options.dir,
191
- isDefault: options.isDefault
192
- });
312
+ export async function loadLocale(code) {
313
+ if (cache.has(code)) return cache.get(code);
314
+ const { messages, options } = await collectMessages(code);
315
+ if (i18nRuntime) {
316
+ i18nRuntime.setLocaleMessages(code, messages);
317
+ i18nRuntime.setLocaleMeta(code, options);
193
318
  }
319
+ cache.set(code, messages);
320
+ return messages;
321
+ }
194
322
 
195
- const defaultCode = ${defaultCode};
196
- if (defaultCode && localeData.has(defaultCode)) {
197
- setLocale(defaultCode);
198
- } else if (localeData.size > 0) {
199
- const firstDefault = [...localeData.entries()].find(([, v]) => v.options.isDefault);
200
- if (firstDefault) {
201
- setLocale(firstDefault[0]);
202
- } else {
203
- setLocale([...localeData.keys()][0]);
204
- }
205
- }
323
+ export async function loadLocales() {
324
+ const codes = localeCodes.length ? localeCodes : [...new Set(Object.keys(localeModules).map(p => parseLocalePath(p).code))];
325
+ await Promise.all(codes.map(loadLocale));
206
326
  }
207
327
 
208
328
  export async function reloadLocale(path) {
209
329
  if (!path.includes('/locales/')) return;
210
- try {
211
- const loader = localeModules[path];
212
- if (loader) {
213
- const { code, namespace } = parseLocalePath(path);
214
- const mod = await loader();
215
- const data = mod.default || mod;
216
- const hasMeta = typeof data.name === 'string' || data.dir === 'ltr' || data.dir === 'rtl' || typeof data.isDefault === 'boolean';
217
- const isWrapper = hasMeta && typeof data.messages === 'object' && data.messages !== null;
218
- const messages = isWrapper ? data.messages : data;
219
- const merged = {};
220
- if (namespace) {
221
- setNestedValue(merged, namespace.split('.'), messages);
222
- } else {
223
- Object.assign(merged, messages);
224
- }
225
- mergeLocale(code, merged);
226
- }
227
- } catch (e) {
228
- console.warn('[ubean] Failed to reload locale:', path, e);
229
- }
330
+ cache.clear();
331
+ const { code } = parseLocalePath(path);
332
+ await loadLocale(code);
333
+ }
334
+
335
+ if (i18nRuntime) {
336
+ i18nRuntime.registerLocaleLoader(loadLocale);
230
337
  }
231
338
 
232
- export default { loadLocales, reloadLocale };
339
+ export default { loadLocale, loadLocales, reloadLocale, localeCodes, i18nConfig };
233
340
 
234
341
  if (import.meta.hot) {
235
342
  import.meta.hot.accept((mod) => {
@@ -244,4 +351,4 @@ if (import.meta.hot) {
244
351
  });
245
352
  }
246
353
  //#endregion
247
- export { createRoutingVirtualModule as a, createPagesVirtualModule as i, createLocalesVirtualModule as n, createMetaVirtualModule as r, createAppVirtualModule as t };
354
+ export { createRoutingVirtualModule as a, createPagesVirtualModule as i, createLocalesVirtualModule as n, stripMacros as o, createMetaVirtualModule as r, transformMacros as s, createAppVirtualModule as t };
@@ -0,0 +1,30 @@
1
+ //#region src/virtual-registry.d.ts
2
+ interface VirtualModuleContext {
3
+ resolveId(id: string): string | undefined;
4
+ load(id: string): string | undefined;
5
+ invalidate(id: string): void;
6
+ }
7
+ type VirtualModuleTransform = (id: string, code: string) => string | Promise<string>;
8
+ interface VirtualModule {
9
+ id: string;
10
+ resolve(id: string, importer?: string): string | undefined;
11
+ load(id?: string): string | Promise<string>;
12
+ }
13
+ declare class VirtualModuleRegistry {
14
+ private modules;
15
+ private invalidated;
16
+ register(mod: VirtualModule): void;
17
+ resolveId(id: string, importer?: string): string | undefined;
18
+ load(id: string): Promise<string | undefined>;
19
+ invalidate(id: string): void;
20
+ isInvalidated(id: string): boolean;
21
+ clearInvalidated(): void;
22
+ getModules(): VirtualModule[];
23
+ clear(): void;
24
+ }
25
+ declare function useVirtualRegistry(): VirtualModuleRegistry;
26
+ declare function resetVirtualRegistry(): void;
27
+ declare function defineVirtualModule(id: string, loader: () => string | Promise<string>): VirtualModule;
28
+ declare function defineVirtualModulePrefix(prefix: string, loader: (id: string) => string | Promise<string>): VirtualModule;
29
+ //#endregion
30
+ export { defineVirtualModule as a, useVirtualRegistry as c, VirtualModuleTransform as i, VirtualModuleContext as n, defineVirtualModulePrefix as o, VirtualModuleRegistry as r, resetVirtualRegistry as s, VirtualModule as t };