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.
- package/AGENTS.md +247 -0
- package/client.js +63 -64
- package/commands/Print.js +11 -15
- package/commands/Validations.js +12 -23
- package/commands/buildProduction/buildProduction.js +23 -26
- package/commands/bundle/bundle.js +10 -11
- package/commands/createComponent/createComponent.js +14 -16
- package/commands/deleteComponent/deleteComponent.js +6 -6
- package/commands/doctor/doctor.js +11 -14
- package/commands/getComponent/getComponent.js +99 -162
- package/commands/init/init.js +77 -26
- package/commands/listComponents/listComponents.js +18 -21
- package/commands/startServer/startServer.js +21 -24
- package/commands/startServer/watchServer.js +7 -7
- package/commands/types/types.js +53 -18
- package/commands/utils/PathHelper.js +9 -2
- package/commands/utils/VersionChecker.js +3 -3
- package/commands/utils/bundling/DependencyAnalyzer.js +8 -16
- package/commands/utils/loadConfig.js +31 -0
- package/commands/utils/updateManager.js +3 -4
- package/docs/superpowers/specs/2026-05-10-pwa-generate-design.md +105 -105
- package/package.json +14 -2
- package/post.js +2 -2
- package/tests/bundle-generator.test.js +3 -20
- package/tests/component-registry-parse.test.js +34 -0
- package/tests/fixtures/components.js +8 -0
- package/tests/fixtures/sliceConfig.json +74 -0
- package/tests/getcomponent.test.js +407 -0
- package/tests/helpers/setup.js +97 -0
- package/tests/init-command-contract.test.js +46 -0
- package/tests/local-cli-delegation.test.js +7 -5
- package/tests/path-helper.test.js +206 -0
- package/tests/types-breakage.test.js +491 -0
- package/tests/types-generator-errors.test.js +361 -0
- package/tests/types-generator.test.js +172 -184
package/commands/types/types.js
CHANGED
|
@@ -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(
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
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
|
|
384
|
-
|
|
385
|
-
|
|
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 =
|
|
393
|
-
|
|
394
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 =
|
|
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(
|
|
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; //
|
|
730
|
+
const self = this; // Save reference to instance
|
|
739
731
|
|
|
740
732
|
traverse.default(ast, {
|
|
741
733
|
ObjectExpression(path) {
|
|
742
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
405
|
-
const
|
|
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
|
-
#
|
|
1
|
+
# Design of `slice generate-pwa` (V1)
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Objective
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
7
|
+
The command must be post-bundle, operate on `dist/`, and maintain a simple V1 experience.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## V1 Scope
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
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
|
-
-
|
|
19
|
+
- Apply effective exclusion of `localhost` and `127.0.0.1` in development.
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Out of V1 Scope
|
|
22
22
|
|
|
23
|
-
- Exclusion
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
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
|
|
28
|
+
## Command UX
|
|
29
29
|
|
|
30
|
-
###
|
|
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 "
|
|
37
|
+
slice generate-pwa --name "My App" --short-name "MyApp"
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
### Flags
|
|
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
|
-
###
|
|
46
|
+
### Execution flow
|
|
47
47
|
|
|
48
|
-
1.
|
|
49
|
-
2.
|
|
50
|
-
3.
|
|
51
|
-
4.
|
|
52
|
-
5.
|
|
53
|
-
6.
|
|
54
|
-
7.
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
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
|
-
##
|
|
59
|
+
## Configuration in `sliceConfig.json`
|
|
60
60
|
|
|
61
|
-
|
|
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
|
-
|
|
73
|
+
Rules:
|
|
74
74
|
|
|
75
|
-
-
|
|
76
|
-
- `excludeDomains`
|
|
77
|
-
-
|
|
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
|
-
##
|
|
79
|
+
## Proposed Architecture
|
|
80
80
|
|
|
81
|
-
###
|
|
81
|
+
### CLI Integration
|
|
82
82
|
|
|
83
|
-
-
|
|
83
|
+
- Add command in `client.js`:
|
|
84
84
|
- `generate-pwa`
|
|
85
|
-
-
|
|
86
|
-
-
|
|
85
|
+
- `--strategy` option
|
|
86
|
+
- name options for manifest
|
|
87
87
|
|
|
88
|
-
###
|
|
88
|
+
### New Modules
|
|
89
89
|
|
|
90
90
|
- `commands/pwa/generatePwa.js`
|
|
91
|
-
-
|
|
91
|
+
- Orchestrator of the complete flow.
|
|
92
92
|
- `commands/pwa/ConfigResolver.js`
|
|
93
|
-
-
|
|
93
|
+
- Reads/creates/normalizes `pwa.cache.excludeDomains`.
|
|
94
94
|
- `commands/pwa/AssetManifestBuilder.js`
|
|
95
|
-
-
|
|
95
|
+
- Iterates `dist/` and builds precache list.
|
|
96
96
|
- `commands/pwa/ManifestGenerator.js`
|
|
97
|
-
-
|
|
97
|
+
- Generates `manifest.json` with defaults and flag overrides.
|
|
98
98
|
- `commands/pwa/ServiceWorkerGenerator.js`
|
|
99
|
-
-
|
|
99
|
+
- Generates `sw.js` with selected strategy and exclusions.
|
|
100
100
|
|
|
101
|
-
##
|
|
101
|
+
## Cache Design
|
|
102
102
|
|
|
103
|
-
###
|
|
103
|
+
### Global Rules
|
|
104
104
|
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
108
|
-
-
|
|
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
|
-
###
|
|
110
|
+
### Strategies
|
|
111
111
|
|
|
112
112
|
- `hybrid` (default):
|
|
113
|
-
- assets
|
|
114
|
-
-
|
|
113
|
+
- static assets -> `cache-first`.
|
|
114
|
+
- HTML navigation -> `network-first` with offline fallback.
|
|
115
115
|
- `offline-first`:
|
|
116
|
-
-
|
|
117
|
-
-
|
|
116
|
+
- navigation + static -> `cache-first`.
|
|
117
|
+
- background update when online.
|
|
118
118
|
- `network-first`:
|
|
119
|
-
-
|
|
120
|
-
-
|
|
119
|
+
- navigation -> `network-first`.
|
|
120
|
+
- precached static assets as fallback.
|
|
121
121
|
|
|
122
|
-
##
|
|
122
|
+
## REST API and Security Handling
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
To prevent unwanted backend caching:
|
|
125
125
|
|
|
126
|
-
-
|
|
127
|
-
-
|
|
128
|
-
-
|
|
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
|
-
|
|
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
|
-
-
|
|
135
|
-
-
|
|
136
|
-
-
|
|
137
|
-
-
|
|
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
|
-
-
|
|
145
|
-
-
|
|
144
|
+
- creates `pwa.cache.excludeDomains` section when it does not exist,
|
|
145
|
+
- respects existing config.
|
|
146
146
|
- `AssetManifestBuilder`:
|
|
147
|
-
-
|
|
148
|
-
-
|
|
147
|
+
- includes expected assets,
|
|
148
|
+
- excludes unsuitable files.
|
|
149
149
|
- `ServiceWorkerGenerator`:
|
|
150
|
-
-
|
|
151
|
-
-
|
|
150
|
+
- generates correct logic per strategy,
|
|
151
|
+
- respects `excludeDomains`.
|
|
152
152
|
|
|
153
|
-
###
|
|
153
|
+
### Integration
|
|
154
154
|
|
|
155
|
-
- `slice generate-pwa`
|
|
156
|
-
-
|
|
157
|
-
-
|
|
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
|
|
159
|
+
### Minimal E2E manual
|
|
160
160
|
|
|
161
161
|
- Build + generate-pwa.
|
|
162
|
-
-
|
|
163
|
-
-
|
|
164
|
-
-
|
|
165
|
-
|
|
166
|
-
## Plan
|
|
167
|
-
|
|
168
|
-
- `excludePaths`
|
|
169
|
-
-
|
|
170
|
-
-
|
|
171
|
-
-
|
|
172
|
-
|
|
173
|
-
##
|
|
174
|
-
|
|
175
|
-
-
|
|
176
|
-
-
|
|
177
|
-
-
|
|
178
|
-
-
|
|
179
|
-
- `hybrid`
|
|
180
|
-
-
|
|
181
|
-
-
|
|
182
|
-
-
|
|
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.
|