slicejs-cli 3.4.0 → 3.5.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.
Files changed (35) hide show
  1. package/AGENTS.md +247 -0
  2. package/client.js +63 -64
  3. package/commands/Print.js +11 -15
  4. package/commands/Validations.js +12 -23
  5. package/commands/buildProduction/buildProduction.js +23 -26
  6. package/commands/bundle/bundle.js +10 -11
  7. package/commands/createComponent/createComponent.js +14 -16
  8. package/commands/deleteComponent/deleteComponent.js +6 -6
  9. package/commands/doctor/doctor.js +11 -14
  10. package/commands/getComponent/getComponent.js +99 -162
  11. package/commands/init/init.js +77 -26
  12. package/commands/listComponents/listComponents.js +18 -21
  13. package/commands/startServer/startServer.js +21 -24
  14. package/commands/startServer/watchServer.js +7 -7
  15. package/commands/types/types.js +53 -18
  16. package/commands/utils/PathHelper.js +9 -2
  17. package/commands/utils/VersionChecker.js +3 -3
  18. package/commands/utils/bundling/DependencyAnalyzer.js +8 -16
  19. package/commands/utils/loadConfig.js +31 -0
  20. package/commands/utils/updateManager.js +3 -4
  21. package/docs/superpowers/specs/2026-05-10-pwa-generate-design.md +105 -105
  22. package/package.json +14 -2
  23. package/post.js +2 -2
  24. package/tests/bundle-generator.test.js +3 -20
  25. package/tests/component-registry-parse.test.js +34 -0
  26. package/tests/fixtures/components.js +8 -0
  27. package/tests/fixtures/sliceConfig.json +74 -0
  28. package/tests/getcomponent.test.js +407 -0
  29. package/tests/helpers/setup.js +97 -0
  30. package/tests/init-command-contract.test.js +46 -0
  31. package/tests/local-cli-delegation.test.js +7 -5
  32. package/tests/path-helper.test.js +206 -0
  33. package/tests/types-breakage.test.js +491 -0
  34. package/tests/types-generator-errors.test.js +361 -0
  35. package/tests/types-generator.test.js +172 -184
@@ -4,6 +4,7 @@ import { parse } from '@babel/parser';
4
4
  import traverse from '@babel/traverse';
5
5
 
6
6
  import Print from '../Print.js';
7
+ import { getConfigPath, getComponentsJsPath, joinRoot } from '../utils/PathHelper.js';
7
8
 
8
9
  const TYPE_MAP = {
9
10
  string: 'string',
@@ -114,22 +115,29 @@ const sortKeys = (obj) => {
114
115
  }, {});
115
116
  };
116
117
 
117
- const parseComponentsRegistry = (content) => {
118
+ const parseComponentsRegistry = (content, filePath) => {
118
119
  const match = content.match(/const components = ({[\s\S]*?});/);
119
120
  if (!match) {
120
- throw new Error('Invalid components.js format. Expected: const components = { ... };');
121
+ throw new Error(`Invalid format in ${filePath}. Expected: const components = { ... };`);
122
+ }
123
+ try {
124
+ return JSON.parse(match[1]);
125
+ } catch (parseError) {
126
+ throw new Error(`Failed to parse components registry in ${filePath}: ${parseError.message}`);
121
127
  }
122
- return JSON.parse(match[1]);
123
128
  };
124
129
 
125
- const extractStaticPropsFromSource = (source) => {
130
+ const extractStaticPropsFromSource = (source, filePath) => {
126
131
  let ast;
127
132
  try {
128
133
  ast = parse(source, {
129
134
  sourceType: 'module',
130
135
  plugins: ['classProperties']
131
136
  });
132
- } catch {
137
+ } catch (parseError) {
138
+ const sourceDesc = filePath || 'unknown file';
139
+ const loc = parseError.loc ? ` at line ${parseError.loc.line}, column ${parseError.loc.column}` : '';
140
+ Print.warning(`Parse error in ${sourceDesc}${loc}: ${parseError.message}`);
133
141
  return null;
134
142
  }
135
143
 
@@ -194,7 +202,7 @@ const DEFAULT_EDITOR_EXCLUDE = ['node_modules', 'dist', 'src/libs/**', 'tests/**
194
202
  const NOISY_INCLUDE_PATTERNS = new Set(['src/**/*.js', 'api/**/*.js', 'tests/**/*.js']);
195
203
 
196
204
  const readPublicFolderExcludes = async (projectRoot) => {
197
- const configPath = path.join(projectRoot, 'src', 'sliceConfig.json');
205
+ const configPath = getConfigPath(import.meta.url, projectRoot);
198
206
  if (!(await fs.pathExists(configPath))) return [];
199
207
 
200
208
  try {
@@ -230,7 +238,7 @@ const collectJavaScriptFiles = async (dirPath) => {
230
238
  };
231
239
 
232
240
  const ensureNoCheckInPublicVendorFiles = async (projectRoot) => {
233
- const configPath = path.join(projectRoot, 'src', 'sliceConfig.json');
241
+ const configPath = getConfigPath(import.meta.url, projectRoot);
234
242
  if (!(await fs.pathExists(configPath))) return { updatedFiles: 0, scannedFiles: 0 };
235
243
 
236
244
  let parsed;
@@ -245,7 +253,7 @@ const ensureNoCheckInPublicVendorFiles = async (projectRoot) => {
245
253
  .map((folder) => String(folder || '').trim())
246
254
  .filter(Boolean)
247
255
  .map((folder) => folder.replace(/^[/\\]+/, ''))
248
- .map((folder) => path.join(projectRoot, 'src', folder));
256
+ .map((folder) => joinRoot(projectRoot, 'src', folder));
249
257
 
250
258
  const uniqueDirs = Array.from(new Set(candidateDirs));
251
259
  let scannedFiles = 0;
@@ -360,9 +368,11 @@ const readCategoryPathFromConfig = async (configPath, category) => {
360
368
  };
361
369
 
362
370
  const loadComponentStaticProps = async ({ projectRoot, registryMap }) => {
363
- const configPath = path.join(projectRoot, 'src', 'sliceConfig.json');
371
+ const configPath = getConfigPath(import.meta.url, projectRoot);
364
372
  const categoryPathCache = new Map();
365
373
  const componentPropsMap = {};
374
+ let skippedCount = 0;
375
+ let processedCount = 0;
366
376
 
367
377
  for (const [componentName, category] of Object.entries(sortKeys(registryMap))) {
368
378
  if (!categoryPathCache.has(category)) {
@@ -372,26 +382,50 @@ const loadComponentStaticProps = async ({ projectRoot, registryMap }) => {
372
382
 
373
383
  const categoryPath = categoryPathCache.get(category);
374
384
  if (!categoryPath) {
385
+ skippedCount++;
375
386
  continue;
376
387
  }
377
388
 
378
- const componentFile = path.join(projectRoot, 'src', categoryPath, componentName, `${componentName}.js`);
389
+ const componentFile = joinRoot(projectRoot, 'src', categoryPath, componentName, `${componentName}.js`);
379
390
  if (!(await fs.pathExists(componentFile))) {
391
+ skippedCount++;
392
+ continue;
393
+ }
394
+
395
+ let source;
396
+ try {
397
+ source = await fs.readFile(componentFile, 'utf8');
398
+ } catch (readError) {
399
+ Print.warning(`Cannot read ${componentFile}: ${readError.message}`);
400
+ skippedCount++;
380
401
  continue;
381
402
  }
382
403
 
383
- const source = await fs.readFile(componentFile, 'utf8');
384
- const props = extractStaticPropsFromSource(source);
385
- componentPropsMap[componentName] = props || { [DYNAMIC_FALLBACK_PROP]: { type: 'any', required: false } };
404
+ const props = extractStaticPropsFromSource(source, componentFile);
405
+ if (props) {
406
+ componentPropsMap[componentName] = props;
407
+ processedCount++;
408
+ } else {
409
+ componentPropsMap[componentName] = { [DYNAMIC_FALLBACK_PROP]: { type: 'any', required: false } };
410
+ }
411
+ }
412
+
413
+ if (skippedCount > 0) {
414
+ Print.info(`Skipped ${skippedCount} component(s) with missing or unreadable files`);
386
415
  }
387
416
 
388
417
  return componentPropsMap;
389
418
  };
390
419
 
391
420
  const generateTypesFile = async ({ projectRoot, outputPath }) => {
392
- const registryPath = path.join(projectRoot, 'src', 'Components', 'components.js');
393
- const registryContent = await fs.readFile(registryPath, 'utf8');
394
- const registryMap = parseComponentsRegistry(registryContent);
421
+ const registryPath = getComponentsJsPath(import.meta.url, projectRoot);
422
+ let registryContent;
423
+ try {
424
+ registryContent = await fs.readFile(registryPath, 'utf8');
425
+ } catch (readError) {
426
+ throw new Error(`Cannot read components registry at ${registryPath}: ${readError.message}`);
427
+ }
428
+ const registryMap = parseComponentsRegistry(registryContent, registryPath);
395
429
 
396
430
  const componentPropsMap = await loadComponentStaticProps({ projectRoot, registryMap });
397
431
 
@@ -412,12 +446,12 @@ const toPosixRelative = (projectRoot, targetPath) => {
412
446
 
413
447
  const ensureEditorConfigForTypes = async ({ projectRoot, outputPath }) => {
414
448
  try {
415
- const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
449
+ const tsconfigPath = joinRoot(projectRoot, 'tsconfig.json');
416
450
  if (await fs.pathExists(tsconfigPath)) {
417
451
  return { mode: 'tsconfig_exists', filePath: tsconfigPath, includeAdded: false };
418
452
  }
419
453
 
420
- const jsconfigPath = path.join(projectRoot, 'jsconfig.json');
454
+ const jsconfigPath = joinRoot(projectRoot, 'jsconfig.json');
421
455
  const declarationGlob = (() => {
422
456
  const relative = toPosixRelative(projectRoot, outputPath);
423
457
  if (!relative) return 'src/**/*.d.ts';
@@ -541,5 +575,6 @@ export {
541
575
  extractStaticPropsFromSource,
542
576
  generateDeclarationContent,
543
577
  generateTypesFile,
578
+ parseComponentsRegistry,
544
579
  runGenerateTypes
545
580
  };
@@ -59,10 +59,17 @@ export function getDistPath(moduleUrl, ...segments) {
59
59
  return joinProject(moduleUrl, 'dist', ...segments)
60
60
  }
61
61
 
62
- export function getConfigPath(moduleUrl) {
62
+ export function getConfigPath(moduleUrl, root) {
63
+ if (root) return path.join(root, 'src', 'sliceConfig.json')
63
64
  return joinProject(moduleUrl, 'src', 'sliceConfig.json')
64
65
  }
65
66
 
66
- export function getComponentsJsPath(moduleUrl) {
67
+ export function getComponentsJsPath(moduleUrl, root) {
68
+ if (root) return path.join(root, 'src', 'Components', 'components.js')
67
69
  return joinProject(moduleUrl, 'src', 'Components', 'components.js')
68
70
  }
71
+
72
+ export function joinRoot(root, ...segments) {
73
+ const clean = segments.map(sanitize)
74
+ return path.join(root, ...clean)
75
+ }
@@ -4,7 +4,7 @@ import fs from "fs-extra";
4
4
  import path from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import Print from "../Print.js";
7
- import { getProjectRoot } from "../utils/PathHelper.js";
7
+ import { getProjectRoot, getPath } from "../utils/PathHelper.js";
8
8
 
9
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
 
@@ -25,14 +25,14 @@ class VersionChecker {
25
25
 
26
26
  // Get Framework version from project node_modules
27
27
  const projectRoot = getProjectRoot(import.meta.url);
28
- const frameworkPackagePath = path.join(projectRoot, 'node_modules', 'slicejs-web-framework', 'package.json');
28
+ const frameworkPackagePath = getPath(import.meta.url, 'node_modules', 'slicejs-web-framework', 'package.json');
29
29
  if (await fs.pathExists(frameworkPackagePath)) {
30
30
  const frameworkPackage = await fs.readJson(frameworkPackagePath);
31
31
  this.currentFrameworkVersion = frameworkPackage.version;
32
32
  }
33
33
 
34
34
  // Get Project's CLI version
35
- const projectPackagePath = path.join(__dirname, '../../../../package.json');
35
+ const projectPackagePath = getPath(import.meta.url, 'package.json');
36
36
  if (await fs.pathExists(projectPackagePath)) {
37
37
  const projectPackage = await fs.readJson(projectPackagePath);
38
38
  if (projectPackage.dependencies && projectPackage.dependencies['slicejs-cli']) {
@@ -3,21 +3,13 @@ import fs from 'fs-extra';
3
3
  import path from 'path';
4
4
  import { parse } from '@babel/parser';
5
5
  import traverse from '@babel/traverse';
6
- import { getSrcPath, getComponentsJsPath, getProjectRoot, getConfigPath } from '../PathHelper.js';
6
+ import { getSrcPath, getComponentsJsPath, getConfigPath, getPath } from '../PathHelper.js';
7
7
 
8
8
  export default class DependencyAnalyzer {
9
9
  constructor(moduleUrl) {
10
10
  this.moduleUrl = moduleUrl;
11
- this.projectRoot = getProjectRoot(moduleUrl);
12
11
  this.componentsPath = path.dirname(getComponentsJsPath(moduleUrl));
13
- this.frameworkComponentsPath = path.join(
14
- this.projectRoot,
15
- 'node_modules',
16
- 'slicejs-web-framework',
17
- 'Slice',
18
- 'Components',
19
- 'Structural'
20
- );
12
+ this.frameworkComponentsPath = getPath(moduleUrl, 'node_modules', 'slicejs-web-framework', 'Slice', 'Components', 'Structural');
21
13
  this.routesPath = getSrcPath(moduleUrl, 'routes.js');
22
14
 
23
15
  // Analysis storage
@@ -229,7 +221,7 @@ export default class DependencyAnalyzer {
229
221
  if (!await fs.pathExists(this.frameworkComponentsPath)) {
230
222
  return;
231
223
  }
232
- const frameworkConfigPath = path.join(this.projectRoot, 'src', 'sliceConfig.json');
224
+ const frameworkConfigPath = getConfigPath(this.moduleUrl);
233
225
  let frameworkConfig = {};
234
226
  try {
235
227
  frameworkConfig = await fs.readJson(frameworkConfigPath);
@@ -723,7 +715,7 @@ export default class DependencyAnalyzer {
723
715
  */
724
716
  async analyzeRoutes() {
725
717
  if (!await fs.pathExists(this.routesPath)) {
726
- throw new Error('routes.js no encontrado');
718
+ throw new Error(`routes.js not found at expected path: ${this.routesPath}. Ensure your routes.js file exists in the src directory.`);
727
719
  }
728
720
 
729
721
  const content = await fs.readFile(this.routesPath, 'utf-8');
@@ -735,11 +727,11 @@ export default class DependencyAnalyzer {
735
727
  });
736
728
 
737
729
  let currentRoute = null;
738
- const self = this; // Guardar referencia a la instancia
730
+ const self = this; // Save reference to instance
739
731
 
740
732
  traverse.default(ast, {
741
733
  ObjectExpression(path) {
742
- // Buscar objetos de ruta: { path: '/', component: 'HomePage' }
734
+ // Look for route objects: { path: '/', component: 'HomePage' }
743
735
  const properties = path.node.properties;
744
736
  const pathProp = properties.find(p => p.key?.name === 'path');
745
737
  const componentProp = properties.find(p => p.key?.name === 'component');
@@ -756,7 +748,7 @@ export default class DependencyAnalyzer {
756
748
 
757
749
  self.routes.set(routePath, currentRoute);
758
750
 
759
- // Marcar el componente como usado por esta ruta
751
+ // Mark the component as used by this route
760
752
  if (self.components.has(componentName)) {
761
753
  self.components.get(componentName).routes.add(routePath);
762
754
  }
@@ -768,7 +760,7 @@ export default class DependencyAnalyzer {
768
760
  this.routeGroups = this.detectRouteGroups();
769
761
 
770
762
  } catch (error) {
771
- console.warn(`⚠️ Error parseando rutas: ${error.message}`);
763
+ console.warn(`⚠️ Error parsing routes at ${this.routesPath}: ${error.message}`);
772
764
  }
773
765
  }
774
766
 
@@ -0,0 +1,31 @@
1
+ import fs from 'fs-extra';
2
+ import { getConfigPath } from './PathHelper.js';
3
+ import Print from '../Print.js';
4
+
5
+ export async function loadConfig(moduleUrl) {
6
+ try {
7
+ const configPath = getConfigPath(moduleUrl);
8
+ if (!await fs.pathExists(configPath)) {
9
+ return null;
10
+ }
11
+ const rawData = await fs.readFile(configPath, 'utf-8');
12
+ return JSON.parse(rawData);
13
+ } catch (error) {
14
+ Print.error(`Error loading configuration: ${error.message}`);
15
+ return null;
16
+ }
17
+ }
18
+
19
+ export function loadConfigSync(moduleUrl) {
20
+ try {
21
+ const configPath = getConfigPath(moduleUrl);
22
+ if (!fs.existsSync(configPath)) {
23
+ return null;
24
+ }
25
+ const rawData = fs.readFileSync(configPath, 'utf-8');
26
+ return JSON.parse(rawData);
27
+ } catch (error) {
28
+ Print.error(`Error loading configuration: ${error.message}`);
29
+ return null;
30
+ }
31
+ }
@@ -6,7 +6,7 @@ import inquirer from "inquirer";
6
6
  import ora from "ora";
7
7
  import Print from "../Print.js";
8
8
  import versionChecker from "./VersionChecker.js";
9
- import { getProjectRoot } from "../utils/PathHelper.js";
9
+ import { getProjectRoot, getApiPath, getPath } from "../utils/PathHelper.js";
10
10
  import path from "path";
11
11
  import { fileURLToPath } from "url";
12
12
  import fs from "fs-extra";
@@ -401,9 +401,8 @@ export class UpdateManager {
401
401
 
402
402
  async updateApiIndexIfNeeded(options = {}) {
403
403
  try {
404
- const projectRoot = getProjectRoot(import.meta.url);
405
- const projectApiPath = path.join(projectRoot, 'api', 'index.js');
406
- const frameworkApiPath = path.join(projectRoot, 'node_modules', 'slicejs-web-framework', 'api', 'index.js');
404
+ const projectApiPath = getApiPath(import.meta.url, 'index.js');
405
+ const frameworkApiPath = getPath(import.meta.url, 'node_modules', 'slicejs-web-framework', 'api', 'index.js');
407
406
 
408
407
  if (!await fs.pathExists(projectApiPath) || !await fs.pathExists(frameworkApiPath)) {
409
408
  return;
@@ -1,64 +1,64 @@
1
- # Diseno de `slice generate-pwa` (V1)
1
+ # Design of `slice generate-pwa` (V1)
2
2
 
3
- ## Objetivo
3
+ ## Objective
4
4
 
5
- Agregar un comando dedicado de CLI, `slice generate-pwa`, que convierta un build de Slice en una PWA usable offline, con estrategia de cache configurable y exclusion explicita de dominios de backend para evitar cache accidental de APIs REST.
5
+ Add a dedicated CLI command, `slice generate-pwa`, that converts a Slice build into an offline-capable PWA, with configurable cache strategy and explicit backend domain exclusion to prevent accidental REST API caching.
6
6
 
7
- El comando debe ser postbundle, operar sobre `dist/` y mantener una experiencia simple de V1.
7
+ The command must be post-bundle, operate on `dist/`, and maintain a simple V1 experience.
8
8
 
9
- ## Alcance V1
9
+ ## V1 Scope
10
10
 
11
- - Nuevo comando `slice generate-pwa`.
12
- - Ejecutar `build` automaticamente antes del proceso PWA.
13
- - Generar `manifest.json` en `dist/`.
14
- - Generar `sw.js` en `dist/`.
15
- - Registrar Service Worker en el HTML de entrada de `dist`.
16
- - Soportar estrategias: `hybrid` (default), `offline-first`, `network-first`.
17
- - Persistir y leer configuracion desde `src/sliceConfig.json` en:
11
+ - New `slice generate-pwa` command.
12
+ - Automatically run `build` before the PWA process.
13
+ - Generate `manifest.json` in `dist/`.
14
+ - Generate `sw.js` in `dist/`.
15
+ - Register Service Worker in the entry HTML of `dist`.
16
+ - Support strategies: `hybrid` (default), `offline-first`, `network-first`.
17
+ - Persist and read configuration from `src/sliceConfig.json` in:
18
18
  - `pwa.cache.excludeDomains`.
19
- - Aplicar exclusion efectiva de `localhost` y `127.0.0.1` en desarrollo.
19
+ - Apply effective exclusion of `localhost` and `127.0.0.1` in development.
20
20
 
21
- ## Fuera de alcance V1
21
+ ## Out of V1 Scope
22
22
 
23
- - Exclusion por paths o headers (`excludePaths`, `excludeHeaders`).
24
- - UI interactiva avanzada para crear iconos PWA.
25
- - Soporte de push notifications, background sync o runtime caching avanzado por tipo de API.
26
- - Plugin system formal; se deja preparado para evolucion futura.
23
+ - Exclusion by paths or headers (`excludePaths`, `excludeHeaders`).
24
+ - Advanced interactive UI for creating PWA icons.
25
+ - Support for push notifications, background sync, or advanced runtime caching by API type.
26
+ - Formal plugin system; left prepared for future evolution.
27
27
 
28
- ## UX del comando
28
+ ## Command UX
29
29
 
30
- ### Sintaxis
30
+ ### Syntax
31
31
 
32
32
  ```bash
33
33
  slice generate-pwa
34
34
  slice generate-pwa --strategy hybrid
35
35
  slice generate-pwa --strategy offline-first
36
36
  slice generate-pwa --strategy network-first
37
- slice generate-pwa --name "Mi App" --short-name "MiApp"
37
+ slice generate-pwa --name "My App" --short-name "MyApp"
38
38
  ```
39
39
 
40
- ### Flags V1
40
+ ### V1 Flags
41
41
 
42
42
  - `--strategy <hybrid|offline-first|network-first>` (default: `hybrid`)
43
43
  - `--name <string>`
44
44
  - `--short-name <string>`
45
45
 
46
- ### Flujo de ejecucion
46
+ ### Execution flow
47
47
 
48
- 1. Ejecuta build de produccion.
49
- 2. Lee y normaliza configuracion PWA en `src/sliceConfig.json`.
50
- 3. Genera manifiesto de assets para precache desde `dist/`.
51
- 4. Genera `dist/manifest.json`.
52
- 5. Genera `dist/sw.js` con la estrategia seleccionada.
53
- 6. Inyecta (o asegura) registro SW en HTML de entrada de `dist`.
54
- 7. Imprime resumen final:
55
- - estrategia usada,
56
- - cantidad de assets precacheados,
57
- - dominios excluidos efectivos.
48
+ 1. Run production build.
49
+ 2. Read and normalize PWA configuration from `src/sliceConfig.json`.
50
+ 3. Generate asset manifest for precache from `dist/`.
51
+ 4. Generate `dist/manifest.json`.
52
+ 5. Generate `dist/sw.js` with the selected strategy.
53
+ 6. Inject (or ensure) SW registration in entry HTML of `dist`.
54
+ 7. Print final summary:
55
+ - strategy used,
56
+ - number of precached assets,
57
+ - effective excluded domains.
58
58
 
59
- ## Configuracion en `sliceConfig.json`
59
+ ## Configuration in `sliceConfig.json`
60
60
 
61
- Seccion minima V1:
61
+ V1 minimal section:
62
62
 
63
63
  ```json
64
64
  {
@@ -70,113 +70,113 @@ Seccion minima V1:
70
70
  }
71
71
  ```
72
72
 
73
- Reglas:
73
+ Rules:
74
74
 
75
- - Si `pwa` no existe, el comando crea la seccion sin romper configuracion previa.
76
- - `excludeDomains` acepta hosts exactos (ej: `api.midominio.com`).
77
- - En ejecucion de desarrollo, se agregan de forma efectiva (no necesariamente persistida) `localhost` y `127.0.0.1`.
75
+ - If `pwa` does not exist, the command creates the section without breaking existing configuration.
76
+ - `excludeDomains` accepts exact hosts (e.g., `api.mydomain.com`).
77
+ - In development execution, `localhost` and `127.0.0.1` are effectively added (not necessarily persisted).
78
78
 
79
- ## Arquitectura propuesta
79
+ ## Proposed Architecture
80
80
 
81
- ### Integracion CLI
81
+ ### CLI Integration
82
82
 
83
- - Agregar comando en `client.js`:
83
+ - Add command in `client.js`:
84
84
  - `generate-pwa`
85
- - opcion `--strategy`
86
- - opciones de nombre para manifest
85
+ - `--strategy` option
86
+ - name options for manifest
87
87
 
88
- ### Modulos nuevos
88
+ ### New Modules
89
89
 
90
90
  - `commands/pwa/generatePwa.js`
91
- - Orquestador del flujo completo.
91
+ - Orchestrator of the complete flow.
92
92
  - `commands/pwa/ConfigResolver.js`
93
- - Lee/crea/normaliza `pwa.cache.excludeDomains`.
93
+ - Reads/creates/normalizes `pwa.cache.excludeDomains`.
94
94
  - `commands/pwa/AssetManifestBuilder.js`
95
- - Recorre `dist/` y arma lista precache.
95
+ - Iterates `dist/` and builds precache list.
96
96
  - `commands/pwa/ManifestGenerator.js`
97
- - Genera `manifest.json` con defaults y overrides por flags.
97
+ - Generates `manifest.json` with defaults and flag overrides.
98
98
  - `commands/pwa/ServiceWorkerGenerator.js`
99
- - Genera `sw.js` con estrategia seleccionada y exclusiones.
99
+ - Generates `sw.js` with selected strategy and exclusions.
100
100
 
101
- ## Diseno de cache
101
+ ## Cache Design
102
102
 
103
- ### Reglas globales
103
+ ### Global Rules
104
104
 
105
- - Interceptar solo requests `GET`.
106
- - Si el host esta en `excludeDomains`, hacer `fetch` directo (sin cache).
107
- - Versionado de cache por build id (timestamp o hash de build).
108
- - Al activar nuevo SW, limpiar caches viejas automaticamente.
105
+ - Intercept only `GET` requests.
106
+ - If the host is in `excludeDomains`, do a direct `fetch` (no cache).
107
+ - Cache versioning by build id (timestamp or build hash).
108
+ - On new SW activation, automatically clean old caches.
109
109
 
110
- ### Estrategias
110
+ ### Strategies
111
111
 
112
112
  - `hybrid` (default):
113
- - assets estaticos -> `cache-first`.
114
- - navegacion HTML -> `network-first` con fallback offline.
113
+ - static assets -> `cache-first`.
114
+ - HTML navigation -> `network-first` with offline fallback.
115
115
  - `offline-first`:
116
- - navegacion + estaticos -> `cache-first`.
117
- - update en background cuando haya red.
116
+ - navigation + static -> `cache-first`.
117
+ - background update when online.
118
118
  - `network-first`:
119
- - navegacion -> `network-first`.
120
- - estaticos precacheados como respaldo.
119
+ - navigation -> `network-first`.
120
+ - precached static assets as fallback.
121
121
 
122
- ## Manejo de API REST y seguridad
122
+ ## REST API and Security Handling
123
123
 
124
- Para evitar cache de backend no deseado:
124
+ To prevent unwanted backend caching:
125
125
 
126
- - Exclusion por dominio con `excludeDomains` (regla principal de V1).
127
- - Limitar runtime cache a activos del frontend y navegacion segun estrategia.
128
- - No cachear metodos distintos de `GET`.
126
+ - Domain exclusion via `excludeDomains` (main V1 rule).
127
+ - Limit runtime cache to frontend assets and navigation per strategy.
128
+ - Do not cache methods other than `GET`.
129
129
 
130
- Resultado: los assets del cliente se aceleran offline, pero el backend queda fuera de cache por configuracion explicita.
130
+ Result: client assets are accelerated offline, but the backend stays out of the cache via explicit configuration.
131
131
 
132
132
  ## Error handling
133
133
 
134
- - Si build falla, abortar `generate-pwa` con mensaje claro.
135
- - Si `dist/` no existe tras build, abortar con diagnostico.
136
- - Si `sliceConfig.json` es invalido, mostrar error con sugerencia de reparacion.
137
- - Si no se puede inyectar registro SW en HTML, reportar warning y ruta objetivo.
134
+ - If build fails, abort `generate-pwa` with a clear message.
135
+ - If `dist/` does not exist after build, abort with diagnostics.
136
+ - If `sliceConfig.json` is invalid, show error with repair suggestion.
137
+ - If SW registration cannot be injected into HTML, report warning and target path.
138
138
 
139
139
  ## Testing
140
140
 
141
141
  ### Unit tests
142
142
 
143
143
  - `ConfigResolver`:
144
- - crea seccion `pwa.cache.excludeDomains` cuando no existe,
145
- - respeta config existente.
144
+ - creates `pwa.cache.excludeDomains` section when it does not exist,
145
+ - respects existing config.
146
146
  - `AssetManifestBuilder`:
147
- - incluye assets esperados,
148
- - excluye archivos no aptos.
147
+ - includes expected assets,
148
+ - excludes unsuitable files.
149
149
  - `ServiceWorkerGenerator`:
150
- - genera logica correcta por estrategia,
151
- - respeta `excludeDomains`.
150
+ - generates correct logic per strategy,
151
+ - respects `excludeDomains`.
152
152
 
153
- ### Integracion
153
+ ### Integration
154
154
 
155
- - `slice generate-pwa` ejecuta build y crea `dist/manifest.json` + `dist/sw.js`.
156
- - registro SW presente en HTML de salida.
157
- - exclusiones de dominio aplicadas en codigo generado.
155
+ - `slice generate-pwa` runs build and creates `dist/manifest.json` + `dist/sw.js`.
156
+ - SW registration present in output HTML.
157
+ - domain exclusions applied in generated code.
158
158
 
159
- ### E2E manual minima
159
+ ### Minimal E2E manual
160
160
 
161
161
  - Build + generate-pwa.
162
- - Abrir app, validar installability (manifest).
163
- - Apagar red, validar navegacion offline en `hybrid`.
164
- - Verificar que requests a dominio excluido no se sirven desde cache SW.
165
-
166
- ## Plan de evolucion (post V1)
167
-
168
- - `excludePaths` y `excludeHeaders`.
169
- - soporte de iconos y shortcuts PWA asistidos.
170
- - estrategia por ruta (ej: `/api/*` network-only).
171
- - extraer pipeline postbundle reusable para otras features.
172
-
173
- ## Criterios de aceptacion
174
-
175
- - Existe comando `slice generate-pwa` funcional.
176
- - Ejecuta build antes de generar artefactos PWA.
177
- - Genera `manifest.json` y `sw.js` en `dist/`.
178
- - Registra SW en HTML principal de salida.
179
- - `hybrid` es default con HTML `network-first` y fallback offline.
180
- - Lee/escribe `pwa.cache.excludeDomains` en `src/sliceConfig.json`.
181
- - Excluye dominios configurados del cache runtime.
182
- - Muestra resumen final legible al usuario.
162
+ - Open app, validate installability (manifest).
163
+ - Turn off network, validate offline navigation in `hybrid`.
164
+ - Verify that requests to excluded domain are not served from SW cache.
165
+
166
+ ## Evolution Plan (post V1)
167
+
168
+ - `excludePaths` and `excludeHeaders`.
169
+ - Assisted PWA icon and shortcut support.
170
+ - Per-route strategy (e.g., `/api/*` network-only).
171
+ - Extract reusable postbundle pipeline for other features.
172
+
173
+ ## Acceptance Criteria
174
+
175
+ - Functional `slice generate-pwa` command exists.
176
+ - Runs build before generating PWA artifacts.
177
+ - Generates `manifest.json` and `sw.js` in `dist/`.
178
+ - Registers SW in main output HTML.
179
+ - `hybrid` is default with HTML `network-first` and offline fallback.
180
+ - Reads/writes `pwa.cache.excludeDomains` in `src/sliceConfig.json`.
181
+ - Excludes configured domains from runtime cache.
182
+ - Shows a readable final summary to the user.