asjs-express 1.2.0 → 1.4.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/README.md +360 -0
- package/bin/create-asjs-app.js +223 -0
- package/lib/asjs.js +306 -13
- package/lib/client/asjs-router.js +46 -1
- package/package.json +12 -2
- package/templates/minimal/README.md.tpl +12 -0
- package/templates/minimal/app.js.tpl +29 -0
- package/templates/minimal/gitignore.tpl +4 -0
- package/templates/minimal/package.json.tpl +15 -0
- package/templates/minimal/views/home.asjs.tpl +5 -0
- package/templates/minimal/views/layouts/main.asjs.tpl +16 -0
- package/templates/starter/README.md.tpl +12 -0
- package/templates/starter/app.js.tpl +54 -0
- package/templates/starter/gitignore.tpl +4 -0
- package/templates/starter/package.json.tpl +15 -0
- package/templates/starter/views/about.asjs.tpl +10 -0
- package/templates/starter/views/home.asjs.tpl +10 -0
- package/templates/starter/views/layouts/main.asjs.tpl +16 -0
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ The intention is simple: this should read like something a careful team would ac
|
|
|
28
28
|
- Built-in client router with transitions, prefetch, and loading bar support
|
|
29
29
|
- Async form enhancement for marked forms
|
|
30
30
|
- Plugin and hook support for Express projects that need to grow over time
|
|
31
|
+
- npx project scaffolding so a new ASJS app can be created with starter files in one command
|
|
31
32
|
- Package-served assets, so you do not have to manually copy router files into your public folder
|
|
32
33
|
|
|
33
34
|
### Install
|
|
@@ -36,6 +37,37 @@ The intention is simple: this should read like something a careful team would ac
|
|
|
36
37
|
npm install asjs-express
|
|
37
38
|
```
|
|
38
39
|
|
|
40
|
+
### Create a new app with npx
|
|
41
|
+
|
|
42
|
+
ASJS now includes a small project generator.
|
|
43
|
+
If you want a React or Next.js-style bootstrap flow, you can create a fresh project folder with one command.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx asjs-express my-app
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
That command creates a ready-to-run ASJS project with `app.js`, `views/layouts/main.asjs`, `views/home.asjs`, `package.json`, `.gitignore`, and a small README.
|
|
50
|
+
|
|
51
|
+
You can also choose a slightly richer starter that already includes two routes and the newer helper API:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx asjs-express create my-app --template starter
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Useful flags:
|
|
58
|
+
|
|
59
|
+
- `--template minimal`
|
|
60
|
+
- `--template starter`
|
|
61
|
+
- `--skip-install`
|
|
62
|
+
- `--force`
|
|
63
|
+
|
|
64
|
+
After generation:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
cd my-app
|
|
68
|
+
npm run dev
|
|
69
|
+
```
|
|
70
|
+
|
|
39
71
|
### Quick start
|
|
40
72
|
|
|
41
73
|
```js
|
|
@@ -75,6 +107,74 @@ app.get('/', asjs.page('home', { title: 'Hello ASJS' }))
|
|
|
75
107
|
app.listen(3000)
|
|
76
108
|
```
|
|
77
109
|
|
|
110
|
+
### Super simple one-page starter
|
|
111
|
+
|
|
112
|
+
If you want the lowest-friction starting point, use a single-page structure like this:
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
my-app/
|
|
116
|
+
app.js
|
|
117
|
+
views/
|
|
118
|
+
layouts/
|
|
119
|
+
main.asjs
|
|
120
|
+
home.asjs
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
const express = require('express')
|
|
125
|
+
const { setupAsjs } = require('asjs-express')
|
|
126
|
+
|
|
127
|
+
const app = express()
|
|
128
|
+
const asjs = setupAsjs(app, {
|
|
129
|
+
rootDir: __dirname,
|
|
130
|
+
defaultLayout: 'layouts/main',
|
|
131
|
+
navItems: [
|
|
132
|
+
{ href: '/', label: 'Home' }
|
|
133
|
+
],
|
|
134
|
+
transitions: 'fade',
|
|
135
|
+
prefetch: true,
|
|
136
|
+
loadingBar: true
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
app.get('/', asjs.page('home', {
|
|
140
|
+
title: 'My first ASJS page',
|
|
141
|
+
headline: 'ASJS works with almost no setup.',
|
|
142
|
+
description: 'Header, router, loading bar, and SPA-ready page transitions are already connected.'
|
|
143
|
+
}))
|
|
144
|
+
|
|
145
|
+
app.use(asjs.errors())
|
|
146
|
+
app.listen(3000)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```asjs
|
|
150
|
+
<!DOCTYPE html>
|
|
151
|
+
<html>
|
|
152
|
+
<head>
|
|
153
|
+
<meta charset="UTF-8">
|
|
154
|
+
<title><%= title %></title>
|
|
155
|
+
<%- asjs.clientTags({ preload: true, theme: true }) %>
|
|
156
|
+
</head>
|
|
157
|
+
<body<%- asjs.bodyAttrs() %>>
|
|
158
|
+
<%- asjs.progressMarkup() %>
|
|
159
|
+
<%- asjs.header() %>
|
|
160
|
+
<main<%- asjs.viewAttrs() %>>
|
|
161
|
+
<%- body %>
|
|
162
|
+
</main>
|
|
163
|
+
</body>
|
|
164
|
+
</html>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```asjs
|
|
168
|
+
<section>
|
|
169
|
+
<h1><%= headline %></h1>
|
|
170
|
+
<p><%= description %></p>
|
|
171
|
+
</section>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
This is enough for a working ASJS page.
|
|
175
|
+
The repository also ships the same idea as a real folder under `example-minimal/`.
|
|
176
|
+
You can run it with `npm run example:minimal`.
|
|
177
|
+
|
|
78
178
|
### Layout usage
|
|
79
179
|
|
|
80
180
|
```asjs
|
|
@@ -97,6 +197,86 @@ app.listen(3000)
|
|
|
97
197
|
`asjs.clientTags()` injects the built-in ASJS stylesheet and router script for you.
|
|
98
198
|
`theme: true` optionally loads the packaged light, corporate WebAS theme.
|
|
99
199
|
|
|
200
|
+
### Easiest Express SPA setup
|
|
201
|
+
|
|
202
|
+
If you want the easiest integration, build it in 3 files: `app.js`, `views/layouts/main.asjs`, and one page such as `views/home.asjs`.
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
const express = require('express')
|
|
206
|
+
const { setupAsjs } = require('asjs-express')
|
|
207
|
+
|
|
208
|
+
const app = express()
|
|
209
|
+
const asjs = setupAsjs(app, {
|
|
210
|
+
rootDir: __dirname,
|
|
211
|
+
defaultLayout: 'layouts/main',
|
|
212
|
+
navItems: [
|
|
213
|
+
{ href: '/', label: 'Home' },
|
|
214
|
+
{ href: '/about', label: 'About' },
|
|
215
|
+
{ href: '/contact', label: 'Contact' }
|
|
216
|
+
],
|
|
217
|
+
transitions: 'fade',
|
|
218
|
+
prefetch: true,
|
|
219
|
+
loadingBar: true
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const buildPage = asjs.createPageModel({
|
|
223
|
+
pageDescription: 'My first ASJS page',
|
|
224
|
+
renderSummary: []
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
app.get('/', asjs.createPageRoute('home', {
|
|
228
|
+
buildPage,
|
|
229
|
+
renderState: {
|
|
230
|
+
delay: 120,
|
|
231
|
+
label: 'Home page ready',
|
|
232
|
+
narrative: 'ASJS prepared this page model before the HTML response was sent.'
|
|
233
|
+
}
|
|
234
|
+
}, () => ({
|
|
235
|
+
title: 'Home',
|
|
236
|
+
heroTitle: 'Hello ASJS',
|
|
237
|
+
heroText: 'This page is server-rendered and SPA navigation is already active.'
|
|
238
|
+
})))
|
|
239
|
+
|
|
240
|
+
app.listen(3000)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
```asjs
|
|
244
|
+
<!DOCTYPE html>
|
|
245
|
+
<html>
|
|
246
|
+
<head>
|
|
247
|
+
<meta charset="UTF-8">
|
|
248
|
+
<title><%= title %></title>
|
|
249
|
+
<%- asjs.clientTags({ preload: true, theme: true }) %>
|
|
250
|
+
</head>
|
|
251
|
+
<body<%- asjs.bodyAttrs() %>>
|
|
252
|
+
<%- asjs.progressMarkup() %>
|
|
253
|
+
<%- asjs.header() %>
|
|
254
|
+
<main<%- asjs.viewAttrs() %>>
|
|
255
|
+
<%- body %>
|
|
256
|
+
</main>
|
|
257
|
+
</body>
|
|
258
|
+
</html>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Default SPA loading and page transition animations are already included.
|
|
262
|
+
If you do not add a custom class, ASJS keeps the built-in loading bar and transition look.
|
|
263
|
+
If you want to style on top of them later, pass a class name instead of replacing the system.
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
const asjs = setupAsjs(app, {
|
|
267
|
+
rootDir: __dirname,
|
|
268
|
+
defaultLayout: 'layouts/main',
|
|
269
|
+
transitions: {
|
|
270
|
+
name: 'fade',
|
|
271
|
+
className: 'my-page-motion'
|
|
272
|
+
},
|
|
273
|
+
loadingBar: {
|
|
274
|
+
enabled: true,
|
|
275
|
+
className: 'my-loading-bar'
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
```
|
|
279
|
+
|
|
100
280
|
### Built-in SPA header
|
|
101
281
|
|
|
102
282
|
ASJS now ships with a built-in header helper for the default SPA flow.
|
|
@@ -542,6 +722,7 @@ WebAS yüzeyi, bu iki geliştiricinin birlikte şekillendirdiği düzenli, açı
|
|
|
542
722
|
- Geçiş, prefetch ve loading bar destekli istemci yönlendirmesi
|
|
543
723
|
- İşaretli formlar için dahili async form akışı
|
|
544
724
|
- Büyüyen Express projeleri için plugin ve hook desteği
|
|
725
|
+
- Tek komutla başlangıç dosyaları oluşturan npx proje oluşturucu
|
|
545
726
|
- Public klasöre dosya kopyalamadan paket içinden asset servisi
|
|
546
727
|
|
|
547
728
|
### Kurulum
|
|
@@ -550,6 +731,37 @@ WebAS yüzeyi, bu iki geliştiricinin birlikte şekillendirdiği düzenli, açı
|
|
|
550
731
|
npm install asjs-express
|
|
551
732
|
```
|
|
552
733
|
|
|
734
|
+
### npx ile yeni proje oluşturma
|
|
735
|
+
|
|
736
|
+
ASJS artık küçük bir proje oluşturucu ile geliyor.
|
|
737
|
+
React veya Next.js tarzı bir başlangıç akışı istiyorsan tek komutla yeni proje klasörü oluşturabilirsin.
|
|
738
|
+
|
|
739
|
+
```bash
|
|
740
|
+
npx asjs-express my-app
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
Bu komut çalışan bir ASJS projesi üretir. İçine `app.js`, `views/layouts/main.asjs`, `views/home.asjs`, `package.json`, `.gitignore` ve kısa bir README koyar.
|
|
744
|
+
|
|
745
|
+
Biraz daha dolu bir başlangıç istersen iki route ve yeni helper API ile gelen starter şablonunu seçebilirsin:
|
|
746
|
+
|
|
747
|
+
```bash
|
|
748
|
+
npx asjs-express create my-app --template starter
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
Kullanışlı bayraklar:
|
|
752
|
+
|
|
753
|
+
- `--template minimal`
|
|
754
|
+
- `--template starter`
|
|
755
|
+
- `--skip-install`
|
|
756
|
+
- `--force`
|
|
757
|
+
|
|
758
|
+
Üretimden sonra:
|
|
759
|
+
|
|
760
|
+
```bash
|
|
761
|
+
cd my-app
|
|
762
|
+
npm run dev
|
|
763
|
+
```
|
|
764
|
+
|
|
553
765
|
### Hızlı başlangıç
|
|
554
766
|
|
|
555
767
|
```js
|
|
@@ -589,6 +801,74 @@ app.get('/', asjs.page('home', { title: 'Merhaba ASJS' }))
|
|
|
589
801
|
app.listen(3000)
|
|
590
802
|
```
|
|
591
803
|
|
|
804
|
+
### Aşırı basit tek sayfa başlangıcı
|
|
805
|
+
|
|
806
|
+
En az sürtünmeli başlangıç için şu kadar basit bir yapı yeterlidir:
|
|
807
|
+
|
|
808
|
+
```text
|
|
809
|
+
my-app/
|
|
810
|
+
app.js
|
|
811
|
+
views/
|
|
812
|
+
layouts/
|
|
813
|
+
main.asjs
|
|
814
|
+
home.asjs
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
```js
|
|
818
|
+
const express = require('express')
|
|
819
|
+
const { setupAsjs } = require('asjs-express')
|
|
820
|
+
|
|
821
|
+
const app = express()
|
|
822
|
+
const asjs = setupAsjs(app, {
|
|
823
|
+
rootDir: __dirname,
|
|
824
|
+
defaultLayout: 'layouts/main',
|
|
825
|
+
navItems: [
|
|
826
|
+
{ href: '/', label: 'Ana Sayfa' }
|
|
827
|
+
],
|
|
828
|
+
transitions: 'fade',
|
|
829
|
+
prefetch: true,
|
|
830
|
+
loadingBar: true
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
app.get('/', asjs.page('home', {
|
|
834
|
+
title: 'İlk ASJS sayfam',
|
|
835
|
+
headline: 'ASJS neredeyse kurulum istemeden çalışır.',
|
|
836
|
+
description: 'Header, router, loading bar ve SPA hazır sayfa geçişleri zaten bağlı gelir.'
|
|
837
|
+
}))
|
|
838
|
+
|
|
839
|
+
app.use(asjs.errors())
|
|
840
|
+
app.listen(3000)
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
```asjs
|
|
844
|
+
<!DOCTYPE html>
|
|
845
|
+
<html>
|
|
846
|
+
<head>
|
|
847
|
+
<meta charset="UTF-8">
|
|
848
|
+
<title><%= title %></title>
|
|
849
|
+
<%- asjs.clientTags({ preload: true, theme: true }) %>
|
|
850
|
+
</head>
|
|
851
|
+
<body<%- asjs.bodyAttrs() %>>
|
|
852
|
+
<%- asjs.progressMarkup() %>
|
|
853
|
+
<%- asjs.header() %>
|
|
854
|
+
<main<%- asjs.viewAttrs() %>>
|
|
855
|
+
<%- body %>
|
|
856
|
+
</main>
|
|
857
|
+
</body>
|
|
858
|
+
</html>
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
```asjs
|
|
862
|
+
<section>
|
|
863
|
+
<h1><%= headline %></h1>
|
|
864
|
+
<p><%= description %></p>
|
|
865
|
+
</section>
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
Bu kadarı çalışan bir ASJS sayfası için yeterlidir.
|
|
869
|
+
Aynı mantığın gerçek klasör örneği repo içinde `example-minimal/` altında da var.
|
|
870
|
+
Çalıştırmak için `npm run example:minimal` kullanabilirsin.
|
|
871
|
+
|
|
592
872
|
### Layout kullanımı
|
|
593
873
|
|
|
594
874
|
```asjs
|
|
@@ -611,6 +891,86 @@ app.listen(3000)
|
|
|
611
891
|
`asjs.clientTags()` çağrısı dahili ASJS stil ve router etiketlerini otomatik üretir.
|
|
612
892
|
`theme: true` ise açık renkli, kurumsal WebAS temasını yükler.
|
|
613
893
|
|
|
894
|
+
### En kolay Express SPA kurulumu
|
|
895
|
+
|
|
896
|
+
En kolay entegrasyon için 3 dosya yeterlidir: `app.js`, `views/layouts/main.asjs` ve örnek olarak `views/home.asjs`.
|
|
897
|
+
|
|
898
|
+
```js
|
|
899
|
+
const express = require('express')
|
|
900
|
+
const { setupAsjs } = require('asjs-express')
|
|
901
|
+
|
|
902
|
+
const app = express()
|
|
903
|
+
const asjs = setupAsjs(app, {
|
|
904
|
+
rootDir: __dirname,
|
|
905
|
+
defaultLayout: 'layouts/main',
|
|
906
|
+
navItems: [
|
|
907
|
+
{ href: '/', label: 'Ana Sayfa' },
|
|
908
|
+
{ href: '/about', label: 'Hakkında' },
|
|
909
|
+
{ href: '/contact', label: 'İletişim' }
|
|
910
|
+
],
|
|
911
|
+
transitions: 'fade',
|
|
912
|
+
prefetch: true,
|
|
913
|
+
loadingBar: true
|
|
914
|
+
})
|
|
915
|
+
|
|
916
|
+
const buildPage = asjs.createPageModel({
|
|
917
|
+
pageDescription: 'İlk ASJS sayfam',
|
|
918
|
+
renderSummary: []
|
|
919
|
+
})
|
|
920
|
+
|
|
921
|
+
app.get('/', asjs.createPageRoute('home', {
|
|
922
|
+
buildPage,
|
|
923
|
+
renderState: {
|
|
924
|
+
delay: 120,
|
|
925
|
+
label: 'Ana sayfa hazır',
|
|
926
|
+
narrative: 'ASJS bu sayfa modelini HTML cevabı gitmeden önce hazırladı.'
|
|
927
|
+
}
|
|
928
|
+
}, () => ({
|
|
929
|
+
title: 'Ana Sayfa',
|
|
930
|
+
heroTitle: 'Merhaba ASJS',
|
|
931
|
+
heroText: 'Bu sayfa server-rendered çalışır ve SPA gezinme hemen aktiftir.'
|
|
932
|
+
})))
|
|
933
|
+
|
|
934
|
+
app.listen(3000)
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
```asjs
|
|
938
|
+
<!DOCTYPE html>
|
|
939
|
+
<html>
|
|
940
|
+
<head>
|
|
941
|
+
<meta charset="UTF-8">
|
|
942
|
+
<title><%= title %></title>
|
|
943
|
+
<%- asjs.clientTags({ preload: true, theme: true }) %>
|
|
944
|
+
</head>
|
|
945
|
+
<body<%- asjs.bodyAttrs() %>>
|
|
946
|
+
<%- asjs.progressMarkup() %>
|
|
947
|
+
<%- asjs.header() %>
|
|
948
|
+
<main<%- asjs.viewAttrs() %>>
|
|
949
|
+
<%- body %>
|
|
950
|
+
</main>
|
|
951
|
+
</body>
|
|
952
|
+
</html>
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
Varsayılan SPA yükleme ve sayfa geçiş animasyonları zaten dahildir.
|
|
956
|
+
Ek bir class vermezsen ASJS dahili loading bar ve transition görünümünü korur.
|
|
957
|
+
Sonradan üstüne kendi stilini yazmak istersen sistemi komple değiştirmek yerine sadece class eklersin.
|
|
958
|
+
|
|
959
|
+
```js
|
|
960
|
+
const asjs = setupAsjs(app, {
|
|
961
|
+
rootDir: __dirname,
|
|
962
|
+
defaultLayout: 'layouts/main',
|
|
963
|
+
transitions: {
|
|
964
|
+
name: 'fade',
|
|
965
|
+
className: 'my-page-motion'
|
|
966
|
+
},
|
|
967
|
+
loadingBar: {
|
|
968
|
+
enabled: true,
|
|
969
|
+
className: 'my-loading-bar'
|
|
970
|
+
}
|
|
971
|
+
})
|
|
972
|
+
```
|
|
973
|
+
|
|
614
974
|
### Dahili SPA header sistemi
|
|
615
975
|
|
|
616
976
|
ASJS artık varsayılan SPA akışı için dahili bir header helper ile geliyor.
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const packageJson = require('../package.json');
|
|
8
|
+
|
|
9
|
+
const TEMPLATE_REGISTRY = {
|
|
10
|
+
minimal: {
|
|
11
|
+
label: 'Minimal starter',
|
|
12
|
+
directory: path.join(__dirname, '..', 'templates', 'minimal')
|
|
13
|
+
},
|
|
14
|
+
starter: {
|
|
15
|
+
label: 'Starter app',
|
|
16
|
+
directory: path.join(__dirname, '..', 'templates', 'starter')
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function printHelp() {
|
|
21
|
+
console.log(`ASJS project creator\n\nUsage:\n npx asjs-express my-app\n npx asjs-express create my-app --template starter\n\nOptions:\n --template <name> Template to use: ${Object.keys(TEMPLATE_REGISTRY).join(', ')}\n --skip-install Do not run npm install automatically\n --force Allow writing into an existing directory\n --help Show this help message\n`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const args = [...argv];
|
|
26
|
+
const options = {
|
|
27
|
+
force: false,
|
|
28
|
+
skipInstall: false,
|
|
29
|
+
template: 'minimal'
|
|
30
|
+
};
|
|
31
|
+
let target = '';
|
|
32
|
+
|
|
33
|
+
if (args[0] === 'create') {
|
|
34
|
+
args.shift();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
while (args.length) {
|
|
38
|
+
const current = args.shift();
|
|
39
|
+
|
|
40
|
+
if (!current) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (current === '--help' || current === '-h') {
|
|
45
|
+
options.help = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (current === '--skip-install') {
|
|
50
|
+
options.skipInstall = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (current === '--force') {
|
|
55
|
+
options.force = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (current === '--template') {
|
|
60
|
+
options.template = String(args.shift() || '').trim() || options.template;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (current.startsWith('--template=')) {
|
|
65
|
+
options.template = current.split('=')[1] || options.template;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!target) {
|
|
70
|
+
target = current;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
options,
|
|
76
|
+
target
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sanitizePackageName(value) {
|
|
81
|
+
return String(value || 'asjs-app')
|
|
82
|
+
.trim()
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replace(/[^a-z0-9-_]+/g, '-')
|
|
85
|
+
.replace(/^-+|-+$/g, '') || 'asjs-app';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toDisplayName(value) {
|
|
89
|
+
return String(value || 'ASJS App')
|
|
90
|
+
.replace(/[-_]+/g, ' ')
|
|
91
|
+
.replace(/\s+/g, ' ')
|
|
92
|
+
.trim()
|
|
93
|
+
.split(' ')
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
96
|
+
.join(' ') || 'ASJS App';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function ensureTargetDirectory(targetDirectory, force) {
|
|
100
|
+
if (!fs.existsSync(targetDirectory)) {
|
|
101
|
+
fs.mkdirSync(targetDirectory, { recursive: true });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const stats = fs.statSync(targetDirectory);
|
|
106
|
+
if (!stats.isDirectory()) {
|
|
107
|
+
throw new Error(`Target exists but is not a directory: ${targetDirectory}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const existingEntries = fs.readdirSync(targetDirectory);
|
|
111
|
+
if (existingEntries.length > 0 && !force) {
|
|
112
|
+
throw new Error(`Target directory is not empty: ${targetDirectory}\nUse --force if you want to write files into it.`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function renderTemplate(content, variables) {
|
|
117
|
+
return content.replace(/__([A-Z0-9_]+)__/g, (match, token) => {
|
|
118
|
+
return Object.prototype.hasOwnProperty.call(variables, token) ? variables[token] : match;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveOutputFileName(fileName) {
|
|
123
|
+
if (fileName === 'gitignore.tpl') {
|
|
124
|
+
return '.gitignore';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return fileName.endsWith('.tpl') ? fileName.slice(0, -4) : fileName;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function copyTemplateDirectory(sourceDirectory, targetDirectory, variables) {
|
|
131
|
+
const entries = fs.readdirSync(sourceDirectory, { withFileTypes: true });
|
|
132
|
+
|
|
133
|
+
entries.forEach((entry) => {
|
|
134
|
+
const sourcePath = path.join(sourceDirectory, entry.name);
|
|
135
|
+
const outputName = resolveOutputFileName(entry.name);
|
|
136
|
+
const targetPath = path.join(targetDirectory, outputName);
|
|
137
|
+
|
|
138
|
+
if (entry.isDirectory()) {
|
|
139
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
140
|
+
copyTemplateDirectory(sourcePath, targetPath, variables);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const content = fs.readFileSync(sourcePath, 'utf8');
|
|
145
|
+
const rendered = renderTemplate(content, variables);
|
|
146
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
147
|
+
fs.writeFileSync(targetPath, rendered, 'utf8');
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function runInstall(targetDirectory) {
|
|
152
|
+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
153
|
+
const result = spawnSync(npmCommand, ['install'], {
|
|
154
|
+
cwd: targetDirectory,
|
|
155
|
+
stdio: 'inherit'
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return result.status === 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function main() {
|
|
162
|
+
const { options, target } = parseArgs(process.argv.slice(2));
|
|
163
|
+
|
|
164
|
+
if (options.help || !target) {
|
|
165
|
+
printHelp();
|
|
166
|
+
process.exit(options.help ? 0 : 1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const template = TEMPLATE_REGISTRY[options.template];
|
|
170
|
+
if (!template) {
|
|
171
|
+
console.error(`Unknown template: ${options.template}`);
|
|
172
|
+
console.error(`Available templates: ${Object.keys(TEMPLATE_REGISTRY).join(', ')}`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const targetDirectory = path.resolve(process.cwd(), target);
|
|
177
|
+
const packageName = sanitizePackageName(path.basename(targetDirectory));
|
|
178
|
+
const displayName = toDisplayName(packageName);
|
|
179
|
+
const expressVersion = packageJson.dependencies && packageJson.dependencies.express
|
|
180
|
+
? packageJson.dependencies.express
|
|
181
|
+
: '^4.21.2';
|
|
182
|
+
|
|
183
|
+
const variables = {
|
|
184
|
+
APP_TITLE: displayName,
|
|
185
|
+
ASJS_VERSION: packageJson.version,
|
|
186
|
+
EXPRESS_VERSION: expressVersion,
|
|
187
|
+
PACKAGE_NAME: packageName,
|
|
188
|
+
PORT: '3000',
|
|
189
|
+
TEMPLATE_LABEL: template.label,
|
|
190
|
+
YEAR: String(new Date().getFullYear())
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
ensureTargetDirectory(targetDirectory, options.force);
|
|
195
|
+
copyTemplateDirectory(template.directory, targetDirectory, variables);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error(error.message);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log(`\nCreated ${template.label} in ${targetDirectory}`);
|
|
202
|
+
|
|
203
|
+
if (!options.skipInstall) {
|
|
204
|
+
console.log('\nInstalling dependencies...');
|
|
205
|
+
const installed = runInstall(targetDirectory);
|
|
206
|
+
|
|
207
|
+
if (!installed) {
|
|
208
|
+
console.log('\nDependency install did not complete successfully. You can run it manually:');
|
|
209
|
+
console.log(` cd ${path.basename(targetDirectory)}`);
|
|
210
|
+
console.log(' npm install');
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log('\nNext steps:');
|
|
216
|
+
console.log(` cd ${path.basename(targetDirectory)}`);
|
|
217
|
+
if (options.skipInstall) {
|
|
218
|
+
console.log(' npm install');
|
|
219
|
+
}
|
|
220
|
+
console.log(' npm run dev');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
main();
|