asjs-express 1.1.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.
package/lib/asjs.js ADDED
@@ -0,0 +1,1579 @@
1
+ const crypto = require('crypto');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const express = require('express');
5
+
6
+ function escapeHtml(value) {
7
+ return String(value ?? '')
8
+ .replace(/&/g, '&')
9
+ .replace(/</g, '&lt;')
10
+ .replace(/>/g, '&gt;')
11
+ .replace(/"/g, '&quot;')
12
+ .replace(/'/g, '&#39;');
13
+ }
14
+
15
+ function ensureExtension(name, extension) {
16
+ return path.extname(name) ? name : `${name}.${extension}`;
17
+ }
18
+
19
+ function normalizeBooleanOption(value, fallback) {
20
+ return value === undefined ? fallback : Boolean(value);
21
+ }
22
+
23
+ function normalizeNumberOption(value, fallback) {
24
+ const normalized = Number(value);
25
+ return Number.isFinite(normalized) && normalized >= 0 ? normalized : fallback;
26
+ }
27
+
28
+ function normalizeAssetMountPath(value) {
29
+ const rawValue = value === undefined || value === null || value === ''
30
+ ? '_asjs'
31
+ : String(value);
32
+ const normalized = rawValue.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
33
+
34
+ return `/${normalized || '_asjs'}`;
35
+ }
36
+
37
+ function joinAssetUrl(basePath, fileName) {
38
+ return `${normalizeAssetMountPath(basePath)}/${String(fileName).replace(/^\/+/, '')}`;
39
+ }
40
+
41
+ function appendAssetVersion(url, version) {
42
+ if (!version) {
43
+ return url;
44
+ }
45
+
46
+ const separator = url.includes('?') ? '&' : '?';
47
+ return `${url}${separator}v=${encodeURIComponent(version)}`;
48
+ }
49
+
50
+ function serializeHtmlAttributes(attributes = {}) {
51
+ return Object.entries(attributes)
52
+ .filter(([, value]) => value !== false && value !== null && value !== undefined)
53
+ .map(([name, value]) => {
54
+ if (value === true) {
55
+ return ` ${name}`;
56
+ }
57
+
58
+ return ` ${name}="${escapeHtml(String(value))}"`;
59
+ })
60
+ .join('');
61
+ }
62
+
63
+ function createFileHash(filePath) {
64
+ try {
65
+ return crypto
66
+ .createHash('sha1')
67
+ .update(fs.readFileSync(filePath))
68
+ .digest('hex')
69
+ .slice(0, 10);
70
+ } catch (error) {
71
+ return 'dev';
72
+ }
73
+ }
74
+
75
+ function getPackageVersion(packagePaths) {
76
+ try {
77
+ const packageJson = JSON.parse(fs.readFileSync(packagePaths.packageJsonPath, 'utf8'));
78
+ return typeof packageJson.version === 'string' && packageJson.version
79
+ ? packageJson.version
80
+ : 'dev';
81
+ } catch (error) {
82
+ return 'dev';
83
+ }
84
+ }
85
+
86
+ function resolveAssetVersion(filePath, config) {
87
+ if (config.assetVersion === false) {
88
+ return '';
89
+ }
90
+
91
+ const baseVersion = typeof config.assetVersion === 'string' && config.assetVersion
92
+ ? config.assetVersion
93
+ : (config.packageVersion || 'dev');
94
+
95
+ return `${baseVersion}-${createFileHash(filePath)}`;
96
+ }
97
+
98
+ function createClientAssets(config) {
99
+ const routerScriptVersion = resolveAssetVersion(config.packagePaths.routerPath, config);
100
+ const coreStylesheetVersion = resolveAssetVersion(config.packagePaths.stylesheetPath, config);
101
+ const themeStylesheetVersion = resolveAssetVersion(config.packagePaths.themeStylesheetPath, config);
102
+
103
+ return {
104
+ mountPath: config.assetMountPath,
105
+ routerScript: appendAssetVersion(
106
+ joinAssetUrl(config.assetMountPath, 'asjs-router.js'),
107
+ routerScriptVersion
108
+ ),
109
+ coreStylesheet: appendAssetVersion(
110
+ joinAssetUrl(config.assetMountPath, 'asjs-core.css'),
111
+ coreStylesheetVersion
112
+ ),
113
+ themeStylesheet: appendAssetVersion(
114
+ joinAssetUrl(config.assetMountPath, 'asjs-theme.css'),
115
+ themeStylesheetVersion
116
+ ),
117
+ versions: {
118
+ routerScript: routerScriptVersion,
119
+ coreStylesheet: coreStylesheetVersion,
120
+ themeStylesheet: themeStylesheetVersion
121
+ }
122
+ };
123
+ }
124
+
125
+ function renderClientTags(assets, options = {}) {
126
+ const settings = options && typeof options === 'object' ? options : {};
127
+ const tags = [];
128
+ const includeTheme = Boolean(settings.theme);
129
+
130
+ if (settings.preload) {
131
+ if (settings.styles !== false) {
132
+ tags.push(`<link${serializeHtmlAttributes({
133
+ rel: 'preload',
134
+ as: 'style',
135
+ href: assets.coreStylesheet
136
+ })}>`);
137
+ }
138
+
139
+ if (settings.script !== false) {
140
+ tags.push(`<link${serializeHtmlAttributes({
141
+ rel: 'preload',
142
+ as: 'script',
143
+ href: assets.routerScript
144
+ })}>`);
145
+ }
146
+
147
+ if (includeTheme) {
148
+ tags.push(`<link${serializeHtmlAttributes({
149
+ rel: 'preload',
150
+ as: 'style',
151
+ href: assets.themeStylesheet
152
+ })}>`);
153
+ }
154
+ }
155
+
156
+ if (settings.styles !== false) {
157
+ tags.push(`<link${serializeHtmlAttributes({
158
+ rel: 'stylesheet',
159
+ href: assets.coreStylesheet,
160
+ ...(settings.styleAttrs || {})
161
+ })}>`);
162
+ }
163
+
164
+ if (includeTheme) {
165
+ tags.push(`<link${serializeHtmlAttributes({
166
+ rel: 'stylesheet',
167
+ href: assets.themeStylesheet,
168
+ ...(settings.themeAttrs || {})
169
+ })}>`);
170
+ }
171
+
172
+ if (settings.script !== false) {
173
+ const scriptAttributes = {
174
+ src: assets.routerScript,
175
+ ...(settings.module ? { type: 'module' } : {}),
176
+ ...((settings.async || settings.defer === false || settings.module) ? {} : { defer: true }),
177
+ ...(settings.async ? { async: true } : {}),
178
+ ...(settings.scriptAttrs || {})
179
+ };
180
+
181
+ tags.push(`<script${serializeHtmlAttributes(scriptAttributes)}></script>`);
182
+ }
183
+
184
+ return tags.join('\n');
185
+ }
186
+
187
+ function normalizeFormMode(value, fallback = 'view') {
188
+ const normalized = String(value || fallback || 'view').toLowerCase();
189
+ return ['view', 'json'].includes(normalized) ? normalized : (fallback || 'view');
190
+ }
191
+
192
+ function normalizeFormSwap(value, fallback = 'replace') {
193
+ const normalized = String(value || fallback || 'replace').toLowerCase();
194
+ return ['replace', 'append', 'prepend'].includes(normalized)
195
+ ? normalized
196
+ : (fallback || 'replace');
197
+ }
198
+
199
+ function normalizeFormOptions(value, fallback = null) {
200
+ const base = fallback
201
+ ? { ...fallback }
202
+ : {
203
+ enabled: true,
204
+ selector: 'form[data-asjs-form], form[data-webas-submit]',
205
+ mode: 'view',
206
+ history: false,
207
+ resetOnSuccess: false,
208
+ target: '',
209
+ swap: 'replace'
210
+ };
211
+
212
+ if (value === undefined) {
213
+ return base;
214
+ }
215
+
216
+ if (value === false || value === null) {
217
+ return {
218
+ ...base,
219
+ enabled: false
220
+ };
221
+ }
222
+
223
+ if (value === true) {
224
+ return {
225
+ ...base,
226
+ enabled: true
227
+ };
228
+ }
229
+
230
+ if (typeof value === 'string') {
231
+ return {
232
+ ...base,
233
+ enabled: true,
234
+ selector: value
235
+ };
236
+ }
237
+
238
+ if (value && typeof value === 'object') {
239
+ return {
240
+ ...base,
241
+ enabled: normalizeBooleanOption(value.enabled, base.enabled),
242
+ selector: value.selector ? String(value.selector) : base.selector,
243
+ mode: normalizeFormMode(value.mode, base.mode),
244
+ history: normalizeBooleanOption(value.history, base.history),
245
+ resetOnSuccess: normalizeBooleanOption(value.resetOnSuccess, base.resetOnSuccess),
246
+ target: value.target ? String(value.target) : base.target,
247
+ swap: normalizeFormSwap(value.swap, base.swap)
248
+ };
249
+ }
250
+
251
+ return base;
252
+ }
253
+
254
+ function renderFormAttrs(asjs, options = {}) {
255
+ const forms = asjs && asjs.forms ? asjs.forms : normalizeFormOptions(true);
256
+ const settings = options && typeof options === 'object' ? options : {};
257
+ const attributes = {
258
+ method: settings.method || 'post',
259
+ action: settings.action || undefined,
260
+ enctype: settings.enctype || undefined,
261
+ novalidate: settings.noValidate ? true : undefined,
262
+ 'data-asjs-form': settings.enabled === false ? undefined : 'true',
263
+ 'data-asjs-form-mode': settings.mode ? normalizeFormMode(settings.mode, forms.mode) : undefined,
264
+ 'data-asjs-form-history': Object.prototype.hasOwnProperty.call(settings, 'history')
265
+ ? (settings.history ? 'true' : 'false')
266
+ : undefined,
267
+ 'data-asjs-form-reset-on-success': Object.prototype.hasOwnProperty.call(settings, 'resetOnSuccess')
268
+ ? (settings.resetOnSuccess ? 'true' : 'false')
269
+ : undefined,
270
+ 'data-asjs-form-target': settings.target || undefined,
271
+ 'data-asjs-form-swap': settings.swap ? normalizeFormSwap(settings.swap, forms.swap) : undefined,
272
+ 'data-asjs-transition': settings.transition || undefined,
273
+ ...((settings.attrs && typeof settings.attrs === 'object') ? settings.attrs : {})
274
+ };
275
+
276
+ return serializeHtmlAttributes(attributes);
277
+ }
278
+
279
+ function normalizePluginList(value) {
280
+ if (Array.isArray(value)) {
281
+ return value.filter(Boolean);
282
+ }
283
+
284
+ return value ? [value] : [];
285
+ }
286
+
287
+ function registerHook(registry, name, handler) {
288
+ if (!(registry instanceof Map) || typeof name !== 'string' || typeof handler !== 'function') {
289
+ return;
290
+ }
291
+
292
+ const key = name.trim();
293
+ if (!key) {
294
+ return;
295
+ }
296
+
297
+ if (!registry.has(key)) {
298
+ registry.set(key, []);
299
+ }
300
+
301
+ registry.get(key).push(handler);
302
+ }
303
+
304
+ function createHookRegistry(inputHooks = {}) {
305
+ const registry = new Map();
306
+
307
+ if (!inputHooks || typeof inputHooks !== 'object') {
308
+ return registry;
309
+ }
310
+
311
+ Object.entries(inputHooks).forEach(([name, handlers]) => {
312
+ const normalizedHandlers = Array.isArray(handlers) ? handlers : [handlers];
313
+
314
+ normalizedHandlers.forEach((handler) => {
315
+ registerHook(registry, name, handler);
316
+ });
317
+ });
318
+
319
+ return registry;
320
+ }
321
+
322
+ function runSyncHooks(registry, name, context) {
323
+ const handlers = registry instanceof Map ? (registry.get(name) || []) : [];
324
+
325
+ return handlers.reduce((currentContext, handler) => {
326
+ const result = handler(currentContext);
327
+
328
+ if (result && typeof result === 'object') {
329
+ return {
330
+ ...currentContext,
331
+ ...result
332
+ };
333
+ }
334
+
335
+ return currentContext;
336
+ }, context);
337
+ }
338
+
339
+ function runHookChain(registry, name, context) {
340
+ const handlers = registry instanceof Map ? (registry.get(name) || []) : [];
341
+
342
+ return handlers.reduce((promise, handler) => {
343
+ return promise.then((currentContext) => {
344
+ return Promise.resolve(handler(currentContext)).then((result) => {
345
+ if (result && typeof result === 'object') {
346
+ return {
347
+ ...currentContext,
348
+ ...result
349
+ };
350
+ }
351
+
352
+ return currentContext;
353
+ });
354
+ });
355
+ }, Promise.resolve(context));
356
+ }
357
+
358
+ function applyAsjsPlugin(plugin, api) {
359
+ if (!plugin) {
360
+ return;
361
+ }
362
+
363
+ if (typeof plugin === 'function') {
364
+ plugin(api);
365
+ return;
366
+ }
367
+
368
+ const candidate = ['setup', 'install', 'register']
369
+ .map((key) => plugin[key])
370
+ .find((handler) => typeof handler === 'function');
371
+
372
+ if (candidate) {
373
+ candidate.call(plugin, api);
374
+ }
375
+ }
376
+
377
+ function renderBodyAttrs(asjs, extraAttributes = {}) {
378
+ return serializeHtmlAttributes({
379
+ 'data-asjs-transition': asjs.transition.enabled ? asjs.transition.name : 'none',
380
+ 'data-asjs-transition-duration': asjs.transition.duration,
381
+ 'data-asjs-prefetch': asjs.prefetch ? 'true' : 'false',
382
+ 'data-asjs-prefetch-ttl': asjs.prefetchTtl,
383
+ 'data-asjs-loading-bar': asjs.loadingBar ? 'true' : 'false',
384
+ 'data-asjs-forms': asjs.forms && asjs.forms.enabled ? 'true' : 'false',
385
+ 'data-asjs-form-selector': asjs.forms ? asjs.forms.selector : undefined,
386
+ 'data-asjs-form-mode': asjs.forms ? asjs.forms.mode : undefined,
387
+ 'data-asjs-form-history': asjs.forms && asjs.forms.history ? 'true' : 'false',
388
+ 'data-asjs-form-reset-on-success': asjs.forms && asjs.forms.resetOnSuccess ? 'true' : 'false',
389
+ 'data-asjs-form-target': asjs.forms && asjs.forms.target ? asjs.forms.target : undefined,
390
+ 'data-asjs-form-swap': asjs.forms ? asjs.forms.swap : undefined,
391
+ ...extraAttributes
392
+ });
393
+ }
394
+
395
+ function renderViewAttrs(asjs, extraAttributes = {}) {
396
+ return serializeHtmlAttributes({
397
+ 'data-asjs-view': true,
398
+ 'data-asjs-transition': asjs.transition.enabled ? asjs.transition.name : 'none',
399
+ 'data-asjs-transition-duration': asjs.transition.duration,
400
+ ...extraAttributes
401
+ });
402
+ }
403
+
404
+ function renderProgressMarkup() {
405
+ return '<div class="asjs-progress" data-asjs-progress aria-hidden="true"><span class="asjs-progress-bar"></span></div>';
406
+ }
407
+
408
+ function createAsjsViewModel(inputAsjs, config, overrides = {}) {
409
+ const current = inputAsjs && typeof inputAsjs === 'object' ? inputAsjs : {};
410
+ const assets = config.clientAssets || createClientAssets(config);
411
+ const transition = Object.prototype.hasOwnProperty.call(overrides, 'transition')
412
+ ? normalizeTransitionOptions(overrides.transition, config.transitions)
413
+ : normalizeTransitionOptions(current.transition, config.transitions);
414
+
415
+ const viewModel = {
416
+ ...current,
417
+ debug: config.debug,
418
+ transition,
419
+ prefetch: Object.prototype.hasOwnProperty.call(current, 'prefetch')
420
+ ? Boolean(current.prefetch)
421
+ : config.prefetch,
422
+ prefetchTtl: Object.prototype.hasOwnProperty.call(current, 'prefetchTtl')
423
+ ? normalizeNumberOption(current.prefetchTtl, config.prefetchTtl)
424
+ : config.prefetchTtl,
425
+ loadingBar: Object.prototype.hasOwnProperty.call(current, 'loadingBar')
426
+ ? Boolean(current.loadingBar)
427
+ : config.loadingBar,
428
+ forms: Object.prototype.hasOwnProperty.call(current, 'forms')
429
+ ? normalizeFormOptions(current.forms, config.forms)
430
+ : normalizeFormOptions(undefined, config.forms),
431
+ cache: Object.prototype.hasOwnProperty.call(current, 'cache')
432
+ ? Boolean(current.cache)
433
+ : config.cache,
434
+ assets,
435
+ assetVersions: assets.versions
436
+ };
437
+
438
+ viewModel.clientTags = (options) => renderClientTags(assets, options);
439
+ viewModel.bodyAttrs = (extraAttributes) => renderBodyAttrs(viewModel, extraAttributes);
440
+ viewModel.viewAttrs = (extraAttributes) => renderViewAttrs(viewModel, extraAttributes);
441
+ viewModel.formAttrs = (options) => renderFormAttrs(viewModel, options);
442
+ viewModel.progressMarkup = () => renderProgressMarkup();
443
+
444
+ return viewModel;
445
+ }
446
+
447
+ function normalizeComponentName(name, extension = 'asjs') {
448
+ return ensureExtension(String(name).replace(/\\/g, '/'), extension);
449
+ }
450
+
451
+ function normalizeComponentSpec(spec) {
452
+ if (!spec || typeof spec !== 'object' || Array.isArray(spec)) {
453
+ return {
454
+ props: spec && typeof spec === 'object' ? spec : {},
455
+ strict: false
456
+ };
457
+ }
458
+
459
+ if (Object.prototype.hasOwnProperty.call(spec, 'props') || Object.prototype.hasOwnProperty.call(spec, 'schema')) {
460
+ return {
461
+ props: spec.props || spec.schema || {},
462
+ strict: Boolean(spec.strict)
463
+ };
464
+ }
465
+
466
+ return {
467
+ props: spec,
468
+ strict: false
469
+ };
470
+ }
471
+
472
+ function normalizeComponentRegistry(components = {}, extension = 'asjs') {
473
+ const registry = {};
474
+
475
+ Object.entries(components).forEach(([name, spec]) => {
476
+ registry[normalizeComponentName(name, extension)] = normalizeComponentSpec(spec);
477
+ });
478
+
479
+ return registry;
480
+ }
481
+
482
+ function normalizeTypeList(type) {
483
+ if (type === undefined || type === null || type === 'any') {
484
+ return ['any'];
485
+ }
486
+
487
+ return Array.isArray(type) ? type : [type];
488
+ }
489
+
490
+ function getTypeLabel(type) {
491
+ if (typeof type === 'string') {
492
+ return type;
493
+ }
494
+
495
+ if (type === String) {
496
+ return 'string';
497
+ }
498
+
499
+ if (type === Number) {
500
+ return 'number';
501
+ }
502
+
503
+ if (type === Boolean) {
504
+ return 'boolean';
505
+ }
506
+
507
+ if (type === Array) {
508
+ return 'array';
509
+ }
510
+
511
+ if (type === Object) {
512
+ return 'object';
513
+ }
514
+
515
+ if (type === Date) {
516
+ return 'date';
517
+ }
518
+
519
+ return type && type.name ? type.name : 'custom';
520
+ }
521
+
522
+ function getValueType(value) {
523
+ if (Array.isArray(value)) {
524
+ return 'array';
525
+ }
526
+
527
+ if (value instanceof Date) {
528
+ return 'date';
529
+ }
530
+
531
+ if (value === null) {
532
+ return 'null';
533
+ }
534
+
535
+ return typeof value;
536
+ }
537
+
538
+ function matchesExpectedType(value, expected) {
539
+ if (expected === 'any') {
540
+ return true;
541
+ }
542
+
543
+ if (typeof expected === 'string') {
544
+ if (expected === 'array') {
545
+ return Array.isArray(value);
546
+ }
547
+
548
+ if (expected === 'date') {
549
+ return value instanceof Date;
550
+ }
551
+
552
+ if (expected === 'null') {
553
+ return value === null;
554
+ }
555
+
556
+ if (expected === 'object') {
557
+ return typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date);
558
+ }
559
+
560
+ return typeof value === expected;
561
+ }
562
+
563
+ if (expected === String) {
564
+ return typeof value === 'string';
565
+ }
566
+
567
+ if (expected === Number) {
568
+ return typeof value === 'number' && !Number.isNaN(value);
569
+ }
570
+
571
+ if (expected === Boolean) {
572
+ return typeof value === 'boolean';
573
+ }
574
+
575
+ if (expected === Array) {
576
+ return Array.isArray(value);
577
+ }
578
+
579
+ if (expected === Object) {
580
+ return typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date);
581
+ }
582
+
583
+ if (expected === Date) {
584
+ return value instanceof Date;
585
+ }
586
+
587
+ return typeof expected === 'function' ? value instanceof expected : false;
588
+ }
589
+
590
+ function normalizePropRule(rule) {
591
+ if (typeof rule === 'string' || Array.isArray(rule) || typeof rule === 'function') {
592
+ return {
593
+ required: true,
594
+ hasDefault: false,
595
+ defaultValue: undefined,
596
+ types: normalizeTypeList(rule),
597
+ validate: null,
598
+ message: null
599
+ };
600
+ }
601
+
602
+ if (rule && typeof rule === 'object') {
603
+ return {
604
+ required: rule.required !== false && !Object.prototype.hasOwnProperty.call(rule, 'default'),
605
+ hasDefault: Object.prototype.hasOwnProperty.call(rule, 'default'),
606
+ defaultValue: rule.default,
607
+ types: normalizeTypeList(rule.type || 'any'),
608
+ validate: typeof rule.validate === 'function' ? rule.validate : null,
609
+ message: rule.message || null
610
+ };
611
+ }
612
+
613
+ return {
614
+ required: false,
615
+ hasDefault: false,
616
+ defaultValue: undefined,
617
+ types: ['any'],
618
+ validate: null,
619
+ message: null
620
+ };
621
+ }
622
+
623
+ function resolveDefaultValue(rule, props) {
624
+ if (!rule.hasDefault) {
625
+ return undefined;
626
+ }
627
+
628
+ return typeof rule.defaultValue === 'function' ? rule.defaultValue(props) : rule.defaultValue;
629
+ }
630
+
631
+ function validateComponentProps(componentName, inputProps, schema = {}, options = {}) {
632
+ const props = inputProps && typeof inputProps === 'object' ? { ...inputProps } : {};
633
+ const validatedProps = { ...props };
634
+ const strict = Boolean(options.strict);
635
+
636
+ Object.entries(schema).forEach(([propName, rawRule]) => {
637
+ const rule = normalizePropRule(rawRule);
638
+ const hasValue = Object.prototype.hasOwnProperty.call(props, propName) && props[propName] !== undefined;
639
+
640
+ if (!hasValue) {
641
+ if (rule.hasDefault) {
642
+ validatedProps[propName] = resolveDefaultValue(rule, validatedProps);
643
+ return;
644
+ }
645
+
646
+ if (rule.required) {
647
+ throw new TypeError(`ASJS component props hatasi (${componentName}): "${propName}" gerekli.`);
648
+ }
649
+
650
+ return;
651
+ }
652
+
653
+ const value = props[propName];
654
+ const validType = rule.types.includes('any') || rule.types.some((expected) => matchesExpectedType(value, expected));
655
+
656
+ if (!validType) {
657
+ throw new TypeError(
658
+ `ASJS component props hatasi (${componentName}): "${propName}" beklenen tip ${rule.types.map(getTypeLabel).join(' | ')}, gelen ${getValueType(value)}.`
659
+ );
660
+ }
661
+
662
+ if (rule.validate) {
663
+ const validationResult = rule.validate(value, validatedProps);
664
+
665
+ if (validationResult !== true && validationResult !== undefined) {
666
+ throw new TypeError(
667
+ typeof validationResult === 'string'
668
+ ? validationResult
669
+ : (rule.message || `ASJS component props hatasi (${componentName}): "${propName}" dogrulamasi basarisiz.`)
670
+ );
671
+ }
672
+ }
673
+ });
674
+
675
+ if (strict) {
676
+ Object.keys(props).forEach((propName) => {
677
+ if (!Object.prototype.hasOwnProperty.call(schema, propName)) {
678
+ throw new TypeError(`ASJS component props hatasi (${componentName}): tanimsiz prop "${propName}".`);
679
+ }
680
+ });
681
+ }
682
+
683
+ return validatedProps;
684
+ }
685
+
686
+ function getCompiledTemplate(filePath, runtime) {
687
+ if (!runtime.cacheEnabled) {
688
+ return compileTemplate(fs.readFileSync(filePath, 'utf8'), filePath);
689
+ }
690
+
691
+ const stats = fs.statSync(filePath);
692
+ const cached = runtime.templateCache.get(filePath);
693
+
694
+ if (cached && cached.mtimeMs === stats.mtimeMs) {
695
+ return cached.template;
696
+ }
697
+
698
+ const template = compileTemplate(fs.readFileSync(filePath, 'utf8'), filePath);
699
+
700
+ runtime.templateCache.set(filePath, {
701
+ mtimeMs: stats.mtimeMs,
702
+ template
703
+ });
704
+
705
+ return template;
706
+ }
707
+
708
+ function resolveComponentSpec(templateName, runtime, explicitSchema) {
709
+ if (explicitSchema) {
710
+ return normalizeComponentSpec(explicitSchema);
711
+ }
712
+
713
+ return runtime.components[normalizeComponentName(templateName, runtime.extension)] || null;
714
+ }
715
+
716
+ function normalizeTransitionOptions(value, fallback = null) {
717
+ if (value === undefined) {
718
+ return fallback
719
+ ? { ...fallback }
720
+ : { enabled: false, name: 'none', duration: 0 };
721
+ }
722
+
723
+ if (value === false || value === null || value === 'none') {
724
+ return { enabled: false, name: 'none', duration: 0 };
725
+ }
726
+
727
+ if (value === true) {
728
+ return { enabled: true, name: 'fade', duration: 260 };
729
+ }
730
+
731
+ if (typeof value === 'string') {
732
+ return { enabled: true, name: value, duration: 260 };
733
+ }
734
+
735
+ if (typeof value === 'object') {
736
+ const enabled = value.enabled !== false;
737
+ const duration = Number.isFinite(Number(value.duration)) && Number(value.duration) > 0
738
+ ? Number(value.duration)
739
+ : 260;
740
+
741
+ return {
742
+ enabled,
743
+ name: enabled ? String(value.name || 'fade') : 'none',
744
+ duration: enabled ? duration : 0
745
+ };
746
+ }
747
+
748
+ return fallback
749
+ ? { ...fallback }
750
+ : { enabled: false, name: 'none', duration: 0 };
751
+ }
752
+
753
+ function resolveTemplatePath(name, currentFile, viewsDir, extension) {
754
+ const normalizedName = ensureExtension(name, extension);
755
+
756
+ if (path.isAbsolute(normalizedName)) {
757
+ return normalizedName;
758
+ }
759
+
760
+ if (normalizedName.startsWith('./') || normalizedName.startsWith('../')) {
761
+ return path.resolve(path.dirname(currentFile), normalizedName);
762
+ }
763
+
764
+ return path.resolve(viewsDir, normalizedName);
765
+ }
766
+
767
+ function compileTemplate(source, filename) {
768
+ const matcher = /<%[-=]?[\s\S]+?%>/g;
769
+ let cursor = 0;
770
+ let match;
771
+
772
+ const chunks = [
773
+ 'let __output = "";',
774
+ 'const __append = (value) => { if (value !== undefined && value !== null) { __output += String(value); } };'
775
+ ];
776
+
777
+ while ((match = matcher.exec(source)) !== null) {
778
+ const text = source.slice(cursor, match.index);
779
+ if (text) {
780
+ chunks.push(`__output += ${JSON.stringify(text)};`);
781
+ }
782
+
783
+ const tag = match[0];
784
+ const body = tag.slice(tag.startsWith('<%=') || tag.startsWith('<%-') ? 3 : 2, -2).trim();
785
+
786
+ if (tag.startsWith('<%=')) {
787
+ chunks.push(`__output += __escape(${body});`);
788
+ } else if (tag.startsWith('<%-')) {
789
+ chunks.push(`__append(${body});`);
790
+ } else {
791
+ chunks.push(body);
792
+ }
793
+
794
+ cursor = match.index + tag.length;
795
+ }
796
+
797
+ const tail = source.slice(cursor);
798
+ if (tail) {
799
+ chunks.push(`__output += ${JSON.stringify(tail)};`);
800
+ }
801
+
802
+ chunks.push('return __output;');
803
+
804
+ try {
805
+ return new Function(
806
+ '__locals',
807
+ '__helpers',
808
+ '__escape',
809
+ `with (__helpers) { with (__locals) { ${chunks.join('\n')} } }`
810
+ );
811
+ } catch (error) {
812
+ error.message = `ASJS derleme hatasi (${filename}): ${error.message}`;
813
+ throw error;
814
+ }
815
+ }
816
+
817
+ function renderTemplateFile(filePath, locals, runtime) {
818
+ const template = getCompiledTemplate(filePath, runtime);
819
+ const renderState = runtime.state || { layout: null };
820
+
821
+ const helpers = {
822
+ escape: escapeHtml,
823
+ raw(value) {
824
+ return value == null ? '' : String(value);
825
+ },
826
+ print(...values) {
827
+ return values.filter((value) => value != null).join('');
828
+ },
829
+ layout(name) {
830
+ renderState.layout = name;
831
+ return '';
832
+ },
833
+ include(templateName, extraLocals = {}) {
834
+ const includePath = resolveTemplatePath(
835
+ templateName,
836
+ filePath,
837
+ runtime.viewsDir,
838
+ runtime.extension
839
+ );
840
+
841
+ return renderTemplateFile(
842
+ includePath,
843
+ { ...locals, ...extraLocals },
844
+ {
845
+ ...runtime,
846
+ state: { layout: null }
847
+ }
848
+ ).html;
849
+ },
850
+ props(schema, values, componentName = path.basename(filePath)) {
851
+ const spec = normalizeComponentSpec(schema);
852
+ return validateComponentProps(componentName, values, spec.props, { strict: spec.strict });
853
+ },
854
+ defineProps(schema, values, componentName = path.basename(filePath)) {
855
+ const spec = normalizeComponentSpec(schema);
856
+ return validateComponentProps(componentName, values, spec.props, { strict: spec.strict });
857
+ },
858
+ component(templateName, componentProps = {}, schema) {
859
+ const includePath = resolveTemplatePath(
860
+ templateName,
861
+ filePath,
862
+ runtime.viewsDir,
863
+ runtime.extension
864
+ );
865
+ const spec = resolveComponentSpec(templateName, runtime, schema);
866
+ const validatedProps = spec
867
+ ? validateComponentProps(templateName, componentProps, spec.props, { strict: spec.strict })
868
+ : componentProps;
869
+
870
+ return renderTemplateFile(
871
+ includePath,
872
+ { ...locals, ...validatedProps },
873
+ {
874
+ ...runtime,
875
+ state: { layout: null }
876
+ }
877
+ ).html;
878
+ }
879
+ };
880
+
881
+ try {
882
+ return {
883
+ html: template(locals, helpers, escapeHtml),
884
+ state: renderState
885
+ };
886
+ } catch (error) {
887
+ error.message = `ASJS render hatasi (${filePath}): ${error.message}`;
888
+ throw error;
889
+ }
890
+ }
891
+
892
+ function createAsjsEngine(options = {}) {
893
+ const extension = String(options.extension || 'asjs').replace(/^\./, '');
894
+
895
+ return function asjsEngine(filePath, data, callback) {
896
+ try {
897
+ const viewsDir = options.viewsDir || data.settings?.views || path.dirname(filePath);
898
+ const initialLayout = Object.prototype.hasOwnProperty.call(data, 'layout')
899
+ ? data.layout
900
+ : options.defaultLayout || null;
901
+
902
+ const locals = {
903
+ ...data
904
+ };
905
+
906
+ delete locals.layout;
907
+ locals.asjs = createAsjsViewModel(locals.asjs, options);
908
+
909
+ const pageState = { layout: initialLayout };
910
+ const page = renderTemplateFile(filePath, locals, {
911
+ cacheEnabled: options.cache !== false,
912
+ components: options.components || {},
913
+ extension,
914
+ templateCache: options.templateCache || new Map(),
915
+ viewsDir,
916
+ state: pageState
917
+ });
918
+
919
+ let html = page.html;
920
+
921
+ if (pageState.layout) {
922
+ const layoutPath = resolveTemplatePath(pageState.layout, filePath, viewsDir, extension);
923
+ html = renderTemplateFile(layoutPath, {
924
+ ...locals,
925
+ asjs: createAsjsViewModel(locals.asjs, options),
926
+ body: html
927
+ }, {
928
+ cacheEnabled: options.cache !== false,
929
+ components: options.components || {},
930
+ extension,
931
+ templateCache: options.templateCache || new Map(),
932
+ viewsDir,
933
+ state: { layout: null }
934
+ }).html;
935
+ }
936
+
937
+ callback(null, html);
938
+ } catch (error) {
939
+ callback(error);
940
+ }
941
+ };
942
+ }
943
+
944
+ function createAsjsConfig(options = {}) {
945
+ const extension = String(options.extension || 'asjs').replace(/^\./, '');
946
+ const packagePaths = getAsjsPackagePaths();
947
+ const packageVersion = getPackageVersion(packagePaths);
948
+ const assetMountPath = normalizeAssetMountPath(options.assetMountPath);
949
+ const rootDir = options.rootDir || process.cwd();
950
+
951
+ const config = {
952
+ extension,
953
+ assetMountPath,
954
+ rootDir,
955
+ viewsDir: options.viewsDir || path.join(rootDir, 'views'),
956
+ publicDir: options.publicDir || null,
957
+ defaultLayout: options.defaultLayout || null,
958
+ debug: Boolean(options.debug),
959
+ cache: normalizeBooleanOption(options.cache, !options.debug),
960
+ navItems: Array.isArray(options.navItems) ? options.navItems : [],
961
+ locals: options.locals && typeof options.locals === 'object' ? options.locals : {},
962
+ components: normalizeComponentRegistry(options.components || {}, extension),
963
+ templateCache: new Map(),
964
+ transitions: normalizeTransitionOptions(options.transitions),
965
+ prefetch: normalizeBooleanOption(options.prefetch, true),
966
+ prefetchTtl: normalizeNumberOption(options.prefetchTtl, 30000),
967
+ loadingBar: normalizeBooleanOption(options.loadingBar, true),
968
+ forms: normalizeFormOptions(
969
+ Object.prototype.hasOwnProperty.call(options, 'forms') ? options.forms : true
970
+ ),
971
+ hooks: createHookRegistry(options.hooks),
972
+ plugins: normalizePluginList(options.plugins),
973
+ packagePaths,
974
+ packageVersion,
975
+ assetVersion: Object.prototype.hasOwnProperty.call(options, 'assetVersion')
976
+ ? options.assetVersion
977
+ : packageVersion,
978
+ serveClientAssets: normalizeBooleanOption(options.serveClientAssets, true)
979
+ };
980
+
981
+ config.clientAssets = createClientAssets(config);
982
+
983
+ return config;
984
+ }
985
+
986
+ function getAsjsPackagePaths() {
987
+ const rootDir = path.resolve(__dirname, '..');
988
+ const clientDir = path.join(__dirname, 'client');
989
+
990
+ return {
991
+ rootDir,
992
+ clientDir,
993
+ entryPath: path.join(rootDir, 'index.js'),
994
+ packageJsonPath: path.join(rootDir, 'package.json'),
995
+ routerPath: path.join(clientDir, 'asjs-router.js'),
996
+ stylesheetPath: path.join(clientDir, 'asjs-core.css'),
997
+ themeStylesheetPath: path.join(clientDir, 'asjs-theme.css')
998
+ };
999
+ }
1000
+
1001
+ function renderDebugErrorPage(error, context = {}) {
1002
+ const status = Number(context.status || error.status || 500);
1003
+ const title = context.title || 'ASJS Debug Hatası';
1004
+ const method = escapeHtml(context.method || 'GET');
1005
+ const requestPath = escapeHtml(context.requestPath || '/');
1006
+ const timestamp = escapeHtml(new Date().toLocaleString('tr-TR'));
1007
+ const message = escapeHtml(error.message || 'Bilinmeyen hata');
1008
+ const errorName = escapeHtml(error.name || 'Error');
1009
+ const stackLines = String(error.stack || error.message || 'Stack bilgisi yok')
1010
+ .split('\n')
1011
+ .map((line) => line.trim())
1012
+ .filter(Boolean);
1013
+ const stack = escapeHtml(stackLines.join('\n'));
1014
+ const primaryFrame = escapeHtml(stackLines[1] || 'Çerçeve bilgisi yok');
1015
+ const cause = error.cause ? escapeHtml(String(error.cause.stack || error.cause.message || error.cause)) : '';
1016
+ const templateMatch = String(error.message || '').match(/\(([^)]+\.asjs)\)/);
1017
+ const templatePath = escapeHtml(templateMatch ? templateMatch[1] : (context.templatePath || 'Bulunamadı'));
1018
+ const hintItems = [
1019
+ 'Şablon içinde <% %> bloklarının açılıp kapandığını kontrol et.',
1020
+ 'Layout ve include yollarının views klasörüne göre doğru çözüldüğünden emin ol.',
1021
+ 'component() kullanıyorsan zorunlu props alanlarının tam gönderildiğini doğrula.',
1022
+ 'Özel CSS veya script ekliyorsan bunların ASJS body ve view alanlarını bozmadığını kontrol et.'
1023
+ ];
1024
+ const environmentItems = [
1025
+ { label: 'Hata türü', value: errorName },
1026
+ { label: 'İstek', value: `${method} ${requestPath}` },
1027
+ { label: 'Şablon', value: templatePath },
1028
+ { label: 'İlk çerçeve', value: primaryFrame },
1029
+ { label: 'Zaman', value: timestamp },
1030
+ { label: 'Sürüm', value: escapeHtml(context.packageVersion || 'dev') }
1031
+ ];
1032
+
1033
+ return `<!DOCTYPE html>
1034
+ <html lang="tr">
1035
+ <head>
1036
+ <meta charset="UTF-8">
1037
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1038
+ <title>${title}</title>
1039
+ <style>
1040
+ :root {
1041
+ --bg: #08111f;
1042
+ --panel: rgba(11, 21, 38, 0.88);
1043
+ --panel-strong: rgba(17, 31, 56, 0.96);
1044
+ --text: #eef4fb;
1045
+ --muted: #a7b2c6;
1046
+ --accent: #ff8a65;
1047
+ --accent-2: #63e6be;
1048
+ --accent-3: #5fd2ff;
1049
+ --line: rgba(255, 255, 255, 0.08);
1050
+ --radius-xl: 30px;
1051
+ --radius-lg: 22px;
1052
+ --radius-md: 16px;
1053
+ --shadow: 0 34px 90px rgba(0, 0, 0, 0.38);
1054
+ }
1055
+
1056
+ * { box-sizing: border-box; }
1057
+
1058
+ body {
1059
+ margin: 0;
1060
+ min-height: 100vh;
1061
+ color: var(--text);
1062
+ font-family: "Space Grotesk", "Segoe UI", sans-serif;
1063
+ background:
1064
+ radial-gradient(circle at top left, rgba(255, 138, 101, 0.18), transparent 28%),
1065
+ radial-gradient(circle at top right, rgba(99, 230, 190, 0.16), transparent 30%),
1066
+ linear-gradient(140deg, #040a14, #08111f 52%, #10203b);
1067
+ }
1068
+
1069
+ body::before {
1070
+ content: "";
1071
+ position: fixed;
1072
+ inset: 0;
1073
+ pointer-events: none;
1074
+ background-image:
1075
+ linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
1076
+ linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
1077
+ background-size: 42px 42px;
1078
+ mask-image: radial-gradient(circle at center, black 55%, transparent 92%);
1079
+ }
1080
+
1081
+ .shell {
1082
+ width: min(1220px, calc(100% - 32px));
1083
+ margin: 24px auto 40px;
1084
+ position: relative;
1085
+ z-index: 1;
1086
+ }
1087
+
1088
+ .hero,
1089
+ .panel,
1090
+ .stack-panel,
1091
+ .meta-card {
1092
+ background: var(--panel);
1093
+ border: 1px solid var(--line);
1094
+ border-radius: var(--radius-xl);
1095
+ backdrop-filter: blur(18px);
1096
+ box-shadow: var(--shadow);
1097
+ }
1098
+
1099
+ .hero {
1100
+ padding: 30px;
1101
+ display: grid;
1102
+ gap: 26px;
1103
+ }
1104
+
1105
+ .eyebrow {
1106
+ display: inline-flex;
1107
+ align-items: center;
1108
+ gap: 8px;
1109
+ width: fit-content;
1110
+ padding: 8px 12px;
1111
+ border-radius: 999px;
1112
+ background: rgba(255, 138, 101, 0.12);
1113
+ color: var(--accent);
1114
+ font-size: 0.84rem;
1115
+ text-transform: uppercase;
1116
+ letter-spacing: 0.08em;
1117
+ }
1118
+
1119
+ .hero-top {
1120
+ display: grid;
1121
+ grid-template-columns: 1.2fr 0.8fr;
1122
+ gap: 20px;
1123
+ align-items: start;
1124
+ }
1125
+
1126
+ h1 {
1127
+ margin: 16px 0 10px;
1128
+ font-size: clamp(2.6rem, 7vw, 5.2rem);
1129
+ line-height: 0.96;
1130
+ letter-spacing: -0.05em;
1131
+ }
1132
+
1133
+ p,
1134
+ li {
1135
+ color: var(--muted);
1136
+ line-height: 1.7;
1137
+ }
1138
+
1139
+ .grid {
1140
+ display: grid;
1141
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1142
+ gap: 18px;
1143
+ }
1144
+
1145
+ .panel {
1146
+ padding: 22px;
1147
+ }
1148
+
1149
+ .meta-grid {
1150
+ display: grid;
1151
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1152
+ gap: 16px;
1153
+ }
1154
+
1155
+ .meta-card {
1156
+ padding: 18px;
1157
+ }
1158
+
1159
+ .headline {
1160
+ margin: 0;
1161
+ font-size: 1.3rem;
1162
+ line-height: 1.3;
1163
+ }
1164
+
1165
+ .status-badge {
1166
+ display: inline-flex;
1167
+ align-items: center;
1168
+ gap: 8px;
1169
+ padding: 10px 14px;
1170
+ border-radius: 999px;
1171
+ background: rgba(255, 138, 101, 0.12);
1172
+ color: var(--text);
1173
+ font-weight: 700;
1174
+ }
1175
+
1176
+ .status-dot {
1177
+ width: 10px;
1178
+ height: 10px;
1179
+ border-radius: 50%;
1180
+ background: linear-gradient(135deg, var(--accent), #ffb38a);
1181
+ box-shadow: 0 0 18px rgba(255, 138, 101, 0.48);
1182
+ }
1183
+
1184
+ .label {
1185
+ display: block;
1186
+ color: var(--muted);
1187
+ font-size: 0.9rem;
1188
+ margin-bottom: 8px;
1189
+ }
1190
+
1191
+ .value {
1192
+ display: block;
1193
+ color: var(--text);
1194
+ font-weight: 700;
1195
+ word-break: break-word;
1196
+ }
1197
+
1198
+ .section-title {
1199
+ margin: 0 0 12px;
1200
+ font-size: 1.08rem;
1201
+ }
1202
+
1203
+ .stack-grid {
1204
+ display: grid;
1205
+ grid-template-columns: 1.1fr 0.9fr;
1206
+ gap: 18px;
1207
+ }
1208
+
1209
+ .stack-panel {
1210
+ padding: 22px;
1211
+ }
1212
+
1213
+ .stack {
1214
+ margin-top: 12px;
1215
+ padding: 20px;
1216
+ border-radius: var(--radius-lg);
1217
+ background: rgba(5, 10, 19, 0.82);
1218
+ border: 1px solid rgba(255, 255, 255, 0.07);
1219
+ overflow-x: auto;
1220
+ }
1221
+
1222
+ pre {
1223
+ margin: 0;
1224
+ white-space: pre-wrap;
1225
+ word-break: break-word;
1226
+ font-family: "Cascadia Code", "Consolas", monospace;
1227
+ color: #f6f8fb;
1228
+ line-height: 1.65;
1229
+ }
1230
+
1231
+ .summary-list,
1232
+ .tips {
1233
+ margin: 0;
1234
+ padding-left: 18px;
1235
+ display: grid;
1236
+ gap: 10px;
1237
+ }
1238
+
1239
+ .tips strong {
1240
+ color: var(--accent-2);
1241
+ }
1242
+
1243
+ .summary-list strong {
1244
+ color: var(--accent-3);
1245
+ }
1246
+
1247
+ .callout {
1248
+ margin-top: 10px;
1249
+ padding: 16px 18px;
1250
+ border-radius: var(--radius-md);
1251
+ background: linear-gradient(180deg, rgba(17, 31, 56, 0.9), rgba(10, 20, 36, 0.84));
1252
+ border: 1px solid rgba(255, 255, 255, 0.08);
1253
+ }
1254
+
1255
+ code {
1256
+ font-family: "Cascadia Code", "Consolas", monospace;
1257
+ font-size: 0.95em;
1258
+ }
1259
+
1260
+ .subtle {
1261
+ color: var(--muted);
1262
+ }
1263
+
1264
+ details {
1265
+ margin-top: 16px;
1266
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
1267
+ padding-top: 14px;
1268
+ }
1269
+
1270
+ summary {
1271
+ cursor: pointer;
1272
+ color: var(--text);
1273
+ font-weight: 700;
1274
+ }
1275
+
1276
+ @media (max-width: 860px) {
1277
+ .hero-top,
1278
+ .grid {
1279
+ grid-template-columns: 1fr;
1280
+ }
1281
+
1282
+ .meta-grid,
1283
+ .stack-grid {
1284
+ grid-template-columns: 1fr;
1285
+ }
1286
+
1287
+ body {
1288
+ padding: 0;
1289
+ }
1290
+
1291
+ .shell {
1292
+ width: min(100%, calc(100% - 24px));
1293
+ margin: 12px auto 24px;
1294
+ }
1295
+
1296
+ .hero {
1297
+ padding: 22px;
1298
+ }
1299
+ }
1300
+ </style>
1301
+ </head>
1302
+ <body>
1303
+ <div class="shell">
1304
+ <section class="hero">
1305
+ <div class="hero-top">
1306
+ <div>
1307
+ <span class="eyebrow">Debug modu açık</span>
1308
+ <h1>ASJS bir hata yakaladı.</h1>
1309
+ <p>Bu sayfa yalnızca debug modu etkinken gösterilir. Amaç, hatayı kısa özetle görünür kılmak ve asıl teknik ayrıntıları aynı ekranda profesyonel şekilde sunmaktır.</p>
1310
+ <div class="callout">
1311
+ <div class="status-badge"><span class="status-dot"></span> HTTP ${status} · ${errorName}</div>
1312
+ <p class="subtle">${message}</p>
1313
+ </div>
1314
+ </div>
1315
+
1316
+ <article class="panel">
1317
+ <h2 class="headline">Hızlı özet</h2>
1318
+ <ul class="summary-list">
1319
+ <li><strong>İstek:</strong> ${method} ${requestPath}</li>
1320
+ <li><strong>Şablon:</strong> <code>${templatePath}</code></li>
1321
+ <li><strong>İlk çerçeve:</strong> <code>${primaryFrame}</code></li>
1322
+ <li><strong>Zaman:</strong> ${timestamp}</li>
1323
+ </ul>
1324
+ </article>
1325
+ </div>
1326
+
1327
+ <div class="meta-grid">
1328
+ ${environmentItems.map((item) => `
1329
+ <article class="meta-card">
1330
+ <span class="label">${item.label}</span>
1331
+ <span class="value">${item.value}</span>
1332
+ </article>`).join('')}
1333
+ </div>
1334
+
1335
+ <div class="stack-grid">
1336
+ <article class="stack-panel">
1337
+ <h2 class="section-title">Stack trace</h2>
1338
+ <div class="stack">
1339
+ <pre>${stack}</pre>
1340
+ </div>
1341
+ ${cause ? `
1342
+ <details>
1343
+ <summary>Asıl sebep</summary>
1344
+ <div class="stack"><pre>${cause}</pre></div>
1345
+ </details>` : ''}
1346
+ </article>
1347
+
1348
+ <article class="stack-panel">
1349
+ <h2 class="section-title">Muhtemel kontrol listesi</h2>
1350
+ <ul class="tips">
1351
+ ${hintItems.map((item) => `<li><strong>Kontrol:</strong> ${escapeHtml(item)}</li>`).join('')}
1352
+ </ul>
1353
+ <details>
1354
+ <summary>Teknik not</summary>
1355
+ <p class="subtle">Bu görünüm, route zinciri kırıldığında ham Express hata çıktısı yerine daha okunur bir teknik özet verir. Üretim ortamında debug kapalıysa bu ekran gösterilmez.</p>
1356
+ </details>
1357
+ </article>
1358
+ </div>
1359
+ </section>
1360
+ </div>
1361
+ </body>
1362
+ </html>`;
1363
+ }
1364
+
1365
+ function buildPageLocals(req, config, pageData = {}) {
1366
+ const input = pageData && typeof pageData === 'object' ? pageData : {};
1367
+ const status = Number.isInteger(input.status) ? input.status : null;
1368
+ const transition = Object.prototype.hasOwnProperty.call(input, 'transition')
1369
+ ? normalizeTransitionOptions(input.transition, config.transitions)
1370
+ : { ...config.transitions };
1371
+ const pageAsjs = input.asjs && typeof input.asjs === 'object' ? input.asjs : {};
1372
+ const locals = {
1373
+ ...config.locals,
1374
+ ...input
1375
+ };
1376
+
1377
+ delete locals.status;
1378
+ delete locals.transition;
1379
+ delete locals.asjs;
1380
+
1381
+ locals.navItems = Array.isArray(input.navItems) ? input.navItems : config.navItems;
1382
+ locals.currentPath = input.currentPath || req.path;
1383
+ locals.asjs = createAsjsViewModel(pageAsjs, config, { transition });
1384
+
1385
+ return {
1386
+ locals,
1387
+ status
1388
+ };
1389
+ }
1390
+
1391
+ function setupAsjs(app, options = {}) {
1392
+ const config = createAsjsConfig(options);
1393
+
1394
+ function extendLocals(extraLocals = {}) {
1395
+ if (extraLocals && typeof extraLocals === 'object') {
1396
+ config.locals = {
1397
+ ...config.locals,
1398
+ ...extraLocals
1399
+ };
1400
+ }
1401
+
1402
+ return api;
1403
+ }
1404
+
1405
+ function defineComponents(components = {}) {
1406
+ config.components = {
1407
+ ...config.components,
1408
+ ...normalizeComponentRegistry(components, config.extension)
1409
+ };
1410
+
1411
+ return api;
1412
+ }
1413
+
1414
+ function addHook(name, handler) {
1415
+ registerHook(config.hooks, name, handler);
1416
+ return api;
1417
+ }
1418
+
1419
+ function clearCache() {
1420
+ config.templateCache.clear();
1421
+ }
1422
+
1423
+ function render(res, viewName, pageData = {}) {
1424
+ const renderContext = runSyncHooks(config.hooks, 'beforeRender', {
1425
+ app,
1426
+ config,
1427
+ req: res.req,
1428
+ res,
1429
+ viewName,
1430
+ pageData
1431
+ });
1432
+ const { locals, status } = buildPageLocals(res.req, config, renderContext.pageData);
1433
+
1434
+ if (status) {
1435
+ res.status(status);
1436
+ }
1437
+
1438
+ const output = res.render(viewName, locals);
1439
+
1440
+ runSyncHooks(config.hooks, 'afterRender', {
1441
+ ...renderContext,
1442
+ locals,
1443
+ status
1444
+ });
1445
+
1446
+ return output;
1447
+ }
1448
+
1449
+ function resolvePageData(req, res, next, viewName, pageData) {
1450
+ return runHookChain(config.hooks, 'beforePage', {
1451
+ app,
1452
+ config,
1453
+ req,
1454
+ res,
1455
+ next,
1456
+ viewName,
1457
+ pageData: undefined
1458
+ }).then((beforePageContext) => {
1459
+ return Promise.resolve(
1460
+ typeof pageData === 'function'
1461
+ ? pageData(beforePageContext.req, beforePageContext.res, beforePageContext.next)
1462
+ : pageData
1463
+ ).then((resolvedPageData) => {
1464
+ return runHookChain(config.hooks, 'afterPage', {
1465
+ ...beforePageContext,
1466
+ pageData: resolvedPageData
1467
+ });
1468
+ });
1469
+ });
1470
+ }
1471
+
1472
+ function page(viewName, pageData) {
1473
+ return function asjsPageHandler(req, res, next) {
1474
+ resolvePageData(req, res, next, viewName, pageData)
1475
+ .then((context) => {
1476
+ render(res, viewName, context.pageData || {});
1477
+ })
1478
+ .catch(next);
1479
+ };
1480
+ }
1481
+
1482
+ function notFound(viewName, pageData) {
1483
+ return function asjsNotFoundHandler(req, res, next) {
1484
+ if (res.headersSent) {
1485
+ next();
1486
+ return;
1487
+ }
1488
+
1489
+ resolvePageData(req, res, next, viewName, pageData)
1490
+ .then((context) => {
1491
+ render(res, viewName, {
1492
+ ...(context.pageData || {}),
1493
+ status: 404
1494
+ });
1495
+ })
1496
+ .catch(next);
1497
+ };
1498
+ }
1499
+
1500
+ function errors() {
1501
+ return function asjsErrorHandler(error, req, res, next) {
1502
+ if (res.headersSent) {
1503
+ next(error);
1504
+ return;
1505
+ }
1506
+
1507
+ const status = Number(error.status || 500);
1508
+
1509
+ if (config.debug) {
1510
+ res.status(status).send(renderDebugErrorPage(error, {
1511
+ packageVersion: config.packageVersion,
1512
+ status,
1513
+ requestPath: req.originalUrl || req.url,
1514
+ method: req.method
1515
+ }));
1516
+ return;
1517
+ }
1518
+
1519
+ res.status(status).send(status === 404 ? 'Sayfa bulunamadi' : 'Sunucu hatasi');
1520
+ };
1521
+ }
1522
+
1523
+ const api = {
1524
+ app,
1525
+ config,
1526
+ clearCache,
1527
+ express,
1528
+ render,
1529
+ page,
1530
+ route: page,
1531
+ notFound,
1532
+ errors,
1533
+ renderDebugErrorPage,
1534
+ packagePaths: config.packagePaths,
1535
+ extendLocals,
1536
+ defineComponents,
1537
+ addHook,
1538
+ use(plugin) {
1539
+ applyAsjsPlugin(plugin, api);
1540
+ return api;
1541
+ }
1542
+ };
1543
+
1544
+ config.plugins.forEach((plugin) => {
1545
+ api.use(plugin);
1546
+ });
1547
+
1548
+ app.engine(config.extension, createAsjsEngine(config));
1549
+ app.set('view engine', config.extension);
1550
+ app.set('views', config.viewsDir);
1551
+
1552
+ if (config.serveClientAssets) {
1553
+ app.use(config.assetMountPath, express.static(config.packagePaths.clientDir, {
1554
+ fallthrough: true,
1555
+ immutable: !config.debug,
1556
+ index: false,
1557
+ maxAge: config.debug ? 0 : '1h'
1558
+ }));
1559
+ }
1560
+
1561
+ if (config.publicDir) {
1562
+ app.use(express.static(config.publicDir));
1563
+ }
1564
+
1565
+ app.locals.asjs = createAsjsViewModel({}, config);
1566
+
1567
+ return api;
1568
+ }
1569
+
1570
+ module.exports = {
1571
+ createAsjsConfig,
1572
+ createAsjsEngine,
1573
+ escapeHtml,
1574
+ getAsjsPackagePaths,
1575
+ renderClientTags,
1576
+ renderDebugErrorPage,
1577
+ validateComponentProps,
1578
+ setupAsjs
1579
+ };