@plaudit/webpack-extensions 2.60.0 → 2.60.2

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,31 @@ 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 readonly isSubwriter;
29
+ private printingInPHP;
22
30
  private buffer;
23
31
  private indentation;
24
- private printingInPHP;
25
32
  private fileNamespace;
26
- private useList;
27
- private scopeStack;
28
- constructor(inlineFirstLine?: boolean);
33
+ private myOpenedScopes;
34
+ constructor(inlineFirstLine?: boolean, scopeStack?: string[][], useList?: string[], allocatedGeneratedFunctionNames?: Set<string>, isSubwriter?: boolean, printingInPHP?: boolean, initialIndentation?: number);
35
+ createSubwriter(): PHPWriter;
29
36
  indent(): this;
30
37
  outdent(): this;
38
+ setIndentation(level: number): this;
31
39
  append(...lines: (string | Expr)[]): this;
32
40
  assign(variable: string | string[], expression: unknown | Expr, opts?: {
33
41
  chain?: boolean;
@@ -44,14 +52,16 @@ export declare class PHPWriter {
44
52
  assignTo?: string | string[];
45
53
  return?: boolean;
46
54
  }): 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;
55
+ action(name: string | Expr, contents: (writer: PHPWriter) => void, args?: ActionOrFilterArgs): this;
56
+ filter(name: string | Expr, contents: (writer: PHPWriter) => void, args?: ActionOrFilterArgs): this;
57
+ actionOrFilter(type: 'action' | 'filter', name: string | Expr, contents: (writer: PHPWriter) => void, args: ActionOrFilterArgs): this;
50
58
  if(condition: string): this;
51
59
  elseIf(condition: string): this;
52
60
  else(): this;
53
61
  endIf(): this;
54
- function(name: string, parameters: string[], body: (writer: this) => void, args?: FunctionCreationArgs): this;
62
+ function(name: true, parameters: string[], body: (writer: PHPWriter) => void, args?: FunctionCreationArgs): string;
63
+ function(name: string | false, parameters: string[], body: (writer: PHPWriter) => void, args?: FunctionCreationArgs): this;
64
+ generateFunctionName(asVariable?: boolean): string;
55
65
  closePHP(): this;
56
66
  openPHP(): this;
57
67
  namespace(namespace: string): this;
@@ -70,10 +80,13 @@ export declare class PHPWriter {
70
80
  * This is used to allow function bodies to be safely created without disrupting their containing scope.
71
81
  */
72
82
  popScope(): this;
73
- withScope(code: (writer: this) => void, opts?: {
83
+ withScope(code: (writer: PHPWriter) => void, opts?: {
74
84
  actionDescription?: string;
75
85
  usePopScopeInstead?: boolean;
76
86
  }): this;
77
- toString(): string;
87
+ toString(args?: {
88
+ includeOpenPHP?: boolean;
89
+ includeNamespaceAndUse?: boolean;
90
+ }): string;
78
91
  emitAsset(compilation: Compilation, file: string, assetInfo?: AssetInfo): void;
79
92
  }
@@ -22,14 +22,27 @@ class Expr {
22
22
  exports.Expr = Expr;
23
23
  class PHPWriter {
24
24
  inlineFirstLine;
25
+ scopeStack;
26
+ useList;
27
+ allocatedGeneratedFunctionNames;
28
+ isSubwriter;
29
+ printingInPHP;
25
30
  buffer = [];
26
- indentation = "";
27
- printingInPHP = true;
31
+ indentation;
28
32
  fileNamespace = "";
29
- useList = [];
30
- scopeStack = [];
31
- constructor(inlineFirstLine = false) {
33
+ myOpenedScopes = 0;
34
+ constructor(inlineFirstLine = false, scopeStack = [], useList = [], allocatedGeneratedFunctionNames = new Set(), isSubwriter = false, printingInPHP = true, initialIndentation = 0) {
32
35
  this.inlineFirstLine = inlineFirstLine;
36
+ this.scopeStack = scopeStack;
37
+ this.useList = useList;
38
+ this.allocatedGeneratedFunctionNames = allocatedGeneratedFunctionNames;
39
+ this.isSubwriter = isSubwriter;
40
+ this.printingInPHP = printingInPHP;
41
+ this.indentation = "\t".repeat(initialIndentation);
42
+ }
43
+ createSubwriter() {
44
+ return new PHPWriter(false, this.scopeStack, this.useList, this.allocatedGeneratedFunctionNames, true, this.printingInPHP, this.indentation.length)
45
+ .setIndentation(this.indentation.length);
33
46
  }
34
47
  indent() {
35
48
  this.indentation += "\t";
@@ -39,6 +52,10 @@ class PHPWriter {
39
52
  this.indentation = this.indentation.slice(0, -1);
40
53
  return this;
41
54
  }
55
+ setIndentation(level) {
56
+ this.indentation = "\t".repeat(level);
57
+ return this;
58
+ }
42
59
  append(...lines) {
43
60
  for (const line of lines) {
44
61
  this.buffer.push(`${this.indentation}${line}`);
@@ -103,31 +120,44 @@ class PHPWriter {
103
120
  return this.actionOrFilter('filter', name, contents, args);
104
121
  }
105
122
  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(", ")})`);
123
+ const { priority = 10, functionArgParameters = [], useVars = [], accountForAlreadyDoing } = args;
124
+ const functionWriter = this.createSubwriter();
125
+ let declarationFunctionText;
126
+ if (accountForAlreadyDoing) {
127
+ if (functionArgParameters.length > 0 && accountForAlreadyDoing === true) {
128
+ console.trace(`The accountForAlreadyDoing flag must be set to a list of default parameters when applied to ${type}s that take arguments`);
129
+ }
130
+ this.openPHP().openScope();
131
+ const functionName = functionWriter.function(true, functionArgParameters, contents, { useVars, scopeActionDescription: `closing the function call for the ${name} ${type}.`, assignToName: true });
132
+ this.append(functionWriter.toString().trim());
133
+ declarationFunctionText = functionName + "(...)";
134
+ this.if(`doing_${type}(${Expr.convertJsonToPHP(name)})`).call(functionName, accountForAlreadyDoing === true ? [] : accountForAlreadyDoing);
111
135
  }
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 = ["}"];
136
+ else {
137
+ functionWriter.function(false, functionArgParameters, contents, { useVars, scopeActionDescription: `closing the function call for the ${name} ${type}.` });
138
+ declarationFunctionText = functionWriter.toString().trim();
139
+ }
140
+ // The trailing comma inside the first item is necessary
141
+ const declarationComponents = [`${Expr.convertJsonToPHP(name)}`, declarationFunctionText];
120
142
  const accepted_args = Math.max(functionArgParameters.length, 1); // This avoids us unnecessarily setting the accepted_args value to 0 for actions
121
143
  if (priority !== 10) {
122
- actionOrFilterArgs.push(priority.toString());
144
+ declarationComponents.push(priority.toString());
123
145
  if (accepted_args !== 1) {
124
- actionOrFilterArgs.push(accepted_args.toString());
146
+ declarationComponents.push(accepted_args.toString());
125
147
  }
126
148
  }
127
149
  else if (accepted_args !== 1) {
128
- actionOrFilterArgs.push(`accepted_args: ${accepted_args}`);
150
+ declarationComponents.push(`accepted_args: ${accepted_args}`);
151
+ }
152
+ const line = `add_${type}(${declarationComponents.join(", ")});`;
153
+ if (accountForAlreadyDoing) {
154
+ return this
155
+ .else()
156
+ .append(line)
157
+ .endIf()
158
+ .closeScope();
129
159
  }
130
- return this.append(actionOrFilterArgs.join(", ") + ");");
160
+ return this.openPHP().append(line);
131
161
  }
132
162
  if(condition) {
133
163
  return this.openPHP().append(`if (${condition}) {`).indent();
@@ -142,14 +172,31 @@ class PHPWriter {
142
172
  return this.openPHP().outdent().append("}");
143
173
  }
144
174
  function(name, parameters, body, args = {}) {
175
+ if (name === false) {
176
+ if (args.includeExistenceCheck) {
177
+ console.trace('Anonymous functions cannot have their existence checked for');
178
+ }
179
+ if (args.assignToName) {
180
+ console.trace('Anonymous functions cannot be assigned to a name');
181
+ }
182
+ }
183
+ const returningString = name === true;
184
+ if (name === true) {
185
+ name = this.generateFunctionName(args.assignToName);
186
+ }
145
187
  if (args.includeExistenceCheck) {
146
188
  this.if(`!function_exists(${Expr.convertJsonToPHP(name)})`);
147
189
  }
148
190
  else {
149
191
  this.openPHP();
150
192
  }
151
- let nameAndParameters = `${name}(${parameters.join(", ")})`;
152
- const declarationComponents = ["function",];
193
+ if (name !== false && args.assignToName) {
194
+ if (this.scopeStack.length > 0) {
195
+ this.scopeStack[this.scopeStack.length - 1].push(name);
196
+ }
197
+ }
198
+ const declarationComponents = name === false ? [] : (args.assignToName ? [name, '='] : ["function"]);
199
+ let nameAndParameters = `${!name || args.assignToName ? 'function' : name}(${parameters.join(", ")})`;
153
200
  if (args.useVars?.length) {
154
201
  let useVars = `use (${args.useVars.join(", ")})`;
155
202
  if (args.returnType) {
@@ -168,12 +215,21 @@ class PHPWriter {
168
215
  this.indent();
169
216
  // We start a new scope here just in case this was called within an existing scope
170
217
  // 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("}");
218
+ this.withScope(body, { actionDescription: args.scopeActionDescription ?? `closing the "${name || "anonymous"}" function`, usePopScopeInstead: true });
219
+ this.openPHP().outdent().append(name !== false && args.assignToName ? "};" : "}");
173
220
  if (args.includeExistenceCheck) {
174
- return this.endIf();
221
+ this.endIf();
175
222
  }
176
- return this;
223
+ return returningString ? name : this;
224
+ }
225
+ generateFunctionName(asVariable = false) {
226
+ let count = 0;
227
+ let name;
228
+ do {
229
+ name = `${asVariable ? '$' : ''}plaudit_webpack_extensions__generated_function__${count++}`;
230
+ } while (this.allocatedGeneratedFunctionNames.has(name));
231
+ this.allocatedGeneratedFunctionNames.add(name);
232
+ return name;
177
233
  }
178
234
  closePHP() {
179
235
  if (!this.printingInPHP) {
@@ -207,21 +263,35 @@ class PHPWriter {
207
263
  */
208
264
  openScope() {
209
265
  this.scopeStack.push([]);
266
+ this.myOpenedScopes++;
210
267
  return this;
211
268
  }
212
269
  /**
213
270
  * Pops the top scope from the stack AND unsets all variables that were assigned within it
214
271
  */
215
272
  closeScope() {
273
+ if (!this.myOpenedScopes) {
274
+ return this;
275
+ }
276
+ this.myOpenedScopes--;
216
277
  const scope = this.scopeStack.pop();
217
- return scope?.length ? this.call("unset", scope.map(v => new Expr(v))) : this;
278
+ if (!scope?.length) {
279
+ return this;
280
+ }
281
+ for (const scopeItem of scope) {
282
+ this.allocatedGeneratedFunctionNames.delete(scopeItem);
283
+ }
284
+ return this.call("unset", scope.map(v => new Expr(v)));
218
285
  }
219
286
  /**
220
287
  * Pops the top scope from the stack WITHOUT calling unset for the variables that were assigned within it.
221
288
  * This is used to allow function bodies to be safely created without disrupting their containing scope.
222
289
  */
223
290
  popScope() {
224
- this.scopeStack.pop();
291
+ if (this.myOpenedScopes) {
292
+ this.scopeStack.pop();
293
+ this.myOpenedScopes--;
294
+ }
225
295
  return this;
226
296
  }
227
297
  withScope(code, opts = {}) {
@@ -239,21 +309,26 @@ class PHPWriter {
239
309
  }
240
310
  return this;
241
311
  }
242
- toString() {
243
- if (this.scopeStack.length) {
312
+ toString(args = this.isSubwriter ? { includeOpenPHP: false, includeNamespaceAndUse: false } : {}) {
313
+ if (this.myOpenedScopes) {
244
314
  console.trace("toString() was called on a PHPWriter that has a dangling scope");
245
315
  }
246
316
  const fileContents = [];
247
317
  let canInline = true;
248
- if (this.fileNamespace) {
249
- canInline = false;
250
- fileContents.push(`namespace ${this.fileNamespace};`);
251
- }
252
- if (this.useList.length) {
253
- canInline = false;
254
- fileContents.push(...this.useList.map(use => `use ${use};`));
318
+ if (args.includeNamespaceAndUse !== false) {
319
+ if (this.fileNamespace) {
320
+ canInline = false;
321
+ fileContents.push(`namespace ${this.fileNamespace};`);
322
+ }
323
+ if (this.useList.length) {
324
+ canInline = false;
325
+ fileContents.push(...this.useList.map(use => `use ${use};`));
326
+ }
255
327
  }
256
328
  fileContents.push(...this.buffer);
329
+ if (args.includeOpenPHP === false) {
330
+ return fileContents.join("\n");
331
+ }
257
332
  if (this.inlineFirstLine && canInline) {
258
333
  return `<?php ${fileContents.join("\n")}`;
259
334
  }
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.2",
4
4
  "license": "UNLICENSED",
5
5
  "files": [
6
6
  "/build"