@despia/local 1.0.1 → 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,48 +432,43 @@ export default {
418
432
 
419
433
  ### Next.js
420
434
 
421
- ```javascript
422
- // next.config.js
423
- const withDespiaLocal = require('@despia/local/next');
435
+ **Recommended: Client-Side Apps for Local/Offline Apps**
424
436
 
425
- module.exports = withDespiaLocal({
426
- entryHtml: 'index.html',
427
- outDir: '.next' // or 'out' for static export
428
- })({
429
- // your Next.js config
430
- });
431
- ```
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:
432
438
 
433
- **For static export:**
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).
434
449
 
435
450
  ```javascript
436
451
  // next.config.js
437
452
  const withDespiaLocal = require('@despia/local/next');
438
453
 
439
454
  module.exports = withDespiaLocal({
440
- outDir: 'out', // Next.js static export directory
441
- entryHtml: 'index.html'
455
+ entryHtml: 'index.html',
456
+ outDir: 'out' // Next.js static export directory
442
457
  })({
443
458
  output: 'export',
444
- // ... rest of config
459
+ // your Next.js config
445
460
  });
446
461
  ```
447
462
 
448
- **Alternative: Webpack Plugin Approach**
463
+ **For SSR Apps with Separate Static Build:**
449
464
 
450
- ```javascript
451
- // next.config.js
452
- const DespiaLocalPlugin = require('@despia/local/webpack');
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:
453
466
 
454
- module.exports = {
455
- webpack: (config) => {
456
- config.plugins.push(
457
- new DespiaLocalPlugin({ outDir: '.next' })
458
- );
459
- return config;
460
- }
461
- };
462
- ```
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
470
+
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.
463
472
 
464
473
  ### Nuxt
465
474
 
@@ -733,6 +742,37 @@ The generated manifest is then used by Despia during app hydration and updates t
733
742
  - Paths starting with `/` are preserved as-is
734
743
  - Windows backslashes are converted to forward slashes
735
744
 
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
754
+
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)
758
+
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
763
+
764
+ **SSR Apps (Not Officially Supported):**
765
+
766
+ **Important**: This plugin does **not** officially support Next.js SSR apps. SSR requires custom tooling specific to your hosting provider.
767
+
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
773
+
774
+ For production SSR apps, implement provider-specific solutions rather than relying on this plugin.
775
+
736
776
  ## Contributing
737
777
 
738
778
  Contributions welcome! Please open an issue or submit a pull request.
@@ -5,30 +5,142 @@
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
- import { resolve } from 'path';
19
+ import { resolve, join } from 'path';
20
+ import { existsSync } from 'fs';
18
21
 
19
- // Get command line arguments
20
- const outputDir = process.argv[2] || 'dist';
21
- 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
+ }
66
+
67
+ // Detect Next.js SSR context
68
+ const isNextJsDir = outputDir.includes('.next');
69
+ const isNextJsStatic = outputDir.includes('.next/static') || outputDir === '.next/static';
70
+ const isNextJsRoot = outputDir === '.next';
71
+ const isNextJsServer = outputDir.includes('.next/server');
72
+
73
+ // Determine if we should skip entryHtml (for SSR apps)
74
+ const skipEntryHtml = isNextJsStatic || isNextJsServer;
75
+
76
+ // Provide helpful messages for Next.js
77
+ if (isNextJsDir && !isNextJsStatic && !isNextJsServer) {
78
+ if (isNextJsRoot) {
79
+ console.warn('⚠ Warning: Scanning .next/ directory directly.');
80
+ console.warn('💡 For SSR apps, use: despia-local .next/static');
81
+ console.warn('💡 For static export, use: despia-local out');
82
+ }
83
+ }
84
+
85
+ if (isNextJsServer) {
86
+ console.error('❌ Error: .next/server/ contains server-side code, not client assets.');
87
+ console.error('💡 For SSR apps, use: despia-local .next/static');
88
+ process.exit(1);
89
+ }
22
90
 
23
91
  try {
24
- console.log(`Scanning ${resolve(process.cwd(), outputDir)} for assets...`);
25
- const paths = generateManifest({ outputDir, entryHtml });
92
+ const resolvedPath = resolve(process.cwd(), outputDir);
93
+ console.log(`Scanning ${resolvedPath} for assets...`);
94
+
95
+ // Check if directory exists, and provide helpful suggestions for Next.js
96
+ if (!existsSync(resolvedPath)) {
97
+ if (isNextJsRoot) {
98
+ console.error(`❌ Directory "${resolvedPath}" does not exist.`);
99
+ console.error('💡 For SSR apps, try: despia-local .next/static');
100
+ console.error('💡 For static export, try: despia-local out');
101
+ }
102
+ throw new Error(`Output directory "${resolvedPath}" does not exist.`);
103
+ }
26
104
 
27
- console.log(`✓ Generated despia/local.json`);
105
+ // Check for Next.js static directory and suggest if scanning wrong location
106
+ if (isNextJsRoot && existsSync(join(resolvedPath, 'static'))) {
107
+ console.warn('⚠ Found .next/static/ subdirectory.');
108
+ console.warn('💡 For SSR apps, consider scanning .next/static directly for better results.');
109
+ }
110
+
111
+ const paths = generateManifest({
112
+ outputDir,
113
+ entryHtml,
114
+ skipEntryHtml,
115
+ manifestOutputPath
116
+ });
117
+
118
+ const manifestLocation = manifestOutputPath || join(outputDir, 'despia', 'local.json');
119
+ console.log(`✓ Generated ${manifestLocation}`);
28
120
  console.log(`✓ Included ${paths.length} assets`);
29
- console.log(`✓ Entry HTML: /${entryHtml}`);
121
+ if (!skipEntryHtml) {
122
+ console.log(`✓ Entry HTML: /${entryHtml}`);
123
+ } else {
124
+ console.log(`✓ Skipped entry HTML (SSR mode)`);
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
+ }
30
133
  } catch (error) {
31
- console.error(`Error: ${error.message}`);
32
- console.error('Please run this script after your build completes.');
134
+ console.error(`❌ Error: ${error.message}`);
135
+ console.error('💡 Please run this script after your build completes.');
136
+
137
+ // Additional help for Next.js
138
+ if (isNextJsDir) {
139
+ console.error('');
140
+ console.error('Next.js tips:');
141
+ console.error(' - SSR apps: Use "despia-local .next/static"');
142
+ console.error(' - Static export: Use "despia-local out"');
143
+ }
144
+
33
145
  process.exit(1);
34
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@despia/local",
3
- "version": "1.0.1",
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,14 +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
+ * @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)
54
56
  * @returns {string[]} Array of all asset paths
55
57
  */
56
- export function generateManifest({ outputDir, entryHtml = 'index.html', additionalPaths = [] }) {
58
+ export function generateManifest({ outputDir, entryHtml = 'index.html', additionalPaths = [], skipEntryHtml = false, manifestOutputPath = null }) {
57
59
  const outputPath = resolve(process.cwd(), outputDir);
58
- 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');
59
65
 
60
66
  // Check if output directory exists
61
67
  if (!existsSync(outputPath)) {
@@ -71,19 +77,23 @@ export function generateManifest({ outputDir, entryHtml = 'index.html', addition
71
77
  assetPaths.add(normalizedPath);
72
78
  });
73
79
 
74
- // Ensure entry HTML is included
75
- const entryPath = entryHtml.startsWith('/')
76
- ? entryHtml
77
- : '/' + entryHtml;
78
- assetPaths.add(entryPath);
80
+ // Ensure entry HTML is included (unless skipped for SSR apps)
81
+ if (!skipEntryHtml) {
82
+ const entryPath = entryHtml.startsWith('/')
83
+ ? entryHtml
84
+ : '/' + entryHtml;
85
+ assetPaths.add(entryPath);
86
+ }
79
87
 
80
88
  // Convert to sorted array
81
89
  const sortedPaths = Array.from(assetPaths).sort();
82
90
 
83
- // Create despia directory if it doesn't exist
84
- const despiaDir = join(outputPath, 'despia');
85
- if (!existsSync(despiaDir)) {
86
- 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 });
87
97
  }
88
98
 
89
99
  // Write formatted JSON array
package/src/next.js CHANGED
@@ -1,15 +1,25 @@
1
1
  /**
2
2
  * Next.js integration for generating despia/local.json manifest
3
3
  *
4
- * Usage in next.config.js:
4
+ * Usage for static export:
5
5
  * const withDespiaLocal = require('@despia/local/next');
6
6
  * module.exports = withDespiaLocal({
7
7
  * entryHtml: 'index.html',
8
- * outDir: '.next' // or 'out' for static export
8
+ * outDir: 'out' // Next.js static export directory
9
9
  * })({
10
- * // your next config
10
+ * output: 'export',
11
+ * // your Next.js config
11
12
  * });
12
13
  *
14
+ * For SSR apps, use the post-build script approach:
15
+ * // package.json
16
+ * {
17
+ * "scripts": {
18
+ * "build": "next build",
19
+ * "postbuild": "despia-local .next/static --output public/despia/local.json"
20
+ * }
21
+ * }
22
+ *
13
23
  * Or use the webpack plugin approach:
14
24
  * const DespiaLocalPlugin = require('@despia/local/webpack');
15
25
  * module.exports = {
package/src/webpack.js CHANGED
@@ -3,49 +3,147 @@
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 = {}) {
9
11
  this.options = {
10
12
  outDir: options.outDir || 'dist',
11
13
  entryHtml: options.entryHtml || 'index.html',
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,
12
17
  ...options
13
18
  };
14
19
  }
15
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
+
16
49
  apply(compiler) {
17
50
  const pluginName = 'DespiaLocalPlugin';
51
+ const isNextJs = this.options.isNextJs || false;
52
+ const injectIntoAssets = this.options.injectIntoAssets || false;
18
53
 
19
- compiler.hooks.afterEmit.tapAsync(pluginName, (compilation, callback) => {
20
- // Get output path from webpack compiler
21
- const outputPath = compilation.compiler.outputPath || this.options.outDir;
22
- const additionalPaths = [];
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;
57
+
58
+ hook.tapAsync(pluginName, (compilation, callback) => {
59
+ // Detect if this is a Next.js server build
60
+ // Next.js server builds have specific compiler name patterns
61
+ const compilerName = compilation.compiler.name || '';
62
+ const isNextJsServerBuild = (compilerName.includes('server') ||
63
+ compilerName.includes('Server') ||
64
+ compilation.compiler.options?.target === 'node') &&
65
+ isNextJs;
23
66
 
24
- // Collect all emitted assets
25
- for (const [filename, asset] of Object.entries(compilation.assets)) {
26
- if (asset) {
27
- const rootRelativePath = '/' + filename.replace(/\\/g, '/');
28
- additionalPaths.push(rootRelativePath);
29
- }
67
+ // Skip manifest generation for server builds (SSR apps don't need server-side assets)
68
+ if (isNextJsServerBuild) {
69
+ callback();
70
+ return;
30
71
  }
31
72
 
32
- // Also collect from compilation.getAssets() if available (webpack 5)
33
- if (compilation.getAssets) {
34
- for (const asset of compilation.getAssets()) {
35
- const rootRelativePath = '/' + asset.name.replace(/\\/g, '/');
36
- additionalPaths.push(rootRelativePath);
37
- }
38
- }
73
+ const assets = new Set();
39
74
 
40
- try {
41
- const paths = generateManifest({
42
- outputDir: outputPath,
43
- entryHtml: this.options.entryHtml,
44
- additionalPaths
45
- });
46
- console.log(`✓ Generated despia/local.json with ${paths.length} assets`);
47
- } catch (error) {
48
- console.error('Error generating despia/local.json:', error.message);
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
+ }
89
+ }
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);
146
+ }
49
147
  }
50
148
 
51
149
  callback();