asjs-express 1.3.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
@@ -690,6 +722,7 @@ WebAS yüzeyi, bu iki geliştiricinin birlikte şekillendirdiği düzenli, açı
690
722
  - Geçiş, prefetch ve loading bar destekli istemci yönlendirmesi
691
723
  - İşaretli formlar için dahili async form akışı
692
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
693
726
  - Public klasöre dosya kopyalamadan paket içinden asset servisi
694
727
 
695
728
  ### Kurulum
@@ -698,6 +731,37 @@ WebAS yüzeyi, bu iki geliştiricinin birlikte şekillendirdiği düzenli, açı
698
731
  npm install asjs-express
699
732
  ```
700
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
+
701
765
  ### Hızlı başlangıç
702
766
 
703
767
  ```js
@@ -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();
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "asjs-express",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Lightweight Express view engine with EJS-like templates, layouts, async page rendering, form enhancement, and a built-in client router.",
5
5
  "main": "index.js",
6
+ "bin": {
7
+ "asjs-express": "./bin/create-asjs-app.js",
8
+ "create-asjs-app": "./bin/create-asjs-app.js"
9
+ },
6
10
  "author": {
7
11
  "name": "Acarfx",
8
12
  "url": "https://acarfx.com"
@@ -21,9 +25,11 @@
21
25
  "./package.json": "./package.json"
22
26
  },
23
27
  "files": [
28
+ "bin",
24
29
  "index.js",
25
30
  "lib",
26
- "README.md"
31
+ "README.md",
32
+ "templates"
27
33
  ],
28
34
  "scripts": {
29
35
  "start": "node example-express/app.js",
@@ -36,14 +42,17 @@
36
42
  "keywords": [
37
43
  "asjs",
38
44
  "component",
45
+ "cli",
39
46
  "ejs",
40
47
  "express",
41
48
  "express-view-engine",
42
49
  "forms",
43
50
  "layout",
44
51
  "prefetch",
52
+ "scaffold",
45
53
  "server-rendered",
46
54
  "ssr",
55
+ "starter-template",
47
56
  "template-engine",
48
57
  "view-engine",
49
58
  "spa-navigation"
@@ -0,0 +1,12 @@
1
+ # __APP_TITLE__
2
+
3
+ This project was generated with `npx asjs-express`.
4
+
5
+ ## Run
6
+
7
+ ```bash
8
+ npm install
9
+ npm run dev
10
+ ```
11
+
12
+ Open http://localhost:__PORT__
@@ -0,0 +1,29 @@
1
+ const express = require('express');
2
+ const { setupAsjs } = require('asjs-express');
3
+
4
+ const app = express();
5
+ const port = process.env.PORT || __PORT__;
6
+
7
+ const asjs = setupAsjs(app, {
8
+ rootDir: __dirname,
9
+ defaultLayout: 'layouts/main',
10
+ navItems: [
11
+ { href: '/', label: 'Home', activeMode: 'exact' }
12
+ ],
13
+ transitions: 'fade',
14
+ prefetch: true,
15
+ loadingBar: true
16
+ });
17
+
18
+ app.get('/', asjs.page('home', {
19
+ title: '__APP_TITLE__',
20
+ headline: '__APP_TITLE__ is ready.',
21
+ description: 'Header, router, loading bar, and SPA-ready page transitions are already connected.',
22
+ nextStep: 'Add a second route whenever you need it. ASJS will keep the same internal navigation flow.'
23
+ }));
24
+
25
+ app.use(asjs.errors());
26
+
27
+ app.listen(port, () => {
28
+ console.log(`__APP_TITLE__ running at http://localhost:${port}`);
29
+ });
@@ -0,0 +1,4 @@
1
+ node_modules/
2
+ .env
3
+ .DS_Store
4
+ npm-debug.log*
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "__PACKAGE_NAME__",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "ASJS minimal starter generated by npx asjs-express",
6
+ "main": "app.js",
7
+ "scripts": {
8
+ "dev": "node app.js",
9
+ "start": "node app.js"
10
+ },
11
+ "dependencies": {
12
+ "asjs-express": "^__ASJS_VERSION__",
13
+ "express": "__EXPRESS_VERSION__"
14
+ }
15
+ }
@@ -0,0 +1,5 @@
1
+ <section style="max-width: 720px; margin: 40px auto; padding: 24px;">
2
+ <h1><%= headline %></h1>
3
+ <p><%= description %></p>
4
+ <p><%= nextStep %></p>
5
+ </section>
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title><%= title %></title>
7
+ <%- asjs.clientTags({ preload: true, theme: true }) %>
8
+ </head>
9
+ <body<%- asjs.bodyAttrs() %>>
10
+ <%- asjs.progressMarkup() %>
11
+ <%- asjs.header() %>
12
+ <main class="view-frame"<%- asjs.viewAttrs() %>>
13
+ <%- body %>
14
+ </main>
15
+ </body>
16
+ </html>
@@ -0,0 +1,12 @@
1
+ # __APP_TITLE__
2
+
3
+ This project was generated with `npx asjs-express --template starter`.
4
+
5
+ ## Run
6
+
7
+ ```bash
8
+ npm install
9
+ npm run dev
10
+ ```
11
+
12
+ Open http://localhost:__PORT__
@@ -0,0 +1,54 @@
1
+ const express = require('express');
2
+ const { setupAsjs } = require('asjs-express');
3
+
4
+ const app = express();
5
+ const port = process.env.PORT || __PORT__;
6
+
7
+ const asjs = setupAsjs(app, {
8
+ rootDir: __dirname,
9
+ defaultLayout: 'layouts/main',
10
+ navItems: [
11
+ { href: '/', label: 'Home', activeMode: 'exact' },
12
+ { href: '/about', label: 'About', activeMode: 'exact' }
13
+ ],
14
+ transitions: 'fade',
15
+ prefetch: true,
16
+ loadingBar: true
17
+ });
18
+
19
+ const buildPage = asjs.createPageModel({
20
+ pageDescription: '__APP_TITLE__ starter created by ASJS.',
21
+ renderSummary: []
22
+ });
23
+
24
+ app.get('/', asjs.createPageRoute('home', {
25
+ buildPage,
26
+ renderState: {
27
+ delay: 120,
28
+ label: 'Home page ready',
29
+ narrative: 'ASJS prepared the page model before the HTML response reached the browser.'
30
+ }
31
+ }, () => ({
32
+ title: '__APP_TITLE__',
33
+ heroTitle: 'Welcome to __APP_TITLE__.',
34
+ heroText: 'This starter already includes the built-in header, SPA navigation, and a render-state example.'
35
+ })));
36
+
37
+ app.get('/about', asjs.createPageRoute('about', {
38
+ buildPage,
39
+ renderState: {
40
+ delay: 140,
41
+ label: 'About page ready',
42
+ narrative: 'The second route uses the same layout and internal navigation flow.'
43
+ }
44
+ }, () => ({
45
+ title: 'About | __APP_TITLE__',
46
+ heroTitle: 'The starter is ready to grow.',
47
+ heroText: 'Add new routes, layouts, forms, and partials without leaving the ASJS server-rendered flow.'
48
+ })));
49
+
50
+ app.use(asjs.errors());
51
+
52
+ app.listen(port, () => {
53
+ console.log(`__APP_TITLE__ running at http://localhost:${port}`);
54
+ });
@@ -0,0 +1,4 @@
1
+ node_modules/
2
+ .env
3
+ .DS_Store
4
+ npm-debug.log*
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "__PACKAGE_NAME__",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "ASJS starter app generated by npx asjs-express",
6
+ "main": "app.js",
7
+ "scripts": {
8
+ "dev": "node app.js",
9
+ "start": "node app.js"
10
+ },
11
+ "dependencies": {
12
+ "asjs-express": "^__ASJS_VERSION__",
13
+ "express": "__EXPRESS_VERSION__"
14
+ }
15
+ }
@@ -0,0 +1,10 @@
1
+ <section style="max-width: 760px; margin: 40px auto; padding: 24px;">
2
+ <span>Starter about</span>
3
+ <h1><%= heroTitle %></h1>
4
+ <p><%= heroText %></p>
5
+ <div style="margin-top: 20px;">
6
+ <% renderSummary.forEach((item) => { %>
7
+ <div><strong><%= item.label %>:</strong> <%= item.value %></div>
8
+ <% }) %>
9
+ </div>
10
+ </section>
@@ -0,0 +1,10 @@
1
+ <section style="max-width: 760px; margin: 40px auto; padding: 24px;">
2
+ <span>Starter home</span>
3
+ <h1><%= heroTitle %></h1>
4
+ <p><%= heroText %></p>
5
+ <div style="margin-top: 20px;">
6
+ <% renderSummary.forEach((item) => { %>
7
+ <div><strong><%= item.label %>:</strong> <%= item.value %></div>
8
+ <% }) %>
9
+ </div>
10
+ </section>
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title><%= title %></title>
7
+ <%- asjs.clientTags({ preload: true, theme: true }) %>
8
+ </head>
9
+ <body<%- asjs.bodyAttrs() %>>
10
+ <%- asjs.progressMarkup() %>
11
+ <%- asjs.header() %>
12
+ <main class="view-frame"<%- asjs.viewAttrs() %>>
13
+ <%- body %>
14
+ </main>
15
+ </body>
16
+ </html>