@despia/local 1.0.2 → 1.0.3

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 CHANGED
@@ -343,7 +343,21 @@ Add to your `package.json`:
343
343
  Or run manually:
344
344
 
345
345
  ```bash
346
- npx despia-local [outputDir] [entryHtml]
346
+ npx despia-local [outputDir] [entryHtml] [--output|-o manifestPath]
347
+ ```
348
+
349
+ **Options:**
350
+ - `--output`, `-o <path>` - Custom output path for manifest file (useful for hosting providers)
351
+ - `--help`, `-h` - Show help message
352
+
353
+ **Examples:**
354
+ ```bash
355
+ # Default: generates manifest in outputDir/despia/local.json
356
+ npx despia-local dist
357
+
358
+ # Custom output location (e.g., for Vercel/Netlify)
359
+ npx despia-local .next/static --output public/despia/local.json
360
+ npx despia-local dist -o public/manifest.json
347
361
  ```
348
362
 
349
363
  ## Framework Support
@@ -418,7 +432,20 @@ export default {
418
432
 
419
433
  ### Next.js
420
434
 
421
- **For static export (recommended for static sites):**
435
+ **Recommended: Client-Side Apps for Local/Offline Apps**
436
+
437
+ For local/offline apps, we **recommend using client-side frameworks** like React + Vite or Create React App instead of Next.js. Client-side apps are better suited for offline/local deployment because:
438
+
439
+ - All features are client-side by default
440
+ - No server-side dependencies
441
+ - Simpler build and deployment
442
+ - Better offline support
443
+
444
+ See the [React/Vite](#react--vite) section for examples.
445
+
446
+ **Supported: Static Export Only**
447
+
448
+ If you're using Next.js, this plugin supports apps using `output: 'export'` (static export mode).
422
449
 
423
450
  ```javascript
424
451
  // next.config.js
@@ -433,82 +460,15 @@ module.exports = withDespiaLocal({
433
460
  });
434
461
  ```
435
462
 
436
- **For static export (alternative):**
463
+ **For SSR Apps with Separate Static Build:**
437
464
 
438
- ```javascript
439
- // next.config.js
440
- const withDespiaLocal = require('@despia/local/next');
465
+ If you need SSR for your main site but want a static build for the local app, Next.js can easily generate a separate static build. You can set up a mini CI/CD pipeline to:
441
466
 
442
- module.exports = withDespiaLocal({
443
- outDir: 'out', // Next.js static export directory
444
- entryHtml: 'index.html'
445
- })({
446
- output: 'export',
447
- // ... rest of config
448
- });
449
- ```
467
+ - Keep your main site as SSR (better for SEO, dynamic content)
468
+ - Generate a separate static export build for the local/offline app
469
+ - Deploy both builds independently
450
470
 
451
- **Alternative: Webpack Plugin Approach (works best for static export):**
452
-
453
- ```javascript
454
- // next.config.js
455
- const DespiaLocalPlugin = require('@despia/local/webpack');
456
-
457
- module.exports = {
458
- output: 'export', // For static export
459
- webpack: (config) => {
460
- config.plugins.push(
461
- new DespiaLocalPlugin({ outDir: 'out' })
462
- );
463
- return config;
464
- }
465
- };
466
- ```
467
-
468
- **Note**: The webpack plugin approach works best for static export. For SSR apps, use the post-build script approach described below.
469
-
470
- **For SSR (Server-Side Rendering) apps:**
471
-
472
- SSR Next.js apps require special handling because:
473
- - Client assets are in `.next/static/` directory (not `.next/`)
474
- - No static HTML files exist (pages are server-rendered)
475
- - Manifest should only include client-side assets (JS, CSS, images)
476
- - Server-side code in `.next/server/` should NOT be included
477
-
478
- **Recommended approach for SSR (most reliable):**
479
-
480
- Use a post-build script in your `package.json`:
481
-
482
- ```json
483
- {
484
- "scripts": {
485
- "build": "next build",
486
- "postbuild": "despia-local .next/static"
487
- }
488
- }
489
- ```
490
-
491
- This approach:
492
- - Runs after Next.js build completes
493
- - Targets `.next/static/` where all client assets are stored
494
- - Works reliably for both SSR and static export
495
- - No `entryHtml` needed (SSR apps don't have static HTML files)
496
-
497
- **Alternative: Using the plugin (may have timing issues):**
498
-
499
- ```javascript
500
- // next.config.js
501
- const withDespiaLocal = require('@despia/local/next');
502
-
503
- module.exports = withDespiaLocal({
504
- // entryHtml is optional for SSR (not used)
505
- outDir: '.next/static' // Target client assets directory
506
- })({
507
- // your Next.js config (SSR mode - no output: 'export')
508
- });
509
- ```
510
-
511
- **Note**: The plugin approach may not work reliably for SSR because Next.js doesn't provide a reliable build completion hook. The post-build script approach is recommended for SSR apps.
471
+ **Note**: Setting up the CI/CD pipeline for dual builds (SSR main site + static local app) requires custom configuration based on your hosting provider and build setup. You'll need to figure out the deployment strategy yourself - this plugin only handles manifest generation for the static export build.
512
472
 
513
473
  ### Nuxt
514
474
 
@@ -782,29 +742,36 @@ The generated manifest is then used by Despia during app hydration and updates t
782
742
  - Paths starting with `/` are preserved as-is
783
743
  - Windows backslashes are converted to forward slashes
784
744
 
785
- ### Next.js SSR Issues
745
+ ### Next.js Troubleshooting
746
+
747
+ **Static Export Issues:**
748
+
749
+ **Manifest not generated:**
750
+ 1. Ensure you're using `output: 'export'` in your `next.config.js`
751
+ 2. Verify the plugin is correctly configured with `withDespiaLocal()`
752
+ 3. Check that `out/` directory exists after build
753
+ 4. Ensure `entryHtml` matches your actual entry HTML file
786
754
 
787
- **Manifest not generated for SSR app:**
755
+ **Wrong directory:**
756
+ - For static export: Use `out/` directory (Next.js default for static export)
757
+ - Never use `.next/` for static export (that's for SSR builds)
788
758
 
789
- 1. Ensure you're targeting `.next/static/` directory (not `.next/`)
790
- 2. Use the post-build script approach: `"postbuild": "despia-local .next/static"`
791
- 3. Verify build completed successfully: `next build` should finish without errors
792
- 4. Check that `.next/static/` directory exists after build
793
- 5. The plugin approach may not work reliably for SSR - prefer post-build script
759
+ **Manifest not accessible:**
760
+ - Manifest is generated in `out/despia/local.json`
761
+ - Ensure your hosting provider serves files from the `out/` directory
762
+ - Static export output should be served as static files
794
763
 
795
- **Missing assets in SSR manifest:**
764
+ **SSR Apps (Not Officially Supported):**
796
765
 
797
- - SSR apps only need client assets (JS, CSS, images)
798
- - Server-side code in `.next/server/` is NOT included (correct behavior)
799
- - Only assets in `.next/static/` should be in the manifest
800
- - Verify you're scanning `.next/static/` not `.next/` or `.next/server/`
766
+ **Important**: This plugin does **not** officially support Next.js SSR apps. SSR requires custom tooling specific to your hosting provider.
801
767
 
802
- **Wrong directory for SSR:**
768
+ If you choose to use post-build scripts for SSR (unsupported):
769
+ 1. Use `.next/static/` directory (client assets only)
770
+ 2. Never include `.next/server/` (server code, not needed)
771
+ 3. Customize the approach based on your hosting provider's requirements
772
+ 4. This is experimental and not guaranteed to work reliably
803
773
 
804
- If you see errors about missing directories:
805
- - For SSR: Use `.next/static/` (client assets only)
806
- - For static export: Use `out/` (full static site)
807
- - Never use `.next/server/` (server code, not needed for manifest)
774
+ For production SSR apps, implement provider-specific solutions rather than relying on this plugin.
808
775
 
809
776
  ## Contributing
810
777
 
@@ -5,21 +5,64 @@
5
5
  * Can be used with any build system by running after build completes
6
6
  *
7
7
  * Usage:
8
- * node generate-offline-manifest.js [outputDir] [entryHtml]
8
+ * node generate-offline-manifest.js [outputDir] [entryHtml] [--output|-o manifestPath]
9
9
  *
10
10
  * Examples:
11
11
  * node generate-offline-manifest.js
12
12
  * node generate-offline-manifest.js dist
13
13
  * node generate-offline-manifest.js dist index.html
14
+ * node generate-offline-manifest.js .next/static --output public/despia/local.json
15
+ * node generate-offline-manifest.js dist -o public/despia/local.json
14
16
  */
15
17
 
16
18
  import { generateManifest } from './src/core.js';
17
19
  import { resolve, join } from 'path';
18
20
  import { existsSync } from 'fs';
19
21
 
20
- // Get command line arguments
21
- const outputDir = process.argv[2] || 'dist';
22
- const entryHtml = process.argv[3] || 'index.html';
22
+ // Parse command line arguments
23
+ let outputDir = 'dist';
24
+ let entryHtml = 'index.html';
25
+ let manifestOutputPath = null;
26
+
27
+ // Check for --help flag
28
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
29
+ console.log(`
30
+ Usage: despia-local [outputDir] [entryHtml] [options]
31
+
32
+ Arguments:
33
+ outputDir Directory to scan for assets (default: 'dist')
34
+ entryHtml Entry HTML filename (default: 'index.html')
35
+
36
+ Options:
37
+ --output, -o <path> Custom output path for manifest file
38
+ --help, -h Show this help message
39
+
40
+ Examples:
41
+ despia-local
42
+ despia-local dist
43
+ despia-local dist index.html
44
+ despia-local .next/static --output public/despia/local.json
45
+ despia-local dist -o public/manifest.json
46
+ `);
47
+ process.exit(0);
48
+ }
49
+
50
+ // Parse arguments
51
+ for (let i = 2; i < process.argv.length; i++) {
52
+ const arg = process.argv[i];
53
+
54
+ if (arg === '--output' || arg === '-o') {
55
+ manifestOutputPath = process.argv[++i];
56
+ if (!manifestOutputPath) {
57
+ console.error('❌ Error: --output/-o requires a path argument');
58
+ process.exit(1);
59
+ }
60
+ } else if (outputDir === 'dist' && !arg.startsWith('-')) {
61
+ outputDir = arg;
62
+ } else if (!arg.startsWith('-') && entryHtml === 'index.html' && manifestOutputPath === null) {
63
+ entryHtml = arg;
64
+ }
65
+ }
23
66
 
24
67
  // Detect Next.js SSR context
25
68
  const isNextJsDir = outputDir.includes('.next');
@@ -68,16 +111,25 @@ try {
68
111
  const paths = generateManifest({
69
112
  outputDir,
70
113
  entryHtml,
71
- skipEntryHtml
114
+ skipEntryHtml,
115
+ manifestOutputPath
72
116
  });
73
117
 
74
- console.log(`✓ Generated despia/local.json`);
118
+ const manifestLocation = manifestOutputPath || join(outputDir, 'despia', 'local.json');
119
+ console.log(`✓ Generated ${manifestLocation}`);
75
120
  console.log(`✓ Included ${paths.length} assets`);
76
121
  if (!skipEntryHtml) {
77
122
  console.log(`✓ Entry HTML: /${entryHtml}`);
78
123
  } else {
79
124
  console.log(`✓ Skipped entry HTML (SSR mode)`);
80
125
  }
126
+
127
+ // Provide helpful hint if using default location for Next.js SSR
128
+ if (isNextJsStatic && !manifestOutputPath) {
129
+ console.log('');
130
+ console.log('💡 Tip: For hosting providers (Vercel, Netlify, etc.), consider using:');
131
+ console.log(` despia-local .next/static --output public/despia/local.json`);
132
+ }
81
133
  } catch (error) {
82
134
  console.error(`❌ Error: ${error.message}`);
83
135
  console.error('💡 Please run this script after your build completes.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@despia/local",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Universal build plugin to generate despia/local.json manifest for offline caching in Despia web-native apps. Supports Vite, Webpack, Rollup, Next.js, Nuxt, SvelteKit, Astro, Remix, esbuild, Parcel, and more.",
5
5
  "type": "module",
6
6
  "main": "./src/core.js",
package/src/core.js CHANGED
@@ -48,15 +48,20 @@ export function collectFiles(dir, baseDir = dir) {
48
48
  /**
49
49
  * Generate the offline manifest file
50
50
  * @param {Object} options
51
- * @param {string} options.outputDir - Output directory path
51
+ * @param {string} options.outputDir - Output directory path (where to scan for assets)
52
52
  * @param {string} options.entryHtml - Entry HTML filename (default: 'index.html')
53
53
  * @param {string[]} options.additionalPaths - Additional paths to include
54
54
  * @param {boolean} options.skipEntryHtml - Skip adding entry HTML to manifest (for SSR apps)
55
+ * @param {string} options.manifestOutputPath - Custom path for manifest file (default: outputDir/despia/local.json)
55
56
  * @returns {string[]} Array of all asset paths
56
57
  */
57
- export function generateManifest({ outputDir, entryHtml = 'index.html', additionalPaths = [], skipEntryHtml = false }) {
58
+ export function generateManifest({ outputDir, entryHtml = 'index.html', additionalPaths = [], skipEntryHtml = false, manifestOutputPath = null }) {
58
59
  const outputPath = resolve(process.cwd(), outputDir);
59
- const manifestPath = join(outputPath, 'despia', 'local.json');
60
+
61
+ // Use custom manifest output path if provided, otherwise default to outputDir/despia/local.json
62
+ const manifestPath = manifestOutputPath
63
+ ? resolve(process.cwd(), manifestOutputPath)
64
+ : join(outputPath, 'despia', 'local.json');
60
65
 
61
66
  // Check if output directory exists
62
67
  if (!existsSync(outputPath)) {
@@ -83,10 +88,12 @@ export function generateManifest({ outputDir, entryHtml = 'index.html', addition
83
88
  // Convert to sorted array
84
89
  const sortedPaths = Array.from(assetPaths).sort();
85
90
 
86
- // Create despia directory if it doesn't exist
87
- const despiaDir = join(outputPath, 'despia');
88
- if (!existsSync(despiaDir)) {
89
- mkdirSync(despiaDir, { recursive: true });
91
+ // Create directory for manifest if it doesn't exist
92
+ const manifestDir = manifestOutputPath
93
+ ? resolve(manifestPath, '..')
94
+ : join(outputPath, 'despia');
95
+ if (!existsSync(manifestDir)) {
96
+ mkdirSync(manifestDir, { recursive: true });
90
97
  }
91
98
 
92
99
  // Write formatted JSON array
package/src/next.js CHANGED
@@ -8,19 +8,19 @@
8
8
  * outDir: 'out' // Next.js static export directory
9
9
  * })({
10
10
  * output: 'export',
11
- * // your next config
11
+ * // your Next.js config
12
12
  * });
13
13
  *
14
- * Usage for SSR (recommended: use post-build script):
14
+ * For SSR apps, use the post-build script approach:
15
15
  * // package.json
16
16
  * {
17
17
  * "scripts": {
18
18
  * "build": "next build",
19
- * "postbuild": "despia-local .next/static"
19
+ * "postbuild": "despia-local .next/static --output public/despia/local.json"
20
20
  * }
21
21
  * }
22
22
  *
23
- * Or use the webpack plugin approach (works best for static export):
23
+ * Or use the webpack plugin approach:
24
24
  * const DespiaLocalPlugin = require('@despia/local/webpack');
25
25
  * module.exports = {
26
26
  * webpack: (config) => {
@@ -41,33 +41,18 @@ export function withDespiaLocal(pluginOptions = {}) {
41
41
  };
42
42
 
43
43
  return (nextConfig = {}) => {
44
- // Detect if this is static export or SSR
45
- const isStaticExport = nextConfig.output === 'export';
46
44
  const existingWebpack = nextConfig.webpack;
47
45
 
48
46
  return {
49
47
  ...nextConfig,
50
48
  webpack: (config, options) => {
51
- // Only add webpack plugin for client builds (not server builds)
52
- // For SSR, the webpack plugin will target .next/static during client build
53
- // For static export, it works normally
54
- if (!options.isServer) {
55
- // Determine the correct output directory
56
- let targetOutDir = localConfig.outDir;
57
-
58
- // For SSR apps, target .next/static where client assets are stored
59
- if (!isStaticExport && targetOutDir === '.next') {
60
- targetOutDir = '.next/static';
61
- }
62
-
63
- config.plugins.push(
64
- new DespiaLocalPlugin({
65
- outDir: targetOutDir,
66
- entryHtml: localConfig.entryHtml,
67
- skipEntryHtml: !isStaticExport // Skip entryHtml for SSR
68
- })
69
- );
70
- }
49
+ // Add Despia Local plugin
50
+ config.plugins.push(
51
+ new DespiaLocalPlugin({
52
+ outDir: localConfig.outDir,
53
+ entryHtml: localConfig.entryHtml
54
+ })
55
+ );
71
56
 
72
57
  // Call existing webpack config if present
73
58
  if (typeof existingWebpack === 'function') {
package/src/webpack.js CHANGED
@@ -3,6 +3,8 @@
3
3
  */
4
4
 
5
5
  import { generateManifest } from './core.js';
6
+ import { readdirSync } from 'fs';
7
+ import { join, relative, extname } from 'path';
6
8
 
7
9
  class DespiaLocalPlugin {
8
10
  constructor(options = {}) {
@@ -10,20 +12,57 @@ class DespiaLocalPlugin {
10
12
  outDir: options.outDir || 'dist',
11
13
  entryHtml: options.entryHtml || 'index.html',
12
14
  skipEntryHtml: options.skipEntryHtml || false,
15
+ extensions: options.extensions || ['.js', '.css', '.mjs', '.woff', '.woff2', '.ttf', '.eot', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico', '.json', '.xml', '.txt'],
16
+ publicDir: options.publicDir || null,
13
17
  ...options
14
18
  };
15
19
  }
16
20
 
21
+ /**
22
+ * Scan public directory for static assets
23
+ */
24
+ scanPublicDir(dir, baseDir, assets) {
25
+ try {
26
+ const entries = readdirSync(dir, { withFileTypes: true });
27
+
28
+ for (const entry of entries) {
29
+ const fullPath = join(dir, entry.name);
30
+
31
+ if (entry.isDirectory()) {
32
+ // Skip despia folder to avoid circular reference
33
+ if (entry.name !== 'despia') {
34
+ this.scanPublicDir(fullPath, baseDir, assets);
35
+ }
36
+ } else {
37
+ const ext = extname(entry.name).toLowerCase();
38
+ if (this.options.extensions.includes(ext)) {
39
+ const relativePath = '/' + relative(baseDir, fullPath).replace(/\\/g, '/');
40
+ assets.add(relativePath);
41
+ }
42
+ }
43
+ }
44
+ } catch (e) {
45
+ // Ignore permission errors
46
+ }
47
+ }
48
+
17
49
  apply(compiler) {
18
50
  const pluginName = 'DespiaLocalPlugin';
51
+ const isNextJs = this.options.isNextJs || false;
52
+ const injectIntoAssets = this.options.injectIntoAssets || false;
53
+
54
+ // For Next.js: Use 'emit' phase to inject into compilation.assets
55
+ // For other bundlers: Use 'afterEmit' phase to write to filesystem
56
+ const hook = injectIntoAssets ? compiler.hooks.emit : compiler.hooks.afterEmit;
19
57
 
20
- compiler.hooks.afterEmit.tapAsync(pluginName, (compilation, callback) => {
58
+ hook.tapAsync(pluginName, (compilation, callback) => {
21
59
  // Detect if this is a Next.js server build
22
60
  // Next.js server builds have specific compiler name patterns
23
61
  const compilerName = compilation.compiler.name || '';
24
- const isNextJsServerBuild = compilerName.includes('server') ||
62
+ const isNextJsServerBuild = (compilerName.includes('server') ||
25
63
  compilerName.includes('Server') ||
26
- compilation.compiler.options?.target === 'node';
64
+ compilation.compiler.options?.target === 'node') &&
65
+ isNextJs;
27
66
 
28
67
  // Skip manifest generation for server builds (SSR apps don't need server-side assets)
29
68
  if (isNextJsServerBuild) {
@@ -31,36 +70,80 @@ class DespiaLocalPlugin {
31
70
  return;
32
71
  }
33
72
 
34
- // Get output path from webpack compiler
35
- const outputPath = compilation.compiler.outputPath || this.options.outDir;
36
- const additionalPaths = [];
73
+ const assets = new Set();
37
74
 
38
- // Collect all emitted assets
39
- for (const [filename, asset] of Object.entries(compilation.assets)) {
40
- if (asset) {
41
- const rootRelativePath = '/' + filename.replace(/\\/g, '/');
42
- additionalPaths.push(rootRelativePath);
75
+ if (injectIntoAssets && isNextJs) {
76
+ // Next.js mode: Collect from webpack compilation and public folder
77
+
78
+ // 1. Collect all webpack-generated assets from .next/static
79
+ for (const filename of Object.keys(compilation.assets)) {
80
+ if (filename === this.options.manifestPath) {
81
+ continue; // Skip the manifest file itself
82
+ }
83
+
84
+ const ext = extname(filename).toLowerCase();
85
+ if (this.options.extensions.includes(ext)) {
86
+ // Next.js serves these at /_next/static/...
87
+ assets.add(`/_next/static/${filename}`);
88
+ }
43
89
  }
44
- }
45
-
46
- // Also collect from compilation.getAssets() if available (webpack 5)
47
- if (compilation.getAssets) {
48
- for (const asset of compilation.getAssets()) {
49
- const rootRelativePath = '/' + asset.name.replace(/\\/g, '/');
50
- additionalPaths.push(rootRelativePath);
90
+
91
+ // 2. Scan /public folder for static assets
92
+ if (this.options.publicDir) {
93
+ this.scanPublicDir(this.options.publicDir, this.options.publicDir, assets);
94
+ }
95
+
96
+ // 3. Generate sorted array (Despia format - just a JSON array)
97
+ const manifest = JSON.stringify([...assets].sort(), null, 2);
98
+
99
+ // 4. Inject into webpack output at despia/local.json
100
+ const manifestPath = this.options.manifestPath || 'despia/local.json';
101
+ compilation.assets[manifestPath] = {
102
+ source: () => manifest,
103
+ size: () => Buffer.byteLength(manifest, 'utf8')
104
+ };
105
+
106
+ console.log(`✓ Injected despia/local.json into build with ${assets.size} assets`);
107
+ } else {
108
+ // Traditional mode: Write to filesystem (for other bundlers)
109
+ const additionalPaths = new Set();
110
+
111
+ // Collect all emitted assets from compilation
112
+ for (const [filename, asset] of Object.entries(compilation.assets)) {
113
+ if (asset && filename !== this.options.manifestPath) {
114
+ let rootRelativePath = filename.replace(/\\/g, '/');
115
+ if (!rootRelativePath.startsWith('/')) {
116
+ rootRelativePath = '/' + rootRelativePath;
117
+ }
118
+ additionalPaths.add(rootRelativePath);
119
+ }
120
+ }
121
+
122
+ // Also collect from compilation.getAssets() if available (webpack 5)
123
+ if (compilation.getAssets) {
124
+ for (const asset of compilation.getAssets()) {
125
+ if (asset.name !== this.options.manifestPath) {
126
+ let assetPath = asset.name.replace(/\\/g, '/');
127
+ if (!assetPath.startsWith('/')) {
128
+ assetPath = '/' + assetPath;
129
+ }
130
+ additionalPaths.add(assetPath);
131
+ }
132
+ }
133
+ }
134
+
135
+ try {
136
+ const outputPath = compilation.compiler.outputPath || this.options.outDir;
137
+ const paths = generateManifest({
138
+ outputDir: outputPath,
139
+ entryHtml: this.options.entryHtml,
140
+ additionalPaths: Array.from(additionalPaths),
141
+ skipEntryHtml: this.options.skipEntryHtml
142
+ });
143
+ console.log(`✓ Generated despia/local.json with ${paths.length} assets`);
144
+ } catch (error) {
145
+ console.error('Error generating despia/local.json:', error.message);
51
146
  }
52
- }
53
-
54
- try {
55
- const paths = generateManifest({
56
- outputDir: outputPath,
57
- entryHtml: this.options.entryHtml,
58
- additionalPaths,
59
- skipEntryHtml: this.options.skipEntryHtml
60
- });
61
- console.log(`✓ Generated despia/local.json with ${paths.length} assets`);
62
- } catch (error) {
63
- console.error('Error generating despia/local.json:', error.message);
64
147
  }
65
148
 
66
149
  callback();