@plaudit/webpack-extensions 2.60.0 → 2.60.1

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/LICENSE.md ADDED
@@ -0,0 +1,13 @@
1
+ # License
2
+
3
+ Unless otherwise stated in a written and signed agreement with Plaudit, the following license terms apply to this software:
4
+ * **Definitions**: ‘Client’ means the legal entity that has engaged Plaudit under a signed Master Services Agreement (MSA) or similar written contract for a project with which this library is provided. ‘Authorized Vendors’ means Client’s employees, contractors, and third‑party service providers who access this library solely on Client’s behalf.
5
+ * **Use**: This library shall remain the exclusive property of Plaudit. Plaudit hereby grants the Client a nonexclusive, nontransferable, worldwide license to use this library only as necessary to use and maintain the final deliverables provided by Plaudit for Client. Authorized Vendors obtain no rights independent of Client, may not sublicense or transfer, and must comply with these restrictions. Client and Authorized Vendors may not directly or indirectly create derivative works or use the library for any other client or project.
6
+ * **Access and Transfer**: Access by Authorized Vendors is permitted only while performing services for Client on the relevant deliverables and must be subject to written obligations no less protective than these terms. Access ends when such services end.
7
+ * **Warranty Disclaimer**: This software is provided **“as is”** without warranty of any kind. Plaudit disclaims all warranties, express or implied, including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement.
8
+ * **Limitation of Liability**: In no event shall Plaudit be liable for any damages, liabilities, costs, losses, or expenses arising out of or in connection with the use of this software, including but not limited to incidental, consequential, or punitive damages, even if advised of the possibility of such damages.
9
+ * **Support and Updates**: No support, maintenance, or future updates are provided under this license. The license does not grant rights to future versions of this software.
10
+
11
+ ---
12
+
13
+ (License revision: 1.0)
package/README.md CHANGED
@@ -1,5 +1,94 @@
1
+ # Plaudit Webpack Extensions
2
+ A webpack configuration and plugin suite designed specifically for WordPress development workflows. This package extends `@wordpress/scripts` with custom webpack plugins, PostCSS configurations, and build optimizations tailored for Plaudit's WordPress projects. It includes specialized handling for Gutenberg blocks, asset management, internationalization (WPML), and development tooling like BrowserSync integration.
3
+
4
+ # Requirements
5
+ - All block- and extension-related capabilities require that the `plaudit/plaudit-common` composer library be installed.
6
+ - All assets must be under a unified root directory and must be emitted to a unified output directory
7
+ - Ex: `src/blocks` and `src/extensions` emitting to `dist/blocks` and `dist/extensions`
8
+
9
+ # Perks
10
+ ## Truly automatic asset loading
11
+ With the `useUnifiedLoader` flag enabled (see the usage example), this package will generate a single file that, when included, automatically registers and enqueues scripts, styles, and assets.
12
+ This includes prefetching assets when the `useWebpackResourceFiltering` flag is enabled.
13
+
14
+ ## Automatic filename mapping in `block.json`
15
+ Instead of requiring developers to plug in the built names of files when developing blocks, this suite will automatically replace the source name with the built name in emitted block.json files *and* will automatically resolve filename conflicts that would otherwise cause the build to fail.
16
+
17
+ In addition to handling filename conflicts, it also automatically detects the presence of `template.php` and `template.twig` files and ensures that they are properly registered in the emitted block.json files.
18
+
19
+ ## Efficient loading
20
+ This suite generates loader code that uses the most efficient mechanisms available for registering assets, and, in the case of blocks in themes, a loading mechanism that is more efficient than any "available" method.
21
+
22
+ ## Unified Plugin and Theme Structures
23
+ The same file structure (both in the webpack.config.js file and the actual directory tree) can be used in plugins and themes.
24
+
25
+ # Usage
26
+ ## An Example Config
27
+ NOTE: This config assumes that `require_once __DIR__.'/dist/unified-loader.php';` has been added to the theme's functions.php file or the plugin's main PHP file
28
+
29
+ ```javascript
30
+ module.exports = require("@plaudit/webpack-extensions/wordpress-scripts-wrapper")({
31
+ src: {
32
+ "blocks": true, // By default, a directory is assumed to contain WordPress blocks, so we don't need to do anything other than tell WebPack to include it
33
+ "extensions": { // Extensions need to have their directory layout specified, but the loader is automatically generated, so there's no need to specify an output path
34
+ directoryLayout: 'extensions'
35
+ },
36
+ "site/index-header.ts": { // "Plain" entrypoints (i.e. those not associated with a block or extension) can have their usage locations specified, at which point they will also be automatically loaded
37
+ locations: {
38
+ clientView: true,
39
+ registerScriptArgs: false
40
+ }
41
+ },
42
+ "site/index-footer.ts": {
43
+ withLegacyBlocksIn: "blocks",
44
+ locations: {
45
+ clientView: true,
46
+ registerScriptArgs: {strategy: "defer", in_footer: true}
47
+ }
48
+ },
49
+ "site/wp-admin.ts": {
50
+ locations: {
51
+ admin: true
52
+ }
53
+ },
54
+ "site/wp-admin.pcss": {
55
+ locations: {
56
+ admin: true
57
+ }
58
+ },
59
+ "site/block-editor.pcss": {
60
+ locations: {
61
+ clientEditor: true
62
+ }
63
+ },
64
+ "site/public.pcss": {
65
+ locations: {
66
+ clientView: true
67
+ }
68
+ }
69
+ },
70
+ useWebpackResourceFiltering: true,
71
+ extensionsVersion: 2,
72
+ plainEntrypointsVersion: 2,
73
+ srcDir: "src",
74
+ outputDir: "dist",
75
+ useUnifiedLoader: true
76
+ });
77
+ ```
78
+
1
79
  # Development
2
- To test locally, either set up yarn to pull from this folder or (ideally) build and copy the resulting built files into another project for testing purposes.
80
+
81
+ ## Setup
82
+
83
+ ```
84
+ pnpm install
85
+ ```
86
+
87
+ # Build
88
+
89
+ ```
90
+ pnpm build
91
+ ```
3
92
 
4
93
  # To Deploy
5
94
  ```
@@ -329,7 +329,7 @@ class BlockJSONManagingPlugin {
329
329
  .use("Plaudit\\Common\\ACF\\BlockManager")
330
330
  .action("init", writer => {
331
331
  writer.call("BlockManager::autoloadSubfolders", [new php_writer_1.Expr(`__DIR__.${php_writer_1.Expr.convertJsonToPHP("/" + this.blocksDest)}`)]);
332
- });
332
+ }, { accountForAlreadyDoing: true });
333
333
  }
334
334
  });
335
335
  });
@@ -182,7 +182,7 @@ class PlainEntrypointsConfigFileGeneratorPlugin {
182
182
  for (const { handle, type, data } of prioritizedHandleList) {
183
183
  writer.call(`wp_register_${type}`, [handle, new php_writer_1.Expr(`$base_uri.${php_writer_1.Expr.jsonToPHPConverter(node_path_1.default.relative(emitDir, data.src))}`), ...data.rest]);
184
184
  }
185
- }, { priority });
185
+ }, { priority, accountForAlreadyDoing: true });
186
186
  }
187
187
  const sortedEditorStyleHandles = sortedStyleHandles
188
188
  .filter(([_, { locations: { clientEditor }, isScript }]) => !isScript && (clientEditor || typeof clientEditor === 'number'))
@@ -227,7 +227,7 @@ class PlainEntrypointsConfigFileGeneratorPlugin {
227
227
  for (const [action, handleList] of enqueuingHandleActions) {
228
228
  if (handleList.length > 0) {
229
229
  for (const [priority, prioritizedHandleList] of PlainEntrypointsConfigFileGeneratorPlugin.separateHandleListByPriority(handleList)) {
230
- writer.action(action, () => {
230
+ writer.action(action, writer => {
231
231
  for (const handle of prioritizedHandleList) {
232
232
  writer.call(`wp_enqueue_${handle.type}`, [handle.handle]);
233
233
  }
@@ -253,7 +253,7 @@ class PlainEntrypointsConfigFileGeneratorPlugin {
253
253
  * The primary benefit of emitting a function instead of baking its contents into each function that uses it is that it allows us to avoid recomputing the base uri multiple times
254
254
  */
255
255
  static emitResolveBaseUriFunction(writer) {
256
- writer.function("plaudit_webpack_extensions__resolve_base_uri", ["$dir"], () => {
256
+ writer.function("plaudit_webpack_extensions__resolve_base_uri", ["$dir"], writer => {
257
257
  writer
258
258
  .static("$base_uris", { initializer: [] })
259
259
  .if("isset($base_uris[$dir])")
@@ -136,7 +136,7 @@ class SpecialAssetHandlingPlugin {
136
136
  writer.append(`<link rel="preload" href="<?= esc_url($base_uri.'${filename}') ?>" ${dynamicAttrs}>`);
137
137
  }
138
138
  writer.openPHP();
139
- });
139
+ }, { accountForAlreadyDoing: true });
140
140
  writer.emitAsset(compilation, outputFile);
141
141
  }
142
142
  }
@@ -11,23 +11,30 @@ export type ActionOrFilterArgs = {
11
11
  priority?: number | Expr;
12
12
  functionArgParameters?: string[];
13
13
  useVars?: string[];
14
+ accountForAlreadyDoing?: boolean | Parameters<PHPWriter['call']>[1];
14
15
  };
15
16
  export type FunctionCreationArgs = {
16
17
  includeExistenceCheck?: boolean;
17
18
  useVars?: string[];
18
19
  returnType?: string;
20
+ assignToName?: boolean;
21
+ scopeActionDescription?: string;
19
22
  };
20
23
  export declare class PHPWriter {
21
24
  private readonly inlineFirstLine;
25
+ private readonly scopeStack;
26
+ private readonly useList;
27
+ private readonly allocatedGeneratedFunctionNames;
28
+ private printingInPHP;
22
29
  private buffer;
23
30
  private indentation;
24
- private printingInPHP;
25
31
  private fileNamespace;
26
- private useList;
27
- private scopeStack;
28
- constructor(inlineFirstLine?: boolean);
32
+ private myOpenedScopes;
33
+ constructor(inlineFirstLine?: boolean, scopeStack?: string[][], useList?: string[], allocatedGeneratedFunctionNames?: Set<string>, printingInPHP?: boolean, initialIndentation?: number);
34
+ createSubwriter(): PHPWriter;
29
35
  indent(): this;
30
36
  outdent(): this;
37
+ setIndentation(level: number): this;
31
38
  append(...lines: (string | Expr)[]): this;
32
39
  assign(variable: string | string[], expression: unknown | Expr, opts?: {
33
40
  chain?: boolean;
@@ -44,14 +51,16 @@ export declare class PHPWriter {
44
51
  assignTo?: string | string[];
45
52
  return?: boolean;
46
53
  }): this;
47
- action(name: string | Expr, contents: (writer: this) => void, args?: ActionOrFilterArgs): this;
48
- filter(name: string | Expr, contents: (writer: this) => void, args?: ActionOrFilterArgs): this;
49
- actionOrFilter(type: 'action' | 'filter', name: string | Expr, contents: (writer: this) => void, args: ActionOrFilterArgs): this;
54
+ action(name: string | Expr, contents: (writer: PHPWriter) => void, args?: ActionOrFilterArgs): this;
55
+ filter(name: string | Expr, contents: (writer: PHPWriter) => void, args?: ActionOrFilterArgs): this;
56
+ actionOrFilter(type: 'action' | 'filter', name: string | Expr, contents: (writer: PHPWriter) => void, args: ActionOrFilterArgs): this;
50
57
  if(condition: string): this;
51
58
  elseIf(condition: string): this;
52
59
  else(): this;
53
60
  endIf(): this;
54
- function(name: string, parameters: string[], body: (writer: this) => void, args?: FunctionCreationArgs): this;
61
+ function(name: true, parameters: string[], body: (writer: PHPWriter) => void, args?: FunctionCreationArgs): string;
62
+ function(name: string | false, parameters: string[], body: (writer: PHPWriter) => void, args?: FunctionCreationArgs): this;
63
+ generateFunctionName(asVariable?: boolean): string;
55
64
  closePHP(): this;
56
65
  openPHP(): this;
57
66
  namespace(namespace: string): this;
@@ -70,10 +79,12 @@ export declare class PHPWriter {
70
79
  * This is used to allow function bodies to be safely created without disrupting their containing scope.
71
80
  */
72
81
  popScope(): this;
73
- withScope(code: (writer: this) => void, opts?: {
82
+ withScope(code: (writer: PHPWriter) => void, opts?: {
74
83
  actionDescription?: string;
75
84
  usePopScopeInstead?: boolean;
76
85
  }): this;
77
- toString(): string;
86
+ toString(args?: {
87
+ includeOpenPHP?: boolean;
88
+ }): string;
78
89
  emitAsset(compilation: Compilation, file: string, assetInfo?: AssetInfo): void;
79
90
  }
@@ -22,14 +22,25 @@ class Expr {
22
22
  exports.Expr = Expr;
23
23
  class PHPWriter {
24
24
  inlineFirstLine;
25
+ scopeStack;
26
+ useList;
27
+ allocatedGeneratedFunctionNames;
28
+ printingInPHP;
25
29
  buffer = [];
26
- indentation = "";
27
- printingInPHP = true;
30
+ indentation;
28
31
  fileNamespace = "";
29
- useList = [];
30
- scopeStack = [];
31
- constructor(inlineFirstLine = false) {
32
+ myOpenedScopes = 0;
33
+ constructor(inlineFirstLine = false, scopeStack = [], useList = [], allocatedGeneratedFunctionNames = new Set(), printingInPHP = true, initialIndentation = 0) {
32
34
  this.inlineFirstLine = inlineFirstLine;
35
+ this.scopeStack = scopeStack;
36
+ this.useList = useList;
37
+ this.allocatedGeneratedFunctionNames = allocatedGeneratedFunctionNames;
38
+ this.printingInPHP = printingInPHP;
39
+ this.indentation = "\t".repeat(initialIndentation);
40
+ }
41
+ createSubwriter() {
42
+ return new PHPWriter(false, this.scopeStack, this.useList, this.allocatedGeneratedFunctionNames, this.printingInPHP, this.indentation.length)
43
+ .setIndentation(this.indentation.length);
33
44
  }
34
45
  indent() {
35
46
  this.indentation += "\t";
@@ -39,6 +50,10 @@ class PHPWriter {
39
50
  this.indentation = this.indentation.slice(0, -1);
40
51
  return this;
41
52
  }
53
+ setIndentation(level) {
54
+ this.indentation = "\t".repeat(level);
55
+ return this;
56
+ }
42
57
  append(...lines) {
43
58
  for (const line of lines) {
44
59
  this.buffer.push(`${this.indentation}${line}`);
@@ -103,31 +118,44 @@ class PHPWriter {
103
118
  return this.actionOrFilter('filter', name, contents, args);
104
119
  }
105
120
  actionOrFilter(type, name, contents, args) {
106
- const { priority = 10, functionArgParameters = [], useVars = [] } = args;
107
- // The trailing comma inside the first item is necessary
108
- const declarationComponents = [`${Expr.convertJsonToPHP(name)},`, `function(${functionArgParameters.join(", ")})`];
109
- if (useVars.length > 0) {
110
- declarationComponents.push(`use (${useVars.join(", ")})`);
121
+ const { priority = 10, functionArgParameters = [], useVars = [], accountForAlreadyDoing } = args;
122
+ const functionWriter = this.createSubwriter();
123
+ let declarationFunctionText;
124
+ if (accountForAlreadyDoing) {
125
+ if (functionArgParameters.length > 0 && accountForAlreadyDoing === true) {
126
+ console.trace(`The accountForAlreadyDoing flag must be set to a list of default parameters when applied to ${type}s that take arguments`);
127
+ }
128
+ this.openPHP().openScope();
129
+ const functionName = functionWriter.function(true, functionArgParameters, contents, { useVars, scopeActionDescription: `closing the function call for the ${name} ${type}.`, assignToName: true });
130
+ this.append(functionWriter.toString({ includeOpenPHP: false }).trim());
131
+ declarationFunctionText = functionName + "(...)";
132
+ this.if(`doing_${type}(${Expr.convertJsonToPHP(name)})`).call(functionName, accountForAlreadyDoing === true ? [] : accountForAlreadyDoing);
111
133
  }
112
- declarationComponents.push("{");
113
- this.openPHP().append(`add_${type}(${declarationComponents.join(" ")}`);
114
- this.indent();
115
- // We start a new scope here just in case this was called within an existing scope
116
- // We pop the scope instead of closing it because we don't actually want to unset any variables that were created within the function
117
- this.withScope(contents, { actionDescription: `closing the function call for the ${name} ${type}.`, usePopScopeInstead: true });
118
- this.openPHP().outdent();
119
- const actionOrFilterArgs = ["}"];
134
+ else {
135
+ functionWriter.function(false, functionArgParameters, contents, { useVars, scopeActionDescription: `closing the function call for the ${name} ${type}.` });
136
+ declarationFunctionText = functionWriter.toString({ includeOpenPHP: false }).trim();
137
+ }
138
+ // The trailing comma inside the first item is necessary
139
+ const declarationComponents = [`${Expr.convertJsonToPHP(name)}`, declarationFunctionText];
120
140
  const accepted_args = Math.max(functionArgParameters.length, 1); // This avoids us unnecessarily setting the accepted_args value to 0 for actions
121
141
  if (priority !== 10) {
122
- actionOrFilterArgs.push(priority.toString());
142
+ declarationComponents.push(priority.toString());
123
143
  if (accepted_args !== 1) {
124
- actionOrFilterArgs.push(accepted_args.toString());
144
+ declarationComponents.push(accepted_args.toString());
125
145
  }
126
146
  }
127
147
  else if (accepted_args !== 1) {
128
- actionOrFilterArgs.push(`accepted_args: ${accepted_args}`);
148
+ declarationComponents.push(`accepted_args: ${accepted_args}`);
129
149
  }
130
- return this.append(actionOrFilterArgs.join(", ") + ");");
150
+ const line = `add_${type}(${declarationComponents.join(", ")});`;
151
+ if (accountForAlreadyDoing) {
152
+ return this
153
+ .else()
154
+ .append(line)
155
+ .endIf()
156
+ .closeScope();
157
+ }
158
+ return this.openPHP().append(line);
131
159
  }
132
160
  if(condition) {
133
161
  return this.openPHP().append(`if (${condition}) {`).indent();
@@ -142,14 +170,31 @@ class PHPWriter {
142
170
  return this.openPHP().outdent().append("}");
143
171
  }
144
172
  function(name, parameters, body, args = {}) {
173
+ if (name === false) {
174
+ if (args.includeExistenceCheck) {
175
+ console.trace('Anonymous functions cannot have their existence checked for');
176
+ }
177
+ if (args.assignToName) {
178
+ console.trace('Anonymous functions cannot be assigned to a name');
179
+ }
180
+ }
181
+ const returningString = name === true;
182
+ if (name === true) {
183
+ name = this.generateFunctionName(args.assignToName);
184
+ }
145
185
  if (args.includeExistenceCheck) {
146
186
  this.if(`!function_exists(${Expr.convertJsonToPHP(name)})`);
147
187
  }
148
188
  else {
149
189
  this.openPHP();
150
190
  }
151
- let nameAndParameters = `${name}(${parameters.join(", ")})`;
152
- const declarationComponents = ["function",];
191
+ if (name !== false && args.assignToName) {
192
+ if (this.scopeStack.length > 0) {
193
+ this.scopeStack[this.scopeStack.length - 1].push(name);
194
+ }
195
+ }
196
+ const declarationComponents = name === false ? [] : (args.assignToName ? [name, '='] : ["function"]);
197
+ let nameAndParameters = `${!name || args.assignToName ? 'function' : name}(${parameters.join(", ")})`;
153
198
  if (args.useVars?.length) {
154
199
  let useVars = `use (${args.useVars.join(", ")})`;
155
200
  if (args.returnType) {
@@ -168,12 +213,21 @@ class PHPWriter {
168
213
  this.indent();
169
214
  // We start a new scope here just in case this was called within an existing scope
170
215
  // We pop the scope instead of closing it because we don't actually want to unset any variables that were created within the function
171
- this.withScope(body, { actionDescription: `closing the "${name}" function`, usePopScopeInstead: true });
172
- this.openPHP().outdent().append("}");
216
+ this.withScope(body, { actionDescription: args.scopeActionDescription ?? `closing the "${name || "anonymous"}" function`, usePopScopeInstead: true });
217
+ this.openPHP().outdent().append(name !== false && args.assignToName ? "};" : "}");
173
218
  if (args.includeExistenceCheck) {
174
- return this.endIf();
219
+ this.endIf();
175
220
  }
176
- return this;
221
+ return returningString ? name : this;
222
+ }
223
+ generateFunctionName(asVariable = false) {
224
+ let count = 0;
225
+ let name;
226
+ do {
227
+ name = `${asVariable ? '$' : ''}plaudit_webpack_extensions__generated_function__${count++}`;
228
+ } while (this.allocatedGeneratedFunctionNames.has(name));
229
+ this.allocatedGeneratedFunctionNames.add(name);
230
+ return name;
177
231
  }
178
232
  closePHP() {
179
233
  if (!this.printingInPHP) {
@@ -207,21 +261,35 @@ class PHPWriter {
207
261
  */
208
262
  openScope() {
209
263
  this.scopeStack.push([]);
264
+ this.myOpenedScopes++;
210
265
  return this;
211
266
  }
212
267
  /**
213
268
  * Pops the top scope from the stack AND unsets all variables that were assigned within it
214
269
  */
215
270
  closeScope() {
271
+ if (!this.myOpenedScopes) {
272
+ return this;
273
+ }
274
+ this.myOpenedScopes--;
216
275
  const scope = this.scopeStack.pop();
217
- return scope?.length ? this.call("unset", scope.map(v => new Expr(v))) : this;
276
+ if (!scope?.length) {
277
+ return this;
278
+ }
279
+ for (const scopeItem of scope) {
280
+ this.allocatedGeneratedFunctionNames.delete(scopeItem);
281
+ }
282
+ return this.call("unset", scope.map(v => new Expr(v)));
218
283
  }
219
284
  /**
220
285
  * Pops the top scope from the stack WITHOUT calling unset for the variables that were assigned within it.
221
286
  * This is used to allow function bodies to be safely created without disrupting their containing scope.
222
287
  */
223
288
  popScope() {
224
- this.scopeStack.pop();
289
+ if (this.myOpenedScopes) {
290
+ this.scopeStack.pop();
291
+ this.myOpenedScopes--;
292
+ }
225
293
  return this;
226
294
  }
227
295
  withScope(code, opts = {}) {
@@ -239,8 +307,8 @@ class PHPWriter {
239
307
  }
240
308
  return this;
241
309
  }
242
- toString() {
243
- if (this.scopeStack.length) {
310
+ toString(args = {}) {
311
+ if (this.myOpenedScopes) {
244
312
  console.trace("toString() was called on a PHPWriter that has a dangling scope");
245
313
  }
246
314
  const fileContents = [];
@@ -254,6 +322,9 @@ class PHPWriter {
254
322
  fileContents.push(...this.useList.map(use => `use ${use};`));
255
323
  }
256
324
  fileContents.push(...this.buffer);
325
+ if (args.includeOpenPHP === false) {
326
+ return fileContents.join("\n");
327
+ }
257
328
  if (this.inlineFirstLine && canInline) {
258
329
  return `<?php ${fileContents.join("\n")}`;
259
330
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plaudit/webpack-extensions",
3
- "version": "2.60.0",
3
+ "version": "2.60.1",
4
4
  "license": "UNLICENSED",
5
5
  "files": [
6
6
  "/build"