@despia/local 1.0.2 → 1.0.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.
package/README.md CHANGED
@@ -8,7 +8,7 @@ Universal build plugin to generate `despia/local.json` manifest for [Despia](htt
8
8
 
9
9
  ## Features
10
10
 
11
- - **Universal Support** - Works with Vite, Webpack, Rollup, Next.js, Nuxt, SvelteKit, Astro, Remix, esbuild, Parcel, and more
11
+ - **Universal Support** - Works with Vite, Webpack, Rollup, Nuxt, SvelteKit, Astro, Remix, esbuild, Parcel, and more
12
12
  - **Zero Dependencies** - Uses only Node.js built-in modules
13
13
  - **Automatic Asset Discovery** - Collects all output files (JS, CSS, images, fonts, HTML, etc.)
14
14
  - **Root-Relative Paths** - Formats all paths as root-relative (starting with `/`)
@@ -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 dist --output public/despia/local.json
360
+ npx despia-local build -o public/manifest.json
347
361
  ```
348
362
 
349
363
  ## Framework Support
@@ -416,100 +430,6 @@ export default {
416
430
  };
417
431
  ```
418
432
 
419
- ### Next.js
420
-
421
- **For static export (recommended for static sites):**
422
-
423
- ```javascript
424
- // next.config.js
425
- const withDespiaLocal = require('@despia/local/next');
426
-
427
- module.exports = withDespiaLocal({
428
- entryHtml: 'index.html',
429
- outDir: 'out' // Next.js static export directory
430
- })({
431
- output: 'export',
432
- // your Next.js config
433
- });
434
- ```
435
-
436
- **For static export (alternative):**
437
-
438
- ```javascript
439
- // next.config.js
440
- const withDespiaLocal = require('@despia/local/next');
441
-
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
- ```
450
-
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.
512
-
513
433
  ### Nuxt
514
434
 
515
435
  ```javascript
@@ -782,30 +702,6 @@ The generated manifest is then used by Despia during app hydration and updates t
782
702
  - Paths starting with `/` are preserved as-is
783
703
  - Windows backslashes are converted to forward slashes
784
704
 
785
- ### Next.js SSR Issues
786
-
787
- **Manifest not generated for SSR app:**
788
-
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
794
-
795
- **Missing assets in SSR manifest:**
796
-
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/`
801
-
802
- **Wrong directory for SSR:**
803
-
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)
808
-
809
705
  ## Contributing
810
706
 
811
707
  Contributions welcome! Please open an issue or submit a pull request.
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "@despia/local",
3
- "version": "1.0.2",
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.",
3
+ "version": "1.0.4",
4
+ "description": "Universal build plugin to generate despia/local.json manifest for offline caching in Despia web-native apps. Supports Vite, Webpack, Rollup, Nuxt, SvelteKit, Astro, Remix, esbuild, Parcel, and more.",
5
5
  "type": "module",
6
6
  "main": "./src/core.js",
7
7
  "bin": {
@@ -20,9 +20,6 @@
20
20
  "./rollup": {
21
21
  "import": "./src/rollup.js"
22
22
  },
23
- "./next": {
24
- "import": "./src/next.js"
25
- },
26
23
  "./nuxt": {
27
24
  "import": "./src/nuxt.js"
28
25
  },
@@ -67,7 +64,6 @@
67
64
  "vite",
68
65
  "webpack",
69
66
  "rollup",
70
- "nextjs",
71
67
  "nuxt",
72
68
  "sveltekit",
73
69
  "astro",
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/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();
package/src/next.js DELETED
@@ -1,84 +0,0 @@
1
- /**
2
- * Next.js integration for generating despia/local.json manifest
3
- *
4
- * Usage for static export:
5
- * const withDespiaLocal = require('@despia/local/next');
6
- * module.exports = withDespiaLocal({
7
- * entryHtml: 'index.html',
8
- * outDir: 'out' // Next.js static export directory
9
- * })({
10
- * output: 'export',
11
- * // your next config
12
- * });
13
- *
14
- * Usage for SSR (recommended: use post-build script):
15
- * // package.json
16
- * {
17
- * "scripts": {
18
- * "build": "next build",
19
- * "postbuild": "despia-local .next/static"
20
- * }
21
- * }
22
- *
23
- * Or use the webpack plugin approach (works best for static export):
24
- * const DespiaLocalPlugin = require('@despia/local/webpack');
25
- * module.exports = {
26
- * webpack: (config) => {
27
- * config.plugins.push(new DespiaLocalPlugin({ outDir: '.next' }));
28
- * return config;
29
- * }
30
- * };
31
- */
32
-
33
- import { generateManifest } from './core.js';
34
- import DespiaLocalPlugin from './webpack.js';
35
-
36
- export function withDespiaLocal(pluginOptions = {}) {
37
- const localConfig = {
38
- outDir: pluginOptions.outDir || '.next',
39
- entryHtml: pluginOptions.entryHtml || 'index.html',
40
- ...pluginOptions
41
- };
42
-
43
- return (nextConfig = {}) => {
44
- // Detect if this is static export or SSR
45
- const isStaticExport = nextConfig.output === 'export';
46
- const existingWebpack = nextConfig.webpack;
47
-
48
- return {
49
- ...nextConfig,
50
- 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
- }
71
-
72
- // Call existing webpack config if present
73
- if (typeof existingWebpack === 'function') {
74
- return existingWebpack(config, options);
75
- }
76
-
77
- return config;
78
- }
79
- };
80
- };
81
- }
82
-
83
- // Also export as CommonJS for Next.js compatibility
84
- export default withDespiaLocal;