create-athenea-app 0.1.1

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 ADDED
@@ -0,0 +1,19 @@
1
+ # create-athenea-app
2
+
3
+ Paquete CLI para generar un proyecto basado en esta plantilla.
4
+
5
+ Uso (cuando este publicado):
6
+
7
+ ```bash
8
+ npm create athenea-app@latest
9
+ ```
10
+
11
+ Tambien podes pasar el nombre directo:
12
+
13
+ ```bash
14
+ npm create athenea-app@latest mi-proyecto
15
+ ```
16
+
17
+ Opciones:
18
+
19
+ - `--no-install`: no ejecuta `npm install`
package/bin/index.js ADDED
@@ -0,0 +1,314 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import fsp from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { createInterface } from "node:readline/promises";
8
+ import { stdin as input, stdout as output } from "node:process";
9
+ import { spawn } from "node:child_process";
10
+ import zlib from "node:zlib";
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+
14
+ function printHelp() {
15
+ console.log(
16
+ [
17
+ "create-athenea-app",
18
+ "",
19
+ "Uso:",
20
+ " npm create athenea-app@latest [nombre]",
21
+ "",
22
+ "Opciones:",
23
+ " --no-install No ejecuta npm install",
24
+ " --help Muestra esta ayuda",
25
+ ].join("\n"),
26
+ );
27
+ }
28
+
29
+ function slugify(inputText) {
30
+ const s = String(inputText)
31
+ .normalize("NFKD")
32
+ .replace(/[\u0300-\u036f]/g, "")
33
+ .toLowerCase()
34
+ .replace(/[^a-z0-9]+/g, "-")
35
+ .replace(/^-+|-+$/g, "");
36
+
37
+ return s;
38
+ }
39
+
40
+ function assertEmptyDir(dirPath) {
41
+ if (!fs.existsSync(dirPath)) return;
42
+ const entries = fs.readdirSync(dirPath);
43
+ if (entries.length > 0) {
44
+ throw new Error(`La carpeta ya existe y no esta vacia: ${dirPath}`);
45
+ }
46
+ }
47
+
48
+ async function replaceTokensInFile(filePath, tokenMap) {
49
+ let content = await fsp.readFile(filePath, "utf8");
50
+ for (const [token, value] of Object.entries(tokenMap)) {
51
+ content = content.split(token).join(value);
52
+ }
53
+ await fsp.writeFile(filePath, content, "utf8");
54
+ }
55
+
56
+ async function updateAppPackageJson(
57
+ projectDir,
58
+ { npmName, productName, appId },
59
+ ) {
60
+ const pkgPath = path.join(projectDir, "package.json");
61
+ const raw = await fsp.readFile(pkgPath, "utf8");
62
+ const pkg = JSON.parse(raw);
63
+
64
+ pkg.name = npmName;
65
+ if (typeof pkg.version !== "string") pkg.version = "0.1.0";
66
+
67
+ if (!pkg.build || typeof pkg.build !== "object") pkg.build = {};
68
+ pkg.build.productName = productName;
69
+ pkg.build.appId = appId;
70
+
71
+ await fsp.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
72
+ }
73
+
74
+ // ---------- Asset generator (white placeholders) ----------
75
+ function crc32(buf) {
76
+ let crc = 0 ^ -1;
77
+ for (let i = 0; i < buf.length; i++) {
78
+ crc ^= buf[i];
79
+ for (let j = 0; j < 8; j++) {
80
+ const mask = -(crc & 1);
81
+ crc = (crc >>> 1) ^ (0xedb88320 & mask);
82
+ }
83
+ }
84
+ return (crc ^ -1) >>> 0;
85
+ }
86
+
87
+ function pngSolidRGBA(width, height, r, g, b, a) {
88
+ const signature = Buffer.from([
89
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
90
+ ]);
91
+
92
+ const ihdrData = Buffer.alloc(13);
93
+ ihdrData.writeUInt32BE(width, 0);
94
+ ihdrData.writeUInt32BE(height, 4);
95
+ ihdrData[8] = 8; // bit depth
96
+ ihdrData[9] = 6; // color type RGBA
97
+ ihdrData[10] = 0; // compression
98
+ ihdrData[11] = 0; // filter
99
+ ihdrData[12] = 0; // interlace
100
+
101
+ function chunk(type, data) {
102
+ const typeBuf = Buffer.from(type, "ascii");
103
+ const lenBuf = Buffer.alloc(4);
104
+ lenBuf.writeUInt32BE(data.length, 0);
105
+ const crcBuf = Buffer.alloc(4);
106
+ const crc = crc32(Buffer.concat([typeBuf, data]));
107
+ crcBuf.writeUInt32BE(crc, 0);
108
+ return Buffer.concat([lenBuf, typeBuf, data, crcBuf]);
109
+ }
110
+
111
+ const row = Buffer.alloc(1 + width * 4);
112
+ row[0] = 0; // filter type 0
113
+ for (let x = 0; x < width; x++) {
114
+ const o = 1 + x * 4;
115
+ row[o + 0] = r;
116
+ row[o + 1] = g;
117
+ row[o + 2] = b;
118
+ row[o + 3] = a;
119
+ }
120
+
121
+ const raw = Buffer.alloc(row.length * height);
122
+ for (let y = 0; y < height; y++) row.copy(raw, y * row.length);
123
+
124
+ const compressed = zlib.deflateSync(raw, { level: 9 });
125
+
126
+ return Buffer.concat([
127
+ signature,
128
+ chunk("IHDR", ihdrData),
129
+ chunk("IDAT", compressed),
130
+ chunk("IEND", Buffer.alloc(0)),
131
+ ]);
132
+ }
133
+
134
+ function icoFromPng(pngBuf, size) {
135
+ const header = Buffer.alloc(6);
136
+ header.writeUInt16LE(0, 0); // reserved
137
+ header.writeUInt16LE(1, 2); // type icon
138
+ header.writeUInt16LE(1, 4); // count
139
+
140
+ const entry = Buffer.alloc(16);
141
+ entry.writeUInt8(size === 256 ? 0 : size, 0); // width
142
+ entry.writeUInt8(size === 256 ? 0 : size, 1); // height
143
+ entry.writeUInt8(0, 2); // color count
144
+ entry.writeUInt8(0, 3); // reserved
145
+ entry.writeUInt16LE(1, 4); // planes
146
+ entry.writeUInt16LE(32, 6); // bit count
147
+ entry.writeUInt32LE(pngBuf.length, 8); // bytes in res
148
+ entry.writeUInt32LE(6 + 16, 12); // offset
149
+
150
+ return Buffer.concat([header, entry, pngBuf]);
151
+ }
152
+
153
+ function bmpSolid24(width, height, r, g, b) {
154
+ const rowSize = (width * 3 + 3) & ~3; // 4-byte aligned
155
+ const pixelDataSize = rowSize * height;
156
+ const fileSize = 14 + 40 + pixelDataSize;
157
+
158
+ const fileHeader = Buffer.alloc(14);
159
+ fileHeader.write("BM", 0, 2, "ascii");
160
+ fileHeader.writeUInt32LE(fileSize, 2);
161
+ fileHeader.writeUInt32LE(0, 6);
162
+ fileHeader.writeUInt32LE(14 + 40, 10);
163
+
164
+ const dib = Buffer.alloc(40);
165
+ dib.writeUInt32LE(40, 0); // header size
166
+ dib.writeInt32LE(width, 4);
167
+ dib.writeInt32LE(height, 8); // bottom-up
168
+ dib.writeUInt16LE(1, 12); // planes
169
+ dib.writeUInt16LE(24, 14); // bpp
170
+ dib.writeUInt32LE(0, 16); // BI_RGB
171
+ dib.writeUInt32LE(pixelDataSize, 20);
172
+ dib.writeInt32LE(2835, 24); // 72 DPI
173
+ dib.writeInt32LE(2835, 28);
174
+ dib.writeUInt32LE(0, 32);
175
+ dib.writeUInt32LE(0, 36);
176
+
177
+ const pixels = Buffer.alloc(pixelDataSize);
178
+ for (let y = 0; y < height; y++) {
179
+ const rowOff = y * rowSize;
180
+ for (let x = 0; x < width; x++) {
181
+ const o = rowOff + x * 3;
182
+ pixels[o + 0] = b;
183
+ pixels[o + 1] = g;
184
+ pixels[o + 2] = r;
185
+ }
186
+ // padding queda en 0
187
+ }
188
+
189
+ return Buffer.concat([fileHeader, dib, pixels]);
190
+ }
191
+
192
+ async function ensureWhiteBuildAssets(projectDir) {
193
+ const assetsDir = path.join(projectDir, "assets");
194
+ const publicDir = path.join(projectDir, "public");
195
+ await fsp.mkdir(assetsDir, { recursive: true });
196
+ await fsp.mkdir(publicDir, { recursive: true });
197
+
198
+ const png256 = pngSolidRGBA(256, 256, 255, 255, 255, 255);
199
+ const ico = icoFromPng(png256, 256);
200
+ await fsp.writeFile(path.join(assetsDir, "build.ico"), ico);
201
+
202
+ await fsp.writeFile(
203
+ path.join(assetsDir, "installer-sidebar.bmp"),
204
+ bmpSolid24(164, 314, 255, 255, 255),
205
+ );
206
+ await fsp.writeFile(
207
+ path.join(assetsDir, "installer-header.bmp"),
208
+ bmpSolid24(150, 57, 255, 255, 255),
209
+ );
210
+
211
+ const viteSvgPath = path.join(publicDir, "vite.svg");
212
+ if (!fs.existsSync(viteSvgPath)) {
213
+ const svg =
214
+ '<?xml version="1.0" encoding="UTF-8"?>\n' +
215
+ '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">\n' +
216
+ ' <rect width="64" height="64" fill="#ffffff"/>\n' +
217
+ "</svg>\n";
218
+ await fsp.writeFile(viteSvgPath, svg, "utf8");
219
+ }
220
+ }
221
+
222
+ async function npmInstall(projectDir) {
223
+ const isWin = process.platform === "win32";
224
+ const npmCmd = isWin ? "npm.cmd" : "npm";
225
+
226
+ await new Promise((resolve, reject) => {
227
+ const child = spawn(npmCmd, ["install"], {
228
+ cwd: projectDir,
229
+ stdio: "inherit",
230
+ shell: isWin,
231
+ });
232
+ child.on("close", (code) => {
233
+ if (code === 0) resolve();
234
+ else reject(new Error(`npm install fallo con codigo ${code}`));
235
+ });
236
+ child.on("error", reject);
237
+ });
238
+ }
239
+
240
+ async function main() {
241
+ const args = process.argv.slice(2);
242
+ if (args.includes("--help") || args.includes("-h")) {
243
+ printHelp();
244
+ return;
245
+ }
246
+
247
+ const noInstall = args.includes("--no-install");
248
+ const nameArg = args.find((a) => a && !a.startsWith("-"));
249
+
250
+ let projectName = nameArg;
251
+ if (!projectName) {
252
+ const rl = createInterface({ input, output });
253
+ projectName = (await rl.question("Nombre del proyecto: ")).trim();
254
+ await rl.close();
255
+ }
256
+
257
+ if (!projectName) {
258
+ console.error("Nombre requerido.");
259
+ process.exit(1);
260
+ }
261
+
262
+ const productName = projectName.trim();
263
+ const npmName = slugify(productName);
264
+ if (!npmName) {
265
+ console.error("Nombre invalido: no se pudo generar un nombre de paquete.");
266
+ process.exit(1);
267
+ }
268
+
269
+ const appId = `com.example.${npmName}`;
270
+ const targetDir = path.resolve(process.cwd(), npmName);
271
+ assertEmptyDir(targetDir);
272
+
273
+ const templateDir = path.resolve(__dirname, "..", "template");
274
+ await fsp.mkdir(targetDir, { recursive: true });
275
+ await fsp.cp(templateDir, targetDir, { recursive: true });
276
+
277
+ await updateAppPackageJson(targetDir, { npmName, productName, appId });
278
+
279
+ const tokenMap = {
280
+ __APP_TITLE__: productName,
281
+ __APP_TITLE_JSON__: JSON.stringify(productName),
282
+ __APP_APP_ID__: appId,
283
+ };
284
+
285
+ await Promise.all([
286
+ replaceTokensInFile(path.join(targetDir, "electron.js"), tokenMap),
287
+ replaceTokensInFile(path.join(targetDir, "index.html"), tokenMap),
288
+ replaceTokensInFile(path.join(targetDir, "README.md"), tokenMap),
289
+ replaceTokensInFile(
290
+ path.join(targetDir, "src", "routes", "Home", "Home.jsx"),
291
+ tokenMap,
292
+ ),
293
+ ]);
294
+
295
+ await ensureWhiteBuildAssets(targetDir);
296
+
297
+ console.log("");
298
+ console.log(`Proyecto creado en: ${targetDir}`);
299
+ console.log("");
300
+ console.log("Siguientes pasos:");
301
+ console.log(` cd ${npmName}`);
302
+ console.log(" npm run dev:electron");
303
+
304
+ if (!noInstall) {
305
+ console.log("");
306
+ console.log("Instalando dependencias...");
307
+ await npmInstall(targetDir);
308
+ }
309
+ }
310
+
311
+ main().catch((err) => {
312
+ console.error("[create-athenea-app] Error:", err?.message || err);
313
+ process.exit(1);
314
+ });
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "create-athenea-app",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "description": "Creador de proyectos Athenea (Electron + Preact + Vite)",
6
+ "keywords": [
7
+ "create",
8
+ "starter",
9
+ "template",
10
+ "electron",
11
+ "preact",
12
+ "vite"
13
+ ],
14
+ "bin": {
15
+ "create-athenea-app": "bin/index.js"
16
+ },
17
+ "files": [
18
+ "bin",
19
+ "template"
20
+ ],
21
+ "engines": {
22
+ "node": ">=18.0.0"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "license": "MIT"
28
+ }
@@ -0,0 +1,19 @@
1
+ # **APP_TITLE**
2
+
3
+ Proyecto generado con `create-athenea-app`.
4
+
5
+ ## Scripts
6
+
7
+ ```bash
8
+ npm run dev:electron
9
+ npm run build
10
+ npm run pack
11
+ npm run dist
12
+ ```
13
+
14
+ ## Branding
15
+
16
+ - Nombre del instalador/app: `package.json` -> `build.productName`
17
+ - ID de la app: `package.json` -> `build.appId`
18
+ - Titulos: `electron.js` y `index.html`
19
+ - Recursos de instalador (placeholders blancos): `assets/build.ico` y `assets/installer-*.bmp`
@@ -0,0 +1,230 @@
1
+ import { app, BrowserWindow, ipcMain, dialog, screen } from "electron";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import fs from "fs";
5
+ import os from "os";
6
+ import keytar from "keytar";
7
+ import pdfPrinter from "pdf-to-printer";
8
+ import { spawn } from "child_process";
9
+ import log from "electron-log";
10
+
11
+ // Inicializar logger
12
+ log.initialize();
13
+ log.transports.file.level = "info";
14
+ log.info("Iniciando aplicacion...");
15
+
16
+ const { print: printPDF, getPrinters } = pdfPrinter;
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+
19
+ app.commandLine.appendSwitch("disable-features", "Autofill");
20
+ if (!app.isPackaged || process.env.ELECTRON_DISABLE_SANDBOX === "1") {
21
+ app.commandLine.appendSwitch("no-sandbox");
22
+ }
23
+
24
+ let backendProcess = null;
25
+
26
+ function getBackendPath() {
27
+ const isWin = process.platform === "win32";
28
+ const executableName = isWin
29
+ ? "api_billing_software.exe"
30
+ : "api_billing_software";
31
+
32
+ if (app.isPackaged) {
33
+ return path.join(
34
+ process.resourcesPath,
35
+ "api_billing_software",
36
+ executableName,
37
+ );
38
+ }
39
+
40
+ return path.join(
41
+ __dirname,
42
+ "..",
43
+ "backend",
44
+ "dist",
45
+ "api_billing_software",
46
+ executableName,
47
+ );
48
+ }
49
+
50
+ function startBackend() {
51
+ const backendPath = getBackendPath();
52
+ log.info("Intentando iniciar backend desde:", backendPath);
53
+
54
+ if (!fs.existsSync(backendPath)) {
55
+ log.error("No se encontro el ejecutable del backend en:", backendPath);
56
+ return;
57
+ }
58
+
59
+ backendProcess = spawn(backendPath, [], {
60
+ cwd: path.dirname(backendPath),
61
+ stdio: "pipe",
62
+ windowsHide: true,
63
+ });
64
+
65
+ backendProcess.on("close", () => {
66
+ backendProcess = null;
67
+ });
68
+ }
69
+
70
+ function stopBackend() {
71
+ if (backendProcess) {
72
+ backendProcess.kill();
73
+ backendProcess = null;
74
+ }
75
+ }
76
+
77
+ /** @type {BrowserWindow | null} */
78
+ let mainWindow = null;
79
+
80
+ /** @type {Map<string, BrowserWindow>} */
81
+ const childWindowsByRoute = new Map();
82
+
83
+ const settingsPath = path.join(app.getPath("userData"), "settings.json");
84
+
85
+ function createWindow() {
86
+ const isDev = !app.isPackaged;
87
+ const devServerUrl = process.env.DEV_SERVER_URL || "http://localhost:5171";
88
+ const { width: screenWidth, height: screenHeight } =
89
+ screen.getPrimaryDisplay().workAreaSize;
90
+
91
+ mainWindow = new BrowserWindow({
92
+ width: screenWidth,
93
+ height: screenHeight,
94
+ title: __APP_TITLE_JSON__,
95
+ webPreferences: {
96
+ preload: path.join(__dirname, "preload.cjs"),
97
+ devTools: isDev,
98
+ contextIsolation: true,
99
+ nodeIntegration: false,
100
+ },
101
+ });
102
+
103
+ mainWindow.setMenuBarVisibility(false);
104
+ mainWindow.setAutoHideMenuBar(true);
105
+ mainWindow.maximize();
106
+
107
+ if (isDev) {
108
+ mainWindow.loadURL(devServerUrl);
109
+ mainWindow.webContents.openDevTools();
110
+ } else {
111
+ const indexPath = path.join(__dirname, "dist", "index.html");
112
+ mainWindow.loadFile(indexPath, { query: { route: "/" } });
113
+ }
114
+
115
+ mainWindow.on("minimize", () => {
116
+ for (const [, child] of childWindowsByRoute) {
117
+ if (!child.isDestroyed() && !child.isMinimized()) child.minimize();
118
+ }
119
+ });
120
+
121
+ mainWindow.on("restore", () => {
122
+ for (const [, child] of childWindowsByRoute) {
123
+ if (!child.isDestroyed()) child.restore();
124
+ }
125
+ });
126
+ }
127
+
128
+ app.whenReady().then(() => {
129
+ startBackend();
130
+ createWindow();
131
+ });
132
+
133
+ app.on("will-quit", () => {
134
+ stopBackend();
135
+ });
136
+
137
+ app.on("window-all-closed", () => {
138
+ if (process.platform !== "darwin") app.quit();
139
+ });
140
+
141
+ app.on("activate", () => {
142
+ if (BrowserWindow.getAllWindows().length === 0) createWindow();
143
+ });
144
+
145
+ function readSettings() {
146
+ try {
147
+ if (!fs.existsSync(settingsPath)) return {};
148
+ return JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
149
+ } catch {
150
+ return {};
151
+ }
152
+ }
153
+
154
+ function writeSettings(data) {
155
+ try {
156
+ fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2), "utf-8");
157
+ return true;
158
+ } catch {
159
+ return false;
160
+ }
161
+ }
162
+
163
+ ipcMain.handle("settings:get", () => readSettings());
164
+ ipcMain.handle("settings:set", (_event, data) => {
165
+ const current = readSettings();
166
+ const merged = { ...current, ...data };
167
+ writeSettings(merged);
168
+ return merged;
169
+ });
170
+
171
+ ipcMain.handle("printer:getPrinters", async () => {
172
+ return await getPrinters();
173
+ });
174
+ ipcMain.handle("printer:printPDF", async (_event, { filePath, options }) => {
175
+ try {
176
+ await printPDF(filePath, options || {});
177
+ return { ok: true };
178
+ } catch (err) {
179
+ return { ok: false, error: err?.message || String(err) };
180
+ }
181
+ });
182
+
183
+ ipcMain.on("window:openRoute", (_event, options) => {
184
+ const route = typeof options === "string" ? options : options?.route;
185
+ const title = typeof options === "object" ? options.title : null;
186
+ if (!route) return;
187
+
188
+ const routeKey = String(route);
189
+ const existing = childWindowsByRoute.get(routeKey);
190
+ if (existing && !existing.isDestroyed()) {
191
+ if (existing.isMinimized()) existing.restore();
192
+ existing.focus();
193
+ return;
194
+ }
195
+
196
+ const isDev = !app.isPackaged;
197
+ const devServerUrl = process.env.DEV_SERVER_URL || "http://localhost:5171";
198
+ const preloadPath = path.join(__dirname, "preload.cjs");
199
+
200
+ const child = new BrowserWindow({
201
+ width: 1200,
202
+ height: 700,
203
+ title: title || __APP_TITLE_JSON__,
204
+ webPreferences: {
205
+ preload: preloadPath,
206
+ devTools: isDev,
207
+ contextIsolation: true,
208
+ nodeIntegration: false,
209
+ },
210
+ });
211
+
212
+ child.setMenuBarVisibility(false);
213
+ child.setAutoHideMenuBar(true);
214
+ if (mainWindow && !mainWindow.isDestroyed())
215
+ child.setParentWindow(mainWindow);
216
+ childWindowsByRoute.set(routeKey, child);
217
+
218
+ if (isDev) {
219
+ child.loadURL(`${devServerUrl}?route=${encodeURIComponent(route)}&child=1`);
220
+ child.webContents.openDevTools();
221
+ } else {
222
+ const indexPath = path.join(__dirname, "dist", "index.html");
223
+ child.loadFile(indexPath, { query: { route, child: "1" } });
224
+ }
225
+
226
+ child.on("closed", () => {
227
+ const current = childWindowsByRoute.get(routeKey);
228
+ if (current === child) childWindowsByRoute.delete(routeKey);
229
+ });
230
+ });
@@ -0,0 +1,3 @@
1
+ import js from "@eslint/js";
2
+
3
+ export default [js.configs.recommended];
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>__APP_TITLE__</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="src/main.jsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,123 @@
1
+ {
2
+ "name": "__APP_TITLE__",
3
+ "private": true,
4
+ "version": "0.1.1",
5
+ "type": "module",
6
+ "main": "electron.js",
7
+ "engines": {
8
+ "node": ">=22.0.0"
9
+ },
10
+ "scripts": {
11
+ "dev": "vite --host --port 0",
12
+ "electron": "cross-env NODE_OPTIONS=\"--experimental-require-module --no-warnings\" electron .",
13
+ "dev:electron": "node scripts/dev-electron.mjs",
14
+ "copy-preload": "node -e \"const fs=require('fs'); if(!fs.existsSync('dist')) fs.mkdirSync('dist'); fs.copyFileSync('preload.cjs','dist/preload.cjs')\"",
15
+ "build": "vite build && npm run copy-preload",
16
+ "preview": "vite preview",
17
+ "pack": "npm run build && electron-builder --dir",
18
+ "dist": "npm run build && electron-builder",
19
+ "dist:linux": "npm run build && electron-builder --linux",
20
+ "dist:win": "npm run build && electron-builder --win",
21
+ "lint": "eslint .",
22
+ "format": "prettier --write .",
23
+ "typecheck": "tsc --noEmit",
24
+ "test": "vitest",
25
+ "postinstall": "electron-builder install-app-deps"
26
+ },
27
+ "dependencies": {
28
+ "@preact/signals": "^2.5.1",
29
+ "@tanstack/react-table": "^8.21.3",
30
+ "dayjs": "^1.11.13",
31
+ "electron-log": "^5.4.3",
32
+ "electron-updater": "^6.6.2",
33
+ "jwt-decode": "^4.0.0",
34
+ "keytar": "7.9.0",
35
+ "ky": "^1.2.4",
36
+ "pdf-to-printer": "5.6.1",
37
+ "preact": "^10.26.2",
38
+ "preact-iso": "^2.9.1",
39
+ "sweetalert2": "^11.26.10",
40
+ "zod": "^3.23.8",
41
+ "zustand": "^5.0.3"
42
+ },
43
+ "devDependencies": {
44
+ "@eslint/eslintrc": "^3.3.1",
45
+ "@eslint/js": "^9.39.1",
46
+ "@preact/preset-vite": "^2.10.1",
47
+ "@testing-library/preact": "3.2.3",
48
+ "@types/node": "^22.7.8",
49
+ "@typescript-eslint/eslint-plugin": "^8.46.4",
50
+ "@typescript-eslint/parser": "^8.46.4",
51
+ "autoprefixer": "^10.4.20",
52
+ "concurrently": "^8.2.0",
53
+ "cross-env": "^7.0.3",
54
+ "dotenv": "^16.4.5",
55
+ "electron": "^35.1.4",
56
+ "electron-builder": "^24.13.1",
57
+ "eslint": "^9.39.1",
58
+ "eslint-config-prettier": "^10.1.8",
59
+ "eslint-plugin-prettier": "^5.5.4",
60
+ "eslint-plugin-react-hooks": "^7.0.1",
61
+ "postcss": "^8.4.47",
62
+ "prettier": "^3.6.2",
63
+ "tailwindcss": "^3.4.14",
64
+ "typescript": "^5.9.3",
65
+ "vite": "^7.2.2",
66
+ "vite-tsconfig-paths": "^5.0.0",
67
+ "vitest": "^4.0.8",
68
+ "wait-on": "^7.0.1"
69
+ },
70
+ "overrides": {
71
+ "tar": "7.5.7"
72
+ },
73
+ "build": {
74
+ "appId": "__APP_APP_ID__",
75
+ "productName": "__APP_TITLE__",
76
+ "asar": true,
77
+ "asarUnpack": [
78
+ "**/*.node",
79
+ "preload.cjs",
80
+ "node_modules/pdf-to-printer/**/*"
81
+ ],
82
+ "directories": {
83
+ "buildResources": "assets",
84
+ "output": "../release"
85
+ },
86
+ "extraResources": [],
87
+ "files": [
88
+ "dist/**/*",
89
+ "electron.js",
90
+ "preload.cjs",
91
+ "!**/*.map",
92
+ "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
93
+ "!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}"
94
+ ],
95
+ "win": {
96
+ "target": [
97
+ "nsis"
98
+ ],
99
+ "signAndEditExecutable": false,
100
+ "icon": "assets/build.ico"
101
+ },
102
+ "nsis": {
103
+ "oneClick": false,
104
+ "perMachine": true,
105
+ "allowToChangeInstallationDirectory": true,
106
+ "createDesktopShortcut": true,
107
+ "createStartMenuShortcut": true,
108
+ "installerIcon": "assets/build.ico",
109
+ "uninstallerIcon": "assets/build.ico",
110
+ "installerSidebar": "assets/installer-sidebar.bmp",
111
+ "uninstallerSidebar": "assets/installer-sidebar.bmp",
112
+ "installerHeader": "assets/installer-header.bmp"
113
+ },
114
+ "linux": {
115
+ "target": [
116
+ "AppImage",
117
+ "deb"
118
+ ],
119
+ "category": "Office",
120
+ "maintainer": "tu-email@ejemplo.com"
121
+ }
122
+ }
123
+ }
@@ -0,0 +1,9 @@
1
+ const { contextBridge, ipcRenderer } = require("electron");
2
+
3
+ contextBridge.exposeInMainWorld("electronAPI", {
4
+ settingsGet: () => ipcRenderer.invoke("settings:get"),
5
+ settingsSet: (data) => ipcRenderer.invoke("settings:set", data),
6
+ openRoute: (options) => ipcRenderer.send("window:openRoute", options),
7
+ getPrinters: () => ipcRenderer.invoke("printer:getPrinters"),
8
+ printPDF: (payload) => ipcRenderer.invoke("printer:printPDF", payload),
9
+ });
@@ -0,0 +1 @@
1
+ // Compat (si alguien lo usa desde ESM). El preload real es preload.cjs
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
3
+ <rect width="256" height="256" fill="#ffffff"/>
4
+ </svg>
@@ -0,0 +1,71 @@
1
+ import { createServer } from "vite";
2
+ import { spawn } from "child_process";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import fs from "fs";
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ async function start() {
10
+ const server = await createServer({
11
+ server: {
12
+ host: "127.0.0.1",
13
+ port: 0,
14
+ },
15
+ });
16
+
17
+ await server.listen();
18
+
19
+ const url =
20
+ server.resolvedUrls?.local?.[0] ||
21
+ `http://localhost:${server.config.server.port || 5173}`;
22
+
23
+ console.log(`[dev-electron] Vite dev server en ${url}`);
24
+
25
+ const isWin = process.platform === "win32";
26
+ const localElectronBin = path.resolve(
27
+ __dirname,
28
+ "..",
29
+ "node_modules",
30
+ ".bin",
31
+ isWin ? "electron.cmd" : "electron",
32
+ );
33
+ const electronBin = fs.existsSync(localElectronBin)
34
+ ? localElectronBin
35
+ : "electron";
36
+
37
+ const electronProcess = spawn(electronBin, [".", "--no-sandbox"], {
38
+ stdio: "inherit",
39
+ shell: isWin,
40
+ env: {
41
+ ...process.env,
42
+ DEV_SERVER_URL: url,
43
+ NODE_OPTIONS: "--experimental-require-module --no-warnings",
44
+ ELECTRON_DISABLE_SANDBOX: "1",
45
+ },
46
+ cwd: path.resolve(__dirname, ".."),
47
+ });
48
+
49
+ const clean = () => {
50
+ electronProcess.kill();
51
+ server.close();
52
+ };
53
+
54
+ electronProcess.on("close", (code) => {
55
+ clean();
56
+ process.exit(code ?? 0);
57
+ });
58
+
59
+ const handleSignal = () => {
60
+ clean();
61
+ process.exit(0);
62
+ };
63
+
64
+ process.on("SIGINT", handleSignal);
65
+ process.on("SIGTERM", handleSignal);
66
+ }
67
+
68
+ start().catch((err) => {
69
+ console.error("[dev-electron] Error:", err);
70
+ process.exit(1);
71
+ });
@@ -0,0 +1,13 @@
1
+ import { useState } from "preact/hooks";
2
+ import "./style.css";
3
+
4
+ export function Counter() {
5
+ const [count, setCount] = useState(0);
6
+ return (
7
+ <div className="counter">
8
+ <button onClick={() => setCount((c) => c - 1)}>-</button>
9
+ <span>{count}</span>
10
+ <button onClick={() => setCount((c) => c + 1)}>+</button>
11
+ </div>
12
+ );
13
+ }
@@ -0,0 +1,9 @@
1
+ .counter {
2
+ display: inline-flex;
3
+ gap: 12px;
4
+ align-items: center;
5
+ }
6
+
7
+ .counter button {
8
+ padding: 8px 12px;
9
+ }
@@ -0,0 +1,7 @@
1
+ :root {
2
+ font-family: system-ui, sans-serif;
3
+ }
4
+
5
+ body {
6
+ margin: 0;
7
+ }
@@ -0,0 +1,16 @@
1
+ import { render } from "preact";
2
+ import { LocationProvider, Router, Route } from "preact-iso";
3
+ import Home from "./routes/Home/Home";
4
+ import "./index.css";
5
+
6
+ function App() {
7
+ return (
8
+ <LocationProvider>
9
+ <Router>
10
+ <Route path="/" component={Home} />
11
+ </Router>
12
+ </LocationProvider>
13
+ );
14
+ }
15
+
16
+ render(<App />, document.getElementById("app"));
@@ -0,0 +1,12 @@
1
+ import { Counter } from "../../components/Counter/Counter";
2
+ import "./style.css";
3
+
4
+ export default function Home() {
5
+ return (
6
+ <div className="home">
7
+ <h1 className="home-title">Bienvenido a __APP_TITLE__</h1>
8
+ <img className="home-logo" src="/logo.svg" alt="" />
9
+ <Counter />
10
+ </div>
11
+ );
12
+ }
@@ -0,0 +1,13 @@
1
+ .home {
2
+ min-height: 100vh;
3
+ display: grid;
4
+ place-content: center;
5
+ gap: 16px;
6
+ text-align: center;
7
+ }
8
+
9
+ .home-logo {
10
+ width: 120px;
11
+ height: 120px;
12
+ margin: 0 auto;
13
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "Bundler",
9
+ "resolveJsonModule": true,
10
+ "isolatedModules": true,
11
+ "noEmit": true,
12
+ "jsx": "react-jsx",
13
+ "jsxImportSource": "preact",
14
+ "strict": true,
15
+ "types": ["node"]
16
+ },
17
+ "include": ["src"]
18
+ }
@@ -0,0 +1,25 @@
1
+ import { defineConfig } from "vite";
2
+ import preact from "@preact/preset-vite";
3
+ import path from "path";
4
+
5
+ export default defineConfig({
6
+ plugins: [preact()],
7
+ base: "./",
8
+ publicDir: "public",
9
+ build: {
10
+ outDir: "dist",
11
+ assetsDir: "assets",
12
+ rollupOptions: {
13
+ output: {
14
+ assetFileNames: "assets/[name].[hash][extname]",
15
+ chunkFileNames: "assets/[name].[hash].js",
16
+ entryFileNames: "assets/[name].[hash].js",
17
+ },
18
+ },
19
+ },
20
+ resolve: {
21
+ alias: {
22
+ "@": path.resolve(__dirname, "./src"),
23
+ },
24
+ },
25
+ });