jskelet 0.2.5 → 0.3.0

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.
Files changed (106) hide show
  1. package/AGENTS.md +132 -132
  2. package/CHANGELOG.md +403 -383
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +103 -103
  5. package/docs/01-baslangic.md +285 -285
  6. package/docs/02-mimari.md +293 -287
  7. package/docs/03-routing.md +486 -480
  8. package/docs/04-render-ve-sablonlar.md +490 -490
  9. package/docs/05-islands.md +482 -482
  10. package/docs/06-cache.md +1231 -1209
  11. package/docs/07-yapilandirma.md +44 -21
  12. package/docs/08-build.md +366 -366
  13. package/docs/09-dev-araclari.md +335 -335
  14. package/docs/10-dagitim.md +329 -329
  15. package/docs/11-tasima.md +1 -0
  16. package/docs/12-panel-ve-oturum.md +384 -384
  17. package/docs/README.md +105 -105
  18. package/docs/en/01-getting-started.md +292 -292
  19. package/docs/en/02-architecture.md +311 -305
  20. package/docs/en/03-routing.md +503 -497
  21. package/docs/en/04-rendering.md +504 -504
  22. package/docs/en/05-islands.md +492 -492
  23. package/docs/en/06-caching.md +1197 -1198
  24. package/docs/en/07-configuration.md +1009 -986
  25. package/docs/en/08-build.md +383 -383
  26. package/docs/en/09-dev-tools.md +342 -342
  27. package/docs/en/10-deployment.md +332 -332
  28. package/docs/en/11-migration.md +360 -359
  29. package/docs/en/12-dashboards-and-sessions.md +392 -392
  30. package/docs/en/README.md +112 -112
  31. package/package.json +102 -102
  32. package/src/build/ensure-build.mjs +15 -15
  33. package/src/build/paths.mjs +143 -143
  34. package/src/build/resolve-peer.mjs +36 -36
  35. package/src/build/tasks/client.mjs +268 -268
  36. package/src/build/tasks/css.mjs +124 -124
  37. package/src/build/tasks/fonts.mjs +146 -146
  38. package/src/build/tasks/icons.mjs +224 -224
  39. package/src/build/tasks/images.mjs +244 -244
  40. package/src/build/tasks/precompress.mjs +78 -78
  41. package/src/client/{cache-panel → admin}/i18n.js +756 -670
  42. package/src/client/{cache-panel → admin}/login.html +74 -74
  43. package/src/client/{cache-panel → admin}/panel.css +804 -756
  44. package/src/client/admin/panel.html +486 -0
  45. package/src/client/{cache-panel → admin}/panel.js +1242 -915
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +725 -725
  48. package/src/client/dom.js +95 -95
  49. package/src/client/form.js +192 -192
  50. package/src/client/index.js +35 -35
  51. package/src/client/registry.js +297 -297
  52. package/src/client/safe-image.js +91 -91
  53. package/src/client/store.js +36 -36
  54. package/src/client/swap.js +188 -188
  55. package/src/config/defaults.js +16 -7
  56. package/src/config/index.js +38 -22
  57. package/src/config/pattern.js +107 -107
  58. package/src/http/control-flow.js +71 -71
  59. package/src/http/cookies.js +257 -257
  60. package/src/http/request-cache.js +46 -46
  61. package/src/http/request-context.js +162 -162
  62. package/src/index.js +83 -83
  63. package/src/init.mjs +221 -221
  64. package/src/log.mjs +58 -0
  65. package/src/runtime/alias-hooks.mjs +119 -119
  66. package/src/runtime/register.mjs +4 -4
  67. package/src/server/admin/actions.js +229 -0
  68. package/src/server/admin/auth.js +125 -0
  69. package/src/server/admin/event-log.js +151 -0
  70. package/src/server/admin/gate.js +209 -0
  71. package/src/server/admin/inventory.js +188 -0
  72. package/src/server/admin/mount.js +56 -0
  73. package/src/server/admin/router.js +216 -0
  74. package/src/server/admin/snapshot.js +126 -0
  75. package/src/server/assets.js +147 -147
  76. package/src/server/cache-deps.js +42 -42
  77. package/src/server/cloudflare.js +607 -607
  78. package/src/server/create-app.js +295 -291
  79. package/src/server/data-cache.js +462 -462
  80. package/src/server/dev/report.js +369 -369
  81. package/src/server/dev/socket.js +170 -170
  82. package/src/server/dev/version-check.mjs +139 -139
  83. package/src/server/html-cache.js +817 -817
  84. package/src/server/metadata.js +102 -102
  85. package/src/server/middleware/compression.js +205 -205
  86. package/src/server/middleware/csrf.js +134 -134
  87. package/src/server/middleware/dev-gate.js +62 -62
  88. package/src/server/middleware/headers.js +37 -37
  89. package/src/server/middleware/redirects.js +32 -32
  90. package/src/server/middleware/static-precompressed.js +100 -100
  91. package/src/server/middleware/trailing-slash.js +53 -0
  92. package/src/server/middleware/upstream-proxy.js +141 -141
  93. package/src/server/prewarm.js +601 -601
  94. package/src/server/redis.js +569 -569
  95. package/src/server/router.js +128 -128
  96. package/src/server/status-page.js +164 -164
  97. package/src/server/upstream-limiter.js +376 -376
  98. package/src/server/upstream-tracking.js +166 -166
  99. package/src/start.mjs +7 -7
  100. package/src/templates/layout.ejs +44 -44
  101. package/src/version.mjs +31 -31
  102. package/src/views/components/loader.js +85 -85
  103. package/src/views/helpers/html.js +102 -102
  104. package/src/views/helpers/tags.js +245 -245
  105. package/src/client/cache-panel/panel.html +0 -308
  106. package/src/server/cache-panel.js +0 -759
@@ -1,245 +1,245 @@
1
- /**
2
- * `next/link`, `next/image` ve `@phosphor-icons/react` karşılıkları.
3
- * Hepsi HTML string döndürür; EJS içinden `<%- %>` ile basılır.
4
- */
5
- import { attrs, esc, cn } from "./html.js";
6
- import { asset, getSpriteIds, optimizedImage } from "../../server/assets.js";
7
- import { getRequestContext, markTainted } from "../../http/request-context.js";
8
- import { getSignedCookie, randomToken, setSignedCookie } from "../../http/cookies.js";
9
- import { getConfig } from "../../config/index.js";
10
-
11
- const isDev = process.env.NODE_ENV === "development";
12
-
13
- /** Aynı eksik ikon için tek uyarı. */
14
- const warnedIcons = new Set();
15
-
16
- /**
17
- * `SeoLink` karşılığı: title otomatik doldurulur.
18
- * @param {{ href: string, text?: string, html?: string, class?: string,
19
- * title?: string, ariaLabel?: string, target?: string, rel?: string,
20
- * attrs?: Record<string, unknown> }} props
21
- * @returns {string}
22
- */
23
- export function link(props) {
24
- const {
25
- href,
26
- text,
27
- html,
28
- class: className,
29
- title,
30
- ariaLabel,
31
- target,
32
- rel,
33
- attrs: extra = {},
34
- } = props;
35
-
36
- const resolvedTitle = title ?? ariaLabel ?? text ?? href;
37
- const isExternal = /^https?:\/\//.test(String(href ?? ""));
38
-
39
- const attributes = attrs({
40
- href,
41
- class: className,
42
- title: resolvedTitle,
43
- "aria-label": ariaLabel,
44
- target: target ?? (isExternal ? "_blank" : undefined),
45
- rel: rel ?? (isExternal ? "noopener noreferrer" : undefined),
46
- ...extra,
47
- });
48
-
49
- return `<a${attributes}>${html ?? esc(text ?? "")}</a>`;
50
- }
51
-
52
- /**
53
- * `next/image` karşılığı. `public/` altındaki yerel görseller için build'de
54
- * üretilen webp varyantları (`build/tasks/images.mjs`) otomatik olarak
55
- * `srcset` + intrinsic `width`/`height` olarak eklenir; manifest'te olmayan
56
- * ya da uzak görseller olduğu gibi basılır.
57
- *
58
- * `priority` LCP görselleri için `fetchpriority=high` + eager yükleme yapar.
59
- * @param {{ src: string, alt: string, width?: number, height?: number,
60
- * class?: string, sizes?: string, srcset?: string, priority?: boolean,
61
- * fill?: boolean, loading?: 'lazy' | 'eager', unoptimized?: boolean,
62
- * attrs?: Record<string, unknown> }} props
63
- * @returns {string}
64
- */
65
- export function image(props) {
66
- const {
67
- src,
68
- alt,
69
- width,
70
- height,
71
- class: className,
72
- sizes,
73
- srcset,
74
- priority = false,
75
- fill = false,
76
- loading,
77
- unoptimized = false,
78
- attrs: extra = {},
79
- } = props;
80
-
81
- const optimized = unoptimized || srcset ? undefined : optimizedImage(src);
82
- const largest = optimized?.variants.at(-1);
83
- // Tek varyant üretilmişse (kaynak zaten küçükse) srcset/sizes gürültüden ibaret.
84
- const responsive = optimized && optimized.variants.length > 1;
85
-
86
- const attributes = attrs({
87
- src: largest?.url ?? src,
88
- alt: alt ?? "",
89
- width: fill ? undefined : (width ?? optimized?.width),
90
- height: fill ? undefined : (height ?? optimized?.height),
91
- class: fill
92
- ? cn("absolute inset-0 h-full w-full object-cover", className)
93
- : className,
94
- sizes: responsive ? (sizes ?? defaultSizes(optimized)) : sizes,
95
- srcset: responsive ? toSrcSet(optimized) : srcset,
96
- loading: loading ?? (priority ? "eager" : "lazy"),
97
- decoding: priority ? "sync" : "async",
98
- fetchpriority: priority ? "high" : undefined,
99
- ...extra,
100
- });
101
-
102
- return `<img${attributes}>`;
103
- }
104
-
105
- /**
106
- * @param {import("../../server/assets.js").OptimizedImage} optimized
107
- * @returns {string}
108
- */
109
- function toSrcSet(optimized) {
110
- return optimized.variants
111
- .map((variant) => `${variant.url} ${variant.width}w`)
112
- .join(", ");
113
- }
114
-
115
- /**
116
- * Çağıran `sizes` vermediyse makul bir varsayılan: görsel kendi intrinsic
117
- * genişliğinden büyütülmez, dar ekranlarda viewport'u kaplar.
118
- * @param {import("../../server/assets.js").OptimizedImage} optimized
119
- * @returns {string}
120
- */
121
- function defaultSizes(optimized) {
122
- const max = optimized.variants.at(-1)?.width ?? optimized.width;
123
- return `(max-width: ${max}px) 100vw, ${max}px`;
124
- }
125
-
126
- /**
127
- * LCP görselleri için `<head>` preload etiketi.
128
- * @param {{ href: string, imagesrcset?: string, imagesizes?: string }} props
129
- * @returns {string}
130
- */
131
- export function preloadImage({ href, imagesrcset, imagesizes }) {
132
- return `<link${attrs({
133
- rel: "preload",
134
- as: "image",
135
- href,
136
- imagesrcset,
137
- imagesizes,
138
- fetchpriority: "high",
139
- })}>`;
140
- }
141
-
142
- /**
143
- * Phosphor ikonları — build zamanı üretilen SVG sprite'tan `<use>`.
144
- * `weight` sprite id'sine dahildir (regular/bold/fill).
145
- * @param {{ name: string, size?: number | string, weight?: string,
146
- * class?: string, attrs?: Record<string, unknown> }} props
147
- * @returns {string}
148
- */
149
- export function icon(props) {
150
- const {
151
- name,
152
- size = 24,
153
- weight = "regular",
154
- class: className,
155
- attrs: extra = {},
156
- } = props;
157
-
158
- const spritePath = asset("sprite.svg");
159
- const id = `${toKebab(name)}-${weight}`;
160
-
161
- if (isDev && !warnedIcons.has(id)) {
162
- const ids = getSpriteIds();
163
- if (ids.size && !ids.has(id)) {
164
- warnedIcons.add(id);
165
- console.warn(
166
- `[icon] missing from sprite: ${id} — write the name as a literal or add it to the build/tasks/icons.mjs scan.`,
167
- );
168
- }
169
- }
170
-
171
- const attributes = attrs({
172
- width: size,
173
- height: size,
174
- class: className,
175
- "aria-hidden": "true",
176
- focusable: "false",
177
- fill: "currentColor",
178
- viewBox: "0 0 256 256",
179
- ...extra,
180
- });
181
-
182
- return `<svg${attributes}><use href="${esc(spritePath)}#${esc(id)}"></use></svg>`;
183
- }
184
-
185
- /**
186
- * Formun içine CSRF token'ını gizli alan olarak basar.
187
- *
188
- * Token **burada** üretilir ve imzalı cookie olarak yazılır: bir sayfada
189
- * token gerçekten gerekiyorsa o sayfa zaten kişiye özeldir. Bu yüzden çağrı
190
- * render'ı işaretler (`tainted`) ve sayfa public HTML cache'ine giremez —
191
- * aksi hâlde tüm ziyaretçiler cache'ten aynı token'ı alırdı ve çift gönderim
192
- * kontrolü hiçbir şey doğrulamazdı.
193
- *
194
- * `security.csrf.token` kapalıysa boş string döner; şablon her koşulda
195
- * render edilebilmeli, korumanın açık olması config'in kararı.
196
- *
197
- * @returns {string}
198
- */
199
- export function csrfField() {
200
- const context = getRequestContext();
201
- if (!context?.res) return "";
202
-
203
- const { csrf } = getConfig().security;
204
- if (!csrf.token) return "";
205
-
206
- if (!context.csrfToken) {
207
- const existing = getSignedCookie(context.res.req, csrf.cookieName);
208
- const token = existing ?? randomToken(24);
209
-
210
- if (!existing) {
211
- try {
212
- setSignedCookie(context.res, csrf.cookieName, token, {
213
- // Token'ı istemci cookie'den değil, basılan gizli alandan okur;
214
- // HttpOnly kalması XSS durumunda bir katman daha demek.
215
- httpOnly: true,
216
- });
217
- } catch (error) {
218
- console.warn(
219
- "[csrf] could not emit the token: no secret for signed cookies",
220
- error instanceof Error ? error.message : error,
221
- );
222
- return "";
223
- }
224
- }
225
-
226
- context.csrfToken = token;
227
- }
228
-
229
- markTainted("csrfField()");
230
-
231
- return `<input type="hidden" name="${esc(csrf.fieldName)}" value="${esc(context.csrfToken)}">`;
232
- }
233
-
234
- /**
235
- * `ArrowRightIcon` / `ArrowRight` → `arrow-right`
236
- * @param {string} name
237
- * @returns {string}
238
- */
239
- export function toKebab(name) {
240
- return String(name)
241
- .replace(/Icon$/, "")
242
- .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
243
- .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
244
- .toLowerCase();
245
- }
1
+ /**
2
+ * `next/link`, `next/image` ve `@phosphor-icons/react` karşılıkları.
3
+ * Hepsi HTML string döndürür; EJS içinden `<%- %>` ile basılır.
4
+ */
5
+ import { attrs, esc, cn } from "./html.js";
6
+ import { asset, getSpriteIds, optimizedImage } from "../../server/assets.js";
7
+ import { getRequestContext, markTainted } from "../../http/request-context.js";
8
+ import { getSignedCookie, randomToken, setSignedCookie } from "../../http/cookies.js";
9
+ import { getConfig } from "../../config/index.js";
10
+
11
+ const isDev = process.env.NODE_ENV === "development";
12
+
13
+ /** Aynı eksik ikon için tek uyarı. */
14
+ const warnedIcons = new Set();
15
+
16
+ /**
17
+ * `SeoLink` karşılığı: title otomatik doldurulur.
18
+ * @param {{ href: string, text?: string, html?: string, class?: string,
19
+ * title?: string, ariaLabel?: string, target?: string, rel?: string,
20
+ * attrs?: Record<string, unknown> }} props
21
+ * @returns {string}
22
+ */
23
+ export function link(props) {
24
+ const {
25
+ href,
26
+ text,
27
+ html,
28
+ class: className,
29
+ title,
30
+ ariaLabel,
31
+ target,
32
+ rel,
33
+ attrs: extra = {},
34
+ } = props;
35
+
36
+ const resolvedTitle = title ?? ariaLabel ?? text ?? href;
37
+ const isExternal = /^https?:\/\//.test(String(href ?? ""));
38
+
39
+ const attributes = attrs({
40
+ href,
41
+ class: className,
42
+ title: resolvedTitle,
43
+ "aria-label": ariaLabel,
44
+ target: target ?? (isExternal ? "_blank" : undefined),
45
+ rel: rel ?? (isExternal ? "noopener noreferrer" : undefined),
46
+ ...extra,
47
+ });
48
+
49
+ return `<a${attributes}>${html ?? esc(text ?? "")}</a>`;
50
+ }
51
+
52
+ /**
53
+ * `next/image` karşılığı. `public/` altındaki yerel görseller için build'de
54
+ * üretilen webp varyantları (`build/tasks/images.mjs`) otomatik olarak
55
+ * `srcset` + intrinsic `width`/`height` olarak eklenir; manifest'te olmayan
56
+ * ya da uzak görseller olduğu gibi basılır.
57
+ *
58
+ * `priority` LCP görselleri için `fetchpriority=high` + eager yükleme yapar.
59
+ * @param {{ src: string, alt: string, width?: number, height?: number,
60
+ * class?: string, sizes?: string, srcset?: string, priority?: boolean,
61
+ * fill?: boolean, loading?: 'lazy' | 'eager', unoptimized?: boolean,
62
+ * attrs?: Record<string, unknown> }} props
63
+ * @returns {string}
64
+ */
65
+ export function image(props) {
66
+ const {
67
+ src,
68
+ alt,
69
+ width,
70
+ height,
71
+ class: className,
72
+ sizes,
73
+ srcset,
74
+ priority = false,
75
+ fill = false,
76
+ loading,
77
+ unoptimized = false,
78
+ attrs: extra = {},
79
+ } = props;
80
+
81
+ const optimized = unoptimized || srcset ? undefined : optimizedImage(src);
82
+ const largest = optimized?.variants.at(-1);
83
+ // Tek varyant üretilmişse (kaynak zaten küçükse) srcset/sizes gürültüden ibaret.
84
+ const responsive = optimized && optimized.variants.length > 1;
85
+
86
+ const attributes = attrs({
87
+ src: largest?.url ?? src,
88
+ alt: alt ?? "",
89
+ width: fill ? undefined : (width ?? optimized?.width),
90
+ height: fill ? undefined : (height ?? optimized?.height),
91
+ class: fill
92
+ ? cn("absolute inset-0 h-full w-full object-cover", className)
93
+ : className,
94
+ sizes: responsive ? (sizes ?? defaultSizes(optimized)) : sizes,
95
+ srcset: responsive ? toSrcSet(optimized) : srcset,
96
+ loading: loading ?? (priority ? "eager" : "lazy"),
97
+ decoding: priority ? "sync" : "async",
98
+ fetchpriority: priority ? "high" : undefined,
99
+ ...extra,
100
+ });
101
+
102
+ return `<img${attributes}>`;
103
+ }
104
+
105
+ /**
106
+ * @param {import("../../server/assets.js").OptimizedImage} optimized
107
+ * @returns {string}
108
+ */
109
+ function toSrcSet(optimized) {
110
+ return optimized.variants
111
+ .map((variant) => `${variant.url} ${variant.width}w`)
112
+ .join(", ");
113
+ }
114
+
115
+ /**
116
+ * Çağıran `sizes` vermediyse makul bir varsayılan: görsel kendi intrinsic
117
+ * genişliğinden büyütülmez, dar ekranlarda viewport'u kaplar.
118
+ * @param {import("../../server/assets.js").OptimizedImage} optimized
119
+ * @returns {string}
120
+ */
121
+ function defaultSizes(optimized) {
122
+ const max = optimized.variants.at(-1)?.width ?? optimized.width;
123
+ return `(max-width: ${max}px) 100vw, ${max}px`;
124
+ }
125
+
126
+ /**
127
+ * LCP görselleri için `<head>` preload etiketi.
128
+ * @param {{ href: string, imagesrcset?: string, imagesizes?: string }} props
129
+ * @returns {string}
130
+ */
131
+ export function preloadImage({ href, imagesrcset, imagesizes }) {
132
+ return `<link${attrs({
133
+ rel: "preload",
134
+ as: "image",
135
+ href,
136
+ imagesrcset,
137
+ imagesizes,
138
+ fetchpriority: "high",
139
+ })}>`;
140
+ }
141
+
142
+ /**
143
+ * Phosphor ikonları — build zamanı üretilen SVG sprite'tan `<use>`.
144
+ * `weight` sprite id'sine dahildir (regular/bold/fill).
145
+ * @param {{ name: string, size?: number | string, weight?: string,
146
+ * class?: string, attrs?: Record<string, unknown> }} props
147
+ * @returns {string}
148
+ */
149
+ export function icon(props) {
150
+ const {
151
+ name,
152
+ size = 24,
153
+ weight = "regular",
154
+ class: className,
155
+ attrs: extra = {},
156
+ } = props;
157
+
158
+ const spritePath = asset("sprite.svg");
159
+ const id = `${toKebab(name)}-${weight}`;
160
+
161
+ if (isDev && !warnedIcons.has(id)) {
162
+ const ids = getSpriteIds();
163
+ if (ids.size && !ids.has(id)) {
164
+ warnedIcons.add(id);
165
+ console.warn(
166
+ `[icon] missing from sprite: ${id} — write the name as a literal or add it to the build/tasks/icons.mjs scan.`,
167
+ );
168
+ }
169
+ }
170
+
171
+ const attributes = attrs({
172
+ width: size,
173
+ height: size,
174
+ class: className,
175
+ "aria-hidden": "true",
176
+ focusable: "false",
177
+ fill: "currentColor",
178
+ viewBox: "0 0 256 256",
179
+ ...extra,
180
+ });
181
+
182
+ return `<svg${attributes}><use href="${esc(spritePath)}#${esc(id)}"></use></svg>`;
183
+ }
184
+
185
+ /**
186
+ * Formun içine CSRF token'ını gizli alan olarak basar.
187
+ *
188
+ * Token **burada** üretilir ve imzalı cookie olarak yazılır: bir sayfada
189
+ * token gerçekten gerekiyorsa o sayfa zaten kişiye özeldir. Bu yüzden çağrı
190
+ * render'ı işaretler (`tainted`) ve sayfa public HTML cache'ine giremez —
191
+ * aksi hâlde tüm ziyaretçiler cache'ten aynı token'ı alırdı ve çift gönderim
192
+ * kontrolü hiçbir şey doğrulamazdı.
193
+ *
194
+ * `security.csrf.token` kapalıysa boş string döner; şablon her koşulda
195
+ * render edilebilmeli, korumanın açık olması config'in kararı.
196
+ *
197
+ * @returns {string}
198
+ */
199
+ export function csrfField() {
200
+ const context = getRequestContext();
201
+ if (!context?.res) return "";
202
+
203
+ const { csrf } = getConfig().security;
204
+ if (!csrf.token) return "";
205
+
206
+ if (!context.csrfToken) {
207
+ const existing = getSignedCookie(context.res.req, csrf.cookieName);
208
+ const token = existing ?? randomToken(24);
209
+
210
+ if (!existing) {
211
+ try {
212
+ setSignedCookie(context.res, csrf.cookieName, token, {
213
+ // Token'ı istemci cookie'den değil, basılan gizli alandan okur;
214
+ // HttpOnly kalması XSS durumunda bir katman daha demek.
215
+ httpOnly: true,
216
+ });
217
+ } catch (error) {
218
+ console.warn(
219
+ "[csrf] could not emit the token: no secret for signed cookies",
220
+ error instanceof Error ? error.message : error,
221
+ );
222
+ return "";
223
+ }
224
+ }
225
+
226
+ context.csrfToken = token;
227
+ }
228
+
229
+ markTainted("csrfField()");
230
+
231
+ return `<input type="hidden" name="${esc(csrf.fieldName)}" value="${esc(context.csrfToken)}">`;
232
+ }
233
+
234
+ /**
235
+ * `ArrowRightIcon` / `ArrowRight` → `arrow-right`
236
+ * @param {string} name
237
+ * @returns {string}
238
+ */
239
+ export function toKebab(name) {
240
+ return String(name)
241
+ .replace(/Icon$/, "")
242
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
243
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
244
+ .toLowerCase();
245
+ }