PluginProbe
Tableberg – Simple Gutenberg Table Block / 1.1.5
Tableberg – Simple Gutenberg Table Block v1.1.5
1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 trunk 0.0.2 0.2.1 0.3.2 0.3.3 0.4.1 0.5.0 0.5.1 0.5.2 0.5.3 0.5.4 0.5.5 0.5.6 0.5.7 All 42 releases
tableberg / webpack-plugins / sync-attributes-plugin.js

sync-attributes-plugin.js in Tableberg – Simple Gutenberg Table Block 1.1.5, at webpack-plugins/sync-attributes-plugin.js

304 lines 8.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Webpack Plugin: Sync Attributes
3 *
4 * Watches src/attributes.ts and automatically syncs changes to:
5 * - src/blocks/table/block.json (attribute types and defaults)
6 * - renderer/Table/Defaults.php (PHP class with defaults)
7 *
8 * Runs during both build and watch modes.
9 */
10
11 const fs = require("fs");
12 const path = require("path");
13
14 class SyncAttributesPlugin {
15 constructor(options = {}) {
16 this.options = {
17 attributesPath:
18 options.attributesPath ||
19 path.resolve(__dirname, "../src/attributes.ts"),
20 blockJsonPath:
21 options.blockJsonPath ||
22 path.resolve(__dirname, "../src/blocks/table/block.json"),
23 phpPath:
24 options.phpPath ||
25 path.resolve(__dirname, "../renderer/Table/Defaults.php"),
26 verbose: options.verbose !== false,
27 ...options,
28 };
29
30 this.lastSyncTime = 0;
31 this.syncDebounceMs = 100;
32 this.hasRunInitialSync = false;
33 }
34
35 apply(compiler) {
36 const pluginName = "SyncAttributesPlugin";
37
38 // Run sync once on initial compilation only
39 compiler.hooks.beforeCompile.tapAsync(
40 pluginName,
41 (params, callback) => {
42 if (!this.hasRunInitialSync) {
43 this.syncAttributes();
44 this.hasRunInitialSync = true;
45 }
46 callback();
47 }
48 );
49
50 // In watch mode, add the attributes file as a watched dependency
51 compiler.hooks.afterCompile.tap(pluginName, compilation => {
52 if (compiler.watchMode) {
53 compilation.fileDependencies.add(this.options.attributesPath);
54 }
55 });
56
57 // Watch for changes to attributes.ts in watch mode
58 compiler.hooks.watchRun.tapAsync(pluginName, (compiler, callback) => {
59 const changedFiles = compiler.modifiedFiles || new Set();
60
61 if (changedFiles.has(this.options.attributesPath)) {
62 // Debounce to avoid multiple syncs
63 const now = Date.now();
64 if (now - this.lastSyncTime > this.syncDebounceMs) {
65 this.log("\nAttributes file changed, re-syncing...");
66 this.syncAttributes();
67 this.lastSyncTime = now;
68 }
69 }
70
71 callback();
72 });
73 }
74
75 syncAttributes() {
76 try {
77 const { types, defaults } = this.parseAttributesFile();
78 this.generateBlockJson(types, defaults);
79 this.generatePhpDefaultsClass(defaults);
80
81 if (this.options.verbose) {
82 this.log("✓ Synced attributes.ts → block.json & Defaults.php");
83 }
84 } catch (error) {
85 console.error(`\n❌ SyncAttributesPlugin Error: ${error.message}`);
86 throw error;
87 }
88 }
89
90 parseAttributesFile() {
91 if (!fs.existsSync(this.options.attributesPath)) {
92 throw new Error(
93 `Attributes file not found: ${this.options.attributesPath}`
94 );
95 }
96
97 const content = fs.readFileSync(this.options.attributesPath, "utf-8");
98
99 // Extract TablebergBlockAttrs interface
100 const interfaceMatch = content.match(
101 /export interface TablebergBlockAttrs\s*{([^}]+)}/s
102 );
103 if (!interfaceMatch) {
104 throw new Error("Could not find TablebergBlockAttrs interface");
105 }
106
107 // Extract attrDefaults object
108 const defaultsMatch = content.match(
109 /export const attrDefaults: TablebergBlockAttrs = ({[\s\S]+?^});$/m
110 );
111 if (!defaultsMatch) {
112 throw new Error("Could not find attrDefaults constant");
113 }
114
115 const arrayTypeAliases = this.parseArrayTypeAliases(content);
116 const types = this.parseInterface(interfaceMatch[1], arrayTypeAliases);
117 const defaults = this.parseDefaults(defaultsMatch[1]);
118
119 return { types, defaults };
120 }
121
122 parseArrayTypeAliases(content) {
123 const aliases = new Set();
124 const aliasRegex = /export type (\w+)\s*=\s*Array[<\s]/g;
125 let match;
126
127 while ((match = aliasRegex.exec(content)) !== null) {
128 aliases.add(match[1]);
129 }
130
131 return aliases;
132 }
133
134 parseInterface(interfaceBody, arrayTypeAliases) {
135 const types = {};
136 const lines = interfaceBody.split("\n");
137
138 for (const line of lines) {
139 const match = line.trim().match(/^(\w+)\??\s*:\s*(.+?);?$/);
140 if (match) {
141 const [, field, type] = match;
142 types[field] = this.mapTypeScriptType(
143 type.trim(),
144 arrayTypeAliases
145 );
146 }
147 }
148
149 return types;
150 }
151
152 mapTypeScriptType(tsType, arrayTypeAliases = new Set()) {
153 if (
154 tsType.includes("Array") ||
155 tsType.endsWith("[]") ||
156 arrayTypeAliases.has(tsType)
157 ) {
158 return "array";
159 }
160 if (tsType.includes("Record") || tsType.includes("object")) {
161 return "object";
162 }
163 if (tsType.includes("string")) return "string";
164 if (tsType.includes("number")) return "number";
165 if (tsType.includes("boolean")) return "boolean";
166 return "object";
167 }
168
169 parseDefaults(objectLiteral) {
170 try {
171 let jsonString = objectLiteral
172 .replace(/(\w+):/g, '"$1":')
173 .replace(/,(\s*[}\]])/g, "$1")
174 .replace(/'/g, '"');
175
176 return JSON.parse(jsonString);
177 } catch (error) {
178 throw new Error(`Failed to parse attrDefaults: ${error.message}`);
179 }
180 }
181
182 generateBlockJson(types, defaults) {
183 if (!fs.existsSync(this.options.blockJsonPath)) {
184 throw new Error(
185 `block.json not found: ${this.options.blockJsonPath}`
186 );
187 }
188
189 const blockJson = JSON.parse(
190 fs.readFileSync(this.options.blockJsonPath, "utf-8")
191 );
192
193 const attributes = {};
194 for (const [key, defaultValue] of Object.entries(defaults)) {
195 attributes[key] = {
196 type: types[key] || "object",
197 default: defaultValue,
198 };
199 }
200
201 blockJson.attributes = attributes;
202
203 fs.writeFileSync(
204 this.options.blockJsonPath,
205 JSON.stringify(blockJson, null, 4) + "\n",
206 "utf-8"
207 );
208 }
209
210 generatePhpDefaultsClass(defaults) {
211 const phpArray = this.convertJsValueToPhp(defaults, 2);
212
213 const phpContent = `<?php
214
215 /**
216 * Block Defaults
217 *
218 * AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
219 * Generated from: packages/tableberg/src/attributes.ts
220 *
221 * @package Tableberg
222 */
223
224 namespace Tableberg\\Renderer\\Table;
225
226 /**
227 * Table Block Defaults
228 */
229 class Defaults {
230 /**
231 * Get default attribute values for the table block
232 *
233 * @return array
234 */
235 public static function get_defaults() {
236 return ${phpArray};
237 }
238 }
239 `;
240
241 fs.writeFileSync(this.options.phpPath, phpContent, "utf-8");
242 }
243
244 convertJsValueToPhp(value, indent = 0) {
245 const indentStr = " ".repeat(indent);
246 const nextIndentStr = " ".repeat(indent + 1);
247
248 if (value === null) {
249 return "null";
250 }
251
252 if (typeof value === "boolean") {
253 return value ? "true" : "false";
254 }
255
256 if (typeof value === "number") {
257 return String(value);
258 }
259
260 if (typeof value === "string") {
261 const escaped = value.replace(/'/g, "\\'");
262 return `'${escaped}'`;
263 }
264
265 if (Array.isArray(value)) {
266 if (value.length === 0) {
267 return "[]";
268 }
269
270 const items = value.map(item => {
271 return (
272 nextIndentStr + this.convertJsValueToPhp(item, indent + 1)
273 );
274 });
275
276 return `[\n${items.join(",\n")},\n${indentStr}]`;
277 }
278
279 if (typeof value === "object") {
280 const entries = Object.entries(value);
281
282 if (entries.length === 0) {
283 return "[]";
284 }
285
286 const items = entries.map(([key, val]) => {
287 const phpKey = this.convertJsValueToPhp(key, 0);
288 const phpVal = this.convertJsValueToPhp(val, indent + 1);
289 return `${nextIndentStr}${phpKey} => ${phpVal}`;
290 });
291
292 return `[\n${items.join(",\n")},\n${indentStr}]`;
293 }
294
295 return "null";
296 }
297
298 log(message) {
299 console.log(message);
300 }
301 }
302
303 module.exports = SyncAttributesPlugin;
304