Skip to content

Commit 0b031af

Browse files
feat(bs-hrml) add webpack browser-sync-plugin and related package commands
1 parent 5acae56 commit 0b031af

File tree

4 files changed

+351
-0
lines changed

4 files changed

+351
-0
lines changed

config/plugins.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPl
1212
const WebpackImageSizesPlugin = require('./webpack-image-sizes-plugin')
1313
const WebpackThemeJsonPlugin = require('./webpack-theme-json-plugin')
1414
const SpriteHashPlugin = require('./webpack-sprite-hash-plugin')
15+
const WebpackBrowserSyncPlugin = require('./webpack-browser-sync-plugin')
1516

1617
module.exports = {
1718
get: function (mode) {
@@ -75,6 +76,7 @@ module.exports = {
7576
filename: '[name].css',
7677
})
7778
)
79+
plugins.push(...WebpackBrowserSyncPlugin.getPlugins(mode))
7880
}
7981

8082
return plugins
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
const fs = require('fs')
2+
const path = require('path')
3+
const chalk = require('chalk')
4+
const webpack = require('webpack')
5+
const BrowserSyncPlugin = require('browser-sync-webpack-plugin')
6+
7+
const logId = '[' + chalk.blue('WebpackBrowserSyncPlugin') + ']'
8+
9+
/**
10+
* Dev-only: BrowserSync + HMR
11+
*
12+
* To use BrowserSync (BS) or Hot Module Replacement (HMR), you must define the following environment variables.
13+
* Two modes are available:
14+
* - BROWSER_SYNC_APP_URL
15+
* - BROWSER_SYNC_APP_PORT
16+
* - BROWSER_SYNC_APP_IP
17+
*
18+
* "host" mode
19+
* Enables live reloading in your local environment by using the domain name to configure the proxy.
20+
*
21+
* "ip" mode
22+
* Uses an IP address for the proxy.
23+
* By entering your local IP, you can access and preview your environment from another device (such as a mobile phone) on the same network.
24+
*
25+
* Use yarn scripts to start the development server: `yarn start:host` | `yarn start:ip`.
26+
*/
27+
class WebpackBrowserSyncPlugin {
28+
/**
29+
* @param {'development'|'production'|string} mode Webpack mode from `plugins.get(mode)`.
30+
* @returns {import('webpack').WebpackPluginInstance[]} Empty in production.
31+
*/
32+
static getPlugins(mode) {
33+
if (mode === 'production') {
34+
return []
35+
}
36+
37+
const hotReload = this.resolveMode()
38+
39+
if (hotReload === '') {
40+
return []
41+
}
42+
43+
const env = this.getLocalAppEnv()
44+
const issues = this.collectBrowserSyncConfigIssues(hotReload, env)
45+
if (issues.missing.length > 0) {
46+
this.fatalConfig(hotReload, issues)
47+
}
48+
49+
const parsed = this.parseAppConnection(env)
50+
if (!parsed) {
51+
this.fatalConfig(hotReload, { missing: ['BROWSER_SYNC_APP_URL'] })
52+
}
53+
54+
const { appUrl, urlHadPort, wordPressPort, localAppPortStr } = parsed
55+
56+
const browserSyncPort = this.pickBrowserSyncPort(wordPressPort, localAppPortStr, urlHadPort)
57+
58+
let appHost = appUrl.replace(/^https?:\/\//i, '').replace(/:\d+$/, '')
59+
let appIp = String(env.BROWSER_SYNC_APP_IP || '')
60+
.trim()
61+
.replace(/:\d+$/, '')
62+
63+
/** @type {import('browser-sync').Options} */
64+
const browserSyncConfig = {
65+
injectCss: true,
66+
proxy: appUrl,
67+
port: browserSyncPort,
68+
files: ['**/*.php', 'dist/images/**/*', 'dist/icons/**/*', 'dist/fonts/**/*', 'dist/**/*.css', 'dist/**/*.js'],
69+
open: false,
70+
reloadDelay: 0,
71+
notify: true,
72+
injectNotification: true,
73+
}
74+
75+
if (hotReload === 'host') {
76+
Object.assign(browserSyncConfig, { host: appHost })
77+
}
78+
79+
if (hotReload === 'ip') {
80+
Object.assign(browserSyncConfig, { host: appIp })
81+
}
82+
83+
// eslint-disable-next-line no-console
84+
console.log(logId, 'BrowserSync enabled (' + chalk.green(hotReload) + '), proxy:', chalk.cyan(appUrl))
85+
// eslint-disable-next-line no-console
86+
console.log(
87+
logId,
88+
chalk.dim(
89+
'BS listen: ' +
90+
chalk.bold(':' + browserSyncPort) +
91+
' | WordPress port (proxy): ' +
92+
chalk.bold(String(wordPressPort)) +
93+
(urlHadPort
94+
? localAppPortStr
95+
? ' | BROWSER_SYNC_APP_PORT = BrowserSync port'
96+
: ' | BROWSER_SYNC_APP_PORT unset → BS default port'
97+
: ' | BROWSER_SYNC_APP_PORT = WordPress port (URL had no port)')
98+
)
99+
)
100+
// eslint-disable-next-line no-console
101+
console.log(logId, chalk.dim('Use BrowserSync Local / External URL below, not the WordPress URL directly.'))
102+
103+
return [
104+
new webpack.HotModuleReplacementPlugin(),
105+
new BrowserSyncPlugin(browserSyncConfig, {
106+
reload: false,
107+
}),
108+
]
109+
}
110+
111+
/**
112+
* @returns {null|{ appUrl: string, urlHadPort: boolean, wordPressPort: number, localAppPortStr: string }}
113+
*/
114+
static parseAppConnection(env) {
115+
let base = String(env.BROWSER_SYNC_APP_URL || '').trim()
116+
const localPort = String(env.BROWSER_SYNC_APP_PORT || '').trim()
117+
118+
if (!base) {
119+
return null
120+
}
121+
122+
if (!/^https?:\/\//i.test(base)) {
123+
base = 'http://' + base
124+
}
125+
126+
try {
127+
const url = new URL(base)
128+
const urlHadPort = Boolean(url.port)
129+
if (!urlHadPort && localPort) {
130+
url.port = localPort
131+
}
132+
const appUrl = url.toString().replace(/\/+$/, '')
133+
const u2 = new URL(appUrl)
134+
const wordPressPort = u2.port ? Number.parseInt(u2.port, 10) : u2.protocol === 'https:' ? 443 : 80
135+
136+
return {
137+
appUrl,
138+
urlHadPort,
139+
wordPressPort,
140+
localAppPortStr: localPort,
141+
}
142+
} catch {
143+
return null
144+
}
145+
}
146+
147+
/**
148+
* @param {number} wordPressPort
149+
* @param {string} localAppPortStr
150+
* @param {boolean} urlHadPort
151+
* @returns {number}
152+
*/
153+
static pickBrowserSyncPort(wordPressPort, localAppPortStr, urlHadPort) {
154+
const wp = wordPressPort
155+
const preferredRaw = Number.parseInt(String(localAppPortStr || '').trim(), 10)
156+
const hasPreferred = Number.isFinite(preferredRaw) && preferredRaw > 0
157+
158+
let candidate
159+
if (urlHadPort) {
160+
if (hasPreferred) {
161+
candidate = preferredRaw
162+
} else {
163+
candidate = 8080
164+
}
165+
} else {
166+
candidate = 8080
167+
}
168+
169+
let warned = false
170+
let safety = 0
171+
while (candidate === wp && safety < 10000) {
172+
if (!warned) {
173+
warned = true
174+
// eslint-disable-next-line no-console
175+
console.warn(
176+
logId,
177+
chalk.yellow('BrowserSync port would match WordPress port ' + wp + '. Using next available port.')
178+
)
179+
}
180+
candidate++
181+
if (candidate > 65535) {
182+
candidate = 3000
183+
}
184+
safety++
185+
}
186+
187+
return candidate
188+
}
189+
190+
/**
191+
* @returns {''|'host'|'ip'}
192+
*/
193+
static resolveMode() {
194+
const fromEnv = String(process.env.BROWSERSYNC_MODE || '')
195+
.trim()
196+
.toLowerCase()
197+
if (fromEnv === 'host' || fromEnv === 'ip') {
198+
return fromEnv
199+
}
200+
return ''
201+
}
202+
203+
static getLocalAppEnv() {
204+
const defaults = this.readWpEnvConfigDefaults()
205+
return {
206+
BROWSER_SYNC_APP_URL: process.env.BROWSER_SYNC_APP_URL ?? defaults.BROWSER_SYNC_APP_URL ?? '',
207+
BROWSER_SYNC_APP_PORT: process.env.BROWSER_SYNC_APP_PORT ?? defaults.BROWSER_SYNC_APP_PORT ?? '',
208+
BROWSER_SYNC_APP_IP: process.env.BROWSER_SYNC_APP_IP ?? defaults.BROWSER_SYNC_APP_IP ?? '',
209+
}
210+
}
211+
212+
/**
213+
* Reads `config` from `.wp-env.json` (project root). Env vars take precedence later.
214+
*
215+
* @returns {{ BROWSER_SYNC_APP_URL: string, BROWSER_SYNC_APP_PORT: string, BROWSER_SYNC_APP_IP: string }}
216+
*/
217+
static readWpEnvConfigDefaults() {
218+
const empty = { BROWSER_SYNC_APP_URL: '', BROWSER_SYNC_APP_PORT: '', BROWSER_SYNC_APP_IP: '' }
219+
const file = path.resolve(__dirname, '../.wp-env.json')
220+
221+
try {
222+
if (!fs.existsSync(file)) {
223+
return empty
224+
}
225+
const json = JSON.parse(fs.readFileSync(file, 'utf8'))
226+
const cfg = json.config
227+
228+
if (!cfg || typeof cfg !== 'object') {
229+
return empty
230+
}
231+
232+
return {
233+
BROWSER_SYNC_APP_URL: cfg.BROWSER_SYNC_APP_URL != null ? String(cfg.BROWSER_SYNC_APP_URL) : '',
234+
BROWSER_SYNC_APP_PORT: cfg.BROWSER_SYNC_APP_PORT != null ? String(cfg.BROWSER_SYNC_APP_PORT) : '',
235+
BROWSER_SYNC_APP_IP: cfg.BROWSER_SYNC_APP_IP != null ? String(cfg.BROWSER_SYNC_APP_IP) : '',
236+
}
237+
} catch {
238+
return empty
239+
}
240+
}
241+
242+
/**
243+
* @returns {{ missing: string[] }}
244+
*/
245+
static collectBrowserSyncConfigIssues(hotReload, env) {
246+
const missing = []
247+
248+
const urlRaw = String(env.BROWSER_SYNC_APP_URL || '').trim()
249+
if (!urlRaw) {
250+
missing.push('BROWSER_SYNC_APP_URL')
251+
} else {
252+
let base = urlRaw
253+
if (!/^https?:\/\//i.test(base)) {
254+
base = 'http://' + base
255+
}
256+
/** @type {URL|null} */
257+
let url = null
258+
try {
259+
url = new URL(base)
260+
} catch {
261+
missing.push('BROWSER_SYNC_APP_URL')
262+
}
263+
if (url) {
264+
const urlHadPort = Boolean(url.port)
265+
const portVar = String(env.BROWSER_SYNC_APP_PORT || '').trim()
266+
if (!urlHadPort && !portVar) {
267+
missing.push('BROWSER_SYNC_APP_PORT')
268+
}
269+
}
270+
}
271+
272+
if (hotReload === 'ip') {
273+
const ip = String(env.BROWSER_SYNC_APP_IP || '')
274+
.trim()
275+
.replace(/:\d+$/, '')
276+
if (!ip) {
277+
missing.push('BROWSER_SYNC_APP_IP')
278+
}
279+
}
280+
281+
return { missing }
282+
}
283+
284+
/**
285+
* First line: fatal summary. Second line: Missing vars only. then exit(1).
286+
*/
287+
static fatalConfig(hotReload, issues) {
288+
const head = `Cannot start BrowserSync with ${chalk.yellow('--' + hotReload)}.`
289+
// eslint-disable-next-line no-console
290+
console.error(`${logId} ${chalk.white.bold.bgRed(' ERROR ')} ${head}`)
291+
292+
const names = [...new Set(issues.missing || [])]
293+
if (names.length > 0) {
294+
// eslint-disable-next-line no-console
295+
console.error(
296+
`${logId} ${chalk.red('Missing:')} ` + names.map((name) => chalk.yellow.bold(name)).join(chalk.dim(', '))
297+
)
298+
}
299+
process.exit(1)
300+
}
301+
}
302+
303+
module.exports = WebpackBrowserSyncPlugin

config/webpack-start.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Dev server entry: `package.json` script `start` runs this so extra CLI flags reach webpack safely.
5+
* Strips `--host` / `--ip` (webpack-cli rejects them) and sets `BROWSERSYNC_MODE` for BrowserSync.
6+
* Examples: `yarn start`, `yarn start -- --host`, `yarn start -- --ip` (or `yarn start:host` / `yarn start:ip`).
7+
*/
8+
9+
const path = require('path')
10+
const { spawnSync } = require('child_process')
11+
12+
const root = path.resolve(__dirname, '..')
13+
const passed = process.argv.slice(2)
14+
15+
let browserSyncMode = ''
16+
const webpackExtraArgs = passed.filter((arg) => {
17+
if (arg === '--host') {
18+
browserSyncMode = 'host'
19+
return false
20+
}
21+
if (arg === '--ip') {
22+
browserSyncMode = 'ip'
23+
return false
24+
}
25+
return true
26+
})
27+
28+
const env = { ...process.env }
29+
if (browserSyncMode) {
30+
env.BROWSERSYNC_MODE = browserSyncMode
31+
}
32+
33+
const webpackCli = require.resolve('webpack-cli/bin/cli.js')
34+
const childArgs = [webpackCli, '--watch', '--config', 'config/webpack.dev.js', ...webpackExtraArgs]
35+
36+
const result = spawnSync(process.execPath, childArgs, {
37+
cwd: root,
38+
stdio: 'inherit',
39+
env,
40+
})
41+
42+
process.exit(result.status !== null && result.status !== undefined ? result.status : 1)

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
},
99
"scripts": {
1010
"start": "yarn webpack --watch --config config/webpack.dev.js",
11+
"start:host": "node config/webpack-start.js --host",
12+
"start:ip": "node config/webpack-start.js --ip",
1113
"build": "yarn webpack --config config/webpack.prod.js",
1214
"lint:css": "node_modules/.bin/stylelint \"src/scss/**/*.scss\"",
1315
"lint:js": "node_modules/.bin/eslint \"src/js/**/*.js\"",
@@ -27,6 +29,8 @@
2729
"@wordpress/dom-ready": "^3.17.0",
2830
"@wordpress/hooks": "^3.17.0",
2931
"@wordpress/stylelint-config": "^21.0.0",
32+
"browser-sync": "^3.0.4",
33+
"browser-sync-webpack-plugin": "^2.4.0",
3034
"clean-webpack-plugin": "^4.0.0-alpha.0",
3135
"concurrently": "^8.2.2",
3236
"css-loader": "^5.2.4",

0 commit comments

Comments
 (0)