@artemsemkin/vite-plugin-wp-hmr 1.1.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.
@@ -0,0 +1,33 @@
1
+ import type { Plugin } from 'vite';
2
+ export interface WpHmrOptions {
3
+ /** Directory to write the mu-plugin PHP file */
4
+ outputDir: string;
5
+ /** PHP filename (default: 'vite-hmr.php') */
6
+ fileName?: string;
7
+ /** Additional TLD/host patterns for dev environment detection.
8
+ * Merged with defaults: localhost, 127.0.0.1, .local, .test, .dev */
9
+ devPatterns?: string[];
10
+ /** Vite HMR event names that trigger CSS stylesheet cache-busting.
11
+ * Omitted = no CSS reload listener injected. */
12
+ cssReloadEvents?: string[];
13
+ /** CSP header behavior: true = default permissive policy,
14
+ * false = disabled, string = custom policy (default: true) */
15
+ csp?: boolean | string;
16
+ /** Remove the PHP file when dev server closes (default: true) */
17
+ cleanup?: boolean;
18
+ /** Transient cache TTL in seconds for port-alive probe (default: 5) */
19
+ cacheTtl?: number;
20
+ /** Full Vite dev server origin override (e.g. 'https://localhost:5173').
21
+ * Bypasses auto-detection from Vite config. */
22
+ origin?: string;
23
+ /** Separate origin for PHP fsockopen probe (e.g. 'http://host.docker.internal:5173').
24
+ * Use when PHP runs in Docker and cannot reach Vite via the browser origin.
25
+ * Falls back to `origin` when omitted. */
26
+ probeOrigin?: string;
27
+ /** Map of WP script handles to Vite source paths.
28
+ * Generates script_loader_tag filters to rewrite src to Vite dev server URLs
29
+ * and add type="module" in dev mode. */
30
+ entries?: Record<string, string>;
31
+ }
32
+ export declare function buildPhp(origin: string, options: WpHmrOptions): string;
33
+ export declare function wpHmr(options: WpHmrOptions): Plugin;
package/dist/index.js ADDED
@@ -0,0 +1,138 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ const DEFAULT_TLDS = ['local', 'test', 'dev'];
4
+ export function buildPhp(origin, options) {
5
+ const url = new URL(origin);
6
+ const probeUrl = options.probeOrigin ? new URL(options.probeOrigin) : url;
7
+ const probeHost = probeUrl.hostname;
8
+ const probePort = probeUrl.port || (probeUrl.protocol === 'https:' ? '443' : '80');
9
+ const cacheTtl = options.cacheTtl ?? 5;
10
+ const extraTlds = options.devPatterns ?? [];
11
+ const allTlds = [...DEFAULT_TLDS, ...extraTlds];
12
+ const tldList = allTlds.map((t) => `'${t}'`).join(', ');
13
+ const lines = [];
14
+ // Header
15
+ lines.push(`<?php`);
16
+ lines.push(`/**`);
17
+ lines.push(` * Plugin Name: Vite HMR`);
18
+ lines.push(` * Description: Injects Vite dev client for live reload. Auto-generated — do not edit.`);
19
+ lines.push(` */`);
20
+ lines.push(``);
21
+ // Dev detection
22
+ lines.push(`if ( ! function_exists( 'vite_hmr_is_dev' ) ) {`);
23
+ lines.push(`\tfunction vite_hmr_is_dev(): bool {`);
24
+ lines.push(`\t\t$host = $_SERVER['HTTP_HOST'] ?? '';`);
25
+ lines.push(`\t\tif ( $host === 'localhost' || $host === '127.0.0.1' ) { return true; }`);
26
+ lines.push(`\t\tforeach ( [ ${tldList} ] as $tld ) {`);
27
+ lines.push(`\t\t\tif ( preg_match( '/\\\\.' . $tld . '$/i', $host ) ) { return true; }`);
28
+ lines.push(`\t\t}`);
29
+ lines.push(`\t\treturn false;`);
30
+ lines.push(`\t}`);
31
+ lines.push(`}`);
32
+ lines.push(``);
33
+ // Port probe
34
+ lines.push(`if ( ! function_exists( 'vite_hmr_is_running' ) ) {`);
35
+ lines.push(`\tfunction vite_hmr_is_running(): bool {`);
36
+ lines.push(`\t\t$key = 'vite_hmr_${probePort}';`);
37
+ lines.push(`\t\t$cached = get_transient( $key );`);
38
+ lines.push(`\t\tif ( $cached !== false ) { return (bool) $cached; }`);
39
+ lines.push(`\t\t$conn = @fsockopen( '${probeHost}', ${probePort}, $errno, $errstr, 1 );`);
40
+ lines.push(`\t\t$result = is_resource( $conn );`);
41
+ lines.push(`\t\tif ( $result ) { fclose( $conn ); }`);
42
+ lines.push(`\t\tset_transient( $key, (int) $result, ${cacheTtl} );`);
43
+ lines.push(`\t\treturn $result;`);
44
+ lines.push(`\t}`);
45
+ lines.push(`}`);
46
+ lines.push(``);
47
+ // Client injection
48
+ lines.push(`if ( ! function_exists( 'vite_hmr_inject' ) ) {`);
49
+ lines.push(`\tfunction vite_hmr_inject(): void {`);
50
+ lines.push(`\t\tif ( ! vite_hmr_is_dev() || ! vite_hmr_is_running() ) { return; }`);
51
+ lines.push(`\t\techo '<script type="module" src="${origin}/@vite/client"></script>' . "\\n";`);
52
+ if (options.cssReloadEvents?.length) {
53
+ lines.push(`\t\techo '<script type="module">' . "\\n";`);
54
+ lines.push(`\t\techo 'import { createHotContext } from "${origin}/@vite/client";' . "\\n";`);
55
+ lines.push(`\t\techo 'const hot = createHotContext("/wp-hmr");' . "\\n";`);
56
+ for (const event of options.cssReloadEvents) {
57
+ lines.push(`\t\techo 'hot.on("${event}", () => {' . "\\n";`);
58
+ lines.push(`\t\techo ' document.querySelectorAll("link[rel=stylesheet]").forEach(l => {' . "\\n";`);
59
+ lines.push(`\t\techo ' const u = new URL(l.href); u.searchParams.set("t", Date.now()); l.href = u.toString();' . "\\n";`);
60
+ lines.push(`\t\techo ' });' . "\\n";`);
61
+ lines.push(`\t\techo '});' . "\\n";`);
62
+ }
63
+ lines.push(`\t\techo '</script>' . "\\n";`);
64
+ }
65
+ lines.push(`\t}`);
66
+ lines.push(`}`);
67
+ lines.push(`add_action( 'wp_head', 'vite_hmr_inject', 1 );`);
68
+ lines.push(``);
69
+ // Entry script rewriting (ESM dev mode)
70
+ if (options.entries && Object.keys(options.entries).length > 0) {
71
+ lines.push(`if ( ! function_exists( 'vite_hmr_rewrite_entry' ) ) {`);
72
+ lines.push(`\tfunction vite_hmr_rewrite_entry( string $tag, string $handle, string $src ): string {`);
73
+ lines.push(`\t\tif ( ! vite_hmr_is_dev() || ! vite_hmr_is_running() ) { return $tag; }`);
74
+ lines.push(`\t\t$entries = [`);
75
+ for (const [handle, srcPath] of Object.entries(options.entries)) {
76
+ lines.push(`\t\t\t'${handle}' => '${origin}${srcPath}',`);
77
+ }
78
+ lines.push(`\t\t];`);
79
+ lines.push(`\t\tif ( ! isset( $entries[ $handle ] ) ) { return $tag; }`);
80
+ lines.push(`\t\t$p = new WP_HTML_Tag_Processor( $tag );`);
81
+ lines.push(`\t\tif ( $p->next_tag( 'script' ) ) {`);
82
+ lines.push(`\t\t\t$p->set_attribute( 'src', $entries[ $handle ] );`);
83
+ lines.push(`\t\t\t$p->set_attribute( 'type', 'module' );`);
84
+ lines.push(`\t\t}`);
85
+ lines.push(`\t\treturn $p->get_updated_html();`);
86
+ lines.push(`\t}`);
87
+ lines.push(`}`);
88
+ lines.push(`add_filter( 'script_loader_tag', 'vite_hmr_rewrite_entry', 10, 3 );`);
89
+ lines.push(``);
90
+ }
91
+ // CSP
92
+ if (options.csp !== false) {
93
+ const policy = typeof options.csp === 'string'
94
+ ? options.csp
95
+ : "Content-Security-Policy: script-src * blob: 'unsafe-inline' 'unsafe-eval'; worker-src * blob:; connect-src * 'unsafe-inline';";
96
+ lines.push(`if ( ! function_exists( 'vite_hmr_csp' ) ) {`);
97
+ lines.push(`\tfunction vite_hmr_csp(): void {`);
98
+ lines.push(`\t\tif ( ! vite_hmr_is_dev() ) { return; }`);
99
+ lines.push(`\t\theader( "${policy}" );`);
100
+ lines.push(`\t}`);
101
+ lines.push(`}`);
102
+ lines.push(`add_action( 'send_headers', 'vite_hmr_csp', 1 );`);
103
+ lines.push(``);
104
+ }
105
+ return lines.join('\n');
106
+ }
107
+ export function wpHmr(options) {
108
+ let origin;
109
+ return {
110
+ name: 'vite-plugin-wp-hmr',
111
+ apply: 'serve',
112
+ configResolved(config) {
113
+ if (options.origin) {
114
+ origin = options.origin;
115
+ }
116
+ else {
117
+ const protocol = config.server.https ? 'https' : 'http';
118
+ const port = config.server.port ?? 5173;
119
+ origin = `${protocol}://localhost:${port}`;
120
+ }
121
+ },
122
+ configureServer(server) {
123
+ const fileName = options.fileName ?? 'vite-hmr.php';
124
+ const phpFile = path.join(options.outputDir, fileName);
125
+ const php = buildPhp(origin, options);
126
+ fs.mkdirpSync(options.outputDir);
127
+ fs.writeFileSync(phpFile, php);
128
+ if (options.cleanup !== false) {
129
+ server.httpServer?.on('close', () => {
130
+ try {
131
+ fs.removeSync(phpFile);
132
+ }
133
+ catch { }
134
+ });
135
+ }
136
+ },
137
+ };
138
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@artemsemkin/vite-plugin-wp-hmr",
3
+ "version": "1.1.1",
4
+ "description": "Vite plugin that generates a WordPress mu-plugin for HMR dev client injection",
5
+ "license": "MIT",
6
+ "author": "Artem Semkin",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/artkrsk/vite-plugin-wp-hmr.git"
10
+ },
11
+ "homepage": "https://github.com/artkrsk/vite-plugin-wp-hmr#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/artkrsk/vite-plugin-wp-hmr/issues"
14
+ },
15
+ "keywords": [
16
+ "vite",
17
+ "vite-plugin",
18
+ "wordpress",
19
+ "hmr",
20
+ "hot-module-replacement"
21
+ ],
22
+ "type": "module",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "default": "./dist/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.build.json",
37
+ "test": "vitest run",
38
+ "prepublishOnly": "pnpm build"
39
+ },
40
+ "peerDependencies": {
41
+ "vite": ">=5"
42
+ },
43
+ "dependencies": {
44
+ "fs-extra": "^11.3.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/fs-extra": "^11.0.4",
48
+ "@types/node": "^22.15.0",
49
+ "typescript": "^5.7.0",
50
+ "vite": "^7.0.0",
51
+ "vitest": "^3.1.0"
52
+ },
53
+ "pnpm": {
54
+ "onlyBuiltDependencies": [
55
+ "esbuild"
56
+ ]
57
+ },
58
+ "packageManager": "pnpm@10.30.0"
59
+ }