next-build-filter 0.1.0 → 0.2.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/README.md CHANGED
@@ -8,10 +8,11 @@ A powerful Next.js plugin that allows you to exclude specific pages/routes from
8
8
  - 📱 **App Router Support** - Full Next.js 13+ App Router compatibility
9
9
  - 📄 **Pages Router Support** - Traditional Pages Router support
10
10
  - 🎯 **Flexible filtering** with multiple configuration options
11
+ - 🌟 **Glob Pattern Matching** - Use powerful wildcards like `admin/**`, `*/test`, `blog/*`, `**/internal/**`
11
12
  - 🔄 **Non-destructive** - files remain in your codebase
12
13
  - 🌍 **Environment-aware** - different configurations for dev/prod
13
14
  - 📝 **Verbose logging** to see what's being filtered
14
- - 🎨 **Pattern matching** support with regex
15
+ - 🎨 **Advanced pattern matching** support with regex (for complex cases)
15
16
  - 🔧 **TypeScript support** with full type definitions
16
17
 
17
18
  ## Installation
@@ -34,8 +35,8 @@ const filterConfig = {
34
35
  supportPagesRouter: true,
35
36
  supportAppRouter: false,
36
37
  excludedPages: [
37
- 'admin',
38
- 'dev/debug',
38
+ 'admin/**', // Exclude all admin pages (supports glob patterns)
39
+ 'dev/**', // Exclude all dev pages
39
40
  ],
40
41
  };
41
42
 
@@ -56,8 +57,8 @@ const filterConfig = {
56
57
  supportAppRouter: true,
57
58
  supportPagesRouter: false,
58
59
  excludedPages: [
59
- 'admin', // Excludes app/admin/page.tsx
60
- 'dev/debug', // Excludes app/dev/debug/page.tsx
60
+ 'admin/**', // Excludes all routes under app/admin/ (glob pattern)
61
+ 'dev/**', // Excludes all routes under app/dev/
61
62
  ],
62
63
  };
63
64
 
@@ -81,8 +82,9 @@ const filterConfig = {
81
82
  supportAppRouter: true,
82
83
  supportPagesRouter: true,
83
84
  excludedPages: [
84
- 'admin', // Excludes both pages/admin.js and app/admin/page.tsx
85
- 'dev/debug', // Excludes both pages/dev/debug.js and app/dev/debug/page.tsx
85
+ 'admin/**', // Excludes both pages/admin/** and app/admin/** (glob pattern)
86
+ 'dev/**', // Excludes both pages/dev/** and app/dev/**
87
+ '**/internal', // Excludes any route ending with /internal
86
88
  ],
87
89
  };
88
90
 
@@ -106,6 +108,30 @@ FILTER_PAGES=true npm run build
106
108
  npm run build:filtered
107
109
  ```
108
110
 
111
+ ## Glob Pattern Quick Reference
112
+
113
+ The plugin supports powerful glob patterns for flexible page/route matching:
114
+
115
+ | Pattern | What it matches | Example |
116
+ |---------|----------------|---------|
117
+ | `admin` | Exact match | `/admin` only |
118
+ | `admin/*` | One level deep | `/admin/users`, `/admin/settings` |
119
+ | `admin/**` | Any depth | `/admin/users`, `/admin/users/edit`, `/admin/settings/advanced` |
120
+ | `**/test` | Ending with | `/api/test`, `/components/test`, `/admin/tools/test` |
121
+ | `*/debug` | One wildcard | `/api/debug`, `/dev/debug` |
122
+ | `**/internal/**` | Containing | Any route with `/internal/` anywhere in path |
123
+ | `{admin,dev}/**` | Multiple patterns | All routes under `/admin` or `/dev` |
124
+
125
+ **Example Usage:**
126
+ ```javascript
127
+ excludedPages: [
128
+ 'admin/**', // ✅ Exclude all admin routes (recommended)
129
+ 'dev/**/test', // ✅ Exclude test pages in dev directory
130
+ '*-draft', // ✅ Exclude pages ending with -draft
131
+ 'api/*/internal', // ✅ Exclude internal API routes
132
+ ]
133
+ ```
134
+
109
135
  ## Configuration Options
110
136
 
111
137
  ### Basic Options
@@ -127,7 +153,40 @@ npm run build:filtered
127
153
 
128
154
  ### Filtering Options
129
155
 
156
+ The plugin supports three types of matching:
157
+ 1. **Exact Match** - Direct string matching
158
+ 2. **Glob Patterns** - Flexible wildcard matching (powered by [minimatch](https://github.com/isaacs/minimatch))
159
+ 3. **Regex Patterns** - Advanced pattern matching via `excludePatterns`
160
+
161
+ #### Glob Pattern Syntax
162
+
163
+ | Pattern | Description | Example Matches |
164
+ |---------|-------------|-----------------|
165
+ | `*` | Matches any characters except `/` | `blog/*` matches `blog/post1`, `blog/post2` but not `blog/category/post1` |
166
+ | `**` | Matches any characters including `/` | `blog/**` matches `blog/post1`, `blog/category/post1`, `blog/a/b/c` |
167
+ | `?` | Matches exactly one character | `user/?/profile` matches `user/a/profile`, `user/1/profile` |
168
+ | `[abc]` | Matches any character in the brackets | `user/[0-9]/profile` matches `user/0/profile`, `user/5/profile` |
169
+ | `{a,b}` | Matches any of the patterns | `{admin,dev}/**` matches `admin/users`, `dev/debug` |
170
+ | `!` at start | Negation (not commonly used in this plugin) | `!admin/**` would match everything except admin routes |
171
+
172
+ **Common Glob Patterns Cheat Sheet:**
173
+
174
+ | Use Case | Pattern | Matches |
175
+ |----------|---------|---------|
176
+ | Exact page | `admin` | `/admin` only |
177
+ | Direct children | `admin/*` | `/admin/users`, `/admin/settings` (not nested) |
178
+ | All nested routes | `admin/**` | `/admin/users`, `/admin/users/edit`, `/admin/settings/advanced` |
179
+ | Ends with | `**/test` or `**/*-test` | `/api/test`, `/users/profile-test` |
180
+ | Starts with | `admin/**` or `admin*` | `/admin`, `/admin-panel`, `/admin/users` |
181
+ | Contains | `**/internal/**` | Any path with `/internal/` segment |
182
+ | Multiple patterns | `{admin,dev,test}/**` | All routes under `/admin`, `/dev`, or `/test` |
183
+ | Specific file patterns | `**/*-draft` | `/blog/post-draft`, `/products/item-draft` |
184
+ | API versioning | `api/v{1,2}/**` | `/api/v1/*`, `/api/v2/*` |
185
+ | Wildcard in middle | `api/*/internal` | `/api/users/internal`, `/api/products/internal` |
186
+
130
187
  #### 1. Include Only Specific Pages
188
+
189
+ **Exact Match:**
131
190
  ```javascript
132
191
  const filterConfig = {
133
192
  enabled: true,
@@ -139,7 +198,22 @@ const filterConfig = {
139
198
  };
140
199
  ```
141
200
 
201
+ **Glob Pattern Matching:**
202
+ ```javascript
203
+ const filterConfig = {
204
+ enabled: true,
205
+ includedPages: [
206
+ 'index', // Exact match: /
207
+ 'blog/*', // All direct children: /blog/post1, /blog/post2
208
+ 'products/**', // All nested routes: /products/*, /products/category/*, etc.
209
+ 'user/*/profile', // Wildcard in middle: /user/123/profile, /user/456/profile
210
+ ],
211
+ };
212
+ ```
213
+
142
214
  #### 2. Exclude Specific Pages
215
+
216
+ **Exact Match:**
143
217
  ```javascript
144
218
  const filterConfig = {
145
219
  enabled: true,
@@ -151,18 +225,52 @@ const filterConfig = {
151
225
  };
152
226
  ```
153
227
 
154
- #### 3. Pattern-Based Exclusion
228
+ **Glob Pattern Matching:**
229
+ ```javascript
230
+ const filterConfig = {
231
+ enabled: true,
232
+ excludedPages: [
233
+ 'admin', // Exact match: /admin
234
+ 'admin/*', // All admin sub-pages: /admin/users, /admin/settings
235
+ 'admin/**', // All nested admin routes: /admin/*, /admin/users/*, etc.
236
+ 'dev/**', // All dev routes: /dev/*, /dev/debug/*, etc.
237
+ '*/test', // Any route ending with /test: /api/test, /dev/test
238
+ '**/internal/**', // Any route containing /internal/
239
+ ],
240
+ };
241
+ ```
242
+
243
+ #### 3. Regex Pattern-Based Exclusion (Advanced)
244
+
245
+ For complex patterns that can't be expressed with glob syntax, use `excludePatterns`:
155
246
  ```javascript
156
247
  const filterConfig = {
157
248
  enabled: true,
158
249
  excludePatterns: [
159
- 'dev/.*', // All pages in /dev/ directory
160
- '.*admin.*', // Any page with 'admin' in the path
161
- '.*test.*', // Any page with 'test' in the path
250
+ 'dev/.*', // Regex: All pages in /dev/ directory
251
+ '.*admin.*', // Regex: Any page with 'admin' in the path
252
+ '.*test.*', // Regex: Any page with 'test' in the path
253
+ '^api/v[0-9]+/', // Regex: API versioned routes like /api/v1/, /api/v2/
162
254
  ],
163
255
  };
164
256
  ```
165
257
 
258
+ ### Glob Patterns vs Regex Patterns
259
+
260
+ Choose the right pattern type for your use case:
261
+
262
+ | Use Case | Glob Pattern | Regex Pattern | Recommendation |
263
+ |----------|--------------|---------------|----------------|
264
+ | Exclude all admin routes | `admin/**` | `admin/.*` | ✅ Use Glob (simpler) |
265
+ | Exclude routes ending with -test | `**/*-test` | `.*-test$` | ✅ Use Glob (simpler) |
266
+ | Exclude versioned API routes (v1, v2) | N/A | `^api/v[0-9]+/` | ✅ Use Regex (complex pattern) |
267
+ | Match any route containing 'internal' | `**/internal/**` | `.*internal.*` | ✅ Use Glob (simpler) |
268
+ | Match routes with date pattern (2024-01-01) | N/A | `^\d{4}-\d{2}-\d{2}$` | ✅ Use Regex (complex pattern) |
269
+
270
+ **Guidelines:**
271
+ - **Use Glob Patterns** (`includedPages`/`excludedPages`) for most cases - they're simpler and more readable
272
+ - **Use Regex Patterns** (`excludePatterns`) only when you need advanced matching like character classes, lookaheads, or complex alternations
273
+
166
274
  ## Usage Examples
167
275
 
168
276
  ### Development Speed Build
@@ -188,19 +296,114 @@ module.exports = withPageFilter(filterConfig)({
188
296
  ```
189
297
 
190
298
  ### Production Admin Exclusion
191
- Exclude admin and debug pages from production builds:
299
+ Exclude admin and debug pages from production builds using glob patterns:
192
300
 
193
301
  ```javascript
194
302
  const filterConfig = {
195
303
  enabled: process.env.NODE_ENV === 'production',
196
304
  excludedPages: [
197
- 'admin',
198
- 'debug',
199
- 'dev/tools',
305
+ 'admin/**', // Exclude all admin routes
306
+ 'debug', // Exclude debug page
307
+ 'dev/**', // Exclude all dev tools and utilities
308
+ '**/test', // Exclude all test pages
309
+ 'internal/**', // Exclude internal pages
200
310
  ],
201
- excludePatterns: [
202
- 'admin/.*',
203
- 'dev/.*',
311
+ };
312
+ ```
313
+
314
+ ### Multi-team Development
315
+ Build only specific feature sets for different teams:
316
+
317
+ ```javascript
318
+ // Team A: Only marketing pages
319
+ const marketingConfig = {
320
+ enabled: true,
321
+ includedPages: [
322
+ 'index',
323
+ 'about',
324
+ 'contact',
325
+ 'blog/**', // All blog routes
326
+ 'marketing/**', // All marketing pages
327
+ ],
328
+ };
329
+
330
+ // Team B: Only product pages
331
+ const productConfig = {
332
+ enabled: true,
333
+ includedPages: [
334
+ 'products/**', // All product routes
335
+ 'checkout/**', // All checkout routes
336
+ 'cart', // Shopping cart
337
+ ],
338
+ };
339
+ ```
340
+
341
+ ### Exclude Test and Debug Routes
342
+ Use glob patterns to exclude testing and debugging routes:
343
+
344
+ ```javascript
345
+ const filterConfig = {
346
+ enabled: process.env.NODE_ENV === 'production',
347
+ excludedPages: [
348
+ '**/*-test', // Exclude all routes ending with -test
349
+ '**/*-debug', // Exclude all routes ending with -debug
350
+ 'test/**', // Exclude all test directory routes
351
+ 'debug/**', // Exclude all debug directory routes
352
+ 'dev/**', // Exclude all development routes
353
+ 'playground/**', // Exclude playground routes
354
+ ],
355
+ };
356
+ ```
357
+
358
+ ### API Route Filtering
359
+ Filter specific API routes using glob patterns:
360
+
361
+ ```javascript
362
+ const filterConfig = {
363
+ enabled: true,
364
+ excludedPages: [
365
+ 'api/internal/**', // Exclude internal API routes
366
+ 'api/*/admin', // Exclude admin endpoints in any API version
367
+ 'api/webhooks/test-*', // Exclude test webhooks
368
+ 'api/v*/deprecated/**', // Exclude deprecated endpoints in all versions
369
+ ],
370
+ };
371
+ ```
372
+
373
+ ### Feature Flag Based Builds
374
+ Create builds with specific features using glob patterns:
375
+
376
+ ```javascript
377
+ const filterConfig = {
378
+ enabled: true,
379
+ includedPages: [
380
+ 'index', // Home page
381
+ 'about', // About page
382
+ // Conditionally include features based on environment
383
+ ...(process.env.ENABLE_BLOG ? ['blog/**'] : []),
384
+ ...(process.env.ENABLE_SHOP ? ['shop/**', 'cart', 'checkout/**'] : []),
385
+ ...(process.env.ENABLE_FORUM ? ['forum/**', 'community/**'] : []),
386
+ ],
387
+ };
388
+ ```
389
+
390
+ ### Multi-language Site Filtering
391
+ Build specific language versions:
392
+
393
+ ```javascript
394
+ const filterConfig = {
395
+ enabled: true,
396
+ // Only build English version
397
+ includedPages: [
398
+ 'en/**', // All English pages
399
+ 'index', // Root page
400
+ ],
401
+ // Or exclude other languages
402
+ excludedPages: [
403
+ 'fr/**', // Exclude French
404
+ 'de/**', // Exclude German
405
+ 'es/**', // Exclude Spanish
406
+ 'ja/**', // Exclude Japanese
204
407
  ],
205
408
  };
206
409
  ```
@@ -251,10 +454,45 @@ npm start
251
454
  The plugin works by integrating with Next.js's webpack configuration and build process:
252
455
 
253
456
  1. **Webpack Plugin Integration**: The plugin hooks into webpack's module resolution process
254
- 2. **Page Detection**: It identifies page files in the `/pages` directory
255
- 3. **Filtering Logic**: Based on your configuration, it determines which pages to exclude
256
- 4. **Module Resolution**: Excluded pages are replaced with empty modules during the build process
257
- 5. **Build Optimization**: The final bundle only includes the pages you want
457
+ 2. **Page Detection**: It identifies page files in the `/pages` and `/app` directories (based on router support configuration)
458
+ 3. **Path Normalization**: Routes are normalized (lowercase, forward slashes) for consistent matching
459
+ 4. **Pattern Matching**: For each page, the plugin checks if it should be filtered using:
460
+ - **Glob patterns** (via [minimatch](https://github.com/isaacs/minimatch)) - checked first
461
+ - **Exact string matching** - fallback for backward compatibility
462
+ - **Regex patterns** (via `excludePatterns`) - for advanced cases
463
+ 5. **Filtering Logic**: Based on your configuration, it determines which pages to include/exclude
464
+ 6. **Module Replacement**: Filtered pages are replaced with empty modules during the build process
465
+ 7. **Build Optimization**: The final bundle only includes meaningful content for pages you want
466
+
467
+ ### Important: Custom 404 Replacement
468
+
469
+ The plugin **replaces** filtered pages with custom 404 pages rather than **removing** them entirely from the build. This approach:
470
+
471
+ - ✅ **Clear user feedback**: Users see a "Page Not Available" message if they access a filtered page
472
+ - ✅ **Preserves routing structure**: Pages still exist in the manifest
473
+ - ✅ **Prevents build errors**: No missing module errors from dependencies
474
+ - ✅ **Testable**: Contains a unique marker (`NEXT_BUILD_FILTER_EXCLUDED_PAGE`) for verification
475
+ - ✅ **Proper HTTP status**: Returns 404 status code
476
+
477
+ **For App Router**: Uses Next.js's built-in `notFound()` function for proper 404 handling
478
+ **For Pages Router**: Returns a custom 404 component with proper status code
479
+
480
+ **What this means:**
481
+ - Filtered pages will still appear in your `.next/server` directory
482
+ - They will show a "Page Not Available" message if accessed
483
+ - The pages contain minimal code (just the 404 component)
484
+ - Build verification can detect filtered pages via the unique marker
485
+
486
+ ### Pattern Matching Priority
487
+
488
+ When matching routes, the plugin uses this order:
489
+
490
+ 1. **Glob Pattern Match** (via minimatch): `admin/**` matches `admin/users/list`
491
+ 2. **Exact Match**: `admin` matches only `admin`
492
+ 3. **Substring Match**: `admin` also matches routes containing `admin` (backward compatibility)
493
+ 4. **Regex Match** (if using `excludePatterns`): `admin/.*` matches `admin/anything`
494
+
495
+ This multi-tiered approach ensures backward compatibility while providing powerful glob pattern support.
258
496
 
259
497
  ## Project Structure
260
498
 
@@ -333,23 +571,228 @@ npm run build:filtered
333
571
  4. **Test Thoroughly**: Always test your filtered builds to ensure functionality
334
572
  5. **Document Configuration**: Keep your filtering logic well-documented for your team
335
573
 
574
+ ## Migration Guide
575
+
576
+ ### Migrating from Regex Patterns to Glob Patterns
577
+
578
+ If you're currently using `excludePatterns` with regex, consider migrating to glob patterns in `excludedPages` for better readability:
579
+
580
+ **Before (Regex):**
581
+ ```javascript
582
+ const filterConfig = {
583
+ excludePatterns: [
584
+ 'admin/.*', // Regex
585
+ 'dev/.*', // Regex
586
+ '.*test.*', // Regex
587
+ ],
588
+ };
589
+ ```
590
+
591
+ **After (Glob):**
592
+ ```javascript
593
+ const filterConfig = {
594
+ excludedPages: [
595
+ 'admin/**', // Glob - clearer intent
596
+ 'dev/**', // Glob - easier to read
597
+ '**/test/**', // Glob - more intuitive
598
+ ],
599
+ };
600
+ ```
601
+
602
+ **Note:** Both approaches work! Use glob patterns for simplicity and regex for complex patterns.
603
+
336
604
  ## Troubleshooting
337
605
 
338
606
  ### Pages Still Appearing in Build
339
- - Check that `enabled: true` is set in your configuration
340
- - Verify the page paths match exactly (case-sensitive)
341
- - Enable verbose logging to see what's being processed
607
+ - Check that `enabled: true` is set in your configuration
608
+ - Verify the page paths match (glob patterns are case-sensitive by default)
609
+ - Enable `verbose: true` to see what's being processed
610
+ - ✅ Test your glob pattern: `admin/**` matches all nested routes, while `admin/*` only matches direct children
611
+ - ✅ Ensure you're running the build with the correct environment variable: `FILTER_PAGES=true npm run build`
612
+
613
+ ### Glob Pattern Not Matching
614
+
615
+ **Common Issues:**
616
+ 1. **Wrong wildcard usage**
617
+ - ❌ `admin/*` only matches direct children like `/admin/users`
618
+ - ✅ `admin/**` matches all nested routes like `/admin/users/edit`
619
+
620
+ 2. **Case sensitivity**
621
+ - Route paths are normalized to lowercase before matching
622
+ - Pattern: `Admin/**` will be normalized to `admin/**`
623
+
624
+ 3. **Missing or extra slashes**
625
+ - ✅ Correct: `admin/users/**`, `**/test`, `api/*`
626
+ - ❌ Avoid: `/admin/users/**` (leading slash not needed)
627
+
628
+ 4. **Not matching what you expect**
629
+ - Enable `verbose: true` to see the actual route paths
630
+ - Example verbose output: `📄 Filtering out: admin/users/edit`
631
+ - Compare the logged path with your pattern
632
+
633
+ **Testing Your Patterns:**
634
+
635
+ Use `verbose: true` and check the console output during build:
636
+ ```javascript
637
+ const filterConfig = {
638
+ enabled: true,
639
+ verbose: true, // Shows which routes are being filtered
640
+ excludedPages: ['admin/**'],
641
+ };
642
+ ```
643
+
644
+ Console output will show:
645
+ ```
646
+ 📄 Filtering out: admin/dashboard
647
+ 📄 Filtering out: admin/users/list
648
+ 📄 Filtering out: admin/settings/profile
649
+ ```
650
+
651
+ **Pattern Matching Examples:**
652
+
653
+ | Route Path | Pattern | Matches? | Why |
654
+ |------------|---------|----------|-----|
655
+ | `admin/users` | `admin/*` | ✅ Yes | Direct child |
656
+ | `admin/users/edit` | `admin/*` | ❌ No | Too deeply nested |
657
+ | `admin/users/edit` | `admin/**` | ✅ Yes | `**` matches any depth |
658
+ | `blog/post-123` | `blog/*-*` | ✅ Yes | `*` matches `post` and `123` |
659
+ | `api/v1/users` | `api/*/users` | ✅ Yes | `*` matches `v1` |
660
+ | `api/v1/internal/users` | `api/*/users` | ❌ No | Too many segments |
661
+ | `anything/internal/data` | `**/internal/**` | ✅ Yes | `**` matches any segments |
342
662
 
343
663
  ### Build Errors
344
- - Ensure all required pages (like `_app.js`, `_document.js`) are not being filtered
664
+ - Ensure all required pages (like `_app.js`, `_document.js`, `_app.tsx`) are not being filtered
345
665
  - Check that your regex patterns are valid if using `excludePatterns`
666
+ - Avoid overly broad patterns that might exclude critical Next.js files
667
+ - Test patterns incrementally: start with one pattern and add more once working
346
668
 
347
669
  ### Development vs Production Differences
348
670
  - Set `enableInDev: true` if you want consistent behavior across environments
349
671
  - Use environment variables to control filtering per environment
672
+ - Note: By default, filtering is disabled in development mode unless `enableInDev` is set
673
+ - Remember: `npm run dev` vs `npm run build` behave differently by default
674
+
675
+ ### Advanced Debugging
676
+
677
+ If you're having trouble with patterns, try these steps:
678
+
679
+ 1. **Start simple**: Test with an exact match first
680
+ ```javascript
681
+ excludedPages: ['admin'] // Start with exact match
682
+ ```
683
+
684
+ 2. **Add verbosity**: See what's being matched
685
+ ```javascript
686
+ verbose: true
687
+ ```
688
+
689
+ 3. **Test one pattern at a time**: Isolate the problematic pattern
690
+ ```javascript
691
+ excludedPages: ['admin/**'] // Test one at a time
692
+ ```
693
+
694
+ 4. **Check the actual route paths**: Look at your project structure
695
+ ```
696
+ pages/
697
+ admin/
698
+ users.js → Route path: admin/users
699
+ settings/
700
+ profile.js → Route path: admin/settings/profile
701
+ ```
702
+
703
+ 5. **Use the demos**: Test your patterns in the included demo projects
704
+ ```bash
705
+ cd demo/pages-router-demo
706
+ FILTER_PAGES=true npm run build
707
+ ```
708
+
709
+ ## Technical Details
710
+
711
+ ### Glob Pattern Matching Library
712
+
713
+ This plugin uses [minimatch](https://github.com/isaacs/minimatch) for glob pattern matching, the same library used by many popular tools like:
714
+ - npm
715
+ - webpack
716
+ - babel
717
+ - eslint
718
+
719
+ Minimatch provides powerful and reliable glob pattern matching with full support for:
720
+ - Brace expansion: `{a,b,c}`
721
+ - Extended glob patterns: `@(pattern|list)`
722
+ - Multiple wildcards: `**/**/`
723
+ - Character classes: `[abc]`, `[0-9]`
724
+
725
+ ### Performance Considerations
726
+
727
+ - Glob pattern matching is performed during the webpack build phase
728
+ - Pattern matching is highly optimized by minimatch
729
+ - Routes are normalized once and cached for efficient matching
730
+ - Only page/route files are checked (not all webpack modules)
731
+
732
+ ### Compatibility
733
+
734
+ - ✅ Next.js 12+ (Pages Router)
735
+ - ✅ Next.js 13+ (App Router)
736
+ - ✅ Node.js 16+
737
+ - ✅ Works with TypeScript
738
+ - ✅ Compatible with all Next.js deployment targets (standalone, static export, etc.)
739
+
740
+ ## Testing
741
+
742
+ This project includes a comprehensive test suite with unit tests and end-to-end tests.
743
+
744
+ ### Running Tests
745
+
746
+ ```bash
747
+ # Run all unit tests
748
+ npm test
749
+
750
+ # Run unit tests in watch mode (for development)
751
+ npm run test:watch
752
+
753
+ # Run unit tests with coverage report
754
+ npm run test:coverage
755
+
756
+ # Run end-to-end tests (actual builds)
757
+ npm run test:e2e
758
+
759
+ # Run all tests (unit + e2e)
760
+ npm run test:all
761
+ ```
762
+
763
+ ### Test Structure
764
+
765
+ - **Unit Tests** (`tests/unit/`): Test core functionality using Vitest
766
+ - `glob-patterns.test.js`: Tests for glob pattern matching
767
+ - `plugin.test.js`: Tests for plugin core functionality
768
+ - `with-page-filter.test.js`: Tests for configuration wrapper
769
+
770
+ - **E2E Tests** (`tests/e2e/`): Test actual Next.js builds
771
+ - Verifies filtering works correctly in real builds
772
+ - Tests both Pages Router and App Router demos
773
+
774
+ ### Test Coverage
775
+
776
+ The test suite covers:
777
+ - ✅ Glob pattern matching with various patterns
778
+ - ✅ Path normalization and route extraction
779
+ - ✅ Page file identification (Pages Router & App Router)
780
+ - ✅ Filtering logic (includedPages, excludedPages, patterns)
781
+ - ✅ Configuration options and defaults
782
+ - ✅ Webpack integration
783
+ - ✅ Actual build output verification
784
+
785
+ See `tests/README.md` for detailed testing documentation.
350
786
 
351
787
  ## Contributing
352
788
 
789
+ Contributions are welcome! When contributing:
790
+
791
+ 1. Write tests for new features
792
+ 2. Ensure all tests pass: `npm run test:all`
793
+ 3. Update documentation as needed
794
+ 4. Submit a pull request
795
+
353
796
  Feel free to submit issues, feature requests, or pull requests to improve this plugin.
354
797
 
355
798
  ## License
package/index.d.ts CHANGED
@@ -11,13 +11,24 @@ declare module 'next-build-filter' {
11
11
  /** Apply filtering in development mode */
12
12
  enableInDev?: boolean;
13
13
 
14
- /** Pages to include (if specified, only these will be built) */
14
+ /**
15
+ * Pages to include (if specified, only these will be built)
16
+ * Supports glob patterns: 'admin/**', 'blog/*', '*\/test'
17
+ * @example ['index', 'about', 'blog/**', 'products/*']
18
+ */
15
19
  includedPages?: string[];
16
20
 
17
- /** Pages to exclude from build */
21
+ /**
22
+ * Pages to exclude from build
23
+ * Supports glob patterns: 'admin/**', 'dev/*', '*\/internal'
24
+ * @example ['admin/**', 'dev/**', '*\/test']
25
+ */
18
26
  excludedPages?: string[];
19
27
 
20
- /** Regex patterns for page exclusion */
28
+ /**
29
+ * Regex patterns for advanced page exclusion
30
+ * @example ['dev/.*', '.*admin.*', '^api/v[0-9]+/']
31
+ */
21
32
  excludePatterns?: string[];
22
33
 
23
34
  /** Pages directory name (default: 'pages') */
@@ -1,29 +1,94 @@
1
- // Empty module to replace filtered pages
2
- // This module provides a default export that's compatible with both Pages Router and App Router
1
+ /**
2
+ * Empty module to replace filtered pages
3
+ *
4
+ * This module provides a custom 404 response with a detectable marker
5
+ * that can be verified in tests and provides clear feedback to users.
6
+ *
7
+ * Marker: NEXT_BUILD_FILTER_EXCLUDED_PAGE
8
+ */
3
9
 
4
- function EmptyPage() {
5
- return null;
10
+ // Try to import notFound for App Router (will fail gracefully for Pages Router)
11
+ let notFound;
12
+ try {
13
+ notFound = require('next/navigation').notFound;
14
+ } catch (e) {
15
+ // Not in App Router context or Next.js not available
16
+ notFound = null;
6
17
  }
7
18
 
8
- // For React components (Pages Router)
9
- module.exports = EmptyPage;
10
- module.exports.default = EmptyPage;
19
+ // Marker string to identify filtered pages
20
+ const FILTER_MARKER = 'NEXT_BUILD_FILTER_EXCLUDED_PAGE';
11
21
 
12
- // For App Router (ES6 export)
13
- if (typeof exports !== 'undefined') {
14
- exports.default = EmptyPage;
22
+ // For Pages Router: Return a component that renders 404
23
+ function FilteredPage() {
24
+ // If we're in App Router and notFound is available, use it
25
+ if (notFound && typeof notFound === 'function') {
26
+ notFound();
27
+ return null; // notFound() throws, so this won't be reached
28
+ }
29
+
30
+ // For Pages Router or fallback: Return a custom 404 component
31
+ // Using React.createElement to avoid JSX
32
+ const React = require('react');
33
+ return React.createElement(
34
+ 'div',
35
+ {
36
+ style: {
37
+ display: 'flex',
38
+ flexDirection: 'column',
39
+ alignItems: 'center',
40
+ justifyContent: 'center',
41
+ minHeight: '100vh',
42
+ fontFamily: 'system-ui, -apple-system, sans-serif',
43
+ backgroundColor: '#f5f5f5',
44
+ },
45
+ 'data-filter-marker': FILTER_MARKER
46
+ },
47
+ [
48
+ React.createElement('h1', { key: 'title', style: { fontSize: '3rem', margin: '0' } }, '404'),
49
+ React.createElement(
50
+ 'p',
51
+ { key: 'message', style: { fontSize: '1.2rem', color: '#666', marginTop: '1rem' } },
52
+ 'Page Not Available'
53
+ ),
54
+ React.createElement(
55
+ 'p',
56
+ { key: 'detail', style: { fontSize: '0.9rem', color: '#999', marginTop: '0.5rem' } },
57
+ 'This page has been filtered out during build.'
58
+ ),
59
+ React.createElement(
60
+ 'div',
61
+ { key: 'marker', style: { display: 'none' } },
62
+ FILTER_MARKER
63
+ ),
64
+ ]
65
+ );
15
66
  }
16
67
 
68
+ // Set status code for Pages Router
69
+ FilteredPage.getInitialProps = ({ res }) => {
70
+ if (res) {
71
+ res.statusCode = 404;
72
+ }
73
+ return {};
74
+ };
75
+
76
+ // For React components (Pages Router)
77
+ module.exports = FilteredPage;
78
+ module.exports.default = FilteredPage;
79
+
80
+ // Export the marker for testing
81
+ module.exports.FILTER_MARKER = FILTER_MARKER;
82
+
17
83
  // Generate metadata for App Router if needed
18
84
  module.exports.generateMetadata = () => ({
19
- title: 'Page Not Available',
20
- description: 'This page has been filtered out during build'
85
+ title: 'Page Not Available - 404',
86
+ description: 'This page has been filtered out during build',
21
87
  });
22
88
 
23
89
  // Generate static params for dynamic routes if needed
24
90
  module.exports.generateStaticParams = () => [];
25
91
 
26
- // Export all common App Router functions as empty
92
+ // Export all common App Router functions
27
93
  module.exports.generateViewport = () => ({});
28
- module.exports.generateStaticParams = () => [];
29
94
  module.exports.revalidate = false;
@@ -1,11 +1,13 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
+ const { minimatch } = require('minimatch');
3
4
 
4
5
  /**
5
6
  * Next.js Build Filter Plugin
6
7
  *
7
8
  * This plugin allows you to exclude specific pages from the build process
8
9
  * without actually removing the files from your project.
10
+ * Supports glob patterns via minimatch for flexible filtering.
9
11
  */
10
12
  class NextBuildFilterPlugin {
11
13
  constructor(options = {}) {
@@ -64,11 +66,13 @@ class NextBuildFilterPlugin {
64
66
 
65
67
  // Check if this is a page file
66
68
  if (this.isPageFile(request, resolveData.context)) {
69
+ const normalizedRequest = this.normalizePath(request);
70
+ const routePath = this.extractRoutePath(normalizedRequest);
67
71
  const shouldExclude = this.shouldExcludePage(request);
68
72
 
69
73
  if (shouldExclude) {
70
74
  if (this.options.verbose) {
71
- console.log(`📄 NextBuildFilterPlugin: Replacing page with empty module: ${request}`);
75
+ console.log(`📄 Filtering out: ${routePath || request}`);
72
76
  }
73
77
 
74
78
  // Replace with empty module instead of blocking the request
@@ -152,6 +156,7 @@ class NextBuildFilterPlugin {
152
156
 
153
157
  /**
154
158
  * Determine if a page should be excluded from the build
159
+ * Supports glob patterns, exact matches, and regex patterns
155
160
  */
156
161
  shouldExcludePage(request) {
157
162
  const normalizedRequest = this.normalizePath(request);
@@ -164,6 +169,11 @@ class NextBuildFilterPlugin {
164
169
  if (this.options.includedPages.length > 0) {
165
170
  const isIncluded = this.options.includedPages.some(page => {
166
171
  const normalizedPage = this.normalizePath(page);
172
+ // Try glob matching first (using minimatch)
173
+ if (minimatch(routePath, normalizedPage)) {
174
+ return true;
175
+ }
176
+ // Fallback to exact match or prefix match for backward compatibility
167
177
  return routePath === normalizedPage || routePath.startsWith(normalizedPage + '/');
168
178
  });
169
179
  return !isIncluded;
@@ -173,12 +183,17 @@ class NextBuildFilterPlugin {
173
183
  if (this.options.excludedPages.length > 0) {
174
184
  const isExcluded = this.options.excludedPages.some(page => {
175
185
  const normalizedPage = this.normalizePath(page);
186
+ // Try glob matching first (using minimatch)
187
+ if (minimatch(routePath, normalizedPage)) {
188
+ return true;
189
+ }
190
+ // Fallback to exact match or prefix match for backward compatibility
176
191
  return routePath === normalizedPage || routePath.startsWith(normalizedPage + '/');
177
192
  });
178
193
  if (isExcluded) return true;
179
194
  }
180
195
 
181
- // Check exclude patterns
196
+ // Check exclude patterns (regex patterns)
182
197
  if (this.options.excludePatterns.length > 0) {
183
198
  const isPatternExcluded = this.options.excludePatterns.some(pattern => {
184
199
  try {
@@ -227,8 +242,8 @@ class NextBuildFilterPlugin {
227
242
 
228
243
  // Remove file name and extension for App Router
229
244
  if (this.options.supportAppRouter && normalizedRequest.includes(`/${this.options.appDir}/`)) {
230
- // Remove /page.tsx, /page.js, etc.
231
- routePath = routePath.replace(/\/page\.(tsx|ts|jsx|js)$/, '');
245
+ // Remove /page.tsx, /page.js, etc. (with or without leading slash)
246
+ routePath = routePath.replace(/\/?page\.(tsx|ts|jsx|js)$/, '');
232
247
  // Remove leading slash if present
233
248
  routePath = routePath.replace(/^\//, '');
234
249
  } else {
@@ -1,9 +1,11 @@
1
1
  const path = require('path');
2
+ const { minimatch } = require('minimatch');
2
3
 
3
4
  /**
4
5
  * Next.js Page Filter Plugin
5
6
  *
6
7
  * Enhanced version that supports both Pages Router and App Router (Next.js 13+)
8
+ * Supports glob patterns for includedPages and excludedPages options
7
9
  */
8
10
  function withPageFilter(options = {}) {
9
11
  return (nextConfig = {}) => {
@@ -91,6 +93,11 @@ function shouldExcludePage(request, options) {
91
93
  if (options.includedPages && options.includedPages.length > 0) {
92
94
  const isIncluded = options.includedPages.some(page => {
93
95
  const normalizedPage = normalizePath(page);
96
+ // Try glob matching first
97
+ if (minimatch(routePath, normalizedPage)) {
98
+ return true;
99
+ }
100
+ // Fallback to exact match or substring match for backward compatibility
94
101
  return routePath === normalizedPage || routePath.includes(normalizedPage);
95
102
  });
96
103
  return !isIncluded;
@@ -100,12 +107,17 @@ function shouldExcludePage(request, options) {
100
107
  if (options.excludedPages && options.excludedPages.length > 0) {
101
108
  const isExcluded = options.excludedPages.some(page => {
102
109
  const normalizedPage = normalizePath(page);
110
+ // Try glob matching first
111
+ if (minimatch(routePath, normalizedPage)) {
112
+ return true;
113
+ }
114
+ // Fallback to exact match or substring match for backward compatibility
103
115
  return routePath === normalizedPage || routePath.includes(normalizedPage);
104
116
  });
105
117
  if (isExcluded) return true;
106
118
  }
107
119
 
108
- // Check exclude patterns
120
+ // Check exclude patterns (regex patterns)
109
121
  if (options.excludePatterns && options.excludePatterns.length > 0) {
110
122
  const isPatternExcluded = options.excludePatterns.some(pattern => {
111
123
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-build-filter",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A Next.js plugin to exclude pages/routes during build without removing files. Supports both Pages Router and App Router (Next.js 13+).",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -11,8 +11,13 @@
11
11
  "README.md"
12
12
  ],
13
13
  "scripts": {
14
- "test": "echo \"Error: no test specified\" && exit 1",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "test:coverage": "vitest run --coverage",
17
+ "test:e2e": "node tests/e2e/run-e2e-tests.js",
18
+ "test:all": "npm run test && npm run test:e2e",
15
19
  "prepublishOnly": "npm run build",
20
+ "postversion": "npm publish",
16
21
  "build": "echo \"No build process needed for now\""
17
22
  },
18
23
  "keywords": [
@@ -26,26 +31,34 @@
26
31
  "app-router",
27
32
  "pages-router",
28
33
  "optimization",
29
- "bundle"
34
+ "bundle",
35
+ "glob",
36
+ "pattern-matching",
37
+ "wildcard"
30
38
  ],
31
- "author": "",
39
+ "author": "banyudu <banyudu@gmail.com>",
32
40
  "license": "MIT",
33
41
  "peerDependencies": {
34
42
  "next": ">=12.0.0",
35
43
  "react": ">=17.0.0"
36
44
  },
45
+ "dependencies": {
46
+ "minimatch": "^9.0.0"
47
+ },
37
48
  "devDependencies": {
38
- "@types/node": "^20.0.0"
49
+ "@types/node": "^20.0.0",
50
+ "@vitest/coverage-v8": "^1.0.0",
51
+ "vitest": "^1.0.0"
39
52
  },
40
53
  "repository": {
41
54
  "type": "git",
42
- "url": "git+https://github.com/your-username/next-build-filter.git"
55
+ "url": "git+https://github.com/banyudu/next-build-filter.git"
43
56
  },
44
57
  "bugs": {
45
- "url": "https://github.com/your-username/next-build-filter/issues"
58
+ "url": "https://github.com/banyudu/next-build-filter/issues"
46
59
  },
47
- "homepage": "https://github.com/your-username/next-build-filter#readme",
60
+ "homepage": "https://github.com/banyudu/next-build-filter#readme",
48
61
  "engines": {
49
62
  "node": ">=16.0.0"
50
63
  }
51
- }
64
+ }