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/LICENSE +21 -0
- package/README.md +883 -0
- package/index.js +1 -0
- package/lib/asjs.js +1579 -0
- package/lib/client/asjs-core.css +92 -0
- package/lib/client/asjs-router.js +989 -0
- package/lib/client/asjs-theme.css +1073 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
# asjs-express
|
|
2
|
+
|
|
3
|
+
ASJS is a lightweight Express view engine built for teams that still want the calm, readable side of server-rendered pages without giving up shared layouts, async page preparation, partials, and modern navigation behavior.
|
|
4
|
+
|
|
5
|
+
Bu depo içindeki örnek yüzey ise WebAS adıyla anlatılıyor. Tonu bilerek daha gerçek tutuldu: gösterişten çok düzeni, süsten çok açıklığı, ilk izlenimden çok sürdürülebilirliği önemseyen bir yapı gibi kurgulandı.
|
|
6
|
+
|
|
7
|
+
## English
|
|
8
|
+
|
|
9
|
+
### Why this project exists
|
|
10
|
+
|
|
11
|
+
ASJS was built for a very common problem in Express projects: pages start simple, then real work arrives. A dashboard needs data before render. A contact form needs validation and a meaningful response. A shared layout needs to stay stable while the inside changes. At that point, many projects either become repetitive or drift into a heavy front-end stack before they actually need one.
|
|
12
|
+
|
|
13
|
+
ASJS keeps that middle ground clean. It gives you an EJS-like template syntax, a small but practical view model, optional client-side navigation, and the ability to finish your data work before the first HTML reaches the browser.
|
|
14
|
+
|
|
15
|
+
The WebAS example in this repository is written in that spirit. It presents a small, believable interface structure shaped by two developers:
|
|
16
|
+
|
|
17
|
+
- [Acarfx](https://acarfx.com)
|
|
18
|
+
- [Seyfullah Kısacık](https://seyfooksck.com/)
|
|
19
|
+
|
|
20
|
+
The intention is simple: this should read like something a careful team would actually maintain, not like a throwaway demo assembled for a screenshot.
|
|
21
|
+
|
|
22
|
+
### What ASJS gives you
|
|
23
|
+
|
|
24
|
+
- EJS-like syntax with `<% %>`, `<%= %>`, and `<%- %>`
|
|
25
|
+
- Layout support without extra ceremony
|
|
26
|
+
- Partial rendering with prop validation
|
|
27
|
+
- Template caching
|
|
28
|
+
- Built-in client router with transitions, prefetch, and loading bar support
|
|
29
|
+
- Async form enhancement for marked forms
|
|
30
|
+
- Plugin and hook support for Express projects that need to grow over time
|
|
31
|
+
- Package-served assets, so you do not have to manually copy router files into your public folder
|
|
32
|
+
|
|
33
|
+
### Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install asjs-express
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Quick start
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
const express = require('express')
|
|
43
|
+
const { setupAsjs } = require('asjs-express')
|
|
44
|
+
|
|
45
|
+
const app = express()
|
|
46
|
+
const asjs = setupAsjs(app, {
|
|
47
|
+
rootDir: __dirname,
|
|
48
|
+
defaultLayout: 'layouts/main',
|
|
49
|
+
debug: true,
|
|
50
|
+
cache: true,
|
|
51
|
+
forms: true,
|
|
52
|
+
transitions: 'lift',
|
|
53
|
+
prefetch: true,
|
|
54
|
+
loadingBar: true
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
app.get('/', asjs.page('home', {
|
|
58
|
+
title: 'Hello ASJS'
|
|
59
|
+
}))
|
|
60
|
+
|
|
61
|
+
app.use(asjs.errors())
|
|
62
|
+
app.listen(3000)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Minimal Express template
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
const express = require('express')
|
|
69
|
+
const { setupAsjs } = require('asjs-express')
|
|
70
|
+
|
|
71
|
+
const app = express()
|
|
72
|
+
const asjs = setupAsjs(app, { rootDir: __dirname })
|
|
73
|
+
|
|
74
|
+
app.get('/', asjs.page('home', { title: 'Hello ASJS' }))
|
|
75
|
+
app.listen(3000)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Layout usage
|
|
79
|
+
|
|
80
|
+
```asjs
|
|
81
|
+
<!DOCTYPE html>
|
|
82
|
+
<html>
|
|
83
|
+
<head>
|
|
84
|
+
<meta charset="UTF-8">
|
|
85
|
+
<title><%= title %></title>
|
|
86
|
+
<%- asjs.clientTags({ preload: true, theme: true }) %>
|
|
87
|
+
</head>
|
|
88
|
+
<body<%- asjs.bodyAttrs() %>>
|
|
89
|
+
<%- asjs.progressMarkup() %>
|
|
90
|
+
<main<%- asjs.viewAttrs() %>>
|
|
91
|
+
<%- body %>
|
|
92
|
+
</main>
|
|
93
|
+
</body>
|
|
94
|
+
</html>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`asjs.clientTags()` injects the built-in ASJS stylesheet and router script for you.
|
|
98
|
+
`theme: true` optionally loads the packaged light, corporate WebAS theme.
|
|
99
|
+
|
|
100
|
+
### Work before the page loads
|
|
101
|
+
|
|
102
|
+
Yes. If you pass an async callback to `asjs.page()`, ASJS waits for that work to finish and only then calls `res.render(viewName, locals)`.
|
|
103
|
+
|
|
104
|
+
That means database reads, API requests, permission checks, formatting, and view-model building can all happen before the browser receives the first HTML.
|
|
105
|
+
|
|
106
|
+
#### Database example before render
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
const db = {
|
|
110
|
+
companies: {
|
|
111
|
+
async findBySlug(slug) {
|
|
112
|
+
return { id: 7, slug, name: 'Northwind Studio' }
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
dashboards: {
|
|
116
|
+
async getMetrics(companyId) {
|
|
117
|
+
return [
|
|
118
|
+
{ label: 'Open tasks', value: 12 },
|
|
119
|
+
{ label: 'Active pages', value: 8 },
|
|
120
|
+
{ label: 'Pending review', value: 3 }
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
app.get('/dashboard/:slug', asjs.page('dashboard', async (req) => {
|
|
127
|
+
const company = await db.companies.findBySlug(req.params.slug)
|
|
128
|
+
const metrics = await db.dashboards.getMetrics(company.id)
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
title: `${company.name} Dashboard`,
|
|
132
|
+
company,
|
|
133
|
+
metrics,
|
|
134
|
+
renderSummary: [
|
|
135
|
+
{ label: 'Source', value: 'Database' },
|
|
136
|
+
{ label: 'Company', value: company.name },
|
|
137
|
+
{ label: 'Mode', value: 'Server render' }
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
}))
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### Returned page model
|
|
144
|
+
|
|
145
|
+
The callback above returns a plain object like this:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
{
|
|
149
|
+
title: 'Northwind Studio Dashboard',
|
|
150
|
+
company: { id: 7, slug: 'northwind', name: 'Northwind Studio' },
|
|
151
|
+
metrics: [
|
|
152
|
+
{ label: 'Open tasks', value: 12 },
|
|
153
|
+
{ label: 'Active pages', value: 8 },
|
|
154
|
+
{ label: 'Pending review', value: 3 }
|
|
155
|
+
],
|
|
156
|
+
renderSummary: [
|
|
157
|
+
{ label: 'Source', value: 'Database' },
|
|
158
|
+
{ label: 'Company', value: 'Northwind Studio' },
|
|
159
|
+
{ label: 'Mode', value: 'Server render' }
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
ASJS then turns that object into view locals and renders the final HTML.
|
|
165
|
+
The browser does not receive a loading shell first and wait for the page model later. The work is already done.
|
|
166
|
+
|
|
167
|
+
### Server-rendered POST example
|
|
168
|
+
|
|
169
|
+
You can use the same pattern for POST routes when you want validation or save results to come back as a fully rendered page state.
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
app.use(express.urlencoded({ extended: false }))
|
|
173
|
+
|
|
174
|
+
app.post('/contact', asjs.page('contact', async (req) => {
|
|
175
|
+
const formValues = {
|
|
176
|
+
name: String(req.body.name || '').trim(),
|
|
177
|
+
email: String(req.body.email || '').trim(),
|
|
178
|
+
brief: String(req.body.brief || '').trim()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const formErrors = {}
|
|
182
|
+
|
|
183
|
+
if (!formValues.name) {
|
|
184
|
+
formErrors.name = 'Please enter a name.'
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!formValues.email.includes('@')) {
|
|
188
|
+
formErrors.email = 'Please enter a valid email address.'
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (Object.keys(formErrors).length) {
|
|
192
|
+
return {
|
|
193
|
+
status: 422,
|
|
194
|
+
title: 'Contact',
|
|
195
|
+
formValues,
|
|
196
|
+
formErrors
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const savedRecord = await saveProjectNote(formValues)
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
title: 'Contact received',
|
|
204
|
+
submission: savedRecord
|
|
205
|
+
}
|
|
206
|
+
}))
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### Validation return
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
{
|
|
213
|
+
status: 422,
|
|
214
|
+
title: 'Contact',
|
|
215
|
+
formValues: {
|
|
216
|
+
name: '',
|
|
217
|
+
email: 'wrong-address',
|
|
218
|
+
brief: 'Need a redesign'
|
|
219
|
+
},
|
|
220
|
+
formErrors: {
|
|
221
|
+
name: 'Please enter a name.',
|
|
222
|
+
email: 'Please enter a valid email address.'
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
#### Success return
|
|
228
|
+
|
|
229
|
+
```js
|
|
230
|
+
{
|
|
231
|
+
title: 'Contact received',
|
|
232
|
+
submission: {
|
|
233
|
+
reference: 'WEBAS-K3J9X2',
|
|
234
|
+
name: 'Lina Howard',
|
|
235
|
+
email: 'lina@northwind.dev',
|
|
236
|
+
brief: 'We need a cleaner delivery and dashboard structure.'
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
In both cases, the route returns data. ASJS handles the render step after that.
|
|
242
|
+
|
|
243
|
+
### Inline JSON response without leaving the page
|
|
244
|
+
|
|
245
|
+
Sometimes you do not want a full page transition. You want to post to a dedicated route, get a short answer, and place that answer inside a specific panel.
|
|
246
|
+
|
|
247
|
+
```asjs
|
|
248
|
+
<form<%- asjs.formAttrs({
|
|
249
|
+
action: '/contact/availability',
|
|
250
|
+
method: 'post',
|
|
251
|
+
mode: 'json',
|
|
252
|
+
target: '#availability-response',
|
|
253
|
+
swap: 'replace'
|
|
254
|
+
}) %>>
|
|
255
|
+
<input type="text" name="company" placeholder="Company name">
|
|
256
|
+
<input type="text" name="scope" placeholder="Dashboard, launch page, corporate site">
|
|
257
|
+
<button type="submit">Check route</button>
|
|
258
|
+
</form>
|
|
259
|
+
|
|
260
|
+
<div id="availability-response"></div>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
```js
|
|
264
|
+
app.post('/contact/availability', async (req, res) => {
|
|
265
|
+
const preview = await availabilityService.check(req.body)
|
|
266
|
+
|
|
267
|
+
res.json({
|
|
268
|
+
html: `
|
|
269
|
+
<div class="asjs-inline-response is-success">
|
|
270
|
+
<strong>${preview.title}</strong>
|
|
271
|
+
<p>${preview.message}</p>
|
|
272
|
+
</div>
|
|
273
|
+
`,
|
|
274
|
+
route: preview.route,
|
|
275
|
+
replyWindow: preview.replyWindow
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### Example JSON response body
|
|
281
|
+
|
|
282
|
+
```json
|
|
283
|
+
{
|
|
284
|
+
"html": "<div class=\"asjs-inline-response is-success\"><strong>Northwind Studio can be routed into the operational track.</strong><p>The request can continue without leaving the current page shell.</p></div>",
|
|
285
|
+
"route": "Operational UI track",
|
|
286
|
+
"replyWindow": "1 business day"
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
ASJS reads that response, finds `#availability-response`, and updates only that part of the page.
|
|
291
|
+
|
|
292
|
+
### Plugins and hooks
|
|
293
|
+
|
|
294
|
+
For projects that need room to grow, `setupAsjs()` now supports plugins and lifecycle hooks.
|
|
295
|
+
|
|
296
|
+
```js
|
|
297
|
+
const asjs = setupAsjs(app, {
|
|
298
|
+
rootDir: __dirname,
|
|
299
|
+
plugins: [
|
|
300
|
+
function studioPlugin(api) {
|
|
301
|
+
const healthRouter = api.express.Router()
|
|
302
|
+
|
|
303
|
+
healthRouter.get('/', (req, res) => {
|
|
304
|
+
res.json({ ok: true, service: 'studio-plugin' })
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
api.app.use('/health', healthRouter)
|
|
308
|
+
|
|
309
|
+
api.extendLocals({ supportEmail: 'team@example.com' })
|
|
310
|
+
|
|
311
|
+
api.addHook('beforeRender', ({ pageData }) => ({
|
|
312
|
+
pageData: {
|
|
313
|
+
...pageData,
|
|
314
|
+
supportEmail: 'team@example.com'
|
|
315
|
+
}
|
|
316
|
+
}))
|
|
317
|
+
}
|
|
318
|
+
]
|
|
319
|
+
})
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Available hook points:
|
|
323
|
+
|
|
324
|
+
- `beforePage`
|
|
325
|
+
- `afterPage`
|
|
326
|
+
- `beforeRender`
|
|
327
|
+
- `afterRender`
|
|
328
|
+
|
|
329
|
+
### Form enhancement
|
|
330
|
+
|
|
331
|
+
```asjs
|
|
332
|
+
<form class="contact-form"<%- asjs.formAttrs({
|
|
333
|
+
action: '/contact',
|
|
334
|
+
method: 'post',
|
|
335
|
+
transition: 'slide'
|
|
336
|
+
}) %>>
|
|
337
|
+
<input type="text" name="name" placeholder="Project lead">
|
|
338
|
+
<button type="submit">Send</button>
|
|
339
|
+
</form>
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
`asjs.formAttrs()` marks a form for the built-in ASJS client handler.
|
|
343
|
+
The response may come back as full HTML or as JSON for an inline target update.
|
|
344
|
+
|
|
345
|
+
### Router lifecycle events
|
|
346
|
+
|
|
347
|
+
- `asjs:before-navigate`
|
|
348
|
+
- `asjs:content-replaced`
|
|
349
|
+
- `asjs:navigated`
|
|
350
|
+
- `asjs:navigation-error`
|
|
351
|
+
- `asjs:before-submit`
|
|
352
|
+
- `asjs:form-response`
|
|
353
|
+
- `asjs:form-success`
|
|
354
|
+
- `asjs:form-error`
|
|
355
|
+
|
|
356
|
+
These events are useful for analytics, loading states, cleanup logic, and custom form feedback.
|
|
357
|
+
|
|
358
|
+
### Component helper
|
|
359
|
+
|
|
360
|
+
```js
|
|
361
|
+
const asjs = setupAsjs(app, {
|
|
362
|
+
components: {
|
|
363
|
+
'partials/card': {
|
|
364
|
+
props: {
|
|
365
|
+
title: 'string',
|
|
366
|
+
text: 'string',
|
|
367
|
+
tone: {
|
|
368
|
+
type: 'string',
|
|
369
|
+
default: 'neutral'
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
strict: true
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
})
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
```asjs
|
|
379
|
+
<%- component('partials/card', { title: 'Card', text: 'Body text' }) %>
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Built-in asset delivery
|
|
383
|
+
|
|
384
|
+
ASJS serves its client files from inside the package by default.
|
|
385
|
+
|
|
386
|
+
- default mount path: `/_asjs`
|
|
387
|
+
- router script: `/_asjs/asjs-router.js?v=...`
|
|
388
|
+
- core stylesheet: `/_asjs/asjs-core.css?v=...`
|
|
389
|
+
- optional theme stylesheet: `/_asjs/asjs-theme.css?v=...`
|
|
390
|
+
|
|
391
|
+
The `v=...` query string is generated automatically for cache busting.
|
|
392
|
+
|
|
393
|
+
### Transition presets
|
|
394
|
+
|
|
395
|
+
- `fade`
|
|
396
|
+
- `lift`
|
|
397
|
+
- `slide`
|
|
398
|
+
- `scale`
|
|
399
|
+
- `blur-soft`
|
|
400
|
+
|
|
401
|
+
Use them globally with `transitions`, per-page with `transition`, or per-link with `data-asjs-transition`.
|
|
402
|
+
|
|
403
|
+
### Highlights
|
|
404
|
+
|
|
405
|
+
- `setupAsjs(app, options)` integrates directly into an existing Express app.
|
|
406
|
+
- `component()` validates props before rendering a partial.
|
|
407
|
+
- `cache: true` keeps compiled templates until file mtimes change.
|
|
408
|
+
- `prefetch` and `loadingBar` improve navigation feel.
|
|
409
|
+
- `forms: true` enables built-in async form enhancement.
|
|
410
|
+
- `plugins` opens room for middleware, routes, and project-specific services.
|
|
411
|
+
- `assetVersion` lets you override or disable cache-busting.
|
|
412
|
+
- `serveClientAssets` serves router assets from the package itself.
|
|
413
|
+
|
|
414
|
+
### Package utilities
|
|
415
|
+
|
|
416
|
+
```js
|
|
417
|
+
const { getAsjsPackagePaths } = require('asjs-express')
|
|
418
|
+
|
|
419
|
+
const paths = getAsjsPackagePaths()
|
|
420
|
+
console.log(paths.routerPath)
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Local demo
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
npm install
|
|
427
|
+
npm run example:express
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Open `http://localhost:3001`.
|
|
431
|
+
|
|
432
|
+
### Repository example app
|
|
433
|
+
|
|
434
|
+
The repository ships a dedicated Express example under `example-express/`.
|
|
435
|
+
|
|
436
|
+
- entry: `example-express/app.js`
|
|
437
|
+
- favicon: `example-express/favicon.svg`
|
|
438
|
+
- views: `example-express/views`
|
|
439
|
+
- start command: `npm run example:express`
|
|
440
|
+
- starter pages: `/`, `/dashboard`, `/products`, `/contact`
|
|
441
|
+
- plugin health route: `/webas-platform/health`
|
|
442
|
+
|
|
443
|
+
The example presents WebAS as a practical interface structure associated with [Acarfx](https://acarfx.com) and [Seyfullah Kısacık](https://seyfooksck.com/). It is intentionally written with a calm, professional tone because the project is meant to feel usable, not theatrical.
|
|
444
|
+
|
|
445
|
+
## Türkçe
|
|
446
|
+
|
|
447
|
+
### Bu proje neden var?
|
|
448
|
+
|
|
449
|
+
ASJS, Express projelerinde çok sık karşılaşılan ama çoğu zaman gereksiz yere karmaşık hâle gelen bir alan için yazıldı: sayfalar ilk gün sade olur, sonra gerçek ihtiyaçlar gelir. Dashboard verisi gerekir. Form doğrulaması gerekir. Aynı layout içinde farklı sayfaları sakin biçimde taşımak gerekir. Bir noktadan sonra ya aynı kod tekrar eder ya da ihtiyaç o kadar büyük değilken proje ağır bir ön yüz yapısına sürüklenir.
|
|
450
|
+
|
|
451
|
+
ASJS tam burada devreye girer. Sunucu tarafını sade tutar, şablon katmanını EJS benzeri bir yapıda bırakır, istersen sayfa geçişlerini güçlendirir, istersen hiçbir şeyi zorlamaz. En önemlisi de şu soruya net cevap verir: Evet, sayfa yüklenmeden önce veriyi hazırlayabilirsin.
|
|
452
|
+
|
|
453
|
+
Bu depodaki WebAS örneği de bu anlayışla yazıldı. Dili özellikle daha insanî ve daha gerçek tutuldu. Amaç “vitrinlik demo” hissi vermek değil, gerçekten çalışan küçük bir ekibin kullanabileceği bir yapı duygusu vermekti.
|
|
454
|
+
|
|
455
|
+
Bu örnek anlatımın arkasında iki geliştirici adı özellikle görünür tutuluyor:
|
|
456
|
+
|
|
457
|
+
- [Acarfx](https://acarfx.com)
|
|
458
|
+
- [Seyfullah Kısacık](https://seyfooksck.com/)
|
|
459
|
+
|
|
460
|
+
WebAS yüzeyi, bu iki geliştiricinin birlikte şekillendirdiği düzenli, açık ve kurumsal bir arayüz dili gibi anlatılıyor. Çünkü iyi bir arayüz çoğu zaman bağırmaz; düzeniyle güven verir.
|
|
461
|
+
|
|
462
|
+
### ASJS neler sağlar?
|
|
463
|
+
|
|
464
|
+
- `<% %>`, `<%= %>`, `<%- %>` tabanlı EJS benzeri sözdizimi
|
|
465
|
+
- Ek yük bindirmeyen layout desteği
|
|
466
|
+
- Prop doğrulamalı partial ve component kullanımı
|
|
467
|
+
- Template cache
|
|
468
|
+
- Geçiş, prefetch ve loading bar destekli istemci yönlendirmesi
|
|
469
|
+
- İşaretli formlar için dahili async form akışı
|
|
470
|
+
- Büyüyen Express projeleri için plugin ve hook desteği
|
|
471
|
+
- Public klasöre dosya kopyalamadan paket içinden asset servisi
|
|
472
|
+
|
|
473
|
+
### Kurulum
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
npm install asjs-express
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Hızlı başlangıç
|
|
480
|
+
|
|
481
|
+
```js
|
|
482
|
+
const express = require('express')
|
|
483
|
+
const { setupAsjs } = require('asjs-express')
|
|
484
|
+
|
|
485
|
+
const app = express()
|
|
486
|
+
const asjs = setupAsjs(app, {
|
|
487
|
+
rootDir: __dirname,
|
|
488
|
+
defaultLayout: 'layouts/main',
|
|
489
|
+
debug: true,
|
|
490
|
+
cache: true,
|
|
491
|
+
forms: true,
|
|
492
|
+
transitions: 'lift',
|
|
493
|
+
prefetch: true,
|
|
494
|
+
loadingBar: true
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
app.get('/', asjs.page('home', {
|
|
498
|
+
title: 'Merhaba ASJS'
|
|
499
|
+
}))
|
|
500
|
+
|
|
501
|
+
app.use(asjs.errors())
|
|
502
|
+
app.listen(3000)
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Minimal Express şablonu
|
|
506
|
+
|
|
507
|
+
```js
|
|
508
|
+
const express = require('express')
|
|
509
|
+
const { setupAsjs } = require('asjs-express')
|
|
510
|
+
|
|
511
|
+
const app = express()
|
|
512
|
+
const asjs = setupAsjs(app, { rootDir: __dirname })
|
|
513
|
+
|
|
514
|
+
app.get('/', asjs.page('home', { title: 'Merhaba ASJS' }))
|
|
515
|
+
app.listen(3000)
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### Layout kullanımı
|
|
519
|
+
|
|
520
|
+
```asjs
|
|
521
|
+
<!DOCTYPE html>
|
|
522
|
+
<html>
|
|
523
|
+
<head>
|
|
524
|
+
<meta charset="UTF-8">
|
|
525
|
+
<title><%= title %></title>
|
|
526
|
+
<%- asjs.clientTags({ preload: true, theme: true }) %>
|
|
527
|
+
</head>
|
|
528
|
+
<body<%- asjs.bodyAttrs() %>>
|
|
529
|
+
<%- asjs.progressMarkup() %>
|
|
530
|
+
<main<%- asjs.viewAttrs() %>>
|
|
531
|
+
<%- body %>
|
|
532
|
+
</main>
|
|
533
|
+
</body>
|
|
534
|
+
</html>
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
`asjs.clientTags()` çağrısı dahili ASJS stil ve router etiketlerini otomatik üretir.
|
|
538
|
+
`theme: true` ise açık renkli, kurumsal WebAS temasını yükler.
|
|
539
|
+
|
|
540
|
+
### Sayfa yüklenmeden önce işlem yapmak
|
|
541
|
+
|
|
542
|
+
Evet, mümkün. `asjs.page()` içine async bir callback verirsen ASJS bu işlemin bitmesini bekler, ardından `res.render(viewName, locals)` çağrısını yapar.
|
|
543
|
+
|
|
544
|
+
Yani veritabanı sorgusu, API isteği, yetki kontrolü, veri biçimlendirme ve view model oluşturma gibi işler ilk HTML tarayıcıya gitmeden önce tamamlanabilir.
|
|
545
|
+
|
|
546
|
+
#### Render öncesi veritabanı örneği
|
|
547
|
+
|
|
548
|
+
```js
|
|
549
|
+
const db = {
|
|
550
|
+
companies: {
|
|
551
|
+
async findBySlug(slug) {
|
|
552
|
+
return { id: 7, slug, name: 'Northwind Studio' }
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
dashboards: {
|
|
556
|
+
async getMetrics(companyId) {
|
|
557
|
+
return [
|
|
558
|
+
{ label: 'Açık görev', value: 12 },
|
|
559
|
+
{ label: 'Aktif sayfa', value: 8 },
|
|
560
|
+
{ label: 'Onay bekleyen', value: 3 }
|
|
561
|
+
]
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
app.get('/dashboard/:slug', asjs.page('dashboard', async (req) => {
|
|
567
|
+
const company = await db.companies.findBySlug(req.params.slug)
|
|
568
|
+
const metrics = await db.dashboards.getMetrics(company.id)
|
|
569
|
+
|
|
570
|
+
return {
|
|
571
|
+
title: `${company.name} Dashboard`,
|
|
572
|
+
company,
|
|
573
|
+
metrics,
|
|
574
|
+
renderSummary: [
|
|
575
|
+
{ label: 'Kaynak', value: 'Veritabanı' },
|
|
576
|
+
{ label: 'Şirket', value: company.name },
|
|
577
|
+
{ label: 'Mod', value: 'Sunucu render' }
|
|
578
|
+
]
|
|
579
|
+
}
|
|
580
|
+
}))
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
#### Dönen sayfa modeli
|
|
584
|
+
|
|
585
|
+
Yukarıdaki callback şu tipte bir nesne döndürür:
|
|
586
|
+
|
|
587
|
+
```js
|
|
588
|
+
{
|
|
589
|
+
title: 'Northwind Studio Dashboard',
|
|
590
|
+
company: { id: 7, slug: 'northwind', name: 'Northwind Studio' },
|
|
591
|
+
metrics: [
|
|
592
|
+
{ label: 'Açık görev', value: 12 },
|
|
593
|
+
{ label: 'Aktif sayfa', value: 8 },
|
|
594
|
+
{ label: 'Onay bekleyen', value: 3 }
|
|
595
|
+
],
|
|
596
|
+
renderSummary: [
|
|
597
|
+
{ label: 'Kaynak', value: 'Veritabanı' },
|
|
598
|
+
{ label: 'Şirket', value: 'Northwind Studio' },
|
|
599
|
+
{ label: 'Mod', value: 'Sunucu render' }
|
|
600
|
+
]
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
ASJS bu nesneyi view locals hâline getirir ve nihai HTML’i üretir.
|
|
605
|
+
Tarayıcı önce boş bir iskelet alıp sonra veriyi beklemez. Veri işi zaten tamamlanmıştır.
|
|
606
|
+
|
|
607
|
+
### Server-rendered POST dönüşü
|
|
608
|
+
|
|
609
|
+
POST rotalarında da aynı yaklaşımı kullanabilirsin. Özellikle form doğrulaması, kayıt sonucu veya başarı ekranı aynı view içinde geri dönecekse bu çok temiz çalışır.
|
|
610
|
+
|
|
611
|
+
```js
|
|
612
|
+
app.use(express.urlencoded({ extended: false }))
|
|
613
|
+
|
|
614
|
+
app.post('/contact', asjs.page('contact', async (req) => {
|
|
615
|
+
const formValues = {
|
|
616
|
+
name: String(req.body.name || '').trim(),
|
|
617
|
+
email: String(req.body.email || '').trim(),
|
|
618
|
+
brief: String(req.body.brief || '').trim()
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const formErrors = {}
|
|
622
|
+
|
|
623
|
+
if (!formValues.name) {
|
|
624
|
+
formErrors.name = 'Lütfen bir isim girin.'
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (!formValues.email.includes('@')) {
|
|
628
|
+
formErrors.email = 'Lütfen geçerli bir e-posta adresi girin.'
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (Object.keys(formErrors).length) {
|
|
632
|
+
return {
|
|
633
|
+
status: 422,
|
|
634
|
+
title: 'İletişim',
|
|
635
|
+
formValues,
|
|
636
|
+
formErrors
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const savedRecord = await saveProjectNote(formValues)
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
title: 'İletişim alındı',
|
|
644
|
+
submission: savedRecord
|
|
645
|
+
}
|
|
646
|
+
}))
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
#### Hatalı dönüş örneği
|
|
650
|
+
|
|
651
|
+
```js
|
|
652
|
+
{
|
|
653
|
+
status: 422,
|
|
654
|
+
title: 'İletişim',
|
|
655
|
+
formValues: {
|
|
656
|
+
name: '',
|
|
657
|
+
email: 'yanlis-adres',
|
|
658
|
+
brief: 'Tasarım yenilemesi istiyoruz'
|
|
659
|
+
},
|
|
660
|
+
formErrors: {
|
|
661
|
+
name: 'Lütfen bir isim girin.',
|
|
662
|
+
email: 'Lütfen geçerli bir e-posta adresi girin.'
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
#### Başarılı dönüş örneği
|
|
668
|
+
|
|
669
|
+
```js
|
|
670
|
+
{
|
|
671
|
+
title: 'İletişim alındı',
|
|
672
|
+
submission: {
|
|
673
|
+
reference: 'WEBAS-K3J9X2',
|
|
674
|
+
name: 'Lina Howard',
|
|
675
|
+
email: 'lina@northwind.dev',
|
|
676
|
+
brief: 'Daha temiz bir teslim ve dashboard yapısı istiyoruz.'
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
Her iki durumda da rota veri döndürür. Render adımını ASJS sonradan kendisi yürütür.
|
|
682
|
+
|
|
683
|
+
### Sayfadan ayrılmadan JSON döndürmek
|
|
684
|
+
|
|
685
|
+
Bazen tam sayfa geçişi istemezsin. Belirli bir rotaya POST atıp kısa bir cevap almak ve yalnızca tek bir paneli güncellemek istersin.
|
|
686
|
+
|
|
687
|
+
```asjs
|
|
688
|
+
<form<%- asjs.formAttrs({
|
|
689
|
+
action: '/contact/availability',
|
|
690
|
+
method: 'post',
|
|
691
|
+
mode: 'json',
|
|
692
|
+
target: '#availability-response',
|
|
693
|
+
swap: 'replace'
|
|
694
|
+
}) %>>
|
|
695
|
+
<input type="text" name="company" placeholder="Şirket adı">
|
|
696
|
+
<input type="text" name="scope" placeholder="Dashboard, lansman sayfası, kurumsal site">
|
|
697
|
+
<button type="submit">Rotayı kontrol et</button>
|
|
698
|
+
</form>
|
|
699
|
+
|
|
700
|
+
<div id="availability-response"></div>
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
```js
|
|
704
|
+
app.post('/contact/availability', async (req, res) => {
|
|
705
|
+
const preview = await availabilityService.check(req.body)
|
|
706
|
+
|
|
707
|
+
res.json({
|
|
708
|
+
html: `
|
|
709
|
+
<div class="asjs-inline-response is-success">
|
|
710
|
+
<strong>${preview.title}</strong>
|
|
711
|
+
<p>${preview.message}</p>
|
|
712
|
+
</div>
|
|
713
|
+
`,
|
|
714
|
+
route: preview.route,
|
|
715
|
+
replyWindow: preview.replyWindow
|
|
716
|
+
})
|
|
717
|
+
})
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
#### Örnek JSON cevap gövdesi
|
|
721
|
+
|
|
722
|
+
```json
|
|
723
|
+
{
|
|
724
|
+
"html": "<div class=\"asjs-inline-response is-success\"><strong>Northwind Studio operasyon hattına yönlendirilebilir.</strong><p>İstek mevcut sayfa kabuğu bozulmadan devam edebilir.</p></div>",
|
|
725
|
+
"route": "Operasyonel arayüz hattı",
|
|
726
|
+
"replyWindow": "1 iş günü"
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
ASJS bu cevabı okur, `#availability-response` alanını bulur ve yalnızca o kısmı günceller.
|
|
731
|
+
|
|
732
|
+
### Plugin ve hook yapısı
|
|
733
|
+
|
|
734
|
+
Proje büyüdüğünde `setupAsjs()` artık plugin ve yaşam döngüsü hook’ları için de alan açar.
|
|
735
|
+
|
|
736
|
+
```js
|
|
737
|
+
const asjs = setupAsjs(app, {
|
|
738
|
+
rootDir: __dirname,
|
|
739
|
+
plugins: [
|
|
740
|
+
function studioPlugin(api) {
|
|
741
|
+
const healthRouter = api.express.Router()
|
|
742
|
+
|
|
743
|
+
healthRouter.get('/', (req, res) => {
|
|
744
|
+
res.json({ ok: true, service: 'studio-plugin' })
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
api.app.use('/health', healthRouter)
|
|
748
|
+
|
|
749
|
+
api.extendLocals({ supportEmail: 'team@example.com' })
|
|
750
|
+
|
|
751
|
+
api.addHook('beforeRender', ({ pageData }) => ({
|
|
752
|
+
pageData: {
|
|
753
|
+
...pageData,
|
|
754
|
+
supportEmail: 'team@example.com'
|
|
755
|
+
}
|
|
756
|
+
}))
|
|
757
|
+
}
|
|
758
|
+
]
|
|
759
|
+
})
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
Kullanılabilen hook noktaları:
|
|
763
|
+
|
|
764
|
+
- `beforePage`
|
|
765
|
+
- `afterPage`
|
|
766
|
+
- `beforeRender`
|
|
767
|
+
- `afterRender`
|
|
768
|
+
|
|
769
|
+
### Form geliştirme
|
|
770
|
+
|
|
771
|
+
```asjs
|
|
772
|
+
<form class="contact-form"<%- asjs.formAttrs({
|
|
773
|
+
action: '/contact',
|
|
774
|
+
method: 'post',
|
|
775
|
+
transition: 'slide'
|
|
776
|
+
}) %>>
|
|
777
|
+
<input type="text" name="name" placeholder="Proje sorumlusu">
|
|
778
|
+
<button type="submit">Gönder</button>
|
|
779
|
+
</form>
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
`asjs.formAttrs()` bir formu dahili ASJS istemci akışı için işaretler.
|
|
783
|
+
Gelen cevap tam HTML de olabilir, belirli bir hedef alanı güncelleyen JSON da olabilir.
|
|
784
|
+
|
|
785
|
+
### Router yaşam döngüsü event’leri
|
|
786
|
+
|
|
787
|
+
- `asjs:before-navigate`
|
|
788
|
+
- `asjs:content-replaced`
|
|
789
|
+
- `asjs:navigated`
|
|
790
|
+
- `asjs:navigation-error`
|
|
791
|
+
- `asjs:before-submit`
|
|
792
|
+
- `asjs:form-response`
|
|
793
|
+
- `asjs:form-success`
|
|
794
|
+
- `asjs:form-error`
|
|
795
|
+
|
|
796
|
+
Bu event’ler loading durumları, analytics, arayüz temizliği ve özel form geri bildirimi için işe yarar.
|
|
797
|
+
|
|
798
|
+
### Component helper
|
|
799
|
+
|
|
800
|
+
```js
|
|
801
|
+
const asjs = setupAsjs(app, {
|
|
802
|
+
components: {
|
|
803
|
+
'partials/card': {
|
|
804
|
+
props: {
|
|
805
|
+
title: 'string',
|
|
806
|
+
text: 'string',
|
|
807
|
+
tone: {
|
|
808
|
+
type: 'string',
|
|
809
|
+
default: 'neutral'
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
strict: true
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
})
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
```asjs
|
|
819
|
+
<%- component('partials/card', { title: 'Kart', text: 'İçerik' }) %>
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
### Dahili asset servisi
|
|
823
|
+
|
|
824
|
+
ASJS istemci dosyalarını varsayılan olarak paketin içinden servis eder.
|
|
825
|
+
|
|
826
|
+
- varsayılan mount path: `/_asjs`
|
|
827
|
+
- router script: `/_asjs/asjs-router.js?v=...`
|
|
828
|
+
- ana stil dosyası: `/_asjs/asjs-core.css?v=...`
|
|
829
|
+
- opsiyonel tema dosyası: `/_asjs/asjs-theme.css?v=...`
|
|
830
|
+
|
|
831
|
+
`v=...` query parametresi otomatik cache-busting için üretilir.
|
|
832
|
+
|
|
833
|
+
### Geçiş preset’leri
|
|
834
|
+
|
|
835
|
+
- `fade`
|
|
836
|
+
- `lift`
|
|
837
|
+
- `slide`
|
|
838
|
+
- `scale`
|
|
839
|
+
- `blur-soft`
|
|
840
|
+
|
|
841
|
+
Bunları global olarak `transitions`, sayfa bazında `transition`, link bazında ise `data-asjs-transition` ile kullanabilirsin.
|
|
842
|
+
|
|
843
|
+
### Öne çıkanlar
|
|
844
|
+
|
|
845
|
+
- `setupAsjs(app, options)` mevcut Express uygulamasına doğrudan bağlanır.
|
|
846
|
+
- `component()` partial render etmeden önce props doğrular.
|
|
847
|
+
- `cache: true` derlenmiş template’leri dosya mtime değişene kadar saklar.
|
|
848
|
+
- `prefetch` ve `loadingBar` geçiş hissini güçlendirir.
|
|
849
|
+
- `forms: true` işaretli formlar için dahili async form akışını açar.
|
|
850
|
+
- `plugins` middleware, route ve servis katmanı için alan bırakır.
|
|
851
|
+
- `assetVersion` ile dahili asset versiyonlamasını ezebilir veya kapatabilirsin.
|
|
852
|
+
- `serveClientAssets` router asset’lerini doğrudan paket içinden servis eder.
|
|
853
|
+
|
|
854
|
+
### Paket yardımcıları
|
|
855
|
+
|
|
856
|
+
```js
|
|
857
|
+
const { getAsjsPackagePaths } = require('asjs-express')
|
|
858
|
+
|
|
859
|
+
const paths = getAsjsPackagePaths()
|
|
860
|
+
console.log(paths.routerPath)
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
### Lokal demo
|
|
864
|
+
|
|
865
|
+
```bash
|
|
866
|
+
npm install
|
|
867
|
+
npm run example:express
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
Tarayıcıda `http://localhost:3001` adresini aç.
|
|
871
|
+
|
|
872
|
+
### Repodaki örnek uygulama
|
|
873
|
+
|
|
874
|
+
Repo içinde ayrı bir Express örneği `example-express/` altında durur.
|
|
875
|
+
|
|
876
|
+
- giriş: `example-express/app.js`
|
|
877
|
+
- favicon: `example-express/favicon.svg`
|
|
878
|
+
- view klasörü: `example-express/views`
|
|
879
|
+
- çalıştırma komutu: `npm run example:express`
|
|
880
|
+
- başlangıç rotaları: `/`, `/dashboard`, `/products`, `/contact`
|
|
881
|
+
- plugin sağlık rotası: `/webas-platform/health`
|
|
882
|
+
|
|
883
|
+
Bu örnek, WebAS’i [Acarfx](https://acarfx.com) ve [Seyfullah Kısacık](https://seyfooksck.com/) ile ilişkilendirilen düzenli ve kurumsal bir arayüz dili gibi sunar. Bu ton bilinçli seçildi. Çünkü bazı projelerde asıl değer, daha çok şey göstermek değil, daha çok şeyi sakin biçimde taşıyabilmektir.
|