@jsonpages/cli 3.0.32 → 3.0.34

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonpages/cli",
3
- "version": "3.0.32",
3
+ "version": "3.0.34",
4
4
  "description": "JsonPages CLI - Sovereign Projection Engine",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -157,6 +157,114 @@ program
157
157
  }
158
158
  });
159
159
 
160
+ /** Returns full vite.config.ts content with /api/save-to-file, /api/upload-asset, /api/list-assets. Inlined so generated tenant always has save-to-disk. */
161
+ function getViteConfigWithSaveToFile() {
162
+ return `/**
163
+ * Generated by @jsonpages/cli. Dev server API: /api/save-to-file, /api/upload-asset, /api/list-assets.
164
+ */
165
+ import { defineConfig } from 'vite';
166
+ import react from '@vitejs/plugin-react';
167
+ import tailwindcss from '@tailwindcss/vite';
168
+ import path from 'path';
169
+ import fs from 'fs';
170
+ import { fileURLToPath } from 'url';
171
+
172
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
173
+
174
+ const ASSETS_IMAGES_DIR = path.resolve(__dirname, 'public', 'assets', 'images');
175
+ const DATA_CONFIG_DIR = path.resolve(__dirname, 'src', 'data', 'config');
176
+ const DATA_PAGES_DIR = path.resolve(__dirname, 'src', 'data', 'pages');
177
+ const IMAGE_EXT = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.avif']);
178
+ const IMAGE_MIMES = new Set([
179
+ 'image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml', 'image/avif',
180
+ ]);
181
+ const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024; // 5MB
182
+
183
+ function safeFilename(original, mimeType) {
184
+ const base = (original.replace(/\\.[^.]+$/, '').replace(/[^a-zA-Z0-9-_]/g, '_').slice(0, 128)) || 'image';
185
+ const ext = original.includes('.') ? path.extname(original).toLowerCase() : (mimeType?.startsWith('image/') ? \`.\${(mimeType.split('/')[1] || 'png').replace('jpeg', 'jpg')}\` : '.png');
186
+ return \`\${Date.now()}-\${base}\${IMAGE_EXT.has(ext) ? ext : '.png'}\`;
187
+ }
188
+
189
+ function listImagesInDir(dir, urlPrefix) {
190
+ const list = [];
191
+ if (!fs.existsSync(dir)) return list;
192
+ for (const name of fs.readdirSync(dir)) {
193
+ if (IMAGE_EXT.has(path.extname(name).toLowerCase())) list.push({ id: name, url: \`\${urlPrefix}/\${name}\`, alt: name, tags: [] });
194
+ }
195
+ return list;
196
+ }
197
+
198
+ function sendJson(res, status, body) {
199
+ res.writeHead(status, { 'Content-Type': 'application/json' });
200
+ res.end(JSON.stringify(body));
201
+ }
202
+
203
+ export default defineConfig({
204
+ plugins: [
205
+ react(),
206
+ tailwindcss(),
207
+ {
208
+ name: 'upload-asset-api',
209
+ configureServer(server) {
210
+ server.middlewares.use((req, res, next) => {
211
+ if (req.method === 'GET' && req.url === '/api/list-assets') {
212
+ try { sendJson(res, 200, listImagesInDir(ASSETS_IMAGES_DIR, '/assets/images')); } catch (e) { sendJson(res, 500, { error: e?.message || 'List failed' }); }
213
+ return;
214
+ }
215
+ const pathname = (req.url || '').split('?')[0];
216
+ if (req.method === 'POST' && pathname === '/api/save-to-file') {
217
+ const chunks = [];
218
+ req.on('data', (chunk) => chunks.push(chunk));
219
+ req.on('end', () => {
220
+ try {
221
+ const raw = Buffer.concat(chunks).toString('utf8');
222
+ if (!raw.trim()) { sendJson(res, 400, { error: 'Empty body' }); return; }
223
+ const body = JSON.parse(raw);
224
+ const { projectState, slug } = body;
225
+ if (!projectState || typeof slug !== 'string') { sendJson(res, 400, { error: 'Missing projectState or slug' }); return; }
226
+ if (!fs.existsSync(DATA_CONFIG_DIR)) fs.mkdirSync(DATA_CONFIG_DIR, { recursive: true });
227
+ if (!fs.existsSync(DATA_PAGES_DIR)) fs.mkdirSync(DATA_PAGES_DIR, { recursive: true });
228
+ if (projectState.site != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'site.json'), JSON.stringify(projectState.site, null, 2), 'utf8');
229
+ if (projectState.theme != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'theme.json'), JSON.stringify(projectState.theme, null, 2), 'utf8');
230
+ if (projectState.menu != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'menu.json'), JSON.stringify(projectState.menu, null, 2), 'utf8');
231
+ if (projectState.page != null) {
232
+ const safeSlug = (slug.replace(/[^a-zA-Z0-9-_]/g, '_') || 'page');
233
+ fs.writeFileSync(path.join(DATA_PAGES_DIR, \`\${safeSlug}.json\`), JSON.stringify(projectState.page, null, 2), 'utf8');
234
+ }
235
+ sendJson(res, 200, { ok: true });
236
+ } catch (e) { sendJson(res, 500, { error: e?.message || 'Save to file failed' }); }
237
+ });
238
+ req.on('error', () => sendJson(res, 500, { error: 'Request error' }));
239
+ return;
240
+ }
241
+ if (req.method !== 'POST' || req.url !== '/api/upload-asset') return next();
242
+ const chunks = [];
243
+ req.on('data', (chunk) => chunks.push(chunk));
244
+ req.on('end', () => {
245
+ try {
246
+ const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
247
+ const { filename, mimeType, data } = body;
248
+ if (!filename || typeof data !== 'string') { sendJson(res, 400, { error: 'Missing filename or data' }); return; }
249
+ const buf = Buffer.from(data, 'base64');
250
+ if (buf.length > MAX_FILE_SIZE_BYTES) { sendJson(res, 413, { error: 'File too large. Max 5MB.' }); return; }
251
+ if (mimeType && !IMAGE_MIMES.has(mimeType)) { sendJson(res, 400, { error: 'Invalid file type' }); return; }
252
+ const name = safeFilename(filename, mimeType);
253
+ if (!fs.existsSync(ASSETS_IMAGES_DIR)) fs.mkdirSync(ASSETS_IMAGES_DIR, { recursive: true });
254
+ fs.writeFileSync(path.join(ASSETS_IMAGES_DIR, name), buf);
255
+ sendJson(res, 200, { url: \`/assets/images/\${name}\` });
256
+ } catch (e) { sendJson(res, 500, { error: e?.message || 'Upload failed' }); }
257
+ });
258
+ req.on('error', () => sendJson(res, 500, { error: 'Request error' }));
259
+ });
260
+ },
261
+ },
262
+ ],
263
+ resolve: { alias: { '@': path.resolve(__dirname, './src') } },
264
+ });
265
+ `;
266
+ }
267
+
160
268
  async function injectInfraFiles(targetDir, name) {
161
269
  const pkg = {
162
270
  name: name,
@@ -171,19 +279,9 @@ async function injectInfraFiles(targetDir, name) {
171
279
  };
172
280
  await fs.writeJson(path.join(targetDir, 'package.json'), pkg, { spaces: 2 });
173
281
 
174
- // Vite config with dev server API: /api/save-to-file, /api/upload-asset, /api/list-assets
175
- const viteTemplatePath = path.join(__dirname, '..', 'assets', 'vite.config.tenant.ts');
176
- const viteConfig = fs.existsSync(viteTemplatePath)
177
- ? await fs.readFile(viteTemplatePath, 'utf8')
178
- : `import { defineConfig } from 'vite';
179
- import react from '@vitejs/plugin-react';
180
- import tailwindcss from '@tailwindcss/vite';
181
- import path from 'path';
182
- export default defineConfig({
183
- plugins: [react(), tailwindcss()],
184
- resolve: { alias: { '@': path.resolve(__dirname, './src') } },
185
- });`;
186
- await fs.writeFile(path.join(targetDir, 'vite.config.ts'), viteConfig.trim());
282
+ // Vite config with dev server API: /api/save-to-file, /api/upload-asset, /api/list-assets (always inline so it works when CLI is installed from npm)
283
+ const viteConfig = getViteConfigWithSaveToFile();
284
+ await fs.writeFile(path.join(targetDir, 'vite.config.ts'), viteConfig);
187
285
 
188
286
  await fs.ensureDir(path.join(targetDir, 'public', 'assets', 'images'));
189
287
  await fs.ensureDir(path.join(targetDir, 'src', 'data', 'config'));