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 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();