webspresso 0.0.7 → 0.0.9

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
@@ -49,13 +49,19 @@ npm run dev
49
49
 
50
50
  ## CLI Commands
51
51
 
52
- ### `webspresso new <project-name>`
52
+ ### `webspresso new [project-name]`
53
53
 
54
54
  Create a new Webspresso project with Tailwind CSS (default).
55
55
 
56
56
  ```bash
57
+ # Create in a new directory
57
58
  webspresso new my-app
58
59
 
60
+ # Create in current directory (interactive)
61
+ webspresso new
62
+ # → Prompts: "Install in current directory?"
63
+ # → If yes, asks for project name (for package.json)
64
+
59
65
  # Auto install dependencies and build CSS
60
66
  webspresso new my-app --install
61
67
 
@@ -63,6 +69,11 @@ webspresso new my-app --install
63
69
  webspresso new my-app --no-tailwind
64
70
  ```
65
71
 
72
+ **Interactive Mode (no arguments):**
73
+ - Asks if you want to install in the current directory
74
+ - If current directory is not empty, shows a warning
75
+ - Prompts for project name (defaults to current folder name)
76
+
66
77
  Options:
67
78
  - `-i, --install` - Auto run `npm install` and `npm run build:css`
68
79
  - `--no-tailwind` - Skip Tailwind CSS setup
@@ -602,7 +613,6 @@ module.exports = {
602
613
  Add JSON files to `pages/locales/`:
603
614
 
604
615
  ```json
605
- // pages/locales/en.json
606
616
  {
607
617
  "nav": {
608
618
  "home": "Home",
package/bin/webspresso.js CHANGED
@@ -18,25 +18,113 @@ program
18
18
 
19
19
  // New project command
20
20
  program
21
- .command('new <project-name>')
21
+ .command('new [project-name]')
22
22
  .description('Create a new Webspresso project')
23
23
  .option('-t, --template <template>', 'Template to use (minimal, full)', 'minimal')
24
24
  .option('--no-tailwind', 'Skip Tailwind CSS setup')
25
25
  .option('-i, --install', 'Auto install dependencies and build CSS')
26
- .action(async (projectName, options) => {
26
+ .action(async (projectNameArg, options) => {
27
27
  const useTailwind = options.tailwind !== false;
28
28
  const autoInstall = options.install === true;
29
- const projectPath = path.resolve(projectName);
30
29
 
31
- if (fs.existsSync(projectPath)) {
32
- console.error(`❌ Directory ${projectName} already exists!`);
33
- process.exit(1);
30
+ let projectName;
31
+ let projectPath;
32
+ let useCurrentDir = false;
33
+
34
+ if (!projectNameArg) {
35
+ // No project name provided - ask if they want to use current directory
36
+ const currentDirName = path.basename(process.cwd());
37
+ const currentDirFiles = fs.readdirSync(process.cwd());
38
+ const hasExistingFiles = currentDirFiles.some(f => !f.startsWith('.'));
39
+
40
+ const { useCurrent } = await inquirer.prompt([
41
+ {
42
+ type: 'confirm',
43
+ name: 'useCurrent',
44
+ message: `Install in current directory (${currentDirName})?`,
45
+ default: true
46
+ }
47
+ ]);
48
+
49
+ if (useCurrent) {
50
+ useCurrentDir = true;
51
+ projectPath = process.cwd();
52
+
53
+ // Check for existing Webspresso files
54
+ if (fs.existsSync(path.join(projectPath, 'server.js')) ||
55
+ fs.existsSync(path.join(projectPath, 'pages'))) {
56
+ console.error('❌ Current directory already contains a Webspresso project!');
57
+ process.exit(1);
58
+ }
59
+
60
+ // Warn if there are existing files
61
+ if (hasExistingFiles) {
62
+ const { continueAnyway } = await inquirer.prompt([
63
+ {
64
+ type: 'confirm',
65
+ name: 'continueAnyway',
66
+ message: '⚠️ Current directory is not empty. Continue anyway?',
67
+ default: false
68
+ }
69
+ ]);
70
+
71
+ if (!continueAnyway) {
72
+ console.log('Cancelled.');
73
+ process.exit(0);
74
+ }
75
+ }
76
+
77
+ // Ask for project name (for package.json)
78
+ const { name } = await inquirer.prompt([
79
+ {
80
+ type: 'input',
81
+ name: 'name',
82
+ message: 'Project name:',
83
+ default: currentDirName,
84
+ validate: (input) => {
85
+ if (!input.trim()) return 'Project name is required';
86
+ if (!/^[a-z0-9-_]+$/i.test(input)) return 'Project name can only contain letters, numbers, hyphens, and underscores';
87
+ return true;
88
+ }
89
+ }
90
+ ]);
91
+
92
+ projectName = name;
93
+ } else {
94
+ // Ask for directory name
95
+ const { dirName } = await inquirer.prompt([
96
+ {
97
+ type: 'input',
98
+ name: 'dirName',
99
+ message: 'Project directory name:',
100
+ validate: (input) => {
101
+ if (!input.trim()) return 'Directory name is required';
102
+ if (!/^[a-z0-9-_]+$/i.test(input)) return 'Directory name can only contain letters, numbers, hyphens, and underscores';
103
+ if (fs.existsSync(path.resolve(input))) return `Directory ${input} already exists!`;
104
+ return true;
105
+ }
106
+ }
107
+ ]);
108
+
109
+ projectName = dirName;
110
+ projectPath = path.resolve(dirName);
111
+ }
112
+ } else {
113
+ projectName = projectNameArg;
114
+ projectPath = path.resolve(projectNameArg);
115
+
116
+ if (fs.existsSync(projectPath)) {
117
+ console.error(`❌ Directory ${projectName} already exists!`);
118
+ process.exit(1);
119
+ }
34
120
  }
35
121
 
36
122
  console.log(`\n🚀 Creating new Webspresso project: ${projectName}\n`);
37
123
 
38
- // Create directory structure
39
- fs.mkdirSync(projectPath, { recursive: true });
124
+ // Create directory structure (skip root if using current dir)
125
+ if (!useCurrentDir) {
126
+ fs.mkdirSync(projectPath, { recursive: true });
127
+ }
40
128
  fs.mkdirSync(path.join(projectPath, 'pages'), { recursive: true });
41
129
  fs.mkdirSync(path.join(projectPath, 'pages', 'locales'), { recursive: true });
42
130
  fs.mkdirSync(path.join(projectPath, 'views'), { recursive: true });
@@ -361,7 +449,9 @@ module.exports = {
361
449
 
362
450
  console.log('\n✅ Project ready!\n');
363
451
  console.log('Start developing:');
364
- console.log(` cd ${projectName}`);
452
+ if (!useCurrentDir) {
453
+ console.log(` cd ${projectName}`);
454
+ }
365
455
  console.log(' npm run dev\n');
366
456
  } catch (err) {
367
457
  console.error('❌ Installation failed:', err.message);
@@ -370,7 +460,9 @@ module.exports = {
370
460
  } else {
371
461
  console.log('\n✅ Project created successfully!\n');
372
462
  console.log('Next steps:');
373
- console.log(` cd ${projectName}`);
463
+ if (!useCurrentDir) {
464
+ console.log(` cd ${projectName}`);
465
+ }
374
466
  console.log(' npm install');
375
467
  if (useTailwind) {
376
468
  console.log(' npm run build:css');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -33,7 +33,8 @@
33
33
  "bin/",
34
34
  "src/",
35
35
  "utils/",
36
- "core/"
36
+ "core/",
37
+ "plugins/"
37
38
  ],
38
39
  "dependencies": {
39
40
  "commander": "^11.1.0",
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Webspresso Analytics Plugin
3
+ * Adds tracking scripts and verification meta tags for Google, Yandex, and Bing
4
+ */
5
+
6
+ /**
7
+ * Generate Google Analytics (gtag.js) script
8
+ */
9
+ function generateGtagScript(config) {
10
+ if (!config || !config.measurementId) return '';
11
+
12
+ const { measurementId, adsId } = config;
13
+
14
+ let script = `<!-- Google Analytics -->
15
+ <script async src="https://www.googletagmanager.com/gtag/js?id=${measurementId}"></script>
16
+ <script>
17
+ window.dataLayer = window.dataLayer || [];
18
+ function gtag(){dataLayer.push(arguments);}
19
+ gtag('js', new Date());
20
+ gtag('config', '${measurementId}');`;
21
+
22
+ if (adsId) {
23
+ script += `
24
+ gtag('config', '${adsId}');`;
25
+ }
26
+
27
+ script += `
28
+ </script>`;
29
+
30
+ return script;
31
+ }
32
+
33
+ /**
34
+ * Generate Google Tag Manager script
35
+ */
36
+ function generateGtmScript(config) {
37
+ if (!config || !config.containerId) return '';
38
+
39
+ const { containerId } = config;
40
+
41
+ return `<!-- Google Tag Manager -->
42
+ <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
43
+ new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
44
+ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
45
+ 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
46
+ })(window,document,'script','dataLayer','${containerId}');</script>`;
47
+ }
48
+
49
+ /**
50
+ * Generate Google Tag Manager noscript fallback
51
+ */
52
+ function generateGtmNoscript(config) {
53
+ if (!config || !config.containerId) return '';
54
+
55
+ return `<!-- Google Tag Manager (noscript) -->
56
+ <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=${config.containerId}"
57
+ height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>`;
58
+ }
59
+
60
+ /**
61
+ * Generate Yandex.Metrika script
62
+ */
63
+ function generateYandexScript(config) {
64
+ if (!config || !config.counterId) return '';
65
+
66
+ const { counterId, clickmap = true, trackLinks = true, accurateTrackBounce = true, webvisor = false } = config;
67
+
68
+ return `<!-- Yandex.Metrika -->
69
+ <script>
70
+ (function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
71
+ m[i].l=1*new Date();
72
+ for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
73
+ k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
74
+ (window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
75
+
76
+ ym(${counterId}, "init", {
77
+ clickmap:${clickmap},
78
+ trackLinks:${trackLinks},
79
+ accurateTrackBounce:${accurateTrackBounce}${webvisor ? ',\n webvisor:true' : ''}
80
+ });
81
+ </script>
82
+ <noscript><div><img src="https://mc.yandex.ru/watch/${counterId}" style="position:absolute; left:-9999px;" alt="" /></div></noscript>`;
83
+ }
84
+
85
+ /**
86
+ * Generate Microsoft/Bing UET script
87
+ */
88
+ function generateBingScript(config) {
89
+ if (!config || !config.uetId) return '';
90
+
91
+ const { uetId } = config;
92
+
93
+ return `<!-- Microsoft UET -->
94
+ <script>
95
+ (function(w,d,t,r,u){var f,n,i;w[u]=w[u]||[],f=function(){var o={ti:"${uetId}",enableAutoSpaTracking:true};
96
+ o.q=w[u],w[u]=new UET(o),w[u].push("pageLoad")},n=d.createElement(t),n.src=r,n.async=1,n.onload=n.onreadystatechange=function(){
97
+ var s=this.readyState;s&&s!=="loaded"&&s!=="complete"||(f(),n.onload=n.onreadystatechange=null)},i=d.getElementsByTagName(t)[0],i.parentNode.insertBefore(n,i)
98
+ })(window,document,"script","//bat.bing.com/bat.js","uetq");
99
+ </script>`;
100
+ }
101
+
102
+ /**
103
+ * Generate Facebook Pixel script
104
+ */
105
+ function generateFacebookScript(config) {
106
+ if (!config || !config.pixelId) return '';
107
+
108
+ const { pixelId } = config;
109
+
110
+ return `<!-- Facebook Pixel -->
111
+ <script>
112
+ !function(f,b,e,v,n,t,s)
113
+ {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
114
+ n.callMethod.apply(n,arguments):n.queue.push(arguments)};
115
+ if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
116
+ n.queue=[];t=b.createElement(e);t.async=!0;
117
+ t.src=v;s=b.getElementsByTagName(e)[0];
118
+ s.parentNode.insertBefore(t,s)}(window, document,'script',
119
+ 'https://connect.facebook.net/en_US/fbevents.js');
120
+ fbq('init', '${pixelId}');
121
+ fbq('track', 'PageView');
122
+ </script>
123
+ <noscript><img height="1" width="1" style="display:none"
124
+ src="https://www.facebook.com/tr?id=${pixelId}&ev=PageView&noscript=1"/></noscript>`;
125
+ }
126
+
127
+ /**
128
+ * Generate verification meta tags
129
+ */
130
+ function generateVerificationTags(options) {
131
+ const tags = [];
132
+
133
+ if (options.google?.verificationCode) {
134
+ tags.push(`<meta name="google-site-verification" content="${options.google.verificationCode}">`);
135
+ }
136
+
137
+ if (options.yandex?.verificationCode) {
138
+ tags.push(`<meta name="yandex-verification" content="${options.yandex.verificationCode}">`);
139
+ }
140
+
141
+ if (options.bing?.verificationCode) {
142
+ tags.push(`<meta name="msvalidate.01" content="${options.bing.verificationCode}">`);
143
+ }
144
+
145
+ if (options.facebook?.domainVerification) {
146
+ tags.push(`<meta name="facebook-domain-verification" content="${options.facebook.domainVerification}">`);
147
+ }
148
+
149
+ if (options.pinterest?.verificationCode) {
150
+ tags.push(`<meta name="p:domain_verify" content="${options.pinterest.verificationCode}">`);
151
+ }
152
+
153
+ return tags.join('\n');
154
+ }
155
+
156
+ /**
157
+ * Create the analytics plugin
158
+ * @param {Object} options - Plugin options
159
+ * @param {Object} options.google - Google Analytics config
160
+ * @param {string} options.google.measurementId - GA4 Measurement ID (G-XXXXXXXXXX)
161
+ * @param {string} options.google.adsId - Google Ads ID (AW-XXXXXXXXXX)
162
+ * @param {string} options.google.verificationCode - Search Console verification
163
+ * @param {Object} options.gtm - Google Tag Manager config
164
+ * @param {string} options.gtm.containerId - GTM Container ID (GTM-XXXXXXX)
165
+ * @param {Object} options.yandex - Yandex.Metrika config
166
+ * @param {string} options.yandex.counterId - Yandex counter ID
167
+ * @param {string} options.yandex.verificationCode - Yandex Webmaster verification
168
+ * @param {boolean} options.yandex.webvisor - Enable Webvisor
169
+ * @param {Object} options.bing - Microsoft/Bing config
170
+ * @param {string} options.bing.uetId - UET tag ID
171
+ * @param {string} options.bing.verificationCode - Bing Webmaster verification
172
+ * @param {Object} options.facebook - Facebook/Meta config
173
+ * @param {string} options.facebook.pixelId - Facebook Pixel ID
174
+ * @param {string} options.facebook.domainVerification - Domain verification code
175
+ */
176
+ function analyticsPlugin(options = {}) {
177
+ const { google, gtm, yandex, bing, facebook, pinterest } = options;
178
+
179
+ return {
180
+ name: 'analytics',
181
+ version: '1.0.0',
182
+ _options: options,
183
+
184
+ /**
185
+ * Public API for other plugins
186
+ */
187
+ api: {
188
+ /**
189
+ * Get configuration
190
+ */
191
+ getConfig() {
192
+ return { ...options };
193
+ },
194
+
195
+ /**
196
+ * Check if a tracker is configured
197
+ */
198
+ hasTracker(name) {
199
+ return !!options[name];
200
+ }
201
+ },
202
+
203
+ /**
204
+ * Register helpers
205
+ */
206
+ register(ctx) {
207
+ // Google Analytics (gtag.js)
208
+ ctx.addHelper('gtag', () => generateGtagScript(google));
209
+
210
+ // Google Tag Manager
211
+ ctx.addHelper('gtm', () => generateGtmScript(gtm));
212
+ ctx.addHelper('gtmNoscript', () => generateGtmNoscript(gtm));
213
+
214
+ // Yandex.Metrika
215
+ ctx.addHelper('yandexMetrika', () => generateYandexScript(yandex));
216
+
217
+ // Microsoft/Bing UET
218
+ ctx.addHelper('bingUET', () => generateBingScript(bing));
219
+
220
+ // Facebook Pixel
221
+ ctx.addHelper('facebookPixel', () => generateFacebookScript(facebook));
222
+
223
+ // Verification meta tags
224
+ ctx.addHelper('verificationTags', () => generateVerificationTags(options));
225
+
226
+ // All analytics scripts combined
227
+ ctx.addHelper('allAnalytics', () => {
228
+ const scripts = [];
229
+
230
+ if (google) scripts.push(generateGtagScript(google));
231
+ if (gtm) scripts.push(generateGtmScript(gtm));
232
+ if (yandex) scripts.push(generateYandexScript(yandex));
233
+ if (bing) scripts.push(generateBingScript(bing));
234
+ if (facebook) scripts.push(generateFacebookScript(facebook));
235
+
236
+ return scripts.filter(Boolean).join('\n\n');
237
+ });
238
+
239
+ // Head scripts (verification + analytics)
240
+ ctx.addHelper('analyticsHead', () => {
241
+ const parts = [];
242
+
243
+ // Verification tags first
244
+ const verificationTags = generateVerificationTags(options);
245
+ if (verificationTags) parts.push(verificationTags);
246
+
247
+ // GTM should be as early as possible
248
+ if (gtm) parts.push(generateGtmScript(gtm));
249
+
250
+ // Other analytics
251
+ if (google) parts.push(generateGtagScript(google));
252
+ if (yandex) parts.push(generateYandexScript(yandex));
253
+ if (bing) parts.push(generateBingScript(bing));
254
+ if (facebook) parts.push(generateFacebookScript(facebook));
255
+
256
+ return parts.filter(Boolean).join('\n\n');
257
+ });
258
+
259
+ // Body open scripts (GTM noscript)
260
+ ctx.addHelper('analyticsBodyOpen', () => {
261
+ return generateGtmNoscript(gtm);
262
+ });
263
+ }
264
+ };
265
+ }
266
+
267
+ module.exports = analyticsPlugin;
268
+
269
+
270
+