viewlogic 1.1.2 β†’ 1.1.3

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
@@ -260,6 +260,75 @@ const config = {
260
260
  };
261
261
  ```
262
262
 
263
+ ### πŸ—οΈ Subfolder Deployment Support
264
+
265
+ ViewLogic Router supports deployment in subfolders with smart path resolution:
266
+
267
+ ```javascript
268
+ // Root deployment: https://example.com/
269
+ ViewLogicRouter({
270
+ basePath: '/src', // β†’ https://example.com/src
271
+ routesPath: '/routes', // β†’ https://example.com/routes
272
+ i18nPath: '/i18n' // β†’ https://example.com/i18n
273
+ });
274
+
275
+ // Subfolder deployment: https://example.com/myapp/
276
+ ViewLogicRouter({
277
+ basePath: 'src', // β†’ https://example.com/myapp/src (relative)
278
+ routesPath: 'routes', // β†’ https://example.com/myapp/routes (relative)
279
+ i18nPath: 'i18n', // β†’ https://example.com/myapp/i18n (relative)
280
+ });
281
+
282
+ // Mixed paths: https://example.com/projects/myapp/
283
+ ViewLogicRouter({
284
+ basePath: './src', // β†’ https://example.com/projects/myapp/src
285
+ routesPath: '../shared/routes', // β†’ https://example.com/projects/shared/routes
286
+ i18nPath: '/global/i18n' // β†’ https://example.com/global/i18n (absolute)
287
+ });
288
+ ```
289
+
290
+ **Path Resolution Rules:**
291
+ - **Absolute paths** (`/path`) β†’ `https://domain.com/path`
292
+ - **Relative paths** (`path`, `./path`) β†’ Resolved from current page location
293
+ - **Parent paths** (`../path`) β†’ Navigate up directory levels
294
+ - **HTTP URLs** β†’ Used as-is (no processing)
295
+
296
+ ### πŸ”„ Hash vs History Mode in Subfolders
297
+
298
+ Both routing modes work seamlessly in subfolder deployments:
299
+
300
+ ```javascript
301
+ // Hash Mode (recommended for subfolders)
302
+ // URL: https://example.com/myapp/#/products?id=123
303
+ ViewLogicRouter({
304
+ mode: 'hash' // Works anywhere, no server config needed
305
+ });
306
+
307
+ // History Mode (requires server configuration)
308
+ // URL: https://example.com/myapp/products?id=123
309
+ ViewLogicRouter({
310
+ mode: 'history' // Cleaner URLs, needs server setup
311
+ });
312
+ ```
313
+
314
+ **History Mode Server Configuration:**
315
+ ```nginx
316
+ # Nginx - redirect all subfolder requests to index.html
317
+ location /myapp/ {
318
+ try_files $uri $uri/ /myapp/index.html;
319
+ }
320
+ ```
321
+
322
+ ```apache
323
+ # Apache .htaccess in /myapp/ folder
324
+ RewriteEngine On
325
+ RewriteBase /myapp/
326
+ RewriteRule ^index\.html$ - [L]
327
+ RewriteCond %{REQUEST_FILENAME} !-f
328
+ RewriteCond %{REQUEST_FILENAME} !-d
329
+ RewriteRule . /myapp/index.html [L]
330
+ ```
331
+
263
332
  ## πŸ“– Complete API Documentation
264
333
 
265
334
  For comprehensive API documentation including all methods, configuration options, and detailed examples, see:
@@ -42,6 +42,8 @@ var I18nManager = class {
42
42
  await this.loadMessages(this.currentLanguage);
43
43
  } catch (error) {
44
44
  this.log("error", "Failed to load initial language file:", error);
45
+ this.messages.set(this.currentLanguage, {});
46
+ this.log("info", "Using empty message object as fallback");
45
47
  }
46
48
  } else {
47
49
  this.log("debug", "Language messages already loaded:", this.currentLanguage);
@@ -98,9 +100,17 @@ var I18nManager = class {
98
100
  this.log("info", "Language changed successfully", { from: oldLanguage, to: language });
99
101
  return true;
100
102
  } catch (error) {
101
- this.currentLanguage = oldLanguage;
102
- this.log("error", "Failed to change language:", error);
103
- return false;
103
+ this.log("error", "Failed to load messages for language change, using empty messages:", error);
104
+ this.messages.set(language, {});
105
+ this.saveLanguageToCache(language);
106
+ this.emit("languageChanged", {
107
+ from: oldLanguage,
108
+ to: language,
109
+ messages: {},
110
+ error: true
111
+ });
112
+ this.log("warn", "Language changed with empty messages", { from: oldLanguage, to: language });
113
+ return true;
104
114
  }
105
115
  }
106
116
  /**
@@ -136,7 +146,10 @@ var I18nManager = class {
136
146
  return messages;
137
147
  } catch (error) {
138
148
  this.loadPromises.delete(language);
139
- throw error;
149
+ this.log("error", "Failed to load messages, using empty fallback for:", language, error);
150
+ const emptyMessages = {};
151
+ this.messages.set(language, emptyMessages);
152
+ return emptyMessages;
140
153
  }
141
154
  }
142
155
  /**
@@ -165,9 +178,15 @@ var I18nManager = class {
165
178
  this.log("error", "Failed to load messages file for:", language, error);
166
179
  if (language !== this.config.fallbackLanguage) {
167
180
  this.log("info", "Trying fallback language:", this.config.fallbackLanguage);
168
- return await this._loadMessagesFromFile(this.config.fallbackLanguage);
181
+ try {
182
+ return await this._loadMessagesFromFile(this.config.fallbackLanguage);
183
+ } catch (fallbackError) {
184
+ this.log("error", "Fallback language also failed:", fallbackError);
185
+ return {};
186
+ }
169
187
  }
170
- throw new Error(`Failed to load messages for language: ${language}`);
188
+ this.log("warn", `No messages available for language: ${language}, using empty fallback`);
189
+ return {};
171
190
  }
172
191
  }
173
192
  /**
@@ -352,7 +371,8 @@ var I18nManager = class {
352
371
  return true;
353
372
  } catch (error) {
354
373
  this.log("error", "I18n initialization failed:", error);
355
- return false;
374
+ this.log("info", "I18n system ready with fallback behavior");
375
+ return true;
356
376
  }
357
377
  }
358
378
  /**
@@ -413,7 +433,8 @@ var I18nManager = class {
413
433
  return true;
414
434
  } catch (error) {
415
435
  this.log("error", "Failed to initialize I18n system:", error);
416
- return false;
436
+ this.log("info", "I18n system will continue with fallback behavior");
437
+ return true;
417
438
  }
418
439
  }
419
440
  };
@@ -972,6 +993,7 @@ var CacheManager = class {
972
993
  this.cacheTimestamps.clear();
973
994
  this.lruOrder = [];
974
995
  this.log(`\u{1F525} Cleared all cache (${size} entries)`);
996
+ return size;
975
997
  }
976
998
  /**
977
999
  * 만료된 μΊμ‹œ ν•­λͺ©λ“€ 정리
@@ -1464,8 +1486,10 @@ var QueryManager = class {
1464
1486
  var RouteLoader = class {
1465
1487
  constructor(router, options = {}) {
1466
1488
  this.config = {
1467
- basePath: options.basePath || "/src",
1468
- routesPath: options.routesPath || "/routes",
1489
+ srcPath: options.srcPath || router.config.srcPath || "/src",
1490
+ // μ†ŒμŠ€ 파일 경둜
1491
+ routesPath: options.routesPath || router.config.routesPath || "/routes",
1492
+ // ν”„λ‘œλ•μ…˜ 라우트 경둜
1469
1493
  environment: options.environment || "development",
1470
1494
  useLayout: options.useLayout !== false,
1471
1495
  defaultLayout: options.defaultLayout || "default",
@@ -1487,7 +1511,7 @@ var RouteLoader = class {
1487
1511
  const module = await import(importPath);
1488
1512
  script = module.default;
1489
1513
  } else {
1490
- const importPath = `${this.config.basePath}/logic/${routeName}.js`;
1514
+ const importPath = `${this.config.srcPath}/logic/${routeName}.js`;
1491
1515
  this.log("debug", `Loading development route: ${importPath}`);
1492
1516
  const module = await import(importPath);
1493
1517
  script = module.default;
@@ -1508,7 +1532,7 @@ var RouteLoader = class {
1508
1532
  */
1509
1533
  async loadTemplate(routeName) {
1510
1534
  try {
1511
- const templatePath = `${this.config.basePath}/views/${routeName}.html`;
1535
+ const templatePath = `${this.config.srcPath}/views/${routeName}.html`;
1512
1536
  const response = await fetch(templatePath);
1513
1537
  if (!response.ok) throw new Error(`Template not found: ${response.status}`);
1514
1538
  const template = await response.text();
@@ -1524,7 +1548,7 @@ var RouteLoader = class {
1524
1548
  */
1525
1549
  async loadStyle(routeName) {
1526
1550
  try {
1527
- const stylePath = `${this.config.basePath}/styles/${routeName}.css`;
1551
+ const stylePath = `${this.config.srcPath}/styles/${routeName}.css`;
1528
1552
  const response = await fetch(stylePath);
1529
1553
  if (!response.ok) throw new Error(`Style not found: ${response.status}`);
1530
1554
  const style = await response.text();
@@ -1540,7 +1564,7 @@ var RouteLoader = class {
1540
1564
  */
1541
1565
  async loadLayout(layoutName) {
1542
1566
  try {
1543
- const layoutPath = `${this.config.basePath}/layouts/${layoutName}.html`;
1567
+ const layoutPath = `${this.config.srcPath}/layouts/${layoutName}.html`;
1544
1568
  const response = await fetch(layoutPath);
1545
1569
  if (!response.ok) throw new Error(`Layout not found: ${response.status}`);
1546
1570
  const layout = await response.text();
@@ -1618,7 +1642,14 @@ ${template}`;
1618
1642
  ...originalData,
1619
1643
  currentRoute: routeName,
1620
1644
  $query: router.queryManager?.getQueryParams() || {},
1621
- $lang: router.i18nManager?.getCurrentLanguage() || router.config.i18nDefaultLanguage,
1645
+ $lang: (() => {
1646
+ try {
1647
+ return router.i18nManager?.getCurrentLanguage() || router.config.i18nDefaultLanguage || router.config.defaultLanguage || "ko";
1648
+ } catch (error) {
1649
+ if (router.errorHandler) router.errorHandler.warn("RouteLoader", "Failed to get current language:", error);
1650
+ return router.config.defaultLanguage || "ko";
1651
+ }
1652
+ })(),
1622
1653
  $dataLoading: false
1623
1654
  };
1624
1655
  return commonData;
@@ -1652,8 +1683,15 @@ ${template}`;
1652
1683
  // ν†΅ν•©λœ νŒŒλΌλ―Έν„° 관리 (λΌμš°νŒ… + 쿼리 νŒŒλΌλ―Έν„°)
1653
1684
  getParams: () => router.queryManager?.getAllParams() || {},
1654
1685
  getParam: (key, defaultValue) => router.queryManager?.getParam(key, defaultValue),
1655
- // i18n κ΄€λ ¨
1656
- $t: (key, params) => router.i18nManager?.t(key, params) || key,
1686
+ // i18n κ΄€λ ¨ (resilient - i18n μ‹€νŒ¨ν•΄λ„ key λ°˜ν™˜)
1687
+ $t: (key, params) => {
1688
+ try {
1689
+ return router.i18nManager?.t(key, params) || key;
1690
+ } catch (error) {
1691
+ if (router.errorHandler) router.errorHandler.warn("RouteLoader", "i18n translation failed, returning key:", error);
1692
+ return key;
1693
+ }
1694
+ },
1657
1695
  // 인증 κ΄€λ ¨
1658
1696
  $isAuthenticated: () => router.authManager?.isUserAuthenticated() || false,
1659
1697
  $logout: () => router.authManager ? router.navigateTo(router.authManager.handleLogout()) : null,
@@ -1968,7 +2006,7 @@ ${template}`;
1968
2006
  getStats() {
1969
2007
  return {
1970
2008
  environment: this.config.environment,
1971
- basePath: this.config.basePath,
2009
+ srcPath: this.config.srcPath,
1972
2010
  routesPath: this.config.routesPath,
1973
2011
  useLayout: this.config.useLayout,
1974
2012
  useComponents: this.config.useComponents
@@ -2272,7 +2310,8 @@ var ErrorHandler = class {
2272
2310
  var ComponentLoader = class {
2273
2311
  constructor(router = null, options = {}) {
2274
2312
  this.config = {
2275
- basePath: options.basePath || "/src/components",
2313
+ componentsPath: options.componentsPath || "/components",
2314
+ // srcPath κΈ°μ€€ μƒλŒ€ 경둜
2276
2315
  debug: options.debug || false,
2277
2316
  environment: options.environment || "development",
2278
2317
  ...options
@@ -2314,7 +2353,20 @@ var ComponentLoader = class {
2314
2353
  * νŒŒμΌμ—μ„œ μ»΄ν¬λ„ŒνŠΈ λ‘œλ“œ
2315
2354
  */
2316
2355
  async _loadComponentFromFile(componentName) {
2317
- const componentPath = `${this.config.basePath}/${componentName}.js`;
2356
+ const componentRelativePath = `${this.config.componentsPath}/${componentName}.js`;
2357
+ let componentPath;
2358
+ if (this.router && this.router.config.srcPath) {
2359
+ const srcPath = this.router.config.srcPath;
2360
+ if (srcPath.startsWith("http")) {
2361
+ const cleanSrcPath = srcPath.endsWith("/") ? srcPath.slice(0, -1) : srcPath;
2362
+ const cleanComponentPath = componentRelativePath.startsWith("/") ? componentRelativePath : `/${componentRelativePath}`;
2363
+ componentPath = `${cleanSrcPath}${cleanComponentPath}`;
2364
+ } else {
2365
+ componentPath = this.router.resolvePath(`${srcPath}${componentRelativePath}`);
2366
+ }
2367
+ } else {
2368
+ componentPath = this.router ? this.router.resolvePath(`/src${componentRelativePath}`) : `/src${componentRelativePath}`;
2369
+ }
2318
2370
  try {
2319
2371
  const module = await import(componentPath);
2320
2372
  const component = module.default;
@@ -2357,7 +2409,7 @@ var ComponentLoader = class {
2357
2409
  */
2358
2410
  async _loadProductionComponents() {
2359
2411
  try {
2360
- const componentsPath = `${this.config.routesPath}/_components.js`;
2412
+ const componentsPath = `${this.router?.config?.routesPath || "/routes"}/_components.js`;
2361
2413
  this.log("info", "[PRODUCTION] Loading unified components from:", componentsPath);
2362
2414
  const componentsModule = await import(componentsPath);
2363
2415
  if (typeof componentsModule.registerComponents === "function") {
@@ -2445,7 +2497,10 @@ var ViewLogicRouter = class {
2445
2497
  _buildConfig(options) {
2446
2498
  const currentOrigin = window.location.origin;
2447
2499
  const defaults = {
2448
- basePath: `${currentOrigin}/src`,
2500
+ basePath: "/",
2501
+ // μ• ν”Œλ¦¬μΌ€μ΄μ…˜ κΈ°λ³Έ 경둜 (μ„œλΈŒν΄λ” 배포용)
2502
+ srcPath: "/src",
2503
+ // μ†ŒμŠ€ 파일 경둜
2449
2504
  mode: "hash",
2450
2505
  cacheMode: "memory",
2451
2506
  cacheTTL: 3e5,
@@ -2453,13 +2508,15 @@ var ViewLogicRouter = class {
2453
2508
  useLayout: true,
2454
2509
  defaultLayout: "default",
2455
2510
  environment: "development",
2456
- routesPath: `${currentOrigin}/routes`,
2511
+ routesPath: "/routes",
2512
+ // ν”„λ‘œλ•μ…˜ 라우트 경둜
2457
2513
  enableErrorReporting: true,
2458
2514
  useComponents: true,
2459
2515
  componentNames: ["Button", "Modal", "Card", "Toast", "Input", "Tabs", "Checkbox", "Alert", "DynamicInclude", "HtmlInclude"],
2460
- useI18n: true,
2516
+ useI18n: false,
2461
2517
  defaultLanguage: "ko",
2462
- i18nPath: `${currentOrigin}/i18n`,
2518
+ i18nPath: "/i18n",
2519
+ // λ‹€κ΅­μ–΄ 파일 경둜
2463
2520
  logLevel: "info",
2464
2521
  authEnabled: false,
2465
2522
  loginRoute: "login",
@@ -2481,17 +2538,56 @@ var ViewLogicRouter = class {
2481
2538
  logSecurityWarnings: true
2482
2539
  };
2483
2540
  const config = { ...defaults, ...options };
2484
- if (options.basePath && !options.basePath.startsWith("http")) {
2485
- config.basePath = `${currentOrigin}${options.basePath}`;
2486
- }
2487
- if (options.routesPath && !options.routesPath.startsWith("http")) {
2488
- config.routesPath = `${currentOrigin}${options.routesPath}`;
2489
- }
2490
- if (options.i18nPath && !options.i18nPath.startsWith("http")) {
2491
- config.i18nPath = `${currentOrigin}${options.i18nPath}`;
2492
- }
2541
+ config.srcPath = this.resolvePath(config.srcPath, config.basePath);
2542
+ config.routesPath = this.resolvePath(config.routesPath, config.basePath);
2543
+ config.i18nPath = this.resolvePath(config.i18nPath, config.basePath);
2493
2544
  return config;
2494
2545
  }
2546
+ /**
2547
+ * 톡합 경둜 ν•΄κ²° - μ„œλΈŒν΄λ” 배포 및 basePath 지원
2548
+ */
2549
+ resolvePath(path, basePath = null) {
2550
+ const currentOrigin = window.location.origin;
2551
+ if (path.startsWith("http")) {
2552
+ return path;
2553
+ }
2554
+ if (path.startsWith("/")) {
2555
+ if (basePath && basePath !== "/") {
2556
+ const cleanBasePath = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
2557
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
2558
+ const fullPath = `${cleanBasePath}${cleanPath}`;
2559
+ const fullUrl2 = `${currentOrigin}${fullPath}`;
2560
+ return fullUrl2.replace(/([^:])\/{2,}/g, "$1/");
2561
+ }
2562
+ return `${currentOrigin}${path}`;
2563
+ }
2564
+ const currentPathname = window.location.pathname;
2565
+ const currentBase = currentPathname.endsWith("/") ? currentPathname : currentPathname.substring(0, currentPathname.lastIndexOf("/") + 1);
2566
+ const resolvedPath = this.normalizePath(currentBase + path);
2567
+ const fullUrl = `${currentOrigin}${resolvedPath}`;
2568
+ return fullUrl.replace(/([^:])\/{2,}/g, "$1/");
2569
+ }
2570
+ /**
2571
+ * URL 경둜 μ •κ·œν™” (이쀑 μŠ¬λž˜μ‹œ 제거 및 ../, ./ 처리)
2572
+ */
2573
+ normalizePath(path) {
2574
+ path = path.replace(/\/+/g, "/");
2575
+ const parts = path.split("/").filter((part) => part !== "" && part !== ".");
2576
+ const stack = [];
2577
+ for (const part of parts) {
2578
+ if (part === "..") {
2579
+ if (stack.length > 0 && stack[stack.length - 1] !== "..") {
2580
+ stack.pop();
2581
+ } else if (!path.startsWith("/")) {
2582
+ stack.push(part);
2583
+ }
2584
+ } else {
2585
+ stack.push(part);
2586
+ }
2587
+ }
2588
+ const normalized = "/" + stack.join("/");
2589
+ return normalized === "/" ? "/" : normalized;
2590
+ }
2495
2591
  /**
2496
2592
  * λ‘œκΉ… 래퍼 λ©”μ„œλ“œ
2497
2593
  */
@@ -2510,9 +2606,16 @@ var ViewLogicRouter = class {
2510
2606
  this.queryManager = new QueryManager(this, this.config);
2511
2607
  this.errorHandler = new ErrorHandler(this, this.config);
2512
2608
  if (this.config.useI18n) {
2513
- this.i18nManager = new I18nManager(this, this.config);
2514
- if (this.i18nManager.initPromise) {
2515
- await this.i18nManager.initPromise;
2609
+ try {
2610
+ this.i18nManager = new I18nManager(this, this.config);
2611
+ if (this.i18nManager.initPromise) {
2612
+ await this.i18nManager.initPromise;
2613
+ }
2614
+ this.log("info", "I18nManager initialized successfully");
2615
+ } catch (i18nError) {
2616
+ this.log("warn", "I18nManager initialization failed, continuing without i18n:", i18nError.message);
2617
+ this.i18nManager = null;
2618
+ this.config.useI18n = false;
2516
2619
  }
2517
2620
  }
2518
2621
  if (this.config.authEnabled) {
@@ -2596,8 +2699,17 @@ var ViewLogicRouter = class {
2596
2699
  queryParams: this.queryManager?.parseQueryString(queryPart || window.location.search.slice(1)) || {}
2597
2700
  };
2598
2701
  } else {
2702
+ const fullPath = window.location.pathname;
2703
+ const basePath = this.config.basePath || "/";
2704
+ let route = fullPath;
2705
+ if (basePath !== "/" && fullPath.startsWith(basePath)) {
2706
+ route = fullPath.slice(basePath.length);
2707
+ }
2708
+ if (route.startsWith("/")) {
2709
+ route = route.slice(1);
2710
+ }
2599
2711
  return {
2600
- route: window.location.pathname.slice(1) || "home",
2712
+ route: route || "home",
2601
2713
  queryParams: this.queryManager?.parseQueryString(window.location.search.slice(1)) || {}
2602
2714
  };
2603
2715
  }
@@ -2730,7 +2842,10 @@ var ViewLogicRouter = class {
2730
2842
  const queryParams = params || this.queryManager?.getQueryParams() || {};
2731
2843
  const queryString = this.queryManager?.buildQueryString(queryParams) || "";
2732
2844
  const buildURL = (route2, queryString2, isHash = true) => {
2733
- const base = route2 === "home" ? "/" : `/${route2}`;
2845
+ let base = route2 === "home" ? "/" : `/${route2}`;
2846
+ if (!isHash && this.config.basePath && this.config.basePath !== "/") {
2847
+ base = `${this.config.basePath}${base}`;
2848
+ }
2734
2849
  const url = queryString2 ? `${base}?${queryString2}` : base;
2735
2850
  return isHash ? `#${url}` : url;
2736
2851
  };
@@ -2741,7 +2856,11 @@ var ViewLogicRouter = class {
2741
2856
  }
2742
2857
  } else {
2743
2858
  const newPath = buildURL(route, queryString, false);
2744
- const isSameRoute = window.location.pathname === (route === "home" ? "/" : `/${route}`);
2859
+ let expectedPath = route === "home" ? "/" : `/${route}`;
2860
+ if (this.config.basePath && this.config.basePath !== "/") {
2861
+ expectedPath = `${this.config.basePath}${expectedPath}`;
2862
+ }
2863
+ const isSameRoute = window.location.pathname === expectedPath;
2745
2864
  if (isSameRoute) {
2746
2865
  window.history.replaceState({}, "", newPath);
2747
2866
  } else {