@stratawp/vite-plugin 0.2.0 → 0.2.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 ADDED
@@ -0,0 +1,21 @@
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2024 Jon Imms
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
18
+
19
+ ---
20
+
21
+ For the full GPL-3.0 license text, see: https://www.gnu.org/licenses/gpl-3.0.txt
package/README.md CHANGED
@@ -84,6 +84,7 @@ export default defineConfig({
84
84
  ### Block Auto-Discovery
85
85
 
86
86
  The plugin scans your `src/blocks` directory for `block.json` files and automatically:
87
+
87
88
  1. Discovers all blocks
88
89
  2. Generates PHP registration code
89
90
  3. Watches for changes during development
@@ -92,6 +93,7 @@ The plugin scans your `src/blocks` directory for `block.json` files and automati
92
93
  ### PHP Hot Reload
93
94
 
94
95
  When you modify PHP template files:
96
+
95
97
  1. Plugin detects the change
96
98
  2. Sends reload signal to browser
97
99
  3. Page refreshes automatically
@@ -100,6 +102,7 @@ When you modify PHP template files:
100
102
  ### Asset Manifest
101
103
 
102
104
  Generates a WordPress-compatible manifest that includes:
105
+
103
106
  - File paths and hashes
104
107
  - CSS dependencies
105
108
  - WordPress script dependencies (wp-element, wp-blocks, etc.)
package/dist/index.d.ts CHANGED
@@ -28,6 +28,20 @@ interface StrataWPPluginOptions {
28
28
  * Performance optimization options
29
29
  */
30
30
  performance?: PerformanceOptions;
31
+ /**
32
+ * Update notification options for dev server
33
+ */
34
+ updateNotification?: UpdateNotificationOptions;
35
+ }
36
+ /**
37
+ * Update Notification Options
38
+ */
39
+ interface UpdateNotificationOptions {
40
+ /**
41
+ * Enable update notifications in dev server
42
+ * @default true
43
+ */
44
+ enabled?: boolean;
31
45
  }
32
46
  /**
33
47
  * Block Options
@@ -184,15 +198,10 @@ interface PerformanceOptions {
184
198
  */
185
199
  criticalCSS?: boolean | CriticalCSSOptions;
186
200
  /**
187
- * Lazy loading options
188
- * @default true
189
- */
190
- lazyLoading?: boolean | LazyLoadingOptions;
191
- /**
192
- * Asset preloading options
201
+ * Build-time image optimization (sharp + webp + svgo)
193
202
  * @default true
194
203
  */
195
- preload?: boolean | PreloadOptions;
204
+ images?: boolean | ImageOptions;
196
205
  }
197
206
  /**
198
207
  * Critical CSS Options
@@ -262,6 +271,39 @@ interface LazyLoadingOptions {
262
271
  */
263
272
  placeholder?: 'blur' | 'color' | 'none';
264
273
  }
274
+ /**
275
+ * Image Pipeline Options
276
+ */
277
+ interface ImageOptions {
278
+ /**
279
+ * Enable the build-time image pipeline
280
+ * @default true
281
+ */
282
+ enabled?: boolean;
283
+ /**
284
+ * Source images directory (relative to project root)
285
+ * @default 'src/images'
286
+ */
287
+ src?: string;
288
+ /**
289
+ * Output directory (relative to project root)
290
+ * @default 'dist/images'
291
+ */
292
+ dest?: string;
293
+ /**
294
+ * Emit sibling .webp for jpg/png
295
+ * @default true
296
+ */
297
+ webp?: boolean;
298
+ /**
299
+ * Per-format quality (0-100)
300
+ */
301
+ quality?: {
302
+ jpeg?: number;
303
+ png?: number;
304
+ webp?: number;
305
+ };
306
+ }
265
307
  /**
266
308
  * Preload Options
267
309
  */
@@ -309,6 +351,10 @@ declare function strataWPPhpHmr(options?: PhpHmrOptions): Plugin;
309
351
  *
310
352
  * Ensures assets are output in WordPress-friendly structure
311
353
  * and can be properly enqueued by WordPress.
354
+ *
355
+ * Also handles the src/icons/ directory:
356
+ * - Build: copies to dist/icons/ with CSS url() path normalization
357
+ * - Dev: watches for changes and triggers full page reload
312
358
  */
313
359
  declare function strataWPAssets(options?: AssetOptions): Plugin;
314
360
 
@@ -322,8 +368,6 @@ declare function strataWPDesignSystem(options?: DesignSystemOptions): Plugin;
322
368
 
323
369
  /**
324
370
  * Performance Orchestrator
325
- *
326
- * Combines all performance plugins with unified configuration
327
371
  */
328
372
 
329
373
  declare function strataWPPerformance(options?: PerformanceOptions): Plugin[];
@@ -342,6 +386,59 @@ declare const strataWPTailwindPreset: Partial<any>;
342
386
  */
343
387
  declare const strataWPUnoPreset: any;
344
388
 
389
+ /**
390
+ * block.json -> TypeScript attribute-type codegen.
391
+ *
392
+ * Given a parsed block.json, emits a deterministic `block-attributes.ts` file
393
+ * exporting an interface that describes the block's attributes. The block's
394
+ * `edit.tsx` consumes this interface so the editor props stay in lockstep with
395
+ * block.json, and a drift gate (`scripts/check-types-drift.mjs`) diffs the
396
+ * committed output to keep them in sync.
397
+ *
398
+ * JSON -> TS type map:
399
+ * string -> string
400
+ * number | integer -> number
401
+ * boolean -> boolean
402
+ * array -> unknown[]
403
+ * object -> Record<string, unknown>
404
+ * (missing/unknown) -> unknown
405
+ *
406
+ * Optionality: an attribute with a `default` is REQUIRED; an attribute without
407
+ * a `default` is optional (`?`).
408
+ */
409
+ /**
410
+ * A parsed block.json (only the fields the codegen needs).
411
+ *
412
+ * `attributes` is intentionally `Record<string, unknown>` so the scanned
413
+ * `BlockMetadata` (which types attributes loosely) is assignable; each entry is
414
+ * narrowed to {@link BlockAttributeSchema} internally.
415
+ */
416
+ interface BlockJsonInput {
417
+ name?: string;
418
+ attributes?: Record<string, unknown>;
419
+ }
420
+ /** A single attribute schema entry from block.json's `attributes` map. */
421
+ interface BlockAttributeSchema {
422
+ type?: string;
423
+ default?: unknown;
424
+ [key: string]: unknown;
425
+ }
426
+ /** The result of generating a block's attribute types. */
427
+ interface GeneratedBlockTypes {
428
+ /** Always `block-attributes.ts` — written next to the block's block.json. */
429
+ fileName: string;
430
+ /** Hand-emitted TypeScript source for the attribute interface. */
431
+ content: string;
432
+ }
433
+ /**
434
+ * Generate the `block-attributes.ts` source for a single block.
435
+ *
436
+ * The output is hand-emitted to be byte-identical to the repo prettier config
437
+ * (no semicolons, single quotes, 2-space indent, LF line endings, trailing
438
+ * newline) without a runtime prettier dependency.
439
+ */
440
+ declare function generateBlockAttributeTypes(blockJson: BlockJsonInput): GeneratedBlockTypes;
441
+
345
442
  /**
346
443
  * StrataWP Vite Plugin
347
444
  *
@@ -353,4 +450,4 @@ declare const strataWPUnoPreset: any;
353
450
  */
354
451
  declare function strataWP(options?: StrataWPPluginOptions): Plugin[];
355
452
 
356
- export { type AssetOptions, type BlockMetadata, type BlockOptions, type CriticalCSSOptions, type DesignSystemOptions, type LazyLoadingOptions, type ManifestEntry, type ManifestOptions, type PerformanceOptions, type PhpHmrOptions, type PreloadOptions, type StrataWPPluginOptions, type WordPressManifest, strataWP, strataWPAssets, strataWPBlocks, strataWPDesignSystem, strataWPManifest, strataWPPerformance, strataWPPhpHmr, strataWPTailwindPreset, strataWPUnoPreset };
453
+ export { type AssetOptions, type BlockAttributeSchema, type BlockJsonInput, type BlockMetadata, type BlockOptions, type CriticalCSSOptions, type DesignSystemOptions, type GeneratedBlockTypes, type ImageOptions, type LazyLoadingOptions, type ManifestEntry, type ManifestOptions, type PerformanceOptions, type PhpHmrOptions, type PreloadOptions, type StrataWPPluginOptions, type UpdateNotificationOptions, type WordPressManifest, generateBlockAttributeTypes, strataWP, strataWPAssets, strataWPBlocks, strataWPDesignSystem, strataWPManifest, strataWPPerformance, strataWPPhpHmr, strataWPTailwindPreset, strataWPUnoPreset };
package/dist/index.js CHANGED
@@ -10,6 +10,58 @@ import fg from "fast-glob";
10
10
  import { readFile, writeFile } from "fs/promises";
11
11
  import { join, dirname, relative } from "path";
12
12
  import pc from "picocolors";
13
+
14
+ // src/plugins/block-types.ts
15
+ function mapJsonTypeToTs(jsonType) {
16
+ switch (jsonType) {
17
+ case "string":
18
+ return "string";
19
+ case "number":
20
+ case "integer":
21
+ return "number";
22
+ case "boolean":
23
+ return "boolean";
24
+ case "array":
25
+ return "unknown[]";
26
+ case "object":
27
+ return "Record<string, unknown>";
28
+ default:
29
+ return "unknown";
30
+ }
31
+ }
32
+ function pascalCaseFromBlockName(blockName) {
33
+ const slug = blockName.includes("/") ? blockName.split("/").pop() : blockName;
34
+ return slug.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
35
+ }
36
+ function generateBlockAttributeTypes(blockJson) {
37
+ const blockName = blockJson.name ?? "block";
38
+ const interfaceName = `${pascalCaseFromBlockName(blockName)}Attributes`;
39
+ const attributes = blockJson.attributes ?? {};
40
+ const sortedKeys = Object.keys(attributes).sort();
41
+ const members = sortedKeys.map((key) => {
42
+ const schema = attributes[key] ?? {};
43
+ const tsType = mapJsonTypeToTs(typeof schema.type === "string" ? schema.type : void 0);
44
+ const optional = Object.prototype.hasOwnProperty.call(schema, "default") ? "" : "?";
45
+ return ` ${key}${optional}: ${tsType}`;
46
+ });
47
+ const body = members.length > 0 ? `{
48
+ ${members.join("\n")}
49
+ }` : "{}";
50
+ const content = `/**
51
+ * AUTO-GENERATED by @stratawp/vite-plugin from block.json \u2014 do not edit.
52
+ *
53
+ * Regenerate by building the theme (\`pnpm build\`). The committed copy is
54
+ * verified by the \`contracts:types:check\` drift gate.
55
+ *
56
+ * Block: ${blockName}
57
+ */
58
+
59
+ export interface ${interfaceName} ${body}
60
+ `;
61
+ return { fileName: "block-attributes.ts", content };
62
+ }
63
+
64
+ // src/plugins/blocks.ts
13
65
  function strataWPBlocks(options = {}) {
14
66
  const {
15
67
  dir = "src/blocks",
@@ -29,6 +81,7 @@ function strataWPBlocks(options = {}) {
29
81
  cwd: rootDir,
30
82
  absolute: true
31
83
  });
84
+ blockJsonFiles.sort();
32
85
  blocks = [];
33
86
  for (const blockJsonPath of blockJsonFiles) {
34
87
  try {
@@ -59,6 +112,29 @@ function strataWPBlocks(options = {}) {
59
112
  const outputPath = join(rootDir, "inc/blocks-generated.php");
60
113
  await writeFile(outputPath, phpCode, "utf-8");
61
114
  console.log(pc.green(` \u2713 Generated block registration: inc/blocks-generated.php`));
115
+ let typesWritten = 0;
116
+ for (const block of blocks) {
117
+ const blockData = block;
118
+ try {
119
+ const { fileName, content } = generateBlockAttributeTypes(block);
120
+ const typesPath = join(rootDir, blockData._dir, fileName);
121
+ let existing = null;
122
+ try {
123
+ existing = await readFile(typesPath, "utf-8");
124
+ } catch {
125
+ existing = null;
126
+ }
127
+ if (existing !== content) {
128
+ await writeFile(typesPath, content, "utf-8");
129
+ typesWritten++;
130
+ }
131
+ } catch {
132
+ this.warn(`Failed to generate attribute types for ${block.name}`);
133
+ }
134
+ }
135
+ if (typesWritten > 0) {
136
+ console.log(pc.green(` \u2713 Generated block attribute types: ${typesWritten} file(s)`));
137
+ }
62
138
  },
63
139
  // Hot reload blocks during development
64
140
  configureServer(server) {
@@ -105,11 +181,7 @@ import { writeFile as writeFile2 } from "fs/promises";
105
181
  import { join as join2, dirname as dirname2 } from "path";
106
182
  import { mkdir } from "fs/promises";
107
183
  function strataWPManifest(options = {}) {
108
- const {
109
- enabled = true,
110
- output = "dist/.vite/manifest.json",
111
- wordpress = true
112
- } = options;
184
+ const { enabled = true, output = "dist/.vite/manifest.json", wordpress = true } = options;
113
185
  let rootDir;
114
186
  return {
115
187
  name: "stratawp:manifest",
@@ -173,11 +245,7 @@ function extractWordPressDependencies(code) {
173
245
  import { watch } from "chokidar";
174
246
  import pc2 from "picocolors";
175
247
  function strataWPPhpHmr(options = {}) {
176
- const {
177
- enabled = true,
178
- watch: patterns = ["**/*.php", "theme.json"],
179
- debounce = 100
180
- } = options;
248
+ const { enabled = true, watch: patterns = ["**/*.php", "theme.json"], debounce = 100 } = options;
181
249
  let server;
182
250
  let watcher = null;
183
251
  let timeout = null;
@@ -191,11 +259,11 @@ function strataWPPhpHmr(options = {}) {
191
259
  persistent: true,
192
260
  ignoreInitial: true
193
261
  });
194
- watcher.on("change", (path5) => {
262
+ watcher.on("change", (path6) => {
195
263
  if (timeout) clearTimeout(timeout);
196
264
  timeout = setTimeout(() => {
197
265
  console.log(pc2.cyan(`
198
- \u2692\uFE0F PHP file changed: ${pc2.dim(path5)}`));
266
+ \u2692\uFE0F PHP file changed: ${pc2.dim(path6)}`));
199
267
  console.log(pc2.cyan(" Reloading page...\n"));
200
268
  server.ws.send({
201
269
  type: "full-reload",
@@ -203,14 +271,14 @@ function strataWPPhpHmr(options = {}) {
203
271
  });
204
272
  }, debounce);
205
273
  });
206
- watcher.on("add", (path5) => {
274
+ watcher.on("add", (path6) => {
207
275
  console.log(pc2.green(`
208
- \u2692\uFE0F New PHP file: ${pc2.dim(path5)}
276
+ \u2692\uFE0F New PHP file: ${pc2.dim(path6)}
209
277
  `));
210
278
  });
211
- watcher.on("unlink", (path5) => {
279
+ watcher.on("unlink", (path6) => {
212
280
  console.log(pc2.yellow(`
213
- \u2692\uFE0F PHP file removed: ${pc2.dim(path5)}
281
+ \u2692\uFE0F PHP file removed: ${pc2.dim(path6)}
214
282
  `));
215
283
  });
216
284
  server.httpServer?.once("listening", () => {
@@ -233,11 +301,28 @@ function strataWPPhpHmr(options = {}) {
233
301
  }
234
302
 
235
303
  // src/plugins/assets.ts
304
+ import fs from "fs";
305
+ import path from "path";
306
+ function copyIconsDir(srcDir, destDir) {
307
+ fs.mkdirSync(destDir, { recursive: true });
308
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
309
+ for (const entry of entries) {
310
+ const srcPath = path.join(srcDir, entry.name);
311
+ const destPath = path.join(destDir, entry.name);
312
+ if (entry.isDirectory()) {
313
+ copyIconsDir(srcPath, destPath);
314
+ } else if (entry.name.endsWith(".css")) {
315
+ let content = fs.readFileSync(srcPath, "utf-8");
316
+ content = content.replace(/url\(\s*['"]?\.\.\/fonts\//g, "url(./fonts/");
317
+ fs.writeFileSync(destPath, content, "utf-8");
318
+ } else {
319
+ fs.copyFileSync(srcPath, destPath);
320
+ }
321
+ }
322
+ }
236
323
  function strataWPAssets(options = {}) {
237
- const {
238
- publicDir = "dist",
239
- baseUrl
240
- } = options;
324
+ const { publicDir = "dist", baseUrl } = options;
325
+ let rootDir;
241
326
  return {
242
327
  name: "stratawp:assets",
243
328
  config() {
@@ -257,19 +342,43 @@ function strataWPAssets(options = {}) {
257
342
  sourcemap: true
258
343
  }
259
344
  };
345
+ },
346
+ configResolved(resolvedConfig) {
347
+ rootDir = resolvedConfig.root;
348
+ },
349
+ writeBundle() {
350
+ const iconsSrc = path.resolve(rootDir, "src/icons");
351
+ const iconsDest = path.resolve(rootDir, publicDir, "icons");
352
+ if (!fs.existsSync(iconsSrc)) {
353
+ return;
354
+ }
355
+ copyIconsDir(iconsSrc, iconsDest);
356
+ },
357
+ configureServer(server) {
358
+ const iconsSrc = path.resolve(rootDir, "src/icons");
359
+ if (!fs.existsSync(iconsSrc)) {
360
+ return;
361
+ }
362
+ server.watcher.add(iconsSrc);
363
+ server.watcher.on("change", (filePath) => {
364
+ if (filePath.startsWith(iconsSrc)) {
365
+ server.ws.send({ type: "full-reload", path: "*" });
366
+ }
367
+ });
368
+ server.watcher.on("add", (filePath) => {
369
+ if (filePath.startsWith(iconsSrc)) {
370
+ server.ws.send({ type: "full-reload", path: "*" });
371
+ }
372
+ });
260
373
  }
261
374
  };
262
375
  }
263
376
 
264
377
  // src/plugins/design-system.ts
265
- import path from "path";
266
- import fs from "fs";
378
+ import path2 from "path";
379
+ import fs2 from "fs";
267
380
  function strataWPDesignSystem(options = {}) {
268
- const {
269
- enabled = false,
270
- framework = "none",
271
- wordpressPresets = true
272
- } = options;
381
+ const { enabled = false, framework = "none", wordpressPresets = true } = options;
273
382
  if (!enabled || framework === "none") {
274
383
  return {
275
384
  name: "stratawp:design-system",
@@ -284,11 +393,11 @@ function strataWPDesignSystem(options = {}) {
284
393
  },
285
394
  async config() {
286
395
  const root = process.cwd();
287
- const tailwindConfig = path.join(root, "tailwind.config.js");
288
- const tailwindConfigTs = path.join(root, "tailwind.config.ts");
289
- const unoConfig = path.join(root, "uno.config.ts");
290
- const hasTailwindConfig = fs.existsSync(tailwindConfig) || fs.existsSync(tailwindConfigTs);
291
- const hasUnoConfig = fs.existsSync(unoConfig);
396
+ const tailwindConfig = path2.join(root, "tailwind.config.js");
397
+ const tailwindConfigTs = path2.join(root, "tailwind.config.ts");
398
+ const unoConfig = path2.join(root, "uno.config.ts");
399
+ const hasTailwindConfig = fs2.existsSync(tailwindConfig) || fs2.existsSync(tailwindConfigTs);
400
+ const hasUnoConfig = fs2.existsSync(unoConfig);
292
401
  if (framework === "tailwind" && !hasTailwindConfig) {
293
402
  console.warn(
294
403
  "\u26A0\uFE0F Tailwind CSS enabled but no config file found. Run: stratawp design-system:setup tailwind"
@@ -336,10 +445,10 @@ function strataWPDesignSystem(options = {}) {
336
445
  },
337
446
  async transform(code, id) {
338
447
  if (wordpressPresets && id.endsWith(".css") && (framework === "tailwind" || framework === "unocss")) {
339
- const themeJsonPath = path.join(process.cwd(), "theme.json");
340
- if (fs.existsSync(themeJsonPath)) {
448
+ const themeJsonPath = path2.join(process.cwd(), "theme.json");
449
+ if (fs2.existsSync(themeJsonPath)) {
341
450
  try {
342
- const themeJson = JSON.parse(fs.readFileSync(themeJsonPath, "utf-8"));
451
+ const themeJson = JSON.parse(fs2.readFileSync(themeJsonPath, "utf-8"));
343
452
  if (themeJson.settings?.color?.palette) {
344
453
  let injectedVars = "\n/* WordPress Color Variables */\n:root {\n";
345
454
  themeJson.settings.color.palette.forEach((color) => {
@@ -359,8 +468,8 @@ function strataWPDesignSystem(options = {}) {
359
468
  }
360
469
 
361
470
  // src/plugins/critical-css.ts
362
- import path2 from "path";
363
- import fs2 from "fs";
471
+ import path3 from "path";
472
+ import fs3 from "fs";
364
473
  function strataWPCriticalCSS(options = {}) {
365
474
  const {
366
475
  enabled = true,
@@ -381,9 +490,9 @@ function strataWPCriticalCSS(options = {}) {
381
490
  apply: "build",
382
491
  async closeBundle() {
383
492
  const root = process.cwd();
384
- const distDir = path2.join(root, "dist");
385
- const templatesDir = path2.join(root, "templates");
386
- const outputDir = path2.join(root, output);
493
+ const distDir = path3.join(root, "dist");
494
+ const templatesDir = path3.join(root, "templates");
495
+ const outputDir = path3.join(root, output);
387
496
  let critical;
388
497
  try {
389
498
  critical = __require("critical");
@@ -393,11 +502,11 @@ function strataWPCriticalCSS(options = {}) {
393
502
  );
394
503
  return;
395
504
  }
396
- if (!fs2.existsSync(outputDir)) {
397
- fs2.mkdirSync(outputDir, { recursive: true });
505
+ if (!fs3.existsSync(outputDir)) {
506
+ fs3.mkdirSync(outputDir, { recursive: true });
398
507
  }
399
508
  console.log("\u{1F4E6} Extracting critical CSS...");
400
- const cssFiles = fs2.readdirSync(path2.join(distDir, "css")).filter((f) => f.endsWith(".css")).map((f) => path2.join(distDir, "css", f));
509
+ const cssFiles = fs3.readdirSync(path3.join(distDir, "css")).filter((f) => f.endsWith(".css")).map((f) => path3.join(distDir, "css", f));
401
510
  if (cssFiles.length === 0) {
402
511
  console.warn(" No CSS files found to extract critical CSS from");
403
512
  return;
@@ -405,8 +514,8 @@ function strataWPCriticalCSS(options = {}) {
405
514
  const mainCss = cssFiles[0];
406
515
  const criticalResults = {};
407
516
  for (const template of templates) {
408
- const templatePath = path2.join(templatesDir, `${template}.html`);
409
- if (!fs2.existsSync(templatePath)) {
517
+ const templatePath = path3.join(templatesDir, `${template}.html`);
518
+ if (!fs3.existsSync(templatePath)) {
410
519
  console.warn(` Template ${template}.html not found, skipping`);
411
520
  continue;
412
521
  }
@@ -422,8 +531,8 @@ function strataWPCriticalCSS(options = {}) {
422
531
  });
423
532
  if (result.css && result.css.length > minSize) {
424
533
  criticalResults[template] = result.css;
425
- const criticalPath = path2.join(outputDir, `${template}-critical.css`);
426
- fs2.writeFileSync(criticalPath, result.css);
534
+ const criticalPath = path3.join(outputDir, `${template}-critical.css`);
535
+ fs3.writeFileSync(criticalPath, result.css);
427
536
  console.log(` \u2713 ${template}: ${(result.css.length / 1024).toFixed(2)}KB`);
428
537
  }
429
538
  } catch (error) {
@@ -435,9 +544,9 @@ function strataWPCriticalCSS(options = {}) {
435
544
  };
436
545
  }
437
546
  async function generatePHPLoader(root, criticalResults, inline) {
438
- const incDir = path2.join(root, "inc");
439
- if (!fs2.existsSync(incDir)) {
440
- fs2.mkdirSync(incDir, { recursive: true });
547
+ const incDir = path3.join(root, "inc");
548
+ if (!fs3.existsSync(incDir)) {
549
+ fs3.mkdirSync(incDir, { recursive: true });
441
550
  }
442
551
  let phpContent = `<?php
443
552
  /**
@@ -496,308 +605,236 @@ function load_critical_css(): void {
496
605
  // Hook into wp_head with high priority
497
606
  add_action('wp_head', __NAMESPACE__ . '\\load_critical_css', 1);
498
607
  `;
499
- fs2.writeFileSync(path2.join(incDir, "critical-css-generated.php"), phpContent);
608
+ fs3.writeFileSync(path3.join(incDir, "critical-css-generated.php"), phpContent);
500
609
  console.log(" \u2713 Generated inc/critical-css-generated.php");
501
610
  }
502
611
 
503
- // src/plugins/lazy-loading.ts
504
- import path3 from "path";
505
- import fs3 from "fs";
506
- function strataWPLazyLoading(options = {}) {
612
+ // src/plugins/images.ts
613
+ import path4 from "path";
614
+ import { stat, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile3, copyFile } from "fs/promises";
615
+ import fg2 from "fast-glob";
616
+ import sharp from "sharp";
617
+ import { optimize as svgoOptimize } from "svgo";
618
+ function strataWPImages(options = {}) {
507
619
  const {
508
620
  enabled = true,
509
- images = "native",
510
- css = true,
511
- chunks = true
621
+ src = "src/images",
622
+ dest = "dist/images",
623
+ webp = true,
624
+ quality = {}
512
625
  } = options;
513
- if (!enabled) {
514
- return {
515
- name: "stratawp:lazy-loading",
516
- apply: () => false
517
- };
518
- }
626
+ const jpegQuality = quality.jpeg ?? 75;
627
+ const pngQuality = quality.png ?? 80;
628
+ const webpQuality = quality.webp ?? 75;
629
+ let root = process.cwd();
519
630
  return {
520
- name: "stratawp:lazy-loading",
631
+ name: "stratawp:images",
521
632
  apply: "build",
522
- config() {
523
- if (chunks) {
524
- return {
525
- build: {
526
- rollupOptions: {
527
- output: {
528
- manualChunks: (id) => {
529
- if (id.includes("node_modules")) {
530
- return "vendor";
531
- }
532
- if (id.includes("@wordpress/")) {
533
- return "wordpress";
534
- }
535
- }
536
- }
633
+ configResolved(config) {
634
+ root = config.root;
635
+ },
636
+ async closeBundle() {
637
+ if (!enabled) {
638
+ return;
639
+ }
640
+ const srcRoot = path4.resolve(root, src);
641
+ const destRoot = path4.resolve(root, dest);
642
+ const files = await fg2("**/*.{jpg,jpeg,png,gif,svg}", {
643
+ cwd: srcRoot,
644
+ caseSensitiveMatch: false,
645
+ onlyFiles: true
646
+ });
647
+ for (const rel of files) {
648
+ const srcFile = path4.join(srcRoot, rel);
649
+ const destFile = path4.join(destRoot, rel);
650
+ const ext = path4.extname(rel).toLowerCase();
651
+ await mkdir2(path4.dirname(destFile), { recursive: true });
652
+ if (await isNewer(srcFile, destFile)) {
653
+ try {
654
+ if (ext === ".svg") {
655
+ const code = await readFile2(srcFile, "utf8");
656
+ const result = svgoOptimize(code, {
657
+ multipass: true,
658
+ plugins: [
659
+ { name: "preset-default", params: { overrides: { removeViewBox: false } } }
660
+ ]
661
+ });
662
+ await writeFile3(destFile, result.data, "utf8");
663
+ } else if (ext === ".jpg" || ext === ".jpeg") {
664
+ await sharp(srcFile, { sequentialRead: true }).rotate().jpeg({ quality: jpegQuality, mozjpeg: true, progressive: true }).toFile(destFile);
665
+ } else if (ext === ".png") {
666
+ await sharp(srcFile, { sequentialRead: true }).rotate().png({ quality: pngQuality, compressionLevel: 9, adaptiveFiltering: true }).toFile(destFile);
667
+ } else {
668
+ await copyFile(srcFile, destFile);
669
+ }
670
+ } catch {
671
+ await copyFile(srcFile, destFile);
672
+ }
673
+ }
674
+ if (webp && (ext === ".jpg" || ext === ".jpeg" || ext === ".png")) {
675
+ const webpFile = destFile.replace(/\.[^.]+$/i, ".webp");
676
+ if (await isNewer(srcFile, webpFile)) {
677
+ try {
678
+ await sharp(srcFile, { sequentialRead: true }).webp({ quality: webpQuality }).toFile(webpFile);
679
+ } catch {
537
680
  }
538
681
  }
539
- };
682
+ }
540
683
  }
541
- return {};
542
- },
543
- async closeBundle() {
544
- const root = process.cwd();
545
- await generatePHPUtilities(root, images, css);
546
684
  }
547
685
  };
548
686
  }
549
- async function generatePHPUtilities(root, images, css) {
550
- const incDir = path3.join(root, "inc");
551
- if (!fs3.existsSync(incDir)) {
552
- fs3.mkdirSync(incDir, { recursive: true });
687
+ async function isNewer(src, dest) {
688
+ try {
689
+ const [s, d] = await Promise.all([stat(src), stat(dest)]);
690
+ return s.mtimeMs > d.mtimeMs;
691
+ } catch {
692
+ return true;
553
693
  }
554
- let phpContent = `<?php
555
- /**
556
- * Lazy Loading Utilities
557
- *
558
- * Auto-generated by StrataWP
559
- * DO NOT EDIT MANUALLY
560
- */
561
-
562
- namespace StrataWP;
563
-
564
- `;
565
- if (images !== "none") {
566
- phpContent += `/**
567
- * Add loading="lazy" to images
568
- */
569
- function add_lazy_loading_to_images( $content ) {
570
- if ( is_admin() || is_feed() ) {
571
- return $content;
572
- }
573
-
574
- // Add loading="lazy" to img tags that don't have it
575
- $content = preg_replace(
576
- '/<img(?![^>]*loading=)([^>]*)>/i',
577
- '<img loading="lazy"$1>',
578
- $content
579
- );
580
-
581
- return $content;
582
694
  }
583
695
 
584
- add_filter( 'the_content', __NAMESPACE__ . '\\add_lazy_loading_to_images', 20 );
585
- add_filter( 'post_thumbnail_html', __NAMESPACE__ . '\\add_lazy_loading_to_images', 20 );
586
-
587
- `;
696
+ // src/plugins/performance.ts
697
+ function strataWPPerformance(options = {}) {
698
+ const plugins = [];
699
+ if (options.criticalCSS === true || typeof options.criticalCSS === "object") {
700
+ const criticalOptions = typeof options.criticalCSS === "object" ? options.criticalCSS : { enabled: true };
701
+ plugins.push(strataWPCriticalCSS(criticalOptions));
588
702
  }
589
- if (css) {
590
- phpContent += `/**
591
- * Lazy load non-critical CSS
592
- */
593
- function lazy_load_css( $html, $handle, $href, $media ) {
594
- // Skip critical CSS
595
- if ( strpos( $handle, 'critical' ) !== false ) {
596
- return $html;
597
- }
598
-
599
- // Change media to "print" and add onload to switch to "all"
600
- $html = str_replace(
601
- "media='{$media}'",
602
- "media='print' onload=\\"this.media='all'; this.onload=null;\\"",
603
- $html
604
- );
605
-
606
- // Add noscript fallback
607
- $noscript = sprintf(
608
- '<noscript><link rel="stylesheet" href="%s" media="%s"></noscript>',
609
- esc_url( $href ),
610
- esc_attr( $media )
611
- );
612
-
613
- return $html . $noscript;
614
- }
615
-
616
- // Uncomment to enable CSS lazy loading
617
- // add_filter( 'style_loader_tag', __NAMESPACE__ . '\\lazy_load_css', 10, 4 );
618
-
619
- `;
703
+ if (options.images !== false) {
704
+ const imageOptions = typeof options.images === "object" ? options.images : {};
705
+ plugins.push(strataWPImages(imageOptions));
620
706
  }
621
- phpContent += `/**
622
- * Preconnect to external domains
623
- */
624
- function add_resource_hints( $urls, $relation_type ) {
625
- if ( 'preconnect' === $relation_type ) {
626
- $urls[] = array(
627
- 'href' => 'https://fonts.googleapis.com',
628
- 'crossorigin',
629
- );
630
- $urls[] = array(
631
- 'href' => 'https://fonts.gstatic.com',
632
- 'crossorigin',
633
- );
634
- }
635
-
636
- return $urls;
637
- }
638
-
639
- add_filter( 'wp_resource_hints', __NAMESPACE__ . '\\add_resource_hints', 10, 2 );
640
- `;
641
- fs3.writeFileSync(path3.join(incDir, "lazy-loading-generated.php"), phpContent);
642
- console.log(" \u2713 Generated inc/lazy-loading-generated.php");
707
+ return plugins;
643
708
  }
644
709
 
645
- // src/plugins/preload.ts
646
- import path4 from "path";
710
+ // src/utils/update-checker.ts
647
711
  import fs4 from "fs";
648
- function strataWPPreload(options = {}) {
649
- const {
650
- enabled = true,
651
- assets = ["fonts", "critical-css"],
652
- strategy = "link-tag"
653
- } = options;
654
- if (!enabled) {
655
- return {
656
- name: "stratawp:preload",
657
- apply: () => false
658
- };
659
- }
660
- return {
661
- name: "stratawp:preload",
662
- apply: "build",
663
- async closeBundle() {
664
- const root = process.cwd();
665
- await generatePHPPreloader(root, assets, strategy);
712
+ import path5 from "path";
713
+ import os from "os";
714
+ import semver from "semver";
715
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
716
+ var STRATAWP_SCOPE = "@stratawp/";
717
+ var REGISTRY_URL = "https://registry.npmjs.org";
718
+ function getCachePath() {
719
+ return path5.join(os.homedir(), ".stratawp", "update-cache.json");
720
+ }
721
+ function loadCache() {
722
+ try {
723
+ const cachePath = getCachePath();
724
+ if (!fs4.existsSync(cachePath)) {
725
+ return null;
666
726
  }
667
- };
727
+ const cacheContent = fs4.readFileSync(cachePath, "utf-8");
728
+ return JSON.parse(cacheContent);
729
+ } catch {
730
+ return null;
731
+ }
668
732
  }
669
- async function generatePHPPreloader(root, assets, strategy) {
670
- const incDir = path4.join(root, "inc");
671
- if (!fs4.existsSync(incDir)) {
672
- fs4.mkdirSync(incDir, { recursive: true });
733
+ function saveCache(cache) {
734
+ try {
735
+ const cachePath = getCachePath();
736
+ const cacheDir = path5.dirname(cachePath);
737
+ if (!fs4.existsSync(cacheDir)) {
738
+ fs4.mkdirSync(cacheDir, { recursive: true });
739
+ }
740
+ fs4.writeFileSync(cachePath, JSON.stringify(cache, null, 2));
741
+ } catch {
673
742
  }
674
- let phpContent = `<?php
675
- /**
676
- * Asset Preloader
677
- *
678
- * Auto-generated by StrataWP
679
- * DO NOT EDIT MANUALLY
680
- */
681
-
682
- namespace StrataWP;
683
-
684
- /**
685
- * Preload critical assets
686
- */
687
- function preload_assets(): void {
688
- $preloads = array();
689
-
690
- `;
691
- if (assets.includes("fonts")) {
692
- phpContent += ` // Preload fonts
693
- $font_dir = get_template_directory() . '/dist/fonts';
694
- $font_url = get_template_directory_uri() . '/dist/fonts';
695
-
696
- if ( is_dir( $font_dir ) ) {
697
- $fonts = glob( $font_dir . '/*.{woff2,woff}', GLOB_BRACE );
698
- foreach ( $fonts as $font ) {
699
- $font_name = basename( $font );
700
- $preloads[] = sprintf(
701
- '<link rel="preload" href="%s/%s" as="font" type="font/%s" crossorigin>',
702
- esc_url( $font_url ),
703
- esc_attr( $font_name ),
704
- esc_attr( pathinfo( $font, PATHINFO_EXTENSION ) )
705
- );
706
- }
743
+ }
744
+ async function fetchLatestVersion(packageName) {
745
+ try {
746
+ const encodedName = packageName.replace("/", "%2F");
747
+ const url = `${REGISTRY_URL}/${encodedName}`;
748
+ const response = await fetch(url, {
749
+ headers: {
750
+ Accept: "application/json"
751
+ }
752
+ });
753
+ if (!response.ok) {
754
+ return null;
707
755
  }
708
-
709
- `;
756
+ const data = await response.json();
757
+ return data["dist-tags"]?.latest || null;
758
+ } catch {
759
+ return null;
710
760
  }
711
- if (assets.includes("critical-css")) {
712
- phpContent += ` // Preload critical CSS
713
- $critical_dir = get_template_directory() . '/dist/critical';
714
- $critical_url = get_template_directory_uri() . '/dist/critical';
715
-
716
- if ( is_dir( $critical_dir ) ) {
717
- $template_name = is_singular() ? 'single' : 'index';
718
- $critical_file = $critical_dir . '/' . $template_name . '-critical.css';
719
-
720
- if ( file_exists( $critical_file ) ) {
721
- $preloads[] = sprintf(
722
- '<link rel="preload" href="%s/%s-critical.css" as="style">',
723
- esc_url( $critical_url ),
724
- esc_attr( $template_name )
725
- );
761
+ }
762
+ function readInstalledPackages(packageJsonPath) {
763
+ if (!fs4.existsSync(packageJsonPath)) {
764
+ return {};
765
+ }
766
+ try {
767
+ const packageJson = JSON.parse(fs4.readFileSync(packageJsonPath, "utf-8"));
768
+ const installed = {};
769
+ if (packageJson.dependencies) {
770
+ for (const [name, version] of Object.entries(packageJson.dependencies)) {
771
+ if (name.startsWith(STRATAWP_SCOPE) && typeof version === "string") {
772
+ installed[name] = version.replace(/^[\^~>=<]+/, "");
726
773
  }
774
+ }
727
775
  }
728
-
729
- `;
730
- }
731
- if (assets.includes("critical-js")) {
732
- phpContent += ` // Preload critical JavaScript
733
- $manifest_file = get_template_directory() . '/dist/.vite/manifest.json';
734
-
735
- if ( file_exists( $manifest_file ) ) {
736
- $manifest = json_decode( file_get_contents( $manifest_file ), true );
737
-
738
- foreach ( $manifest as $entry ) {
739
- if ( isset( $entry['isEntry'] ) && $entry['isEntry'] ) {
740
- $preloads[] = sprintf(
741
- '<link rel="modulepreload" href="%s/%s">',
742
- esc_url( get_template_directory_uri() . '/dist' ),
743
- esc_attr( $entry['file'] )
744
- );
745
- break; // Only preload main entry
746
- }
776
+ if (packageJson.devDependencies) {
777
+ for (const [name, version] of Object.entries(packageJson.devDependencies)) {
778
+ if (name.startsWith(STRATAWP_SCOPE) && typeof version === "string") {
779
+ installed[name] = version.replace(/^[\^~>=<]+/, "");
747
780
  }
781
+ }
748
782
  }
749
-
750
- `;
783
+ return installed;
784
+ } catch {
785
+ return {};
751
786
  }
752
- phpContent += ` // Output preload tags
753
- foreach ( $preloads as $preload ) {
754
- echo $preload . "
755
- ";
756
- }
757
- }
758
-
759
- add_action( 'wp_head', __NAMESPACE__ . '\\preload_assets', 2 );
760
-
761
- /**
762
- * DNS prefetch for external resources
763
- */
764
- function dns_prefetch(): void {
765
- $domains = array(
766
- '//fonts.googleapis.com',
767
- '//fonts.gstatic.com',
768
- );
769
-
770
- foreach ( $domains as $domain ) {
771
- printf(
772
- '<link rel="dns-prefetch" href="%s">' . "
773
- ",
774
- esc_url( $domain )
775
- );
776
- }
777
787
  }
778
-
779
- add_action( 'wp_head', __NAMESPACE__ . '\\dns_prefetch', 1 );
780
- `;
781
- fs4.writeFileSync(path4.join(incDir, "preload-generated.php"), phpContent);
782
- console.log(" \u2713 Generated inc/preload-generated.php");
783
- }
784
-
785
- // src/plugins/performance.ts
786
- function strataWPPerformance(options = {}) {
787
- const plugins = [];
788
- if (options.criticalCSS !== false) {
789
- const criticalOptions = typeof options.criticalCSS === "object" ? options.criticalCSS : { enabled: true };
790
- plugins.push(strataWPCriticalCSS(criticalOptions));
791
- }
792
- if (options.lazyLoading !== false) {
793
- const lazyOptions = typeof options.lazyLoading === "object" ? options.lazyLoading : { enabled: true };
794
- plugins.push(strataWPLazyLoading(lazyOptions));
795
- }
796
- if (options.preload !== false) {
797
- const preloadOptions = typeof options.preload === "object" ? options.preload : { enabled: true };
798
- plugins.push(strataWPPreload(preloadOptions));
788
+ async function checkForUpdates(root) {
789
+ const packageJsonPath = path5.join(root, "package.json");
790
+ const installedPackages = readInstalledPackages(packageJsonPath);
791
+ const packageNames = Object.keys(installedPackages);
792
+ if (packageNames.length === 0) {
793
+ return null;
799
794
  }
800
- return plugins;
795
+ const cache = loadCache();
796
+ const newCache = {
797
+ packages: cache?.packages || {},
798
+ lastCheck: Date.now()
799
+ };
800
+ const updates = [];
801
+ await Promise.all(
802
+ packageNames.map(async (name) => {
803
+ const current = installedPackages[name];
804
+ let latest = null;
805
+ const cached = cache?.packages[name];
806
+ if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
807
+ latest = cached.version;
808
+ } else {
809
+ latest = await fetchLatestVersion(name);
810
+ if (latest) {
811
+ newCache.packages[name] = {
812
+ version: latest,
813
+ fetchedAt: Date.now()
814
+ };
815
+ }
816
+ }
817
+ if (latest && semver.lt(current, latest)) {
818
+ updates.push({ name, current, latest });
819
+ }
820
+ })
821
+ );
822
+ saveCache(newCache);
823
+ return updates.length > 0 ? updates : null;
824
+ }
825
+ function checkForUpdatesAsync(logger, root) {
826
+ checkForUpdates(root).then((updates) => {
827
+ if (updates && updates.length > 0) {
828
+ logger.info("");
829
+ logger.info(" \x1B[33m\u{1F4E6} Updates available:\x1B[0m");
830
+ for (const update of updates) {
831
+ logger.info(` ${update.name}: ${update.current} -> \x1B[32m${update.latest}\x1B[0m`);
832
+ }
833
+ logger.info(" Run `stratawp update` to update");
834
+ logger.info("");
835
+ }
836
+ }).catch(() => {
837
+ });
801
838
  }
802
839
 
803
840
  // src/integrations/tailwind-preset.ts
@@ -986,9 +1023,8 @@ function strataWP(options = {}) {
986
1023
  framework: "none"
987
1024
  },
988
1025
  performance = {
989
- criticalCSS: true,
990
- lazyLoading: true,
991
- preload: true
1026
+ criticalCSS: false,
1027
+ images: true
992
1028
  }
993
1029
  } = options;
994
1030
  const plugins = [
@@ -1029,6 +1065,14 @@ function strataWPCore(_options) {
1029
1065
  return "fonts/[name].[hash][extname]";
1030
1066
  }
1031
1067
  return "assets/[name].[hash][extname]";
1068
+ },
1069
+ manualChunks: (id) => {
1070
+ if (id.includes("@wordpress/")) {
1071
+ return "wordpress";
1072
+ }
1073
+ if (id.includes("node_modules")) {
1074
+ return "vendor";
1075
+ }
1032
1076
  }
1033
1077
  }
1034
1078
  }
@@ -1052,17 +1096,21 @@ function strataWPCore(_options) {
1052
1096
  configureServer(server) {
1053
1097
  server.httpServer?.once("listening", () => {
1054
1098
  setTimeout(() => {
1055
- const { logger } = config;
1099
+ const { logger, root } = config;
1056
1100
  logger.info("\n \u26A1 StrataWP Dev Server Ready!\n");
1057
1101
  logger.info(" WordPress integration active");
1058
1102
  logger.info(" Block auto-discovery enabled");
1059
1103
  logger.info(" PHP HMR watching for changes\n");
1104
+ if (_options.updateNotification?.enabled !== false) {
1105
+ checkForUpdatesAsync(logger, root);
1106
+ }
1060
1107
  }, 100);
1061
1108
  });
1062
1109
  }
1063
1110
  };
1064
1111
  }
1065
1112
  export {
1113
+ generateBlockAttributeTypes,
1066
1114
  strataWP,
1067
1115
  strataWPAssets,
1068
1116
  strataWPBlocks,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stratawp/vite-plugin",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Vite plugin for WordPress theme development with StrataWP",
5
5
  "author": "Jon Imms",
6
6
  "license": "GPL-3.0-or-later",
@@ -33,26 +33,32 @@
33
33
  "files": [
34
34
  "dist"
35
35
  ],
36
- "scripts": {
37
- "dev": "tsup --watch",
38
- "build": "tsup",
39
- "test": "vitest",
40
- "lint": "eslint src",
41
- "clean": "rm -rf dist"
42
- },
43
36
  "dependencies": {
44
37
  "chokidar": "^3.5.3",
45
38
  "fast-glob": "^3.3.2",
46
39
  "picocolors": "^1.0.0",
40
+ "semver": "^7.5.4",
41
+ "sharp": "^0.33.5",
42
+ "svgo": "^3.3.2",
47
43
  "vite": "^5.0.10"
48
44
  },
49
45
  "devDependencies": {
50
46
  "@types/node": "^20.10.5",
47
+ "@types/semver": "^7.5.8",
48
+ "@vitest/coverage-v8": "^1.0.4",
51
49
  "tsup": "^8.0.1",
52
50
  "typescript": "^5.3.3",
53
51
  "vitest": "^1.0.4"
54
52
  },
55
53
  "peerDependencies": {
56
54
  "vite": "^5.0.0"
55
+ },
56
+ "scripts": {
57
+ "dev": "tsup --watch",
58
+ "build": "tsup",
59
+ "typecheck": "tsc --noEmit",
60
+ "test": "vitest run",
61
+ "test:coverage": "vitest run --coverage",
62
+ "clean": "rm -rf dist"
57
63
  }
58
- }
64
+ }