@remix-run/node-hmr 0.0.0 → 0.1.0

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +306 -2
  3. package/dist/index.d.ts +128 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +107 -0
  6. package/dist/lib/browser-events.d.ts +99 -0
  7. package/dist/lib/browser-events.d.ts.map +1 -0
  8. package/dist/lib/browser-events.js +11 -0
  9. package/dist/lib/events.d.ts +29 -0
  10. package/dist/lib/events.d.ts.map +1 -0
  11. package/dist/lib/events.js +32 -0
  12. package/dist/lib/hmr-analysis.d.ts +17 -0
  13. package/dist/lib/hmr-analysis.d.ts.map +1 -0
  14. package/dist/lib/hmr-analysis.js +130 -0
  15. package/dist/lib/module-store.d.ts +27 -0
  16. package/dist/lib/module-store.d.ts.map +1 -0
  17. package/dist/lib/module-store.js +161 -0
  18. package/dist/lib/process-state.d.ts +3 -0
  19. package/dist/lib/process-state.d.ts.map +1 -0
  20. package/dist/lib/process-state.js +7 -0
  21. package/dist/lib/runner.d.ts +62 -0
  22. package/dist/lib/runner.d.ts.map +1 -0
  23. package/dist/lib/runner.js +1046 -0
  24. package/dist/lib/runtime-api.d.ts +7 -0
  25. package/dist/lib/runtime-api.d.ts.map +1 -0
  26. package/dist/lib/runtime-api.js +1 -0
  27. package/dist/lib/runtime.d.ts +46 -0
  28. package/dist/lib/runtime.d.ts.map +1 -0
  29. package/dist/lib/runtime.js +374 -0
  30. package/dist/register.d.ts +2 -0
  31. package/dist/register.d.ts.map +1 -0
  32. package/dist/register.js +317 -0
  33. package/dist/runtime.d.ts +26 -0
  34. package/dist/runtime.d.ts.map +1 -0
  35. package/dist/runtime.js +32 -0
  36. package/dist/runtime.node-hmr.d.ts +27 -0
  37. package/dist/runtime.node-hmr.d.ts.map +1 -0
  38. package/dist/runtime.node-hmr.js +33 -0
  39. package/dist/types.d.ts +36 -0
  40. package/package.json +55 -5
  41. package/src/index.ts +244 -0
  42. package/src/lib/browser-events.ts +123 -0
  43. package/src/lib/events.ts +61 -0
  44. package/src/lib/hmr-analysis.ts +178 -0
  45. package/src/lib/module-store.ts +228 -0
  46. package/src/lib/process-state.ts +9 -0
  47. package/src/lib/runner.ts +1427 -0
  48. package/src/lib/runtime-api.ts +9 -0
  49. package/src/lib/runtime.ts +534 -0
  50. package/src/register.ts +401 -0
  51. package/src/runtime.node-hmr.ts +40 -0
  52. package/src/runtime.ts +40 -0
  53. package/src/types.d.ts +36 -0
package/src/index.ts ADDED
@@ -0,0 +1,244 @@
1
+ import { fileURLToPath } from 'node:url'
2
+ import process from 'node:process'
3
+
4
+ import { createHmrSupervisor } from './lib/runner.ts'
5
+
6
+ /**
7
+ * Options for running a Node.js entry module with HMR supervision.
8
+ */
9
+ export interface RunOptions {
10
+ /**
11
+ * Configures the parent-owned EventSource server used to coordinate browser HMR, or disables it
12
+ * with `false`. Enabled with default options when omitted or set to `true`.
13
+ */
14
+ browserHmrChannel?: boolean | BrowserHmrChannelOptions
15
+ /** Working directory used to resolve the entry path and relative watch options. (`process.cwd()`) */
16
+ cwd?: string
17
+ /** Arguments passed to the entry module after the entry path. */
18
+ entryArgs?: readonly string[]
19
+ /** Complete environment for the child process. (`process.env`) */
20
+ env?: NodeJS.ProcessEnv
21
+ /** Node.js arguments passed before the entry path. */
22
+ nodeArgs?: readonly string[]
23
+ /** File watching options for the supervised process. */
24
+ watch?: NodeHmrWatchOptions
25
+ }
26
+
27
+ /**
28
+ * Browser HMR event stream options hosted by the parent process.
29
+ */
30
+ export interface BrowserHmrChannelOptions {
31
+ /** Hostname for the browser HMR event server. (`'127.0.0.1'`) */
32
+ host?: string
33
+ /** Port for the browser HMR event server. Uses an available ephemeral port when omitted. */
34
+ port?: number
35
+ /** URL pathname for the browser HMR event stream. (`'/hmr'`) */
36
+ pathname?: string
37
+ }
38
+
39
+ /**
40
+ * File watching options for a Node HMR runner.
41
+ */
42
+ export interface NodeHmrWatchOptions {
43
+ /**
44
+ * Ignore matching glob patterns or file paths. Relative values are resolved
45
+ * from the runner's `cwd`.
46
+ */
47
+ ignore?: readonly string[]
48
+ /**
49
+ * Use polling instead of native filesystem events. Defaults to `true` on
50
+ * Windows and `false` elsewhere.
51
+ */
52
+ poll?: boolean
53
+ /**
54
+ * Polling interval in milliseconds when `poll` is enabled. Defaults to `100`.
55
+ */
56
+ pollInterval?: number
57
+ }
58
+
59
+ /**
60
+ * Handle returned by {@link run} for controlling the supervised process.
61
+ */
62
+ export interface NodeHmrRunner {
63
+ /**
64
+ * Stops the runner and waits for the child process to exit.
65
+ *
66
+ * @returns A promise that resolves once the runner has stopped.
67
+ */
68
+ close(): Promise<void>
69
+ /**
70
+ * Current server generation, incremented after every accepted hot update or process restart.
71
+ */
72
+ readonly generation: number
73
+ /**
74
+ * Waits until the latest update or restart has settled and the current child process is ready.
75
+ *
76
+ * If the app uses `emitServerReady()`, restart readiness also waits for that signal.
77
+ *
78
+ * @returns A promise that resolves when the latest requested generation is ready.
79
+ */
80
+ ready(): Promise<void>
81
+ }
82
+
83
+ type HmrReadyFetchRetryContext = {
84
+ /** Child process lifecycle generation that handled the fetch attempt. */
85
+ generation: number
86
+ /** Request passed to the wrapped fetch handler. */
87
+ request: Request
88
+ } & (
89
+ | {
90
+ /** Error thrown by the wrapped fetch handler. */
91
+ error: unknown
92
+ /** Response is absent when the wrapped fetch handler throws. */
93
+ response?: never
94
+ }
95
+ | {
96
+ /** Error is absent when the wrapped fetch handler returns a response. */
97
+ error?: never
98
+ /** Response returned by the wrapped fetch handler. */
99
+ response: Response
100
+ }
101
+ )
102
+
103
+ /**
104
+ * Options for {@link createHmrReadyFetch}.
105
+ */
106
+ export interface HmrReadyFetchOptions {
107
+ /**
108
+ * Determines whether a response or thrown error should be retried if the
109
+ * runner moves to a new generation while the request is in flight. Defaults
110
+ * to retrying `GET` and `HEAD` requests when the fetch throws or returns
111
+ * `502`, `503`, or `504`.
112
+ */
113
+ shouldRetry?: (context: HmrReadyFetchRetryContext) => boolean | Promise<boolean>
114
+ }
115
+
116
+ /**
117
+ * Wraps a fetch handler so requests wait for the current HMR generation to be ready.
118
+ *
119
+ * If the wrapped fetch handler returns a retryable response or throws a retryable error, the
120
+ * request is attempted again only when the runner moved to a new generation while the request was
121
+ * in flight.
122
+ *
123
+ * @param runner HMR runner that controls server readiness.
124
+ * @param fetch Fetch handler to call once the runner is ready.
125
+ * @param options Retry behavior for responses and thrown errors.
126
+ * @returns A fetch handler that waits for HMR readiness before forwarding requests.
127
+ */
128
+ export function createHmrReadyFetch(
129
+ runner: NodeHmrRunner,
130
+ fetch: (request: Request) => Response | Promise<Response>,
131
+ options: HmrReadyFetchOptions = {},
132
+ ): (request: Request) => Promise<Response> {
133
+ let shouldRetry = options.shouldRetry ?? shouldRetrySafeUnavailableRequest
134
+
135
+ return async (request) => {
136
+ while (true) {
137
+ await runner.ready()
138
+ let generation = runner.generation
139
+
140
+ try {
141
+ let response = await fetch(request)
142
+
143
+ if (!(await shouldRetry({ generation, request, response }))) {
144
+ return response
145
+ }
146
+
147
+ await runner.ready()
148
+ if (runner.generation !== generation) continue
149
+ return response
150
+ } catch (error) {
151
+ await runner.ready()
152
+ if (
153
+ runner.generation !== generation &&
154
+ (await shouldRetry({ error, generation, request }))
155
+ ) {
156
+ continue
157
+ }
158
+ throw error
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Starts a Node.js entry module in a supervised child process and watches its loaded module graph.
166
+ *
167
+ * Accepted module changes are applied in place; unaccepted changes restart the child. The returned
168
+ * handle exposes readiness across both paths and closes the watcher, child process, and browser HMR
169
+ * event server when stopped.
170
+ *
171
+ * @param entry Entry module path, resolved from `options.cwd`.
172
+ * @param options Runner options.
173
+ * @returns A runner handle for the supervised process.
174
+ */
175
+ export function run(entry: string, options: RunOptions = {}): NodeHmrRunner {
176
+ let supervisor = createHmrSupervisor({
177
+ browserHmrChannel: normalizeBrowserHmrChannelOptions(options.browserHmrChannel),
178
+ cwd: options.cwd ?? process.cwd(),
179
+ entry,
180
+ entryArgs: [...(options.entryArgs ?? [])],
181
+ env: options.env ?? process.env,
182
+ nodeArgs: [...(options.nodeArgs ?? [])],
183
+ registerPath: resolveRegisterPath(),
184
+ watch: options.watch,
185
+ })
186
+
187
+ let closed = supervisor.start()
188
+ closed.catch((error: unknown) => {
189
+ console.error(error)
190
+ })
191
+
192
+ return {
193
+ close() {
194
+ return supervisor.stop()
195
+ },
196
+
197
+ get generation() {
198
+ return supervisor.generation
199
+ },
200
+
201
+ ready() {
202
+ return supervisor.ready()
203
+ },
204
+ }
205
+ }
206
+
207
+ function shouldRetrySafeUnavailableRequest({
208
+ request,
209
+ response,
210
+ }: HmrReadyFetchRetryContext): boolean {
211
+ if (request.method !== 'GET' && request.method !== 'HEAD') return false
212
+
213
+ return (
214
+ response === undefined ||
215
+ response.status === 502 ||
216
+ response.status === 503 ||
217
+ response.status === 504
218
+ )
219
+ }
220
+
221
+ function normalizeBrowserHmrChannelOptions(
222
+ options: RunOptions['browserHmrChannel'],
223
+ ): BrowserHmrChannelOptions | null {
224
+ if (options === false) return null
225
+ if (options === undefined || options === true) return {}
226
+
227
+ if (options.port !== undefined) {
228
+ assertValidPort(options.port)
229
+ }
230
+
231
+ return options
232
+ }
233
+
234
+ function resolveRegisterPath(): string {
235
+ let extension = import.meta.url.endsWith('.ts') ? 'ts' : 'js'
236
+
237
+ return fileURLToPath(new URL(`./register.${extension}`, import.meta.url))
238
+ }
239
+
240
+ function assertValidPort(port: number): void {
241
+ if (!Number.isInteger(port) || port < 0 || port > 65_535) {
242
+ throw new TypeError(`Invalid browser HMR channel port: ${port}`)
243
+ }
244
+ }
@@ -0,0 +1,123 @@
1
+ import process from 'node:process'
2
+
3
+ import { hasNodeHmrParentProcess } from './process-state.ts'
4
+
5
+ export const defaultBrowserHmrPathname = '/hmr'
6
+
7
+ /**
8
+ * Event payload sent to browser HMR clients.
9
+ */
10
+ export interface HmrEventPayload {
11
+ /** Event type string consumed by browser HMR clients. */
12
+ type: string
13
+ [key: string]: unknown
14
+ }
15
+
16
+ /** JavaScript or CSS module update sent to a browser HMR client. */
17
+ export type HmrBrowserUpdate =
18
+ | {
19
+ /** Importing module whose dependency-accept handler accepts this update. */
20
+ acceptedPath?: string
21
+ /** Public URL of the changed JavaScript module. */
22
+ path: string
23
+ /** Identifies a JavaScript module update. */
24
+ type: 'js'
25
+ }
26
+ | {
27
+ /** Public URL of the changed stylesheet. */
28
+ path: string
29
+ /** Identifies a stylesheet update. */
30
+ type: 'css'
31
+ }
32
+
33
+ /**
34
+ * Browser HMR event emitted to connected clients.
35
+ */
36
+ export type BrowserHmrEvent =
37
+ | {
38
+ /** Absolute source file paths that triggered this update. */
39
+ files?: string[]
40
+ /** Update timestamp used to bust module and stylesheet caches. */
41
+ timestamp: number
42
+ /** Browser update event. */
43
+ type: 'update'
44
+ /** JavaScript and CSS updates for the browser to apply. */
45
+ updates: HmrBrowserUpdate[]
46
+ }
47
+ | {
48
+ /** Absolute source file paths that could not be handled in place. */
49
+ files?: string[]
50
+ /** Browser reload event. */
51
+ type: 'reload'
52
+ }
53
+
54
+ /**
55
+ * File watcher event reported to a browser HMR channel.
56
+ */
57
+ export type BrowserHmrFileEvent = {
58
+ /** Filesystem operation observed by the parent watcher. */
59
+ event: 'add' | 'change' | 'unlink'
60
+ /** Absolute path of the source file that changed. */
61
+ filePath: string
62
+ }
63
+
64
+ /**
65
+ * Handles file events and returns browser HMR events to emit.
66
+ *
67
+ * @param events File additions, changes, and removals reported together by the parent watcher.
68
+ * @returns Browser events to publish in their returned order.
69
+ */
70
+ export type BrowserHmrFileEventHandler = (
71
+ events: readonly BrowserHmrFileEvent[],
72
+ ) => Promise<readonly BrowserHmrEvent[]>
73
+
74
+ /**
75
+ * Watched file delta for a browser HMR channel.
76
+ */
77
+ export interface BrowserHmrWatchedFileDelta {
78
+ /** Absolute source file paths newly required by this channel. */
79
+ add: readonly string[]
80
+ /** Absolute source file paths no longer required by this channel. */
81
+ remove: readonly string[]
82
+ }
83
+
84
+ /**
85
+ * Child-process bridge between browser asset tooling and the parent `node-hmr` runtime.
86
+ *
87
+ * The channel contributes files to the parent's shared watcher and converts matching file changes
88
+ * into browser update or reload events. Close it when its owning asset server shuts down.
89
+ */
90
+ export interface BrowserHmrChannel {
91
+ /** Absolute URL of the parent-owned EventSource endpoint for browser HMR clients. */
92
+ readonly url: string
93
+ /** Closes this channel, unregisters its handlers, and removes its files from the parent watcher. */
94
+ close(): void
95
+ /**
96
+ * Registers a handler that converts matching watcher events into events for browser clients.
97
+ *
98
+ * Multiple handlers may be registered; their returned browser events are concatenated. Calling
99
+ * the returned cleanup function stops invoking this handler without closing the channel.
100
+ *
101
+ * @param handler Callback that maps a batch of file changes to browser HMR events.
102
+ * @returns A cleanup function that unregisters only this handler.
103
+ */
104
+ onFileEvents(handler: BrowserHmrFileEventHandler): () => void
105
+ /**
106
+ * Adds and removes absolute file paths from the parent process's watcher for this channel.
107
+ *
108
+ * Paths remain watched until removed by a later delta or until the channel is closed. Repeated
109
+ * additions and removals are idempotent.
110
+ *
111
+ * @param delta Files to add and remove from the watcher.
112
+ */
113
+ updateWatchedFiles(delta: BrowserHmrWatchedFileDelta): void
114
+ }
115
+
116
+ export function sendHmrEventPayload(payload: HmrEventPayload): void {
117
+ if (!hasNodeHmrParentProcess()) return
118
+
119
+ process.send?.({
120
+ payload,
121
+ type: 'node-hmr:child:browser-event-emitted',
122
+ })
123
+ }
@@ -0,0 +1,61 @@
1
+ import process from 'node:process'
2
+
3
+ import { hasNodeHmrParentProcess } from './process-state.ts'
4
+
5
+ export type ServerHmrEvent =
6
+ | {
7
+ acceptedUrl?: string
8
+ filePath: string
9
+ timestamp: number
10
+ type: 'update'
11
+ url: string
12
+ }
13
+ | {
14
+ filePath?: string
15
+ reason?: string
16
+ timestamp: number
17
+ type: 'restart'
18
+ url?: string
19
+ }
20
+
21
+ export interface ServerHmrEventSource {
22
+ subscribe(listener: (event: ServerHmrEvent) => void): () => void
23
+ }
24
+
25
+ type ServerHmrEventListener = (event: ServerHmrEvent) => void
26
+
27
+ class ServerHmrEvents implements ServerHmrEventSource {
28
+ #listeners = new Set<ServerHmrEventListener>()
29
+
30
+ subscribe(listener: ServerHmrEventListener): () => void {
31
+ this.#listeners.add(listener)
32
+ return () => {
33
+ this.#listeners.delete(listener)
34
+ }
35
+ }
36
+
37
+ emit(event: ServerHmrEvent): void {
38
+ for (let listener of this.#listeners) {
39
+ listener(event)
40
+ }
41
+ }
42
+ }
43
+
44
+ export const serverHmrEvents = new ServerHmrEvents()
45
+
46
+ export function emitServerHmrEvent(event: ServerHmrEvent): void {
47
+ serverHmrEvents.emit(event)
48
+ }
49
+
50
+ export function emitServerHmrUpdate(event: Extract<ServerHmrEvent, { type: 'update' }>): void {
51
+ emitServerHmrEvent(event)
52
+ if (!hasNodeHmrParentProcess()) return
53
+
54
+ process.send?.({
55
+ acceptedUrl: event.acceptedUrl,
56
+ filePath: event.filePath,
57
+ timestamp: event.timestamp,
58
+ type: 'node-hmr:child:hot-module-updated',
59
+ url: event.url,
60
+ })
61
+ }
@@ -0,0 +1,178 @@
1
+ import { parseSync, visitorKeys } from 'oxc-parser'
2
+ import type { Node, Program } from 'oxc-parser'
3
+
4
+ export interface NodeHmrAnalysis {
5
+ acceptedDeps: NodeHmrAcceptedDependency[]
6
+ selfAccepting: boolean
7
+ usesImportMetaHot: boolean
8
+ }
9
+
10
+ export interface ResolvedNodeHmrAnalysis {
11
+ acceptedDeps: string[]
12
+ selfAccepting: boolean
13
+ usesImportMetaHot: boolean
14
+ }
15
+
16
+ export interface NodeHmrAcceptedDependency {
17
+ end: number
18
+ specifier: string
19
+ start: number
20
+ }
21
+
22
+ const invalidAcceptMessage =
23
+ 'import.meta.hot.accept() can only accept a callback, a string literal, or an array of string literals.'
24
+
25
+ export function analyzeNodeHmrSource(importerUrl: string, source: string): NodeHmrAnalysis {
26
+ if (!source.includes('import.meta.hot')) {
27
+ return {
28
+ acceptedDeps: [],
29
+ selfAccepting: false,
30
+ usesImportMetaHot: false,
31
+ }
32
+ }
33
+
34
+ let acceptedDeps: NodeHmrAcceptedDependency[] = []
35
+ let selfAccepting = false
36
+ let usesImportMetaHot = false
37
+ let parseResult = parseSync('node-hmr-analysis.js', source, {
38
+ lang: 'js',
39
+ sourceType: 'module',
40
+ })
41
+
42
+ if (parseResult.errors.length > 0) {
43
+ return {
44
+ acceptedDeps: [],
45
+ selfAccepting: false,
46
+ usesImportMetaHot: false,
47
+ }
48
+ }
49
+
50
+ walkAst(parseResult.program, (node) => {
51
+ if (isImportMetaHotNode(node)) {
52
+ usesImportMetaHot = true
53
+ }
54
+
55
+ if (node.type !== 'CallExpression') return
56
+ if (!isImportMetaHotAcceptCallee(node.callee)) return
57
+
58
+ let [firstArgument] = node.arguments
59
+ if (firstArgument === undefined || isSelfAcceptArgument(firstArgument)) {
60
+ selfAccepting = true
61
+ return
62
+ }
63
+
64
+ let deps = getAcceptedDependencies(firstArgument)
65
+ if (deps === null) {
66
+ throw new TypeError(invalidAcceptMessage)
67
+ }
68
+
69
+ acceptedDeps.push(...deps)
70
+ })
71
+
72
+ return {
73
+ acceptedDeps,
74
+ selfAccepting,
75
+ usesImportMetaHot,
76
+ }
77
+ }
78
+
79
+ function isImportMetaHotAcceptCallee(node: Node): boolean {
80
+ let callee = unwrapChainExpression(node)
81
+ if (callee.type !== 'MemberExpression') return false
82
+ if (callee.computed || !isIdentifierNode(callee.property, 'accept')) return false
83
+
84
+ return isImportMetaHotNode(callee.object)
85
+ }
86
+
87
+ function isImportMetaHotNode(node: Node): boolean {
88
+ let hot = unwrapChainExpression(node)
89
+ if (hot.type !== 'MemberExpression') return false
90
+ if (hot.computed || !isIdentifierNode(hot.property, 'hot')) return false
91
+
92
+ let meta = unwrapChainExpression(hot.object)
93
+ return (
94
+ meta.type === 'MetaProperty' &&
95
+ isIdentifierNode(meta.meta, 'import') &&
96
+ isIdentifierNode(meta.property, 'meta')
97
+ )
98
+ }
99
+
100
+ function unwrapChainExpression(node: Node): Node {
101
+ return node.type === 'ChainExpression' ? node.expression : node
102
+ }
103
+
104
+ function isIdentifierNode(node: Node, name: string): boolean {
105
+ return node.type === 'Identifier' && node.name === name
106
+ }
107
+
108
+ function isSelfAcceptArgument(node: Node): boolean {
109
+ return (
110
+ node.type === 'FunctionExpression' ||
111
+ node.type === 'ArrowFunctionExpression' ||
112
+ isIdentifierNode(node, 'undefined')
113
+ )
114
+ }
115
+
116
+ function getAcceptedDependencies(node: Node): NodeHmrAcceptedDependency[] | null {
117
+ if (isStringLiteralNode(node)) {
118
+ return [toAcceptedDependency(node)]
119
+ }
120
+
121
+ if (node.type !== 'ArrayExpression') return null
122
+
123
+ let deps: NodeHmrAcceptedDependency[] = []
124
+ for (let element of node.elements) {
125
+ if (!isStringLiteralNode(element)) return null
126
+ deps.push(toAcceptedDependency(element))
127
+ }
128
+ return deps
129
+ }
130
+
131
+ function toAcceptedDependency(node: Node & { end: number; start: number; value: string }) {
132
+ return {
133
+ end: node.end - 1,
134
+ specifier: node.value,
135
+ start: node.start + 1,
136
+ }
137
+ }
138
+
139
+ function walkAst(node: Program | Node, visit: (node: Program | Node) => void): void {
140
+ visit(node)
141
+
142
+ let keys = visitorKeys[node.type]
143
+ if (!keys) return
144
+
145
+ let walkableNode = node as unknown as Record<string, unknown>
146
+ for (let key of keys) {
147
+ let value = walkableNode[key]
148
+ if (Array.isArray(value)) {
149
+ for (let child of value) {
150
+ if (isAstNode(child)) {
151
+ walkAst(child, visit)
152
+ }
153
+ }
154
+ continue
155
+ }
156
+
157
+ if (isAstNode(value)) {
158
+ walkAst(value, visit)
159
+ }
160
+ }
161
+ }
162
+
163
+ function isAstNode(value: unknown): value is Node {
164
+ return typeof value === 'object' && value !== null && 'type' in value
165
+ }
166
+
167
+ function isStringLiteralNode(node: Node | null | undefined): node is Node & {
168
+ end: number
169
+ start: number
170
+ value: string
171
+ } {
172
+ return (
173
+ node?.type === 'Literal' &&
174
+ typeof node.end === 'number' &&
175
+ typeof node.start === 'number' &&
176
+ typeof node.value === 'string'
177
+ )
178
+ }