create-openclass-uniminuto 1.6.1 → 1.6.4

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.
@@ -6,17 +6,39 @@ import { spawnSync } from "node:child_process";
6
6
 
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = path.dirname(__filename);
9
+
9
10
  const packageRoot = path.resolve(__dirname, "..");
10
11
  const templateRoot = path.join(packageRoot, "template");
11
12
 
12
13
  const args = process.argv.slice(2);
13
14
  const flags = new Set(args.filter((arg) => arg.startsWith("--")));
14
15
  const positional = args.filter((arg) => !arg.startsWith("--"));
16
+
15
17
  const targetArg = positional[0] || "openclass-uniminuto";
16
18
  const targetDir = path.resolve(process.cwd(), targetArg);
17
19
  const isCurrentDirectoryTarget = targetArg === "." || targetArg === "./";
18
- const forceOverwrite = flags.has("--force");
19
- const updateTheme = flags.has("--update-theme") || flags.has("--sync-theme");
20
+
21
+ const IGNORED_DIRS = new Set(["node_modules", "dist", ".slidev", ".git"]);
22
+ const IGNORED_FILES = new Set(["audit-report.json"]);
23
+ const IGNORED_EXTENSIONS = new Set([".tgz"]);
24
+
25
+ function envFlag(name) {
26
+ const value = process.env[name];
27
+
28
+ if (value === undefined) return false;
29
+
30
+ return ["1", "true", "yes", "on"].includes(String(value).toLowerCase());
31
+ }
32
+
33
+ const forceOverwrite =
34
+ flags.has("--force") ||
35
+ envFlag("npm_config_force");
36
+
37
+ const updateTheme =
38
+ flags.has("--update-theme") ||
39
+ flags.has("--sync-theme") ||
40
+ envFlag("npm_config_update_theme") ||
41
+ envFlag("npm_config_sync_theme");
20
42
 
21
43
  function log(message = "") {
22
44
  console.log(message);
@@ -27,31 +49,73 @@ function fail(message) {
27
49
  process.exit(1);
28
50
  }
29
51
 
52
+ function shouldIgnore(src) {
53
+ const base = path.basename(src);
54
+ const ext = path.extname(src).toLowerCase();
55
+
56
+ return IGNORED_DIRS.has(base) || IGNORED_FILES.has(base) || IGNORED_EXTENSIONS.has(ext);
57
+ }
58
+
30
59
  function cleanPackageName(value) {
31
- return String(value || "openclass-uniminuto")
32
- .normalize("NFD")
33
- .replace(/[\u0300-\u036f]/g, "")
34
- .toLowerCase()
35
- .replace(/[^a-z0-9.-]+/g, "-")
36
- .replace(/^-+|-+$/g, "") || "openclass-uniminuto";
60
+ return (
61
+ String(value || "openclass-uniminuto")
62
+ .normalize("NFD")
63
+ .replace(/[\u0300-\u036f]/g, "")
64
+ .toLowerCase()
65
+ .replace(/[^a-z0-9.-]+/g, "-")
66
+ .replace(/^-+|-+$/g, "") || "openclass-uniminuto"
67
+ );
68
+ }
69
+
70
+ function relativeToTarget(filePath) {
71
+ return path.relative(targetDir, filePath).replaceAll("\\", "/");
72
+ }
73
+
74
+ function readJson(filePath) {
75
+ if (!fs.existsSync(filePath)) return null;
76
+
77
+ try {
78
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
79
+ } catch (error) {
80
+ fail(`No se pudo leer JSON válido en ${filePath}\n${error.message}`);
81
+ }
82
+ }
83
+
84
+ function writeJson(filePath, data) {
85
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
86
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
87
+ }
88
+
89
+ function patchJson(filePath, patcher) {
90
+ const data = readJson(filePath);
91
+
92
+ if (!data) return false;
93
+
94
+ patcher(data);
95
+ writeJson(filePath, data);
96
+
97
+ return true;
37
98
  }
38
99
 
39
100
  function copyTemplate(src, dest) {
40
- const base = path.basename(src);
41
- if (["node_modules", "dist", ".slidev", ".git"].includes(base)) return;
101
+ if (shouldIgnore(src)) return;
42
102
 
43
103
  const stat = fs.statSync(src);
104
+
44
105
  if (stat.isDirectory()) {
45
106
  fs.mkdirSync(dest, { recursive: true });
107
+
46
108
  for (const child of fs.readdirSync(src)) {
47
109
  copyTemplate(path.join(src, child), path.join(dest, child));
48
110
  }
111
+
49
112
  return;
50
113
  }
51
114
 
52
115
  fs.mkdirSync(path.dirname(dest), { recursive: true });
116
+
53
117
  if (fs.existsSync(dest) && !forceOverwrite) {
54
- log(`↷ Conservado archivo existente: ${path.relative(targetDir, dest)}`);
118
+ log(`↷ Conservado archivo existente: ${relativeToTarget(dest)}`);
55
119
  return;
56
120
  }
57
121
 
@@ -59,33 +123,172 @@ function copyTemplate(src, dest) {
59
123
  }
60
124
 
61
125
  function copyPathAlways(src, dest) {
62
- const base = path.basename(src);
63
- if (["node_modules", "dist", ".slidev", ".git"].includes(base)) return;
64
-
65
- if (!fs.existsSync(src)) return;
126
+ if (!fs.existsSync(src) || shouldIgnore(src)) return;
66
127
 
67
128
  const stat = fs.statSync(src);
129
+
68
130
  if (stat.isDirectory()) {
69
131
  fs.mkdirSync(dest, { recursive: true });
132
+
70
133
  for (const child of fs.readdirSync(src)) {
71
134
  copyPathAlways(path.join(src, child), path.join(dest, child));
72
135
  }
136
+
73
137
  return;
74
138
  }
75
139
 
76
140
  fs.mkdirSync(path.dirname(dest), { recursive: true });
77
141
  fs.copyFileSync(src, dest);
78
- log(`✓ Actualizado: ${path.relative(targetDir, dest)}`);
142
+
143
+ log(`✓ Actualizado: ${relativeToTarget(dest)}`);
79
144
  }
80
145
 
81
- function ensureFileFromTemplate(relativePath, options = {}) {
82
- const src = path.join(templateRoot, relativePath);
83
- const dest = path.join(targetDir, relativePath);
84
- if (!fs.existsSync(src)) return;
85
- if (fs.existsSync(dest) && options.onlyIfMissing) return;
146
+ function copyPathIfMissing(src, dest) {
147
+ if (!fs.existsSync(src) || shouldIgnore(src)) return false;
148
+
149
+ if (fs.existsSync(dest)) {
150
+ log(`⏭ Conservado: ${relativeToTarget(dest)}`);
151
+ return false;
152
+ }
153
+
154
+ const stat = fs.statSync(src);
155
+
156
+ if (stat.isDirectory()) {
157
+ fs.mkdirSync(dest, { recursive: true });
158
+
159
+ for (const child of fs.readdirSync(src)) {
160
+ copyPathIfMissing(path.join(src, child), path.join(dest, child));
161
+ }
162
+
163
+ return true;
164
+ }
165
+
86
166
  fs.mkdirSync(path.dirname(dest), { recursive: true });
87
167
  fs.copyFileSync(src, dest);
88
- log(`✓ Actualizado: ${relativePath}`);
168
+
169
+ log(`✓ Creado: ${relativeToTarget(dest)}`);
170
+ return true;
171
+ }
172
+
173
+ function copyTemplatePathAlways(relativePath) {
174
+ copyPathAlways(path.join(templateRoot, relativePath), path.join(targetDir, relativePath));
175
+ }
176
+
177
+ function copyTemplatePathIfMissing(relativePath) {
178
+ copyPathIfMissing(path.join(templateRoot, relativePath), path.join(targetDir, relativePath));
179
+ }
180
+
181
+ function appendGitignoreRules() {
182
+ const gitignorePath = path.join(targetDir, ".gitignore");
183
+
184
+ const rules = [
185
+ "",
186
+ "# Archivos generados por exportación Slidev",
187
+ "public/descargas/*.pdf",
188
+ "public/descargas/*.pptx",
189
+ "!public/descargas/.gitkeep",
190
+ "",
191
+ "# Reportes locales de auditoría",
192
+ "audit-report.json",
193
+ "",
194
+ ];
195
+
196
+ const existing = fs.existsSync(gitignorePath)
197
+ ? fs.readFileSync(gitignorePath, "utf-8")
198
+ : "";
199
+
200
+ const missingRules = rules.filter((rule) => {
201
+ if (rule.trim() === "") return false;
202
+ if (rule.startsWith("#")) return false;
203
+ return !existing.split(/\r?\n/).includes(rule);
204
+ });
205
+
206
+ if (!missingRules.length) {
207
+ log("⏭ .gitignore ya contiene las reglas necesarias.");
208
+ return;
209
+ }
210
+
211
+ const block = rules.join("\n");
212
+ const separator = existing.endsWith("\n") || existing.length === 0 ? "" : "\n";
213
+
214
+ fs.writeFileSync(gitignorePath, `${existing}${separator}${block}`, "utf-8");
215
+ log("✓ Actualizado: .gitignore");
216
+ }
217
+
218
+ function mergeObject(target, source) {
219
+ if (!source || typeof source !== "object") return target || {};
220
+
221
+ const output = target && typeof target === "object" ? { ...target } : {};
222
+
223
+ for (const [key, value] of Object.entries(source)) {
224
+ output[key] = value;
225
+ }
226
+
227
+ return output;
228
+ }
229
+
230
+ function mergePackageFromTemplate({ updateName = false } = {}) {
231
+ const templatePackagePath = path.join(templateRoot, "package.json");
232
+ const targetPackagePath = path.join(targetDir, "package.json");
233
+
234
+ const templatePackage = readJson(templatePackagePath);
235
+ const targetPackage = readJson(targetPackagePath);
236
+
237
+ if (!templatePackage || !targetPackage) return;
238
+
239
+ if (updateName) {
240
+ targetPackage.name = cleanPackageName(path.basename(targetDir));
241
+ }
242
+
243
+ targetPackage.scripts = mergeObject(targetPackage.scripts, {
244
+ dev: targetPackage.scripts?.dev || "slidev slides.md --open --port 3000",
245
+ config: "node scripts/generar-desde-config.mjs",
246
+ semana: "node scripts/semana.mjs",
247
+ "pages:check": "node scripts/preparar-github-pages.mjs",
248
+ "actualizar:tema": "npm create openclass-uniminuto@latest . -- --update-theme",
249
+ "actualizar:tema:preview": "npm run actualizar:tema && npm run config && npm run dev",
250
+ "dev:all": "node scripts/dev-all.mjs",
251
+ "dev:todo": "node scripts/dev-all.mjs",
252
+ "build:all": "node scripts/build-site.mjs",
253
+ "build:incremental": "node scripts/build-incremental.mjs",
254
+ "export:downloads": "node scripts/export-downloads.mjs",
255
+ "export:incremental": "node scripts/export-incremental.mjs",
256
+ "pages:build": "npm run config && npm run export:downloads && npm run build:all",
257
+ "pages:preview": "npm run preview:pages",
258
+ publicar: "node scripts/publicar.mjs",
259
+ nuevo: "node scripts/nuevo-curso.mjs",
260
+ "zip:template": "node scripts/zip-template.mjs",
261
+ });
262
+
263
+ targetPackage.dependencies = mergeObject(
264
+ targetPackage.dependencies,
265
+ templatePackage.dependencies,
266
+ );
267
+
268
+ targetPackage.devDependencies = mergeObject(
269
+ targetPackage.devDependencies,
270
+ templatePackage.devDependencies,
271
+ );
272
+
273
+ writeJson(targetPackagePath, targetPackage);
274
+ log("✓ Actualizado: package.json");
275
+ }
276
+
277
+ function patchPackageLockName() {
278
+ const lockPath = path.join(targetDir, "package-lock.json");
279
+ const lock = readJson(lockPath);
280
+
281
+ if (!lock) return;
282
+
283
+ const innerName = cleanPackageName(path.basename(targetDir));
284
+
285
+ lock.name = innerName;
286
+
287
+ if (lock.packages && lock.packages[""]) {
288
+ lock.packages[""].name = innerName;
289
+ }
290
+
291
+ writeJson(lockPath, lock);
89
292
  }
90
293
 
91
294
  function updateThemeOnly() {
@@ -94,47 +297,79 @@ function updateThemeOnly() {
94
297
  }
95
298
 
96
299
  log("\n┌──────────────────────────────────────────────┐");
97
- log("│ Actualizar tema Open Class UNIMINUTO │");
300
+ log("│ Actualizar infraestructura Open Class │");
98
301
  log("└──────────────────────────────────────────────┘\n");
99
302
  log(`Destino: ${targetDir}`);
100
- log("Modo: solo theme/components/styles/layouts y recursos base seguros\n");
303
+ log("Modo: actualizar tema, scripts, plantillas y recursos base seguros.\n");
304
+ log("No se sobrescriben: semanas/, slides.md ni openclass.config.json existente.\n");
101
305
 
102
306
  const pathsToUpdate = [
307
+ "README.md",
308
+
103
309
  "theme/uniminuto/components",
104
310
  "theme/uniminuto/layouts",
105
311
  "theme/uniminuto/styles",
106
312
  "theme/uniminuto/package.json",
107
313
  "theme/uniminuto/README-AutoFit.md",
314
+
108
315
  "setup",
109
- "snippets"
316
+ "snippets",
317
+
318
+ "scripts/build-site.mjs",
319
+ "scripts/build-incremental.mjs",
320
+ "scripts/decks.mjs",
321
+ "scripts/dev-all.mjs",
322
+ "scripts/export-downloads.mjs",
323
+ "scripts/export-incremental.mjs",
324
+ "scripts/generar-desde-config.mjs",
325
+ "scripts/nuevo-curso.mjs",
326
+ "scripts/preparar-github-pages.mjs",
327
+ "scripts/publicar.mjs",
328
+ "scripts/semana.mjs",
329
+ "scripts/zip-template.mjs",
330
+
331
+ "plantillas",
332
+
333
+ ".github/workflows/deploy.yml",
334
+
335
+ "public/fondos",
110
336
  ];
111
337
 
112
338
  for (const relativePath of pathsToUpdate) {
113
- copyPathAlways(path.join(templateRoot, relativePath), path.join(targetDir, relativePath));
339
+ copyTemplatePathAlways(relativePath);
114
340
  }
115
341
 
116
- ensureFileFromTemplate("public/favicon.png", { onlyIfMissing: true });
117
- ensureFileFromTemplate("public/imagenes/favicon.png", { onlyIfMissing: true });
118
- ensureFileFromTemplate("public/imagenes/avion.png", { onlyIfMissing: false });
342
+ copyTemplatePathIfMissing("openclass.config.json");
343
+
344
+ copyTemplatePathIfMissing("public/descargas/.gitkeep");
345
+ copyTemplatePathIfMissing("public/videos/.gitkeep");
346
+ copyTemplatePathIfMissing("public/imagenes/.gitkeep");
347
+
348
+ copyTemplatePathIfMissing("public/favicon.png");
349
+ copyTemplatePathIfMissing("public/imagenes/favicon.png");
119
350
 
120
- log("\n✅ Tema actualizado sin tocar semanas, slides.md ni openclass.config.json.");
351
+ copyTemplatePathAlways("public/imagenes/avion.png");
352
+
353
+ appendGitignoreRules();
354
+
355
+ mergePackageFromTemplate({ updateName: false });
356
+
357
+ log("\n✅ Infraestructura actualizada sin tocar el contenido académico.");
121
358
  log(" Recomendado ahora:");
122
- log(" npm run dev");
123
- log(" npm run export:downloads");
359
+ log(" npm install");
360
+ log(" npm run config");
361
+ log(" npm run pages:build");
124
362
  log("");
125
363
  }
126
364
 
127
365
  function isEffectivelyEmptyProject(dir) {
128
366
  if (!fs.existsSync(dir)) return true;
129
- const entries = fs.readdirSync(dir).filter((entry) => ![".git", ".gitkeep"].includes(entry));
130
- return entries.length === 0;
131
- }
132
367
 
133
- function patchJson(filePath, patcher) {
134
- if (!fs.existsSync(filePath)) return;
135
- const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
136
- patcher(data);
137
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
368
+ const entries = fs
369
+ .readdirSync(dir)
370
+ .filter((entry) => ![".git", ".gitkeep"].includes(entry));
371
+
372
+ return entries.length === 0;
138
373
  }
139
374
 
140
375
  if (!fs.existsSync(templateRoot)) {
@@ -150,10 +385,14 @@ if (fs.existsSync(targetDir) && !isEffectivelyEmptyProject(targetDir) && !forceO
150
385
  fail(`La carpeta destino ya contiene archivos: ${targetDir}
151
386
 
152
387
  Para trabajar dentro de un repositorio ya creado en GitHub, usa una carpeta vacía o ejecuta con --force si deseas sobrescribir archivos existentes.
388
+
153
389
  Ejemplo:
154
- npm create openclass-uniminuto@latest . -- --iot --force
390
+ npm create openclass-uniminuto@latest . -- --force
391
+
392
+ Para aplicar la configuración IoT inicial:
393
+ npm create openclass-uniminuto@latest . -- --iot
155
394
 
156
- Para actualizar únicamente layouts/tema de un proyecto existente, usa:
395
+ Para actualizar únicamente la infraestructura de un proyecto existente:
157
396
  npm create openclass-uniminuto@latest . -- --update-theme`);
158
397
  }
159
398
 
@@ -161,34 +400,57 @@ log("\n┌───────────────────────
161
400
  log("│ Crear proyecto Open Class UNIMINUTO · Slidev │");
162
401
  log("└──────────────────────────────────────────────┘\n");
163
402
  log(`Destino: ${targetDir}`);
164
- if (isCurrentDirectoryTarget) log("Modo: aplicar plantilla en el directorio actual");
165
- if (forceOverwrite) log("Modo: --force activo; se podrán sobrescribir archivos existentes");
403
+
404
+ if (isCurrentDirectoryTarget) {
405
+ log("Modo: aplicar plantilla en el directorio actual");
406
+ }
407
+
408
+ if (forceOverwrite) {
409
+ log("Modo: --force activo; se podrán sobrescribir archivos existentes");
410
+ }
166
411
 
167
412
  copyTemplate(templateRoot, targetDir);
168
413
 
169
414
  const innerName = cleanPackageName(path.basename(targetDir));
415
+
170
416
  patchJson(path.join(targetDir, "package.json"), (pkg) => {
171
417
  pkg.name = innerName;
172
418
  });
419
+
173
420
  patchJson(path.join(targetDir, "package-lock.json"), (lock) => {
174
421
  lock.name = innerName;
175
- if (lock.packages && lock.packages[""]) lock.packages[""].name = innerName;
422
+
423
+ if (lock.packages && lock.packages[""]) {
424
+ lock.packages[""].name = innerName;
425
+ }
176
426
  });
177
427
 
178
428
  if (flags.has("--iot")) {
179
- const sourceConfig = path.join(targetDir, "config", "openclass.config.iot-desde-openclass-iot.json");
429
+ const sourceConfig = path.join(
430
+ targetDir,
431
+ "config",
432
+ "openclass.config.iot-desde-openclass-iot.json",
433
+ );
434
+
180
435
  const destConfig = path.join(targetDir, "openclass.config.json");
436
+
181
437
  if (!fs.existsSync(sourceConfig)) {
182
438
  fail("No se encontró la configuración IoT incluida en la plantilla.");
183
439
  }
440
+
184
441
  fs.copyFileSync(sourceConfig, destConfig);
185
442
  log("✓ Configuración IoT aplicada en openclass.config.json");
186
443
 
187
- const result = spawnSync(process.execPath, ["scripts/generar-desde-config.mjs", "--force"], {
188
- cwd: targetDir,
189
- stdio: "inherit",
190
- shell: false,
191
- });
444
+ const result = spawnSync(
445
+ process.execPath,
446
+ ["scripts/generar-desde-config.mjs", "--force"],
447
+ {
448
+ cwd: targetDir,
449
+ stdio: "inherit",
450
+ shell: false,
451
+ },
452
+ );
453
+
192
454
  if (result.status !== 0) {
193
455
  fail("El generador de configuración no pudo completar el proyecto IoT.");
194
456
  }
@@ -196,27 +458,39 @@ if (flags.has("--iot")) {
196
458
 
197
459
  if (flags.has("--install")) {
198
460
  log("\nInstalando dependencias con npm install...\n");
461
+
199
462
  const result = spawnSync("npm", ["install"], {
200
463
  cwd: targetDir,
201
464
  stdio: "inherit",
202
465
  shell: process.platform === "win32",
203
466
  });
467
+
204
468
  if (result.status !== 0) {
205
469
  fail("npm install terminó con error. Revisa la salida de consola.");
206
470
  }
207
471
  }
208
472
 
209
473
  log("\n✅ Proyecto creado correctamente.\n");
210
- log(`Siguientes pasos:`);
474
+ log("Siguientes pasos:");
211
475
  log(` cd ${path.relative(process.cwd(), targetDir) || "."}`);
212
- if (!flags.has("--install")) log(" npm install");
213
- if (!flags.has("--iot")) log(" npm run nuevo");
476
+
477
+ if (!flags.has("--install")) {
478
+ log(" npm install");
479
+ }
480
+
481
+ if (!flags.has("--iot")) {
482
+ log(" npm run nuevo");
483
+ }
484
+
214
485
  log(" npm run semana -- 1");
215
486
  log(" npm run dev");
216
- log("\nPara activar una semana adicional:");
217
- log(" npm run semana -- 2 --title \"Título de la semana 2\"");
218
- log("\nPara actualizar layouts/tema en un proyecto existente:");
487
+ log("");
488
+ log("Para activar una semana adicional:");
489
+ log(' npm run semana -- 2 --title "Título de la semana 2"');
490
+ log("");
491
+ log("Para actualizar infraestructura en un proyecto existente:");
219
492
  log(" npm create openclass-uniminuto@latest . -- --update-theme");
220
- log("\nPara revisar la publicación en GitHub Pages:");
221
- log(" npm run pages:check");
222
493
  log("");
494
+ log("Para revisar la publicación en GitHub Pages:");
495
+ log(" npm run pages:check");
496
+ log("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-openclass-uniminuto",
3
- "version": "1.6.1",
3
+ "version": "1.6.4",
4
4
  "description": "Creador de proyectos Open Class UNIMINUTO basados en Slidev, con generación incremental por semanas, actualización de tema y GitHub Pages.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,7 +42,7 @@ jobs:
42
42
  - name: Install Chromium for Slidev export
43
43
  run: npx playwright install chromium --with-deps
44
44
 
45
- - name: Generate and build Open Class
45
+ - name: Generate, export and build Open Class
46
46
  run: npm run pages:build
47
47
 
48
48
  - name: Upload GitHub Pages artifact
@@ -61,4 +61,4 @@ jobs:
61
61
  steps:
62
62
  - name: Deploy to GitHub Pages
63
63
  id: deployment
64
- uses: actions/deploy-pages@v4
64
+ uses: actions/deploy-pages@v4