@cloudcatch/wp-esbuild 1.1.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/lib/build.mjs ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Build orchestrator for WordPress blocks, JS, modules, and SCSS assets.
3
+ */
4
+
5
+ import path from 'path';
6
+ import chokidar from 'chokidar';
7
+ import { loadProjectConfig } from './load-config.mjs';
8
+ import { getEntriesForChangedPath } from './normalize-config.mjs';
9
+ import { runEntry } from './handlers/index.mjs';
10
+ import { buildBlocksManifest } from './build-blocks-manifest.mjs';
11
+
12
+ /**
13
+ * @param {string} projectRoot
14
+ * @param {object} config
15
+ * @param {object[]} [entries]
16
+ * @return {Promise<void>}
17
+ */
18
+ async function runUserPlugins( projectRoot, config, entries = config.entries ) {
19
+ for ( const plugin of config.plugins || [] ) {
20
+ if ( typeof plugin.build === 'function' ) {
21
+ await plugin.build( { projectRoot, config, entries } );
22
+ }
23
+ }
24
+ }
25
+
26
+ /**
27
+ * @param {string} projectRoot
28
+ * @param {object} config
29
+ * @param {object[]} entries
30
+ * @return {Promise<void>}
31
+ */
32
+ async function runEntries( projectRoot, config, entries ) {
33
+ const buildContext = {
34
+ minify: config.minify,
35
+ sourcemap: config.sourcemap,
36
+ projectRoot,
37
+ globalEsbuild: config.esbuild,
38
+ wordpressExternals: config.wordpressExternals,
39
+ postcss: config.postcss,
40
+ rtl: config.rtl,
41
+ };
42
+
43
+ for ( const entry of entries ) {
44
+ await runEntry( projectRoot, entry, buildContext );
45
+ }
46
+
47
+ const hasBlocksEntry = entries.some( ( entry ) => entry.type === 'blocks' );
48
+ if ( hasBlocksEntry && config.blocksManifest?.enabled ) {
49
+ await buildBlocksManifest( {
50
+ projectRoot,
51
+ inputDir: config.blocksManifest.input,
52
+ outputFile: config.blocksManifest.output,
53
+ } );
54
+ }
55
+
56
+ await runUserPlugins( projectRoot, config, entries );
57
+ }
58
+
59
+ /**
60
+ * @param {string} projectRoot
61
+ * @param {object} [options]
62
+ * @param {boolean} [options.watch]
63
+ * @return {Promise<void>}
64
+ */
65
+ export async function build( projectRoot, options = {} ) {
66
+ const config = await loadProjectConfig( projectRoot );
67
+
68
+ const runBuild = async ( entries = config.entries ) => {
69
+ console.log( `Building ${ projectRoot }...` );
70
+ await runEntries( projectRoot, config, entries );
71
+ console.log( 'Build complete.' );
72
+ };
73
+
74
+ await runBuild();
75
+
76
+ if ( options.watch ) {
77
+ const watchPaths = config.watchPaths.map( ( watchPath ) =>
78
+ path.join( projectRoot, watchPath )
79
+ );
80
+
81
+ console.log( 'Watching for changes...' );
82
+
83
+ const watcher = chokidar.watch( watchPaths, {
84
+ ignoreInitial: true,
85
+ ignored: /(^|[/\\])\../,
86
+ } );
87
+
88
+ let debounceTimer;
89
+ const scheduleRebuild = ( changedPath ) => {
90
+ clearTimeout( debounceTimer );
91
+ debounceTimer = setTimeout( () => {
92
+ const entries = changedPath
93
+ ? getEntriesForChangedPath( changedPath, projectRoot, config )
94
+ : config.entries;
95
+
96
+ runBuild( entries ).catch( ( error ) => {
97
+ console.error( error );
98
+ } );
99
+ }, 100 );
100
+ };
101
+
102
+ watcher.on( 'all', ( eventName, changedPath ) => {
103
+ scheduleRebuild( changedPath );
104
+ } );
105
+
106
+ await new Promise( () => {} );
107
+ }
108
+ }
109
+
110
+ export { normalizeConfig } from './normalize-config.mjs';
111
+ export { defineConfig } from './define-config.mjs';
package/lib/config.mjs ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Default esbuild and project configuration for @cloudcatch/wp-esbuild.
3
+ */
4
+
5
+ export const defaultWordpressBundledPackages = [
6
+ '@wordpress/admin-ui',
7
+ '@wordpress/dataviews',
8
+ '@wordpress/fields',
9
+ '@wordpress/global-styles-engine',
10
+ '@wordpress/global-styles-ui',
11
+ '@wordpress/grid',
12
+ '@wordpress/icons',
13
+ '@wordpress/image-cropper',
14
+ '@wordpress/interface',
15
+ '@wordpress/ui',
16
+ '@wordpress/views',
17
+ ];
18
+
19
+ export const defaultProjectConfig = {
20
+ srcDir: 'src',
21
+ outDir: 'build',
22
+ blocks: {
23
+ src: 'src/blocks',
24
+ out: 'build/blocks',
25
+ discover: '*/block.json',
26
+ copy: [ 'block.json', 'render.php' ],
27
+ },
28
+ js: {
29
+ src: 'src/js',
30
+ out: 'build/js',
31
+ },
32
+ modules: {
33
+ src: 'src/js/modules',
34
+ out: 'build/js/modules',
35
+ },
36
+ scss: {
37
+ src: 'src/scss',
38
+ out: 'build/css',
39
+ },
40
+ copy: [],
41
+ blocksManifest: {
42
+ enabled: true,
43
+ input: 'build/blocks',
44
+ output: 'build/blocks/blocks-manifest.php',
45
+ },
46
+ wordpressExternals: {
47
+ bundle: [],
48
+ external: [],
49
+ vendors: {},
50
+ },
51
+ postcss: true,
52
+ rtl: false,
53
+ esbuild: {},
54
+ plugins: [],
55
+ minify: process.env.NODE_ENV === 'production',
56
+ sourcemap: process.env.NODE_ENV !== 'production',
57
+ };
58
+
59
+ /**
60
+ * Default esbuild options for WordPress browser bundles.
61
+ *
62
+ * @param {object} options
63
+ * @param {boolean} options.minify
64
+ * @param {boolean} options.sourcemap
65
+ * @return {import('esbuild').BuildOptions}
66
+ */
67
+ export function getDefaultEsbuildOptions( { minify, sourcemap } ) {
68
+ return {
69
+ bundle: true,
70
+ platform: 'browser',
71
+ target: 'es2018',
72
+ jsx: 'automatic',
73
+ jsxImportSource: 'react',
74
+ loader: {
75
+ '.js': 'jsx',
76
+ '.jsx': 'jsx',
77
+ '.ts': 'tsx',
78
+ '.tsx': 'tsx',
79
+ '.png': 'file',
80
+ '.jpg': 'file',
81
+ '.jpeg': 'file',
82
+ '.gif': 'file',
83
+ '.svg': 'file',
84
+ '.webp': 'file',
85
+ '.woff': 'file',
86
+ '.woff2': 'file',
87
+ '.ttf': 'file',
88
+ '.eot': 'file',
89
+ },
90
+ assetNames: 'assets/[name]-[hash]',
91
+ minify,
92
+ sourcemap,
93
+ logLevel: 'info',
94
+ define: {},
95
+ };
96
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * defineConfig helper with basic validation warnings.
3
+ */
4
+
5
+ const KNOWN_KEYS = new Set( [
6
+ 'srcDir',
7
+ 'outDir',
8
+ 'entries',
9
+ 'blocks',
10
+ 'js',
11
+ 'modules',
12
+ 'scss',
13
+ 'copy',
14
+ 'blocksManifest',
15
+ 'minify',
16
+ 'sourcemap',
17
+ 'esbuild',
18
+ 'wordpressExternals',
19
+ 'postcss',
20
+ 'rtl',
21
+ 'plugins',
22
+ ] );
23
+
24
+ /**
25
+ * @param {object} config
26
+ * @return {object}
27
+ */
28
+ export function defineConfig( config ) {
29
+ if ( config && typeof config === 'object' ) {
30
+ for ( const key of Object.keys( config ) ) {
31
+ if ( ! KNOWN_KEYS.has( key ) ) {
32
+ console.warn( `[wp-esbuild] Unknown config key "${ key }".` );
33
+ }
34
+ }
35
+
36
+ if ( Array.isArray( config.entries ) ) {
37
+ for ( const entry of config.entries ) {
38
+ if ( ! entry.type ) {
39
+ console.warn( '[wp-esbuild] entries item missing "type".' );
40
+ }
41
+ if ( entry.type !== 'copy' && ! entry.src && ! entry.from ) {
42
+ console.warn(
43
+ `[wp-esbuild] entry "${ entry.name || '(unnamed)' }" missing src/from.`
44
+ );
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ return config;
51
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Gutenberg blocks handler.
3
+ */
4
+
5
+ import glob from 'fast-glob';
6
+ import path from 'path';
7
+ import { cp, mkdir, readFile } from 'fs/promises';
8
+ import { fileExists } from '../utils.mjs';
9
+ import { buildScssFile } from './scss.mjs';
10
+ import { buildScriptFile } from './script.mjs';
11
+
12
+ /**
13
+ * @param {string} blockDir
14
+ * @param {string} projectRoot
15
+ * @param {object} entryConfig
16
+ * @return {Promise<object>}
17
+ */
18
+ async function readBlockMetadata( blockDir ) {
19
+ const blockJsonPath = path.join( blockDir, 'block.json' );
20
+ if ( ! ( await fileExists( blockJsonPath ) ) ) {
21
+ return {};
22
+ }
23
+
24
+ return JSON.parse( await readFile( blockJsonPath, 'utf8' ) );
25
+ }
26
+
27
+ /**
28
+ * @param {string} projectRoot
29
+ * @param {object} entryConfig
30
+ * @return {Promise<string[]>}
31
+ */
32
+ async function discoverBlocks( projectRoot, entryConfig ) {
33
+ const blocksSrc = path.join( projectRoot, entryConfig.src );
34
+ if ( ! ( await fileExists( blocksSrc ) ) ) {
35
+ return [];
36
+ }
37
+
38
+ const discoverPattern = entryConfig.discover || '*/block.json';
39
+ const blockJsonFiles = await glob( discoverPattern, {
40
+ cwd: blocksSrc,
41
+ absolute: true,
42
+ } );
43
+
44
+ return blockJsonFiles.map( ( blockJsonPath ) => path.dirname( blockJsonPath ) );
45
+ }
46
+
47
+ /**
48
+ * @param {string} blockDir
49
+ * @param {string} blocksOut
50
+ * @param {object} buildContext
51
+ * @param {object} entryConfig
52
+ * @return {Promise<void>}
53
+ */
54
+ async function buildBlock( blockDir, blocksOut, buildContext, entryConfig ) {
55
+ const slug = path.basename( blockDir );
56
+ const outDir = path.join( blocksOut, slug );
57
+ const metadata = await readBlockMetadata( blockDir );
58
+ await mkdir( outDir, { recursive: true } );
59
+
60
+ const indexJs = path.join( blockDir, 'index.js' );
61
+ if ( await fileExists( indexJs ) ) {
62
+ await buildScriptFile( {
63
+ entry: indexJs,
64
+ outfile: path.join( outDir, 'index.js' ),
65
+ buildContext,
66
+ entryConfig: {
67
+ ...entryConfig,
68
+ format: 'iife',
69
+ wordpressExternals: true,
70
+ assetPhp: true,
71
+ },
72
+ } );
73
+ }
74
+
75
+ const styleScss = path.join( blockDir, 'style.scss' );
76
+ if ( await fileExists( styleScss ) ) {
77
+ await buildScssFile( {
78
+ entry: styleScss,
79
+ outfile: path.join( outDir, 'style-index.css' ),
80
+ buildContext,
81
+ entryConfig,
82
+ } );
83
+ }
84
+
85
+ const editorScss = path.join( blockDir, 'editor.scss' );
86
+ if ( await fileExists( editorScss ) ) {
87
+ await buildScssFile( {
88
+ entry: editorScss,
89
+ outfile: path.join( outDir, 'index.css' ),
90
+ buildContext,
91
+ entryConfig,
92
+ } );
93
+ }
94
+
95
+ const viewEntry = await findViewScript( blockDir );
96
+ if ( viewEntry ) {
97
+ const viewOut = path.join( outDir, 'view.js' );
98
+ const isModule = Boolean( metadata.viewScriptModule );
99
+
100
+ await buildScriptFile( {
101
+ entry: viewEntry,
102
+ outfile: viewOut,
103
+ buildContext,
104
+ entryConfig: {
105
+ ...entryConfig,
106
+ format: isModule ? 'esm' : 'iife',
107
+ wordpressExternals: ! isModule,
108
+ assetPhp: true,
109
+ },
110
+ } );
111
+ }
112
+
113
+ const copyList = entryConfig.copy || [ 'block.json', 'render.php' ];
114
+ for ( const fileName of copyList ) {
115
+ const source = path.join( blockDir, fileName );
116
+ if ( ! ( await fileExists( source ) ) ) {
117
+ continue;
118
+ }
119
+
120
+ const destination = path.join( outDir, fileName );
121
+ await mkdir( path.dirname( destination ), { recursive: true } );
122
+ await cp( source, destination, { recursive: true, force: true } );
123
+ }
124
+ }
125
+
126
+ /**
127
+ * @param {string} blockDir
128
+ * @return {Promise<string|null>}
129
+ */
130
+ async function findViewScript( blockDir ) {
131
+ for ( const fileName of [ 'view.ts', 'view.tsx', 'view.js', 'view.mjs' ] ) {
132
+ const candidate = path.join( blockDir, fileName );
133
+ if ( await fileExists( candidate ) ) {
134
+ return candidate;
135
+ }
136
+ }
137
+
138
+ return null;
139
+ }
140
+
141
+ /**
142
+ * @param {string} projectRoot
143
+ * @param {object} entryConfig
144
+ * @param {object} buildContext
145
+ * @return {Promise<void>}
146
+ */
147
+ export async function runBlocksHandler( projectRoot, entryConfig, buildContext ) {
148
+ const blocksOut = path.join( projectRoot, entryConfig.out );
149
+ const blockDirs = await discoverBlocks( projectRoot, entryConfig );
150
+
151
+ for ( const blockDir of blockDirs ) {
152
+ await buildBlock( blockDir, blocksOut, buildContext, entryConfig );
153
+ }
154
+ }
155
+
156
+ export const blocksHandler = {
157
+ type: 'blocks',
158
+ run: runBlocksHandler,
159
+ };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Static copy handler.
3
+ */
4
+
5
+ import glob from 'fast-glob';
6
+ import path from 'path';
7
+ import { cp, copyFile, mkdir } from 'fs/promises';
8
+ import { fileExists } from '../utils.mjs';
9
+
10
+ /**
11
+ * @param {string} projectRoot
12
+ * @param {object} entryConfig
13
+ * @return {Promise<void>}
14
+ */
15
+ export async function runCopyHandler( projectRoot, entryConfig ) {
16
+ const fromPath = path.join( projectRoot, entryConfig.from );
17
+ const toPath = path.join( projectRoot, entryConfig.to );
18
+
19
+ if ( ! entryConfig.from ) {
20
+ return;
21
+ }
22
+
23
+ const hasGlob = /[*?[\]]/.test( entryConfig.from );
24
+
25
+ if ( ! hasGlob ) {
26
+ if ( ! ( await fileExists( fromPath ) ) ) {
27
+ return;
28
+ }
29
+
30
+ await mkdir( path.dirname( toPath ), { recursive: true } );
31
+ await cp( fromPath, toPath, { recursive: true, force: true } );
32
+ return;
33
+ }
34
+
35
+ const matchedFiles = await glob( entryConfig.from, {
36
+ cwd: projectRoot,
37
+ absolute: true,
38
+ onlyFiles: true,
39
+ } );
40
+
41
+ const baseDir = path.dirname(
42
+ path.join( projectRoot, entryConfig.from.split( '*' )[ 0 ] )
43
+ );
44
+
45
+ for ( const sourceFile of matchedFiles ) {
46
+ const relativePath = path.relative( baseDir, sourceFile );
47
+ const destination = entryConfig.flatten
48
+ ? path.join( toPath, path.basename( sourceFile ) )
49
+ : path.join( toPath, relativePath );
50
+
51
+ await mkdir( path.dirname( destination ), { recursive: true } );
52
+ await copyFile( sourceFile, destination );
53
+ }
54
+ }
55
+
56
+ export const copyHandler = {
57
+ type: 'copy',
58
+ run: runCopyHandler,
59
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Handler registry for wp-esbuild pipelines.
3
+ */
4
+
5
+ import { blocksHandler } from './blocks.mjs';
6
+ import { copyHandler } from './copy.mjs';
7
+ import { scssHandler } from './scss.mjs';
8
+ import { scriptHandler } from './script.mjs';
9
+
10
+ /** @type {Record<string, { type: string, run: Function }>} */
11
+ export const handlersByType = {
12
+ blocks: blocksHandler,
13
+ script: scriptHandler,
14
+ scss: scssHandler,
15
+ copy: copyHandler,
16
+ };
17
+
18
+ /**
19
+ * @param {object} entry
20
+ * @return {{ type: string, run: Function }|undefined}
21
+ */
22
+ export function getHandlerForEntry( entry ) {
23
+ return handlersByType[ entry.type ];
24
+ }
25
+
26
+ /**
27
+ * @param {string} projectRoot
28
+ * @param {object} entry
29
+ * @param {object} buildContext
30
+ * @return {Promise<void>}
31
+ */
32
+ export async function runEntry( projectRoot, entry, buildContext ) {
33
+ const handler = getHandlerForEntry( entry );
34
+ if ( ! handler ) {
35
+ throw new Error( `Unknown entry type "${ entry.type }" for entry "${ entry.name }".` );
36
+ }
37
+
38
+ if ( entry.type === 'copy' ) {
39
+ await handler.run( projectRoot, entry );
40
+ return;
41
+ }
42
+
43
+ await handler.run( projectRoot, entry, buildContext );
44
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Script bundle handler (IIFE and ESM).
3
+ */
4
+
5
+ import esbuild from 'esbuild';
6
+ import glob from 'fast-glob';
7
+ import path from 'path';
8
+ import { mkdir } from 'fs/promises';
9
+ import { mergeEsbuildOptions } from '../merge-esbuild-options.mjs';
10
+ import { fileExists } from '../utils.mjs';
11
+
12
+ /**
13
+ * @param {string} srcDir
14
+ * @param {string} outDir
15
+ * @param {string} entry
16
+ * @param {object} entryConfig
17
+ * @return {string}
18
+ */
19
+ function resolveScriptOutfile( srcDir, outDir, entry, entryConfig ) {
20
+ const relativeEntryPath = path.relative( srcDir, entry );
21
+ const outRelativePath = relativeEntryPath.replace( /\.(tsx|ts|mjs|jsx|js)$/, '.js' );
22
+ const globPattern = entryConfig.glob || '*.{js,jsx,mjs,ts,tsx}';
23
+ const preserveStructure = globPattern.includes( '**' );
24
+
25
+ if ( entryConfig.outName === 'entry-dir' ) {
26
+ const parentDir = path.dirname( relativeEntryPath );
27
+ const baseName =
28
+ parentDir === '.'
29
+ ? path.basename( outRelativePath, '.js' )
30
+ : parentDir.split( path.sep ).join( '-' );
31
+ return path.join( outDir, `${ baseName }.js` );
32
+ }
33
+
34
+ if ( preserveStructure ) {
35
+ return path.join( outDir, outRelativePath );
36
+ }
37
+
38
+ return path.join( outDir, `${ path.basename( outRelativePath, '.js' ) }.js` );
39
+ }
40
+ import {
41
+ extractStyleImportsPlugin,
42
+ stripStyleImportsPlugin,
43
+ wordpressExternalsPlugin,
44
+ wordpressModuleExternalsPlugin,
45
+ } from '../wordpress-externals-plugin.mjs';
46
+
47
+ /**
48
+ * @param {object} options
49
+ * @param {string} options.entry
50
+ * @param {string} options.outfile
51
+ * @param {object} options.buildContext
52
+ * @param {object} options.entryConfig
53
+ * @return {Promise<import('esbuild').BuildResult>}
54
+ */
55
+ export async function buildScriptFile( { entry, outfile, buildContext, entryConfig } ) {
56
+ const {
57
+ minify,
58
+ sourcemap,
59
+ projectRoot,
60
+ globalEsbuild,
61
+ wordpressExternals: globalExternals,
62
+ } = buildContext;
63
+
64
+ const format = entryConfig.format || 'iife';
65
+ const extractCss = entryConfig.extractCss === true;
66
+ const assetPhp = entryConfig.assetPhp !== false;
67
+ const assetBaseName = path.basename( outfile, path.extname( outfile ) );
68
+
69
+ const stylePlugin = extractCss ? extractStyleImportsPlugin() : stripStyleImportsPlugin();
70
+ const plugins = [ stylePlugin, ...( entryConfig.esbuild?.plugins || [] ), ...( globalEsbuild?.plugins || [] ) ];
71
+
72
+ if ( assetPhp ) {
73
+ if ( format === 'esm' ) {
74
+ plugins.push(
75
+ wordpressModuleExternalsPlugin( assetBaseName, globalExternals )
76
+ );
77
+ } else if ( entryConfig.wordpressExternals !== false ) {
78
+ plugins.push(
79
+ wordpressExternalsPlugin( assetBaseName, [], globalExternals )
80
+ );
81
+ }
82
+ }
83
+
84
+ await mkdir( path.dirname( outfile ), { recursive: true } );
85
+
86
+ const esbuildOptions = mergeEsbuildOptions( {
87
+ minify,
88
+ sourcemap,
89
+ globalEsbuild,
90
+ entryEsbuild: entryConfig.esbuild,
91
+ projectRoot,
92
+ } );
93
+
94
+ return esbuild.build( {
95
+ ...esbuildOptions,
96
+ entryPoints: [ entry ],
97
+ outfile,
98
+ format,
99
+ plugins: [ ...plugins, ...( esbuildOptions.plugins || [] ) ].filter(
100
+ ( plugin, index, all ) =>
101
+ all.findIndex( ( item ) => item.name === plugin.name ) === index
102
+ ),
103
+ } );
104
+ }
105
+
106
+ /**
107
+ * @param {string} projectRoot
108
+ * @param {object} entryConfig
109
+ * @param {object} buildContext
110
+ * @return {Promise<void>}
111
+ */
112
+ export async function runScriptHandler( projectRoot, entryConfig, buildContext ) {
113
+ const srcDir = path.join( projectRoot, entryConfig.src );
114
+ const outDir = path.join( projectRoot, entryConfig.out );
115
+
116
+ if ( ! ( await fileExists( srcDir ) ) ) {
117
+ return;
118
+ }
119
+
120
+ const globPattern = entryConfig.glob || '*.{js,jsx,mjs,ts,tsx}';
121
+ const entries = await glob( globPattern, {
122
+ cwd: srcDir,
123
+ absolute: true,
124
+ } );
125
+
126
+ for ( const entry of entries ) {
127
+ const outfile = resolveScriptOutfile( srcDir, outDir, entry, entryConfig );
128
+
129
+ await buildScriptFile( {
130
+ entry,
131
+ outfile,
132
+ buildContext,
133
+ entryConfig,
134
+ } );
135
+ }
136
+ }
137
+
138
+ export const scriptHandler = {
139
+ type: 'script',
140
+ run: runScriptHandler,
141
+ };