@roots/bud-client 6.6.8 → 6.6.10

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.
@@ -1,8 +1,6 @@
1
1
  /// <reference types="webpack-env" />
2
2
  /**
3
3
  * Initializes bud.js HMR handling
4
- *
5
- * @public
6
4
  */
7
5
  export declare const client: (queryString: string, webpackHot: __WebpackModuleApi.Hot) => Promise<boolean>;
8
6
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/hot/client.ts"],"names":[],"mappings":";AASA;;;;GAIG;AACH,eAAO,MAAM,MAAM,gBACJ,MAAM,cACP,mBAAmB,GAAG,qBA6InC,CAAA"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/hot/client.ts"],"names":[],"mappings":";AASA;;GAEG;AACH,eAAO,MAAM,MAAM,gBACJ,MAAM,cACP,mBAAmB,GAAG,qBA6InC,CAAA"}
package/lib/hot/client.js CHANGED
@@ -16,8 +16,6 @@ import { makeLogger } from './log.js';
16
16
  import * as clientOptions from './options.js';
17
17
  /**
18
18
  * Initializes bud.js HMR handling
19
- *
20
- * @public
21
19
  */
22
20
  export const client = (queryString, webpackHot) => __awaiter(void 0, void 0, void 0, function* () {
23
21
  /* Guard: EventSource browser support */
@@ -1,7 +1,5 @@
1
1
  /**
2
2
  * Component container
3
- *
4
- * @public
5
3
  */
6
4
  export declare class Component extends HTMLElement {
7
5
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"overlay.component.d.ts","sourceRoot":"","sources":["../../../../src/hot/components/overlay/overlay.component.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,WAAW;IACjC,IAAI,EAAE,MAAM,CAAgB;IAEnC;;;;OAIG;IACI,OAAO,EAAE,GAAG,CAAA;IAEZ,iBAAiB,EAAE,GAAG,CAAA;IAE7B,IAAW,OAAO,WAEjB;;IAOM,YAAY,IAAI,IAAI;IA+G3B,WAAkB,kBAAkB,aAEnC;IAEM,wBAAwB;IAsBxB,iBAAiB;CAGzB"}
1
+ {"version":3,"file":"overlay.component.d.ts","sourceRoot":"","sources":["../../../../src/hot/components/overlay/overlay.component.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,SAAU,SAAQ,WAAW;IACjC,IAAI,EAAE,MAAM,CAAgB;IAEnC;;;;OAIG;IACI,OAAO,EAAE,GAAG,CAAA;IAEZ,iBAAiB,EAAE,GAAG,CAAA;IAE7B,IAAW,OAAO,WAEjB;;IAOM,YAAY,IAAI,IAAI;IA+G3B,WAAkB,kBAAkB,aAEnC;IAEM,wBAAwB;IAsBxB,iBAAiB;CAGzB"}
@@ -1,7 +1,5 @@
1
1
  /**
2
2
  * Component container
3
- *
4
- * @public
5
3
  */
6
4
  export class Component extends HTMLElement {
7
5
  get message() {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@roots/bud-client",
3
3
  "description": "Client scripts for @roots/bud",
4
- "version": "6.6.8",
4
+ "version": "6.6.10",
5
5
  "homepage": "https://roots.io/bud",
6
6
  "repository": {
7
7
  "type": "git",
@@ -10,12 +10,14 @@
10
10
  },
11
11
  "contributors": [
12
12
  {
13
- "name": "kellymears",
13
+ "name": "Kelly Mears",
14
+ "email": "developers@tinypixel.dev",
14
15
  "url": "https://github.com/kellymears"
15
16
  },
16
17
  {
17
- "name": "QWp6t",
18
- "url": "https://github.com/QWp6t"
18
+ "name": "Ben Word",
19
+ "email": "ben@benword.com",
20
+ "url": "https://github.com/retlehs"
19
21
  }
20
22
  ],
21
23
  "license": "MIT",
@@ -34,7 +36,9 @@
34
36
  "node": ">=16"
35
37
  },
36
38
  "files": [
37
- "lib/"
39
+ "docs",
40
+ "lib",
41
+ "src"
38
42
  ],
39
43
  "main": "./lib/index.cjs",
40
44
  "module": "./lib/index.mjs",
@@ -52,10 +56,23 @@
52
56
  }
53
57
  },
54
58
  "devDependencies": {
59
+ "@roots/bud": "6.6.10",
55
60
  "@skypack/package-check": "0.2.2",
56
- "@types/node": "16.18.6",
61
+ "@types/node": "16.18.11",
57
62
  "@types/webpack-env": "1.18.0"
58
63
  },
64
+ "peerDependencies": {
65
+ "@roots/bud": "*",
66
+ "@types/webpack-env": "*"
67
+ },
68
+ "peerDependenciesMeta": {
69
+ "@roots/bud": {
70
+ "optional": true
71
+ },
72
+ "@types/webpack-env": {
73
+ "optional": true
74
+ }
75
+ },
59
76
  "volta": {
60
77
  "extends": "../../../package.json"
61
78
  }
@@ -0,0 +1,78 @@
1
+ /* eslint-disable no-console */
2
+ /**
3
+ * @vitest-environment jsdom
4
+ */
5
+ import {describe, expect, it, vi} from 'vitest'
6
+
7
+ import {client} from './client.js'
8
+ import {injectEvents} from './events.js'
9
+ import * as options from './options.js'
10
+
11
+ // @ts-ignore
12
+ global.EventSource = class Events {
13
+ public constructor() {}
14
+ }
15
+
16
+ window.EventSource = global.EventSource
17
+
18
+ const Events = injectEvents(global.EventSource)
19
+
20
+ // @ts-ignore
21
+ const webpackHotMock = {
22
+ hot: vi.fn(),
23
+ status: vi.fn(),
24
+ } as __WebpackModuleApi.Hot
25
+
26
+ describe(`@roots/bud-client`, () => {
27
+ it(`should be a fn module`, () => {
28
+ expect(client).toBeInstanceOf(Function)
29
+ })
30
+
31
+ it(`should add window.bud`, async () => {
32
+ await client(`?name=test`, webpackHotMock)
33
+ expect(window.bud).toBeDefined()
34
+ })
35
+
36
+ it(`should add window.bud.hmr as an instance of EventSource`, async () => {
37
+ await client(`?name=test`, webpackHotMock)
38
+ expect(window.bud?.hmr?.test).toBeInstanceOf(EventSource)
39
+ })
40
+
41
+ it(`should set clientOptions`, async () => {
42
+ await client(`?name=test`, webpackHotMock)
43
+ expect(options.data).toEqual(
44
+ expect.objectContaining({
45
+ debug: true,
46
+ indicator: true,
47
+ log: true,
48
+ name: `@roots/bud-client`,
49
+ overlay: true,
50
+ path: `/bud/hot`,
51
+ reload: true,
52
+ timeout: 2000,
53
+ }),
54
+ )
55
+ })
56
+
57
+ it(`should call listener from onmessage`, async () => {
58
+ await client(`?name=test`, webpackHotMock)
59
+ const events = Events.make(options.data)
60
+
61
+ const listenerMock = vi.fn(async () => {})
62
+ events.addListener(listenerMock)
63
+
64
+ await events.onmessage(
65
+ // @ts-ignore
66
+ {
67
+ data: `{
68
+ "data": {
69
+ "hash": "test",
70
+ "action": "update"
71
+ }
72
+ }`,
73
+ },
74
+ )
75
+
76
+ expect(listenerMock).toHaveBeenCalled()
77
+ })
78
+ })
@@ -0,0 +1,156 @@
1
+ /* eslint-disable no-console */
2
+ /* global __resourceQuery */
3
+ /* global __webpack_hash__ */
4
+
5
+ import * as components from './components/index.js'
6
+ import {injectEvents} from './events.js'
7
+ import {makeLogger} from './log.js'
8
+ import * as clientOptions from './options.js'
9
+
10
+ /**
11
+ * Initializes bud.js HMR handling
12
+ */
13
+ export const client = async (
14
+ queryString: string,
15
+ webpackHot: __WebpackModuleApi.Hot,
16
+ ) => {
17
+ /* Guard: EventSource browser support */
18
+ if (typeof window?.EventSource === `undefined`) {
19
+ console.error(
20
+ `[bud] hot module reload requires EventSource to work. https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events#Tools`,
21
+ )
22
+ return false
23
+ }
24
+ /* Guard: webpackHot api availability */
25
+ if (!webpackHot) {
26
+ console.error(
27
+ `[bud] hot module reload requires the webpack hot api to be available`,
28
+ )
29
+ return false
30
+ }
31
+
32
+ /* Set client options from URL params */
33
+ const options = clientOptions.setFromParameters(queryString)
34
+ /* Setup logger */
35
+ const logger = makeLogger(options)
36
+
37
+ if (typeof window.bud === `undefined`) {
38
+ window.bud = {
39
+ current: {},
40
+ hmr: {},
41
+ controllers: [],
42
+ listeners: {},
43
+ }
44
+ }
45
+
46
+ if (!window.bud.current[options.name]) {
47
+ window.bud.current[options.name] = null
48
+ }
49
+
50
+ const isStale = (hash?: string) => {
51
+ if (hash) window.bud.current[options.name] = hash
52
+ return __webpack_hash__ === window.bud.current[options.name]
53
+ }
54
+
55
+ /**
56
+ * Webpack HMR check handler
57
+ *
58
+ * @internal
59
+ */
60
+ const check = async () => {
61
+ if (webpackHot.status() === `idle`) {
62
+ await webpackHot.check(false)
63
+
64
+ requestAnimationFrame(async function whenReady() {
65
+ if (webpackHot.status() === `ready`) {
66
+ await update()
67
+ } else {
68
+ requestAnimationFrame(whenReady)
69
+ }
70
+ })
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Webpack HMR unaccepted module handler
76
+ *
77
+ * @internal
78
+ */
79
+ const onUnacceptedOrDeclined = (
80
+ info: __WebpackModuleApi.HotNotifierInfo,
81
+ ) => {
82
+ console.warn(`[${options.name}] ${info.type}`, info)
83
+ options.reload && window.location.reload()
84
+ }
85
+
86
+ /**
87
+ * Webpack HMR error handler
88
+ *
89
+ * @internal
90
+ */
91
+ const onErrored = (error: any) => {
92
+ window.bud.controllers.map(controller =>
93
+ controller?.update({
94
+ errors: [error],
95
+ }),
96
+ )
97
+ }
98
+
99
+ /**
100
+ * Webpack HMR update handler
101
+ *
102
+ * @internal
103
+ */
104
+ const update = async () => {
105
+ try {
106
+ await webpackHot.apply({
107
+ ignoreUnaccepted: true,
108
+ ignoreDeclined: true,
109
+ ignoreErrored: true,
110
+ onErrored,
111
+ onUnaccepted: onUnacceptedOrDeclined,
112
+ onDeclined: onUnacceptedOrDeclined,
113
+ })
114
+
115
+ if (!isStale()) await check()
116
+ } catch (error) {
117
+ logger.error(error)
118
+ }
119
+ }
120
+
121
+ /* Instantiate indicator, overlay */
122
+ await components.make(options)
123
+
124
+ /* Instantiate eventSource */
125
+ const events = injectEvents(EventSource).make(options)
126
+
127
+ if (!window.bud.listeners[options.name]) {
128
+ window.bud.listeners[options.name] = async payload => {
129
+ if (!payload) return
130
+
131
+ if (options.reload && payload.action === `reload`)
132
+ return window.location.reload()
133
+
134
+ if (payload.name !== options.name) return
135
+ window.bud.controllers.map(controller => controller?.update(payload))
136
+
137
+ if (payload.errors?.length > 0) return
138
+
139
+ if (payload.action === `built` || payload.action === `sync`) {
140
+ if (isStale(payload.hash)) return
141
+
142
+ if (payload.action === `built`) {
143
+ logger.log(`built in ${payload.time}ms`)
144
+ }
145
+
146
+ await check()
147
+ }
148
+ }
149
+
150
+ /*
151
+ * Instantiate HMR event source
152
+ * and register client listeners
153
+ */
154
+ events.addListener(window.bud.listeners[options.name])
155
+ }
156
+ }
@@ -0,0 +1,33 @@
1
+ interface ControllerModule {
2
+ make: () => Promise<Controller>
3
+ }
4
+
5
+ export const make: (
6
+ options: Options,
7
+ ) => Promise<Array<Controller>> = async options => {
8
+ if (options.indicator && !customElements.get(`bud-activity-indicator`)) {
9
+ await import(`./indicator/index.js`)
10
+ .then(makeController)
11
+ .then(maybePushController)
12
+ }
13
+
14
+ if (options.overlay && !customElements.get(`bud-error`)) {
15
+ await import(`./overlay/index.js`)
16
+ .then(makeController)
17
+ .then(maybePushController)
18
+ }
19
+
20
+ return window.bud.controllers
21
+ }
22
+
23
+ const makeController = async (
24
+ module: ControllerModule,
25
+ ): Promise<Controller | undefined> => {
26
+ if (!module) return
27
+ return await module.make()
28
+ }
29
+
30
+ const maybePushController = (controller: Controller | undefined) => {
31
+ if (!controller) return
32
+ window.bud.controllers.push(controller)
33
+ }
@@ -0,0 +1,11 @@
1
+ import {Component} from './indicator.component.js'
2
+ import {Controller} from './indicator.controller.js'
3
+
4
+ export const make = async (): Promise<{
5
+ update: (data: Payload) => void
6
+ }> => {
7
+ if (customElements.get(`bud-activity-indicator`)) return
8
+ customElements.define(`bud-activity-indicator`, Component)
9
+
10
+ return new Controller()
11
+ }
@@ -0,0 +1,216 @@
1
+ import {pulse} from './indicator.pulse.js'
2
+
3
+ /**
4
+ * Indicator web component
5
+ * @public
6
+ */
7
+ export class Component extends HTMLElement {
8
+ /**
9
+ * Has component rendered
10
+ * @public
11
+ */
12
+ public rendered: boolean
13
+
14
+ /**
15
+ * Component name
16
+ * @public
17
+ */
18
+ public name: string = `bud-activity-indicator`
19
+
20
+ /**
21
+ * Root div querySelector selector
22
+ * @public
23
+ */
24
+ public get selector() {
25
+ return `.${this.name}`
26
+ }
27
+
28
+ /**
29
+ * Timer
30
+ * @public
31
+ */
32
+ public hideTimeout: NodeJS.Timer
33
+
34
+ /**
35
+ * Get accessor: has errors
36
+ * @public
37
+ */
38
+ public get hasErrors(): boolean {
39
+ return this.getAttribute(`has-errors`) == `true`
40
+ }
41
+
42
+ /**
43
+ * Get accessor: has warnings
44
+ * @public
45
+ */
46
+ public get hasWarnings(): boolean {
47
+ return this.getAttribute(`has-warnings`) == `true`
48
+ }
49
+
50
+ /**
51
+ * Status indicator colors
52
+ * @public
53
+ */
54
+ public colors: Record<string, [number, number, number, number]> = {
55
+ success: [4, 120, 87, 1],
56
+ error: [220, 38, 38, 1],
57
+ warn: [252, 211, 77, 1],
58
+ pending: [59, 130, 246, 1],
59
+ }
60
+
61
+ /**
62
+ * Class constructor
63
+ * @public
64
+ */
65
+ public constructor() {
66
+ super()
67
+ this.renderShadow()
68
+ }
69
+
70
+ /**
71
+ * Render status indicator
72
+ * @public
73
+ */
74
+ public renderShadow() {
75
+ const container = document.createElement(`div`)
76
+ container.classList.add(this.name)
77
+ container.innerHTML = `
78
+ <style>
79
+ .bud-activity-indicator {
80
+ position: fixed;
81
+ width: 10px;
82
+ height: 10px;
83
+ left: 10px;
84
+ bottom: 10px;
85
+ z-index: 9999;
86
+ margin: 5px;
87
+ padding: 5px;
88
+ -webkit-transition:
89
+ all .6s ease-in-out,
90
+ transition:
91
+ all .6s ease-in-out;
92
+ animation-fill-mode: forwards;
93
+ pointer-events: none;
94
+ border-radius: 50%;
95
+ transform: scale(0);
96
+ opacity: 0;
97
+ }
98
+
99
+ .show {
100
+ opacity: 1;
101
+ background-color: rgba(255, 255, 255, 1);
102
+ transform: scale(1);
103
+ transition:
104
+ all .6s ease-in-out;
105
+ }
106
+
107
+ ${pulse(`success`, this.colors.success)}
108
+ ${pulse(`error`, this.colors.error)}
109
+ ${pulse(`warning`, this.colors.warn)}
110
+ ${pulse(`pending`, this.colors.pending)}
111
+
112
+ </style>
113
+ `
114
+
115
+ this.attachShadow({mode: `open`}).appendChild(container)
116
+ }
117
+
118
+ /**
119
+ * Show status indicator
120
+ * @public
121
+ */
122
+ public show() {
123
+ this.hideTimeout && clearTimeout(this.hideTimeout)
124
+ this.shadowRoot.querySelector(this.selector).classList.add(`show`)
125
+ }
126
+
127
+ /**
128
+ * Hide status indicator
129
+ */
130
+ public hide() {
131
+ this.hideTimeout = setTimeout(() => {
132
+ this.shadowRoot.querySelector(this.selector).classList.remove(`show`)
133
+ }, 2000)
134
+ }
135
+
136
+ /**
137
+ * Status is pending
138
+ * @public
139
+ */
140
+ public onPending() {
141
+ this.show()
142
+
143
+ this.shadowRoot
144
+ .querySelector(this.selector)
145
+ .classList.remove(`error`, `warning`, `success`)
146
+
147
+ this.shadowRoot.querySelector(this.selector).classList.add(`pending`)
148
+
149
+ this.hide()
150
+ }
151
+
152
+ /**
153
+ * Status is success
154
+ * @public
155
+ */
156
+ public onSuccess() {
157
+ this.show()
158
+
159
+ this.shadowRoot
160
+ .querySelector(this.selector)
161
+ .classList.remove(`error`, `warning`, `pending`)
162
+
163
+ this.shadowRoot.querySelector(this.selector).classList.add(`success`)
164
+
165
+ this.hide()
166
+ }
167
+
168
+ /**
169
+ * Status is error
170
+ * @public
171
+ */
172
+ public onError() {
173
+ this.show()
174
+
175
+ this.shadowRoot
176
+ .querySelector(this.selector)
177
+ .classList.remove(`warning`, `success`, `pending`)
178
+ this.shadowRoot.querySelector(this.selector).classList.add(`error`)
179
+ }
180
+
181
+ /**
182
+ * Status is warning
183
+ * @public
184
+ */
185
+ public onWarning() {
186
+ this.show()
187
+
188
+ this.shadowRoot
189
+ .querySelector(this.selector)
190
+ .classList.remove(`error`, `success`, `pending`)
191
+
192
+ this.shadowRoot.querySelector(this.selector).classList.add(`warning`)
193
+ }
194
+
195
+ public static get observedAttributes() {
196
+ return [`has-errors`, `has-warnings`, `action`]
197
+ }
198
+
199
+ public attributeChangedCallback() {
200
+ if (this.hasAttribute(`has-errors`)) return this.onError()
201
+ if (this.hasAttribute(`has-warnings`)) return this.onWarning()
202
+
203
+ if (
204
+ !this.hasAttribute(`has-errors`) &&
205
+ !this.hasAttribute(`has-warnings`) &&
206
+ this.getAttribute(`action`) === `built`
207
+ )
208
+ return this.onSuccess()
209
+
210
+ if (
211
+ this.getAttribute(`action`) == `building` ||
212
+ this.getAttribute(`action`) == `sync`
213
+ )
214
+ return this.onPending()
215
+ }
216
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Activity indicator controller
3
+ * @public
4
+ */
5
+ export class Controller {
6
+ /**
7
+ * DOM node
8
+ * @public
9
+ */
10
+ public node: HTMLElement
11
+
12
+ /**
13
+ * Active WHM payload
14
+ * @public
15
+ */
16
+ public payload = null
17
+
18
+ /**
19
+ * Timer handler
20
+ * @public
21
+ */
22
+ public timer: NodeJS.Timeout
23
+
24
+ /**
25
+ * Initialization
26
+ * @public
27
+ */
28
+ public constructor() {
29
+ this.node = document.createElement(`bud-activity-indicator`)
30
+ this.update = this.update.bind(this)
31
+ }
32
+
33
+ /**
34
+ * Append `bud-error` element to the DOM
35
+ *
36
+ * @public
37
+ */
38
+ public addNode() {
39
+ if (document.body.querySelector(`bud-activity-indicator`)) {
40
+ if (typeof this.timer.unref === `function`) this.timer.unref()
41
+ this.removeNode()
42
+ }
43
+
44
+ document.body?.appendChild(this.node)
45
+ this.timer = setTimeout(this.removeNode, 3000)
46
+ }
47
+
48
+ /**
49
+ * Remove `bud-error` element from the DOM (if present)
50
+ *
51
+ * @public
52
+ */
53
+ public removeNode() {
54
+ document.body.querySelector(`bud-activity-indicator`)?.remove()
55
+ }
56
+
57
+ /**
58
+ * Update activity indicator
59
+ * @public
60
+ */
61
+ public update(payload: Payload) {
62
+ this.node.toggleAttribute(
63
+ `has-errors`,
64
+ payload.errors?.length ? true : false,
65
+ )
66
+
67
+ this.node.toggleAttribute(
68
+ `has-warnings`,
69
+ payload.warnings?.length ? true : false,
70
+ )
71
+
72
+ this.node.setAttribute(`action`, payload.action)
73
+
74
+ this.addNode()
75
+ }
76
+ }
@@ -0,0 +1,43 @@
1
+ export interface pulse {
2
+ (name: string, color: [number, number, number]): string
3
+ }
4
+
5
+ /**
6
+ * CSS animation for reload indicator
7
+ * @public
8
+ */
9
+ export const pulse = (
10
+ name: string,
11
+ color: [number, number, number, number],
12
+ ): string => `
13
+ .${name} {
14
+ box-shadow: 0 0 0 0 rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3]});
15
+ animation: ${name}__pulse 2s infinite;
16
+ transition: all 0.4s ease-in-out;
17
+ }
18
+
19
+ .${name}:not(.show) {
20
+ background-color: rgba(${color[0]}, ${color[1]}, ${color[2]}, 0);
21
+ }
22
+
23
+ .${name}.show {
24
+ background-color: rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3]});
25
+ }
26
+
27
+ @keyframes ${name}__pulse {
28
+ 0% {
29
+ transform: scale(0.95);
30
+ box-shadow: 0 0 0 0 rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.7);
31
+ }
32
+
33
+ 70% {
34
+ transform: scale(1);
35
+ box-shadow: 0 0 0 10px rgba(${color[0]}, ${color[1]}, ${color[2]}, 0);
36
+ }
37
+
38
+ 100% {
39
+ transform: scale(0.95);
40
+ box-shadow: 0 0 0 0 rgba(${color[0]}, ${color[1]}, ${color[2]}, 0);
41
+ }
42
+ }
43
+ `
@@ -0,0 +1,12 @@
1
+ import {Component} from './overlay.component.js'
2
+ import {Controller} from './overlay.controller.js'
3
+
4
+ export const make = async (): Promise<{
5
+ update: (data: Payload) => void
6
+ }> => {
7
+ if (customElements.get(`bud-error`)) return
8
+
9
+ customElements.define(`bud-error`, Component)
10
+
11
+ return new Controller()
12
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Component container
3
+ */
4
+ export class Component extends HTMLElement {
5
+ public name: string = `bud-overlay`
6
+
7
+ /**
8
+ * WHM payload
9
+ *
10
+ * @public
11
+ */
12
+ public payload: any
13
+
14
+ public documentBodyStyle: any
15
+
16
+ public get message() {
17
+ return this.getAttribute(`message`)
18
+ }
19
+
20
+ public constructor() {
21
+ super()
22
+ this.renderShadow()
23
+ }
24
+
25
+ public renderShadow(): void {
26
+ const container = document.createElement(`div`)
27
+ container.classList.add(`overlay`)
28
+ container.innerHTML = `
29
+ <style>
30
+ .overlay {
31
+ width: 100vw;
32
+ backdrop-filter: blur(10px);
33
+ display: flex;
34
+ height: 100vh;
35
+ border-top: 2px solid transparent;
36
+ overflow-x: hidden;
37
+ overflow-y: scroll;
38
+ position: absolute;
39
+ top: -1000px;
40
+ left: 0;
41
+ right: 0;
42
+ bottom: 0;
43
+ opacity: 0;
44
+ transition: opacity 0.2s ease-in-out, border 0.4s ease-in-out;
45
+ justify-content: center;
46
+ }
47
+
48
+ .visible {
49
+ position: fixed;
50
+ top: 0;
51
+ z-index: 9998;
52
+ opacity: 1;
53
+ border-top: 5px solid red;
54
+ transition: opacity 0.2s ease-in-out, border 0.4s ease-in-out;
55
+ max-width: 100vw;
56
+ }
57
+
58
+ .messages {
59
+ background-color: white;
60
+ border-radius: 5px;
61
+ filter: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06));
62
+ display: flex;
63
+ align-self: center;
64
+ width: 800px;
65
+ max-width: 90vw;
66
+ margin-left: auto;
67
+ margin-right: auto;
68
+ flex-direction: column;
69
+ flex-wrap: wrap;
70
+ align-items: center;
71
+ align-content: center;
72
+ padding: 2rem 2rem 0rem 2rem;
73
+ }
74
+
75
+ .visible .messages > div {
76
+ position: relative;
77
+ top: 0;
78
+ opacity: 1;
79
+ transition: all: 0.2s ease-in-out;
80
+ }
81
+
82
+ .messages > div {
83
+ position: relative;
84
+ top: 20px;
85
+ opacity: 0;
86
+ transition: all: 0.2s ease-in-out;
87
+ align-items: center;
88
+ align-content: center;
89
+ color: rgba(0, 0, 0, 0.87);
90
+ flex-direction: column;
91
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
92
+ padding: 0rem 2rem 2rem 2rem;
93
+ width: 100%;
94
+ max-width:95vw;
95
+ }
96
+
97
+ .messages > div > pre {
98
+ font-weight: 300;
99
+ font-size: 0.8rem;
100
+ overflow-x: scroll;
101
+ }
102
+
103
+ pre {
104
+ background: #303030;
105
+ color: #f1f1f1;
106
+ padding: 10px 16px;
107
+ border-radius: 2px;
108
+ border-top: 4px solid #dd0303;
109
+ -moz-box-shadow: inset 0 0 10px #000;
110
+ box-shadow: inset 0 0 10px #000;
111
+ counter-reset: line;
112
+ }
113
+
114
+ pre span {
115
+ display: block;
116
+ line-height: 1.5rem;
117
+ }
118
+
119
+ pre span:before {
120
+ counter-increment: line;
121
+ content: counter(line);
122
+ display: inline-block;
123
+ border-right: 1px solid #ddd;
124
+ padding: 0 .5em;
125
+ margin-right: .5em;
126
+ color: #888;
127
+ width: 30px;
128
+ }
129
+ </style>
130
+ <div class="messages"></div>
131
+ `
132
+
133
+ this.attachShadow({mode: `open`}).appendChild(container)
134
+ }
135
+
136
+ public static get observedAttributes() {
137
+ return [`message`]
138
+ }
139
+
140
+ public attributeChangedCallback() {
141
+ if (!this.documentBodyStyle)
142
+ this.documentBodyStyle = document.body?.style
143
+
144
+ if (this.getAttribute(`message`)) {
145
+ document.body.style.overflow = `hidden`
146
+
147
+ this.shadowRoot.querySelector(`.overlay`).classList.add(`visible`)
148
+
149
+ this.shadowRoot.querySelector(`.messages`).innerHTML =
150
+ this.getAttribute(`message`)
151
+
152
+ return
153
+ }
154
+
155
+ if (this.documentBodyStyle?.overflow && document?.body?.style) {
156
+ document.body.style.overflow = this.documentBodyStyle.overflow
157
+ }
158
+
159
+ this.shadowRoot.querySelector(`.overlay`).classList.remove(`visible`)
160
+ }
161
+
162
+ public connectedCallback() {
163
+ if (document.body?.style) this.documentBodyStyle = document.body.style
164
+ }
165
+ }
@@ -0,0 +1,86 @@
1
+ const ansiPattern = [
2
+ `[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)`,
3
+ `(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))`,
4
+ ].join(`|`)
5
+
6
+ const stripAnsi = (body: string) =>
7
+ body?.replace?.(new RegExp(ansiPattern, `g`), ``) ?? body
8
+
9
+ /**
10
+ * Overlay controller
11
+ * @public
12
+ */
13
+ export class Controller {
14
+ /**
15
+ * Element
16
+ * @public
17
+ */
18
+ public element: HTMLElement
19
+
20
+ /**
21
+ * HMR update
22
+ * @public
23
+ */
24
+ public payload: Payload
25
+
26
+ /**
27
+ * Formatted error message
28
+ * @public
29
+ */
30
+ public get message(): string {
31
+ return this.payload.errors?.reduce((a, c) => {
32
+ const msg = c?.message ?? c?.error ?? c
33
+ if (!msg) return a
34
+ return `${a}
35
+ <div>
36
+ <pre>${stripAnsi(msg)}</pre>
37
+ </div>`
38
+ }, ``)
39
+ }
40
+
41
+ /**
42
+ * Class constructor
43
+ *
44
+ * @public
45
+ */
46
+ public constructor() {
47
+ this.update = this.update.bind(this)
48
+ this.element = document.createElement(`bud-error`)
49
+ }
50
+
51
+ /**
52
+ * Append `bud-error` element to the DOM
53
+ *
54
+ * @public
55
+ */
56
+ public createError() {
57
+ !document.body.querySelector(`bud-error`) &&
58
+ document.body?.appendChild(this.element)
59
+ }
60
+
61
+ /**
62
+ * Remove `bud-error` element from the DOM (if present)
63
+ *
64
+ * @public
65
+ */
66
+ public removeError() {
67
+ document.body.querySelector(`bud-error`)?.remove()
68
+ }
69
+
70
+ /**
71
+ * Update DOM
72
+ *
73
+ * @public
74
+ */
75
+ public update(payload: Payload): void {
76
+ this.payload = payload
77
+
78
+ this.element.setAttribute(`message`, this.message ?? ``)
79
+
80
+ if (this.payload.errors?.length > 0) {
81
+ return this.createError()
82
+ }
83
+
84
+ this.removeError()
85
+ }
86
+ }
@@ -0,0 +1,95 @@
1
+ /* eslint-disable no-console */
2
+
3
+ export const injectEvents = (
4
+ eventSource: new (path: string) => EventSource,
5
+ ) => {
6
+ /**
7
+ * EventSource wrapper
8
+ *
9
+ * @remarks
10
+ * wraps EventSource in a function to allow for
11
+ * mocking in tests
12
+ *
13
+ * @public
14
+ */
15
+ return class Events extends eventSource {
16
+ /**
17
+ * Registered listeners
18
+ *
19
+ * @public
20
+ */
21
+ public listeners: Set<Listener> = new Set<Listener>()
22
+
23
+ /**
24
+ * Class constructor
25
+ *
26
+ * @remarks
27
+ * Singleton interface, so this is private.
28
+ *
29
+ * @public
30
+ */
31
+ private constructor(
32
+ public options: Partial<Options> & {name: string; path: string},
33
+ ) {
34
+ super(options.path)
35
+
36
+ this.onopen = this.onopen.bind(this)
37
+ this.onmessage = this.onmessage.bind(this)
38
+ this.addListener = this.addListener.bind(this)
39
+ }
40
+
41
+ /**
42
+ * Singleton constructor
43
+ *
44
+ * @public
45
+ */
46
+ public static make(
47
+ options: Partial<Options> & {name: string; path: string},
48
+ ): Events {
49
+ if (typeof window.bud.hmr[options.name] === `undefined`)
50
+ Object.assign(window.bud.hmr, {
51
+ [options.name]: new Events(options),
52
+ })
53
+
54
+ return window.bud.hmr[options.name]
55
+ }
56
+
57
+ /**
58
+ * EventSource `onopen` handler
59
+ * @public
60
+ */
61
+ public override onopen = function () {}
62
+
63
+ /**
64
+ * EventSource `onmessage` handler
65
+ * @public
66
+ */
67
+ public override onmessage = async function (payload: MessageEvent) {
68
+ if (!payload?.data || payload.data == `\uD83D\uDC93`) {
69
+ return
70
+ }
71
+
72
+ try {
73
+ const data = JSON.parse(payload.data)
74
+ if (!data) return
75
+
76
+ await Promise.all(
77
+ [...this.listeners].map(async listener => {
78
+ return await listener(data)
79
+ }),
80
+ )
81
+ } catch (ex) {
82
+ console.warn(`Invalid HMR message: ${payload.data} \n ${ex}`)
83
+ }
84
+ }
85
+
86
+ /**
87
+ * EventSource `addMessageListener` handler
88
+ * @public
89
+ */
90
+ public addListener(listener: Listener): this {
91
+ this.listeners.add(listener)
92
+ return this
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,7 @@
1
+ /* eslint-disable no-console */
2
+ /* global __resourceQuery */
3
+ /* global module */
4
+
5
+ import {client} from './client.js'
6
+
7
+ client(__resourceQuery, module.hot)
@@ -0,0 +1,9 @@
1
+ /* eslint-disable no-console */
2
+ /* global __resourceQuery */
3
+ /* global module */
4
+
5
+ ;(async () =>
6
+ await import(`./client.js`).then(
7
+ async module =>
8
+ await module.client(__resourceQuery, import.meta.webpackHot),
9
+ ))()
package/src/hot/log.ts ADDED
@@ -0,0 +1,42 @@
1
+ /* eslint-disable no-console */
2
+
3
+ export const makeLogger = (options: Options) => {
4
+ return {
5
+ log: makeLog(options),
6
+ error: makeError(options),
7
+ warn: makeWarn(options),
8
+ info: makeInfo(options),
9
+ }
10
+ }
11
+
12
+ let lastLog: string = null
13
+ export const makeLog = options => {
14
+ return (...args) => {
15
+ if (options.log) {
16
+ if (lastLog === args.join(``)) return
17
+ lastLog = args.join(``)
18
+
19
+ console.log(`[${options.name}]`, ...args)
20
+ }
21
+ }
22
+ }
23
+
24
+ export const makeInfo = options => {
25
+ return (...args) => {
26
+ if (options.log) {
27
+ console.info(`[${options.name}]`, ...args)
28
+ }
29
+ }
30
+ }
31
+
32
+ export const makeError = options => {
33
+ return (...args) => {
34
+ console.error(`[${options.name}]`, ...args)
35
+ }
36
+ }
37
+
38
+ export const makeWarn = options => {
39
+ return (...args) => {
40
+ console.warn(`[${options.name}]`, ...args)
41
+ }
42
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Client options
3
+ * @public
4
+ */
5
+ let data: Options = {
6
+ timeout: 2000,
7
+ reload: true,
8
+ name: `@roots/bud-client`,
9
+ debug: true,
10
+ log: true,
11
+ indicator: true,
12
+ overlay: true,
13
+ path: `/bud/hot`,
14
+ }
15
+
16
+ /**
17
+ * Get client option
18
+ * @public
19
+ */
20
+ const get = (name?: string, key?: string) =>
21
+ key ? data[name][key] : data[name]
22
+
23
+ /**
24
+ * Set client data based on URL parameters
25
+ * @public
26
+ */
27
+ const setFromParameters = (query: string): Options => {
28
+ let parsedParams: Partial<Options> = {}
29
+
30
+ new window.URLSearchParams(query).forEach((value, key) => {
31
+ parsedParams[key] =
32
+ value === `true` ? true : value === `false` ? false : value
33
+ })
34
+
35
+ data[parsedParams.name] = {...data, ...parsedParams}
36
+
37
+ return data[parsedParams.name]
38
+ }
39
+
40
+ export {data, get, setFromParameters}
package/src/index.cts ADDED
@@ -0,0 +1,16 @@
1
+ // Copyright © Roots Software Foundation LLC
2
+ // Licensed under the MIT license.
3
+
4
+ /**
5
+ * `@roots/bud` client scripts
6
+ *
7
+ * You should not import this root module.
8
+ * Import the components from the submodules instead.
9
+ *
10
+ * @see https://bud.js.org
11
+ * @see https://github.com/roots/bud
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+
16
+ export {}
package/src/index.mts ADDED
@@ -0,0 +1,16 @@
1
+ // Copyright © Roots Software Foundation LLC
2
+ // Licensed under the MIT license.
3
+
4
+ /**
5
+ * `@roots/bud` client scripts
6
+ *
7
+ * You should not import this root module.
8
+ * Import the components from the submodules instead.
9
+ *
10
+ * @see https://bud.js.org
11
+ * @see https://github.com/roots/bud
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+
16
+ export {}
@@ -0,0 +1,33 @@
1
+ type Target = HTMLAnchorElement | HTMLLinkElement | HTMLFormElement
2
+ type ElementTuple = [HTMLCollectionOf<Target>, any]
3
+
4
+ const intercept = (...args: [string, string]) => {
5
+ args.every(arg => typeof arg === `string`) &&
6
+ setInterval(
7
+ () =>
8
+ [
9
+ [document.getElementsByTagName(`a`), `href`],
10
+ [document.getElementsByTagName(`link`), `href`],
11
+ ]
12
+ .map(
13
+ ([elements, attribute]: ElementTuple): [
14
+ Array<Target>,
15
+ any,
16
+ ] => [Array.from(elements), attribute],
17
+ )
18
+ .forEach(([elements, attribute]: [Array<Target>, any]) =>
19
+ elements
20
+ .filter(el => el.hasAttribute(attribute))
21
+ .filter(el => !el.hasAttribute(`__bud_processed`))
22
+ .filter(el => el.getAttribute(attribute).startsWith(args[0]))
23
+ .map(el => {
24
+ const value = el.getAttribute(attribute).replace(...args)
25
+ el.setAttribute(attribute, value)
26
+ el.toggleAttribute(`__bud_processed`)
27
+ }),
28
+ ),
29
+ 1000,
30
+ )
31
+ }
32
+
33
+ export default intercept
@@ -0,0 +1,18 @@
1
+ /* eslint-disable no-console */
2
+ /* global __resourceQuery */
3
+
4
+ import intercept from './index.js'
5
+
6
+ window.requestAnimationFrame(async function ready() {
7
+ if (!__resourceQuery) return
8
+
9
+ const params = new URLSearchParams(__resourceQuery)
10
+ if (!params || !params.has(`search`) || !params.has(`replace`)) return
11
+
12
+ const search = decodeURI(params.get(`search`))
13
+ const replace = decodeURI(params.get(`replace`))
14
+
15
+ return document.body
16
+ ? intercept(search, replace)
17
+ : window.requestAnimationFrame(ready)
18
+ })
@@ -0,0 +1,54 @@
1
+ declare var __resourceQuery: string
2
+ declare var __webpack_hash__: string
3
+
4
+ interface Listener {
5
+ (event: Payload): void
6
+ }
7
+
8
+ declare interface Events {
9
+ options: Partial<Options> & {name: string; path: string}
10
+ listeners: Set<Listener>
11
+ addListener(fn: Listener): this
12
+ onopen: (ev?: Event) => void
13
+ onmessage: (ev?: MessageEvent) => void
14
+ }
15
+
16
+ declare interface Payload {
17
+ name: string
18
+ type: `middleware` | __WebpackModuleApi.HotNotifierInfo[`type`]
19
+ action: 'reload' | 'sync' | 'building' | 'built'
20
+ hash?: string
21
+ time?: number
22
+ errors?: Array<Record<string, any>>
23
+ warnings?: Array<string>
24
+ message?: string
25
+ modules?: Record<string, Array<string>>
26
+ }
27
+
28
+ declare interface Controller {
29
+ update: (payload: Partial<Payload>) => void
30
+ }
31
+
32
+ declare interface Options {
33
+ timeout: number
34
+ reload: boolean
35
+ debug: boolean
36
+ log: boolean
37
+ name: string
38
+ path: string
39
+ indicator: boolean
40
+ overlay: boolean
41
+ }
42
+
43
+ declare var bud: {
44
+ current?: Record<string, string>
45
+ controllers?: Array<Controller>
46
+ hmr?: Record<string, Events & EventSource>
47
+ listeners?: Record<string, Listener>
48
+ }
49
+
50
+ declare module global {
51
+ interface Window {
52
+ bud: typeof bud
53
+ }
54
+ }