Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,60 @@ Add `sass` and `sass-loader` when using `getReactifiedConfig` with `sassLoadPath

Generates webpack configurations for vanilla JS and CSS files. Creates both minified and unminified versions.

By default (`strategy: 'per-file'`), each source file gets its own webpack config per variant. With many files, webpack-cli runs them all in parallel via MultiCompiler, which increases peak memory and build time. Use `strategy: 'bundled'` to emit up to four configs total (JS min/unmin + CSS min/unmin) with multi-entry builds — recommended for projects with more than a handful of vanilla assets. Invalid `strategy` values throw at config generation time. Glob matches are deduplicated (stable order) before entries are built.

**Required options:**
- `workingDirectory` - Working directory for glob resolution (typically `__dirname`)
- `jsBuildDirectory` - Output directory for JS files
- `cssBuildDirectory` - Output directory for CSS files
- `isProduction` - Enable production mode optimizations

**Optional options:**
- `strategy` - `'per-file'` (default, backward compatible) or `'bundled'` (one config per asset type and variant)
- `entryKey` - When `strategy` is `'bundled'`: `'basename'` (default, flat output names matching the legacy per-file layout, e.g. `js/src/settings.js` → `js/build/settings.min.js`) or `'relative'` (nested paths under the build directory)
- `jsPatterns` - Glob patterns for JS files (default: `['**/*.js']`)
- `jsIgnorePatterns` - Patterns to ignore for JS files (default: `[]`)
- `cssPatterns` - Glob patterns for CSS files (default: `['**/*.css']`)
- `cssIgnorePatterns` - Patterns to ignore for CSS files (default: `[]`)

**Example:**
**Entry naming**

| Mode | Option | Entry key example | Output example |
|------|--------|-------------------|----------------|
| Per-file (default) | — | `settings` from any path | `js/build/settings.min.js` (basename only; collisions possible) |
| Bundled (default) | `entryKey: 'basename'` | `settings` | `js/build/settings.min.js` (same flat layout as per-file) |
| Bundled | `entryKey: 'relative'` | `modules/admin/settings` | `js/build/modules/admin/settings.min.js` |

With bundled builds, two entry sources that share a basename (e.g. `admin/settings.js` and `frontend/settings.js`) throw a clear error when configs are generated — entries are not silently overwritten.

Bundled configs disable code splitting (`splitChunks` / `runtimeChunk`) so each entry stays a single output file even when entries share local imports.

**Polylang Pro (flat public URLs, bundled builds):**

```javascript
const vanillaConfigs = getVanillaConfig( {
workingDirectory: __dirname,
strategy: 'bundled',
jsPatterns: [ 'js/src/**/*.js' ],
jsIgnorePatterns: [ '**/*.min.js' ],
cssPatterns: [ 'css/src/**/*.css' ],
jsBuildDirectory: path.resolve( __dirname, 'js/build' ),
cssBuildDirectory: path.resolve( __dirname, 'css/build' ),
isProduction: mode === 'production',
} );
```

**Glob scoping:** Keep `jsPatterns`, `cssPatterns`, and ignore lists tight so build scripts only pick up intended sources (for example, exclude `vendor/**/tmp/**`).

**Nested outputs (bundled + relative keys):**

```javascript
const { getVanillaConfig } = require( '@wpsyntex/polylang-build-scripts' );

const vanillaConfigs = getVanillaConfig( {
workingDirectory: __dirname,
strategy: 'bundled',
entryKey: 'relative',
jsPatterns: [ '**/*.js' ],
jsIgnorePatterns: [ 'node_modules/**', '**/build/**', '**/*.min.js' ],
cssPatterns: [ '**/*.css' ],
Expand Down Expand Up @@ -91,6 +126,28 @@ const reactConfigs = getReactifiedConfig( {
module.exports = reactConfigs;
```

## runWebpackConfigs

Runs an array of webpack configs with optional concurrency limiting. Useful when combining many configs (for example, legacy per-file vanilla plus reactified builds) without letting webpack-cli spawn unbounded parallel compilations. Rejects with `TypeError` when `configs` is not an array or when `concurrency` is invalid. Fails fast on the first compilation error.

Vanilla CSS builds use distinct intermediate filenames for minified (`[name].min.work`) and unminified (`[name].unmin.work`) variants, with cleanup scoped to the CSS output directory. Min and unmin configs are therefore safe to run in parallel (for example `runWebpackConfigs( vanillaConfigs, { concurrency: 4 } )`). This applies to both `per-file` and `bundled` strategies (per-file previously cleaned `*.work` from `process.cwd()`, which is now aligned with the output directory).

```javascript
const { runWebpackConfigs } = require( '@wpsyntex/polylang-build-scripts' );

const configs = [
...getVanillaConfig( { /* options */ } ),
...getReactifiedConfig( { /* options */ } ),
];

runWebpackConfigs( configs, { concurrency: 2 } )
.then( () => console.log( 'Build complete' ) )
.catch( ( error ) => {
console.error( error );
process.exit( 1 );
} );
```

## Combined Usage

```javascript
Expand Down
3 changes: 3 additions & 0 deletions configs/__fixtures__/vanilla/admin/lib/partial.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.partial {
color: red;
}
1 change: 1 addition & 0 deletions configs/__fixtures__/vanilla/admin/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
window.pllAdminSettings = true;
5 changes: 5 additions & 0 deletions configs/__fixtures__/vanilla/admin/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import './lib/partial.css';

.entry {
color: blue;
}
2 changes: 2 additions & 0 deletions configs/__fixtures__/vanilla/js/src/entry-a.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require( './shared-util.js' );
window.pllEntryA = true;
2 changes: 2 additions & 0 deletions configs/__fixtures__/vanilla/js/src/entry-b.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require( './shared-util.js' );
window.pllEntryB = true;
1 change: 1 addition & 0 deletions configs/__fixtures__/vanilla/js/src/shared-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
window.pllSharedUtil = true;
1 change: 1 addition & 0 deletions configs/__fixtures__/vanilla/modules/admin/widget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
window.pllWidget = true;
226 changes: 226 additions & 0 deletions configs/vanilla.integration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/**
* External dependencies
*/
const fs = require( 'fs' );
const path = require( 'path' );
const webpack = require( 'webpack' );

/**
* Internal dependencies
*/
const { runWebpackConfigs } = require( '../runner' );
const { getVanillaConfig } = require( './vanilla' );

describe( 'getVanillaConfig bundled integration', () => {
const fixturesDirectory = path.join( __dirname, '__fixtures__/vanilla' );

const runWebpack = ( config ) =>
new Promise( ( resolve, reject ) => {
webpack( config, ( error, stats ) => {
if ( error ) {
reject( error );
return;
}

if ( stats.hasErrors() ) {
reject( new Error( stats.toString( { colors: false } ) ) );
return;
}

resolve( stats );
} );
} );

it( 'should build nested JS/CSS outputs and resolve @import', async () => {
const jsOutputDirectory = path.join( fixturesDirectory, 'build-js' );
const cssOutputDirectory = path.join( fixturesDirectory, 'build-css' );

fs.rmSync( jsOutputDirectory, { recursive: true, force: true } );
fs.rmSync( cssOutputDirectory, { recursive: true, force: true } );
fs.mkdirSync( jsOutputDirectory, { recursive: true } );
fs.mkdirSync( cssOutputDirectory, { recursive: true } );

const configs = getVanillaConfig( {
workingDirectory: fixturesDirectory,
jsPatterns: [ '**/*.js' ],
cssPatterns: [ 'admin/style.css' ],
jsBuildDirectory: jsOutputDirectory,
cssBuildDirectory: cssOutputDirectory,
isProduction: true,
strategy: 'bundled',
entryKey: 'relative',
} );

expect( configs ).toHaveLength( 4 );

try {
for ( const config of configs ) {
await runWebpack( config );
}

expect(
fs.existsSync(
path.join( jsOutputDirectory, 'admin/settings.js' )
)
).toBe( true );
expect(
fs.existsSync(
path.join( jsOutputDirectory, 'admin/settings.min.js' )
)
).toBe( true );
expect(
fs.existsSync(
path.join( jsOutputDirectory, 'modules/admin/widget.js' )
)
).toBe( true );
expect(
fs.existsSync(
path.join(
jsOutputDirectory,
'modules/admin/widget.min.js'
)
)
).toBe( true );

const unminifiedCss = fs.readFileSync(
path.join( cssOutputDirectory, 'admin/style.css' ),
'utf8'
);
const minifiedCss = fs.readFileSync(
path.join( cssOutputDirectory, 'admin/style.min.css' ),
'utf8'
);

expect( unminifiedCss ).not.toContain( '@import' );
expect( minifiedCss ).not.toContain( '@import' );
expect( unminifiedCss ).toContain( '.partial {' );
expect( unminifiedCss ).toContain( '.entry {' );
expect( minifiedCss ).toContain( '.partial{' );
expect( minifiedCss ).toContain( '.entry{' );
} finally {
fs.rmSync( jsOutputDirectory, { recursive: true, force: true } );
fs.rmSync( cssOutputDirectory, { recursive: true, force: true } );
}
} );

it( 'should build flat JS outputs with default basename entry keys', async () => {
const jsOutputDirectory = path.join(
fixturesDirectory,
'build-js-basename'
);

fs.rmSync( jsOutputDirectory, { recursive: true, force: true } );
fs.mkdirSync( jsOutputDirectory, { recursive: true } );

const configs = getVanillaConfig( {
workingDirectory: fixturesDirectory,
jsPatterns: [ 'admin/settings.js' ],
cssPatterns: [],
jsBuildDirectory: jsOutputDirectory,
cssBuildDirectory: jsOutputDirectory,
isProduction: true,
strategy: 'bundled',
} );

expect( configs ).toHaveLength( 2 );

try {
for ( const config of configs ) {
await runWebpack( config );
}

expect(
fs.existsSync( path.join( jsOutputDirectory, 'settings.js' ) )
).toBe( true );
expect(
fs.existsSync(
path.join( jsOutputDirectory, 'settings.min.js' )
)
).toBe( true );
expect(
fs.existsSync(
path.join( jsOutputDirectory, 'admin/settings.js' )
)
).toBe( false );
} finally {
fs.rmSync( jsOutputDirectory, { recursive: true, force: true } );
}
} );

it( 'should run bundled CSS min and unmin configs in parallel', async () => {
const cssOutputDirectory = path.join(
fixturesDirectory,
'build-css-parallel'
);

fs.rmSync( cssOutputDirectory, { recursive: true, force: true } );
fs.mkdirSync( cssOutputDirectory, { recursive: true } );

const configs = getVanillaConfig( {
workingDirectory: fixturesDirectory,
jsPatterns: [],
cssPatterns: [ 'admin/style.css' ],
jsBuildDirectory: cssOutputDirectory,
cssBuildDirectory: cssOutputDirectory,
isProduction: true,
strategy: 'bundled',
} );

expect( configs ).toHaveLength( 2 );

try {
await runWebpackConfigs( configs, { concurrency: 2 } );

expect(
fs.existsSync( path.join( cssOutputDirectory, 'style.css' ) )
).toBe( true );
expect(
fs.existsSync(
path.join( cssOutputDirectory, 'style.min.css' )
)
).toBe( true );
} finally {
fs.rmSync( cssOutputDirectory, { recursive: true, force: true } );
}
} );

it( 'should not emit extra chunks when entries share a local import', async () => {
const jsOutputDirectory = path.join(
fixturesDirectory,
'build-js-shared'
);

fs.rmSync( jsOutputDirectory, { recursive: true, force: true } );
fs.mkdirSync( jsOutputDirectory, { recursive: true } );

const configs = getVanillaConfig( {
workingDirectory: fixturesDirectory,
jsPatterns: [ 'js/src/entry-a.js', 'js/src/entry-b.js' ],
cssPatterns: [],
jsBuildDirectory: jsOutputDirectory,
cssBuildDirectory: jsOutputDirectory,
isProduction: true,
strategy: 'bundled',
} );

try {
await runWebpackConfigs( configs, { concurrency: 2 } );

const outputFiles = fs.readdirSync( jsOutputDirectory );
const jsOutputs = outputFiles.filter( ( file ) =>
file.endsWith( '.js' )
);

expect( jsOutputs.sort() ).toEqual(
[
'entry-a.js',
'entry-a.min.js',
'entry-b.js',
'entry-b.min.js',
].sort()
);
} finally {
fs.rmSync( jsOutputDirectory, { recursive: true, force: true } );
}
} );
} );
Loading
Loading