@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.
@@ -0,0 +1,113 @@
1
+ /**
2
+ * SCSS/CSS handler.
3
+ */
4
+
5
+ import esbuild from 'esbuild';
6
+ import { sassPlugin } from 'esbuild-sass-plugin';
7
+ import glob from 'fast-glob';
8
+ import path from 'path';
9
+ import { mkdir, readFile, writeFile } from 'fs/promises';
10
+ import { fileExists } from '../utils.mjs';
11
+ import { generateRtlCss, loadPostcssPlugins, postprocessCss } from '../postcss.mjs';
12
+ import { resolveScssOutfile } from '../resolve-scss-outfile.mjs';
13
+ import { writeAssetPhp } from '../write-asset-php.mjs';
14
+
15
+ /**
16
+ * @param {object} options
17
+ * @param {string} options.entry
18
+ * @param {string} options.outfile
19
+ * @param {object} options.buildContext
20
+ * @param {object} options.entryConfig
21
+ * @return {Promise<void>}
22
+ */
23
+ export async function buildScssFile( { entry, outfile, buildContext, entryConfig } ) {
24
+ const { minify, sourcemap, projectRoot, postcss: postcssSetting, rtl } = buildContext;
25
+ const postcssPlugins = await loadPostcssPlugins( projectRoot, postcssSetting );
26
+ const shouldRtl = entryConfig.rtl ?? rtl;
27
+
28
+ await mkdir( path.dirname( outfile ), { recursive: true } );
29
+
30
+ const result = await esbuild.build( {
31
+ entryPoints: [ entry ],
32
+ outfile,
33
+ bundle: true,
34
+ minify,
35
+ sourcemap,
36
+ logLevel: 'silent',
37
+ plugins: [
38
+ sassPlugin( {
39
+ type: 'css',
40
+ sourceMap: sourcemap,
41
+ } ),
42
+ ],
43
+ } );
44
+
45
+ if ( result.errors.length > 0 ) {
46
+ throw new Error( result.errors.map( ( e ) => e.text ).join( '\n' ) );
47
+ }
48
+
49
+ const css = await readFile( outfile, 'utf8' );
50
+ const processed = await postprocessCss( css, postcssPlugins );
51
+ await writeFile( outfile, processed );
52
+
53
+ if ( shouldRtl ) {
54
+ const rtlCss = await generateRtlCss( processed );
55
+ const rtlOutfile = outfile.replace( /\.css$/, '-rtl.css' );
56
+ await writeFile( rtlOutfile, rtlCss );
57
+
58
+ if ( entryConfig.assetPhp ) {
59
+ await writeAssetPhp( rtlOutfile );
60
+ }
61
+ }
62
+
63
+ if ( entryConfig.assetPhp ) {
64
+ await writeAssetPhp( outfile, {
65
+ dependencies: entryConfig.assetDependencies || [],
66
+ } );
67
+ }
68
+ }
69
+
70
+ /**
71
+ * @param {string} projectRoot
72
+ * @param {object} entryConfig
73
+ * @param {object} buildContext
74
+ * @return {Promise<void>}
75
+ */
76
+ export async function runScssHandler( projectRoot, entryConfig, buildContext ) {
77
+ const srcDir = path.join( projectRoot, entryConfig.src );
78
+ const outDir = path.join( projectRoot, entryConfig.out );
79
+
80
+ if ( ! ( await fileExists( srcDir ) ) ) {
81
+ return;
82
+ }
83
+
84
+ const globPattern = entryConfig.glob || '*.{scss,sass}';
85
+ const ignore = entryConfig.ignore || [ '**/_*' ];
86
+
87
+ const entries = await glob( globPattern, {
88
+ cwd: srcDir,
89
+ absolute: true,
90
+ ignore,
91
+ } );
92
+
93
+ for ( const entry of entries ) {
94
+ const outfile = resolveScssOutfile( {
95
+ srcDir,
96
+ outDir,
97
+ entry,
98
+ entryConfig,
99
+ } );
100
+
101
+ await buildScssFile( {
102
+ entry,
103
+ outfile,
104
+ buildContext,
105
+ entryConfig,
106
+ } );
107
+ }
108
+ }
109
+
110
+ export const scssHandler = {
111
+ type: 'scss',
112
+ run: runScssHandler,
113
+ };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Load and merge project wp-esbuild.config.mjs.
3
+ */
4
+
5
+ import path from 'path';
6
+ import { pathToFileURL } from 'url';
7
+ import { defaultProjectConfig } from './config.mjs';
8
+ import { normalizeConfig } from './normalize-config.mjs';
9
+
10
+ const CONFIG_FILENAME = 'wp-esbuild.config.mjs';
11
+
12
+ /**
13
+ * @param {string} projectRoot
14
+ * @return {Promise<object>}
15
+ */
16
+ export async function loadProjectConfig( projectRoot ) {
17
+ const configPath = path.join( projectRoot, CONFIG_FILENAME );
18
+
19
+ try {
20
+ const module = await import( pathToFileURL( configPath ).href );
21
+ const userConfig =
22
+ typeof module.default === 'function'
23
+ ? module.default( { env: process.env } )
24
+ : module.default;
25
+
26
+ return normalizeConfig( mergeConfig( defaultProjectConfig, userConfig || {} ) );
27
+ } catch ( error ) {
28
+ if (
29
+ error &&
30
+ typeof error === 'object' &&
31
+ 'code' in error &&
32
+ error.code === 'ERR_MODULE_NOT_FOUND'
33
+ ) {
34
+ return normalizeConfig( { ...defaultProjectConfig } );
35
+ }
36
+ throw error;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * @param {object} base
42
+ * @param {object} override
43
+ * @return {object}
44
+ */
45
+ function mergeConfig( base, override ) {
46
+ const merged = {
47
+ ...base,
48
+ ...override,
49
+ };
50
+
51
+ if ( override.blocks !== undefined ) {
52
+ merged.blocks = Array.isArray( override.blocks )
53
+ ? override.blocks
54
+ : { ...base.blocks, ...override.blocks };
55
+ }
56
+
57
+ if ( override.js !== undefined ) {
58
+ merged.js = Array.isArray( override.js )
59
+ ? override.js
60
+ : { ...base.js, ...override.js };
61
+ }
62
+
63
+ if ( override.modules !== undefined ) {
64
+ merged.modules = Array.isArray( override.modules )
65
+ ? override.modules
66
+ : { ...base.modules, ...override.modules };
67
+ }
68
+
69
+ if ( override.scss !== undefined ) {
70
+ merged.scss = Array.isArray( override.scss )
71
+ ? override.scss
72
+ : { ...base.scss, ...override.scss };
73
+ }
74
+
75
+ if ( override.copy !== undefined ) {
76
+ merged.copy = Array.isArray( override.copy ) ? override.copy : override.copy;
77
+ }
78
+
79
+ if ( override.blocksManifest ) {
80
+ merged.blocksManifest = { ...base.blocksManifest, ...override.blocksManifest };
81
+ }
82
+
83
+ if ( override.wordpressExternals ) {
84
+ merged.wordpressExternals = {
85
+ ...base.wordpressExternals,
86
+ ...override.wordpressExternals,
87
+ };
88
+ }
89
+
90
+ if ( override.esbuild ) {
91
+ merged.esbuild = { ...base.esbuild, ...override.esbuild };
92
+ }
93
+
94
+ if ( override.plugins ) {
95
+ merged.plugins = override.plugins;
96
+ }
97
+
98
+ if ( override.entries ) {
99
+ merged.entries = override.entries;
100
+ }
101
+
102
+ return merged;
103
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Merge default, global, and per-entry esbuild options.
3
+ */
4
+
5
+ import path from 'path';
6
+ import { getDefaultEsbuildOptions } from './config.mjs';
7
+
8
+ /**
9
+ * @param {object} params
10
+ * @param {boolean} params.minify
11
+ * @param {boolean} params.sourcemap
12
+ * @param {object} [params.globalEsbuild]
13
+ * @param {object} [params.entryEsbuild]
14
+ * @param {string} [params.projectRoot]
15
+ * @return {import('esbuild').BuildOptions}
16
+ */
17
+ export function mergeEsbuildOptions( {
18
+ minify,
19
+ sourcemap,
20
+ globalEsbuild = {},
21
+ entryEsbuild = {},
22
+ projectRoot,
23
+ } ) {
24
+ const defaults = getDefaultEsbuildOptions( { minify, sourcemap } );
25
+ const merged = {
26
+ ...defaults,
27
+ ...globalEsbuild,
28
+ ...entryEsbuild,
29
+ loader: {
30
+ ...defaults.loader,
31
+ ...globalEsbuild.loader,
32
+ ...entryEsbuild.loader,
33
+ },
34
+ define: {
35
+ ...defaults.define,
36
+ ...globalEsbuild.define,
37
+ ...entryEsbuild.define,
38
+ },
39
+ };
40
+
41
+ if ( merged.alias && projectRoot ) {
42
+ merged.alias = Object.fromEntries(
43
+ Object.entries( merged.alias ).map( ( [ key, value ] ) => [
44
+ key,
45
+ path.isAbsolute( value ) ? value : path.join( projectRoot, value ),
46
+ ] )
47
+ );
48
+ }
49
+
50
+ return merged;
51
+ }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Normalize raw merged config into executable entries + watch metadata.
3
+ */
4
+
5
+ import path from 'path';
6
+ import { defaultProjectConfig } from './config.mjs';
7
+
8
+ const SCRIPT_GLOB = '*.{js,jsx,mjs,ts,tsx}';
9
+ const MODULE_GLOB = '**/*.{js,jsx,mjs,ts,tsx}';
10
+ const SCSS_GLOB = '*.{scss,sass}';
11
+
12
+ /**
13
+ * @param {object|object[]|undefined} value
14
+ * @param {object} defaults
15
+ * @return {object[]}
16
+ */
17
+ function toEntryArray( value, defaults ) {
18
+ if ( ! value ) {
19
+ return [];
20
+ }
21
+
22
+ if ( Array.isArray( value ) ) {
23
+ return value.map( ( entry, index ) => ( {
24
+ ...defaults,
25
+ ...entry,
26
+ name: entry.name || `${ defaults.type }-${ index }`,
27
+ } ) );
28
+ }
29
+
30
+ return [
31
+ {
32
+ ...defaults,
33
+ ...value,
34
+ name: value.name || defaults.type,
35
+ },
36
+ ];
37
+ }
38
+
39
+ /**
40
+ * @param {object} raw
41
+ * @param {string} srcDir
42
+ * @param {string} outDir
43
+ * @return {object}
44
+ */
45
+ function applyRootDirs( raw, srcDir, outDir ) {
46
+ const mapSection = ( section, defaults, relativeSrc, relativeOut ) => {
47
+ if ( Array.isArray( section ) ) {
48
+ return section.map( ( entry ) => ( {
49
+ ...entry,
50
+ src: entry.src ?? `${ srcDir }/${ relativeSrc }`,
51
+ out: entry.out ?? `${ outDir }/${ relativeOut }`,
52
+ } ) );
53
+ }
54
+
55
+ const merged = {
56
+ ...defaults,
57
+ ...section,
58
+ };
59
+
60
+ const userProvidedSrc = section && typeof section === 'object' && 'src' in section;
61
+ const userProvidedOut = section && typeof section === 'object' && 'out' in section;
62
+
63
+ if ( ! userProvidedSrc && ( ! merged.src || merged.src === defaults.src ) ) {
64
+ merged.src = `${ srcDir }/${ relativeSrc }`;
65
+ }
66
+
67
+ if ( ! userProvidedOut && ( ! merged.out || merged.out === defaults.out ) ) {
68
+ merged.out = `${ outDir }/${ relativeOut }`;
69
+ }
70
+
71
+ return merged;
72
+ };
73
+
74
+ return {
75
+ ...raw,
76
+ srcDir,
77
+ outDir,
78
+ blocks: mapSection( raw.blocks, defaultProjectConfig.blocks, 'blocks', 'blocks' ),
79
+ js: mapSection( raw.js, defaultProjectConfig.js, 'js', 'js' ),
80
+ modules: mapSection(
81
+ raw.modules,
82
+ defaultProjectConfig.modules,
83
+ 'js/modules',
84
+ 'js/modules'
85
+ ),
86
+ scss: mapSection( raw.scss, defaultProjectConfig.scss, 'scss', 'css' ),
87
+ blocksManifest: {
88
+ ...defaultProjectConfig.blocksManifest,
89
+ ...raw.blocksManifest,
90
+ input:
91
+ raw.blocksManifest?.input === defaultProjectConfig.blocksManifest.input
92
+ ? `${ outDir }/blocks`
93
+ : raw.blocksManifest?.input ?? `${ outDir }/blocks`,
94
+ output:
95
+ raw.blocksManifest?.output === defaultProjectConfig.blocksManifest.output
96
+ ? `${ outDir }/blocks/blocks-manifest.php`
97
+ : raw.blocksManifest?.output ??
98
+ `${ outDir }/blocks/blocks-manifest.php`,
99
+ },
100
+ };
101
+ }
102
+
103
+ /**
104
+ * @param {object} raw
105
+ * @return {object}
106
+ */
107
+ export function normalizeConfig( raw ) {
108
+ const srcDir = raw.srcDir ?? defaultProjectConfig.srcDir;
109
+ const outDir = raw.outDir ?? defaultProjectConfig.outDir;
110
+ const rooted = applyRootDirs( raw, srcDir, outDir );
111
+
112
+ const withDefaults = {
113
+ ...defaultProjectConfig,
114
+ ...rooted,
115
+ blocksManifest: {
116
+ ...defaultProjectConfig.blocksManifest,
117
+ ...rooted.blocksManifest,
118
+ },
119
+ wordpressExternals: {
120
+ bundle: [],
121
+ external: [],
122
+ vendors: {},
123
+ ...( raw.wordpressExternals || {} ),
124
+ },
125
+ esbuild: raw.esbuild || {},
126
+ postcss: raw.postcss ?? true,
127
+ rtl: raw.rtl ?? false,
128
+ plugins: raw.plugins || [],
129
+ };
130
+
131
+ let entries = [];
132
+
133
+ if ( Array.isArray( withDefaults.entries ) && withDefaults.entries.length > 0 ) {
134
+ entries = withDefaults.entries.map( ( entry, index ) => ( {
135
+ enabled: true,
136
+ ...entry,
137
+ name: entry.name || `entry-${ index }`,
138
+ } ) );
139
+ } else {
140
+ entries = [
141
+ ...toEntryArray( withDefaults.blocks, {
142
+ type: 'blocks',
143
+ src: `${ srcDir }/blocks`,
144
+ out: `${ outDir }/blocks`,
145
+ discover: '*/block.json',
146
+ copy: [ 'block.json', 'render.php' ],
147
+ } ),
148
+ ...toEntryArray( withDefaults.js, {
149
+ type: 'script',
150
+ src: `${ srcDir }/js`,
151
+ out: `${ outDir }/js`,
152
+ glob: SCRIPT_GLOB,
153
+ format: 'iife',
154
+ wordpressExternals: true,
155
+ assetPhp: true,
156
+ extractCss: false,
157
+ } ),
158
+ ...toEntryArray( withDefaults.modules, {
159
+ type: 'script',
160
+ src: `${ srcDir }/js/modules`,
161
+ out: `${ outDir }/js/modules`,
162
+ glob: MODULE_GLOB,
163
+ format: 'esm',
164
+ wordpressExternals: true,
165
+ assetPhp: true,
166
+ extractCss: false,
167
+ } ),
168
+ ...toEntryArray( withDefaults.scss, {
169
+ type: 'scss',
170
+ src: `${ srcDir }/scss`,
171
+ out: `${ outDir }/css`,
172
+ glob: SCSS_GLOB,
173
+ ignore: [ '**/_*' ],
174
+ } ),
175
+ ...toEntryArray( withDefaults.copy, {
176
+ type: 'copy',
177
+ from: '',
178
+ to: '',
179
+ flatten: false,
180
+ } ),
181
+ ];
182
+ }
183
+
184
+ entries = entries.filter( ( entry ) => entry.enabled !== false );
185
+
186
+ const watchPaths = [];
187
+ const entryWatchMap = new Map();
188
+
189
+ for ( const entry of entries ) {
190
+ const paths = getEntryWatchPaths( entry );
191
+ entryWatchMap.set( entry.name, paths );
192
+ for ( const watchPath of paths ) {
193
+ if ( ! watchPaths.includes( watchPath ) ) {
194
+ watchPaths.push( watchPath );
195
+ }
196
+ }
197
+ }
198
+
199
+ for ( const plugin of withDefaults.plugins ) {
200
+ if ( Array.isArray( plugin.watch ) ) {
201
+ for ( const watchPath of plugin.watch ) {
202
+ if ( ! watchPaths.includes( watchPath ) ) {
203
+ watchPaths.push( watchPath );
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ return {
210
+ ...withDefaults,
211
+ entries,
212
+ watchPaths,
213
+ entryWatchMap,
214
+ };
215
+ }
216
+
217
+ /**
218
+ * @param {object} entry
219
+ * @return {string[]}
220
+ */
221
+ function getEntryWatchPaths( entry ) {
222
+ switch ( entry.type ) {
223
+ case 'blocks':
224
+ return [ entry.src ];
225
+ case 'script':
226
+ case 'scss':
227
+ return [ entry.src ];
228
+ case 'copy': {
229
+ const from = entry.from || '';
230
+ if ( /[*?[\]]/.test( from ) ) {
231
+ const globRoot = from.split( /[*?[\]]/ )[ 0 ].replace( /\/$/, '' );
232
+ return [ globRoot || '.' ];
233
+ }
234
+ return [ from ];
235
+ }
236
+ default:
237
+ return entry.src ? [ entry.src ] : [];
238
+ }
239
+ }
240
+
241
+ /**
242
+ * @param {string} changedPath
243
+ * @param {string} projectRoot
244
+ * @param {object} normalizedConfig
245
+ * @return {object[]}
246
+ */
247
+ export function getEntriesForChangedPath( changedPath, projectRoot, normalizedConfig ) {
248
+ const relativePath = path.relative( projectRoot, changedPath ).replace( /\\/g, '/' );
249
+ const affected = [];
250
+
251
+ for ( const entry of normalizedConfig.entries ) {
252
+ const watchPaths = normalizedConfig.entryWatchMap.get( entry.name ) || [];
253
+ const matches = watchPaths.some( ( watchPath ) => {
254
+ const normalizedWatch = watchPath.replace( /\\/g, '/' ).replace( /\/$/, '' );
255
+ return (
256
+ relativePath === normalizedWatch ||
257
+ relativePath.startsWith( `${ normalizedWatch }/` )
258
+ );
259
+ } );
260
+
261
+ if ( matches ) {
262
+ affected.push( entry );
263
+ }
264
+ }
265
+
266
+ return affected.length > 0 ? affected : normalizedConfig.entries;
267
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Export JSON-serializable values as PHP array literals.
3
+ *
4
+ * @param {unknown} value
5
+ * @param {string} indent
6
+ * @return {string}
7
+ */
8
+ export function exportToPhp( value, indent = '\t' ) {
9
+ if ( value === null ) {
10
+ return 'null';
11
+ }
12
+
13
+ if ( typeof value === 'boolean' ) {
14
+ return value ? 'true' : 'false';
15
+ }
16
+
17
+ if ( typeof value === 'number' ) {
18
+ return Number.isFinite( value ) ? String( value ) : 'null';
19
+ }
20
+
21
+ if ( typeof value === 'string' ) {
22
+ return `'${ value
23
+ .replace( /\\/g, '\\\\' )
24
+ .replace( /'/g, "\\'" ) }'`;
25
+ }
26
+
27
+ if ( Array.isArray( value ) ) {
28
+ if ( value.length === 0 ) {
29
+ return 'array()';
30
+ }
31
+
32
+ const childIndent = indent + '\t';
33
+ const items = value
34
+ .map( ( item ) => `${ childIndent }${ exportToPhp( item, childIndent ) }` )
35
+ .join( ',\n' );
36
+
37
+ return `array(\n${ items }\n${ indent })`;
38
+ }
39
+
40
+ if ( typeof value === 'object' ) {
41
+ const entries = Object.entries( value );
42
+
43
+ if ( entries.length === 0 ) {
44
+ return 'array()';
45
+ }
46
+
47
+ const childIndent = indent + '\t';
48
+ const items = entries
49
+ .map(
50
+ ( [ key, item ] ) =>
51
+ `${ childIndent }'${ String( key ).replace( /'/g, "\\'" ) }' => ${ exportToPhp(
52
+ item,
53
+ childIndent
54
+ ) }`
55
+ )
56
+ .join( ',\n' );
57
+
58
+ return `array(\n${ items }\n${ indent })`;
59
+ }
60
+
61
+ return 'null';
62
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Load PostCSS plugins from project config or postcss.config.*.
3
+ */
4
+
5
+ import path from 'path';
6
+ import { pathToFileURL } from 'url';
7
+ import autoprefixer from 'autoprefixer';
8
+ import { fileExists } from './utils.mjs';
9
+
10
+ /** @type {Map<string, import('postcss').AcceptedPlugin[]|false>} */
11
+ const cache = new Map();
12
+
13
+ /**
14
+ * @param {string} projectRoot
15
+ * @param {boolean|import('postcss').AcceptedPlugin[]|object} postcssConfig
16
+ * @return {Promise<import('postcss').AcceptedPlugin[]|false>}
17
+ */
18
+ export async function loadPostcssPlugins( projectRoot, postcssConfig = true ) {
19
+ const cacheKey = `${ projectRoot }:${ JSON.stringify( postcssConfig ) }`;
20
+ if ( cache.has( cacheKey ) ) {
21
+ return cache.get( cacheKey );
22
+ }
23
+
24
+ if ( postcssConfig === false ) {
25
+ cache.set( cacheKey, false );
26
+ return false;
27
+ }
28
+
29
+ if ( Array.isArray( postcssConfig ) ) {
30
+ cache.set( cacheKey, postcssConfig );
31
+ return postcssConfig;
32
+ }
33
+
34
+ for ( const fileName of [
35
+ 'postcss.config.mjs',
36
+ 'postcss.config.js',
37
+ 'postcss.config.cjs',
38
+ ] ) {
39
+ const configPath = path.join( projectRoot, fileName );
40
+ if ( await fileExists( configPath ) ) {
41
+ const module = await import( pathToFileURL( configPath ).href );
42
+ const config = module.default ?? module;
43
+ const plugins =
44
+ typeof config === 'function'
45
+ ? config( { env: process.env.NODE_ENV || 'development' } ).plugins
46
+ : config.plugins;
47
+
48
+ if ( Array.isArray( plugins ) ) {
49
+ cache.set( cacheKey, plugins );
50
+ return plugins;
51
+ }
52
+ }
53
+ }
54
+
55
+ const fallback = [ autoprefixer ];
56
+ cache.set( cacheKey, fallback );
57
+ return fallback;
58
+ }
59
+
60
+ /**
61
+ * @param {string} css
62
+ * @param {import('postcss').AcceptedPlugin[]|false} plugins
63
+ * @return {Promise<string>}
64
+ */
65
+ export async function postprocessCss( css, plugins ) {
66
+ if ( plugins === false ) {
67
+ return css;
68
+ }
69
+
70
+ const postcss = ( await import( 'postcss' ) ).default;
71
+ const result = await postcss( plugins ).process( css, { from: undefined } );
72
+ return result.css;
73
+ }
74
+
75
+ /**
76
+ * @param {string} css
77
+ * @return {Promise<string>}
78
+ */
79
+ export async function generateRtlCss( css ) {
80
+ const rtlcss = ( await import( 'rtlcss' ) ).default;
81
+ return rtlcss.process( css );
82
+ }