@sdeverywhere/plugin-check 0.3.20 → 0.3.21

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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { Bundle, ConfigInitOptions, SuiteSummary } from '@sdeverywhere/check-core'
4
4
 
5
+ import type { BundleLocation, BundleSpec } from '@sdeverywhere/check-ui-shell'
5
6
  import { initAppShell } from '@sdeverywhere/check-ui-shell'
6
7
  import '@sdeverywhere/check-ui-shell/dist/style.css'
7
8
 
@@ -14,64 +15,39 @@ import { createBundle as createBaselineBundle } from '@_baseline_bundle_'
14
15
  import { createBundle as createCurrentBundle } from '@_current_bundle_'
15
16
  import { getConfigOptions } from '@_test_config_'
16
17
 
17
- function loadBundleName(key: string): string | undefined {
18
+ interface BundleMetadata {
19
+ name: string
20
+ url: string
21
+ }
22
+
23
+ function loadBundleMetadata(side: 'left' | 'right'): BundleMetadata | undefined {
18
24
  if (import.meta.hot) {
19
- return localStorage.getItem(`sde-check-selected-bundle-${key}`)
20
- } else {
21
- return undefined
25
+ const metadataJson = localStorage.getItem(`sde-check-selected-bundle-${side}`)
26
+ if (metadataJson) {
27
+ const parsed = JSON.parse(metadataJson)
28
+ if (parsed.name && parsed.url) {
29
+ return parsed as BundleMetadata
30
+ }
31
+ }
22
32
  }
33
+ return undefined
23
34
  }
24
35
 
25
- function saveBundleName(key: string, value: string): void {
36
+ function saveBundleMetadata(side: 'left' | 'right', metadata: BundleMetadata): void {
26
37
  if (import.meta.hot) {
27
- localStorage.setItem(`sde-check-selected-bundle-${key}`, value)
38
+ localStorage.setItem(`sde-check-selected-bundle-${side}`, JSON.stringify(metadata))
28
39
  }
29
40
  }
30
41
 
31
- // For local development mode, load the list of available baseline bundles
32
- type BundleModule = {
33
- createBundle(): Bundle
34
- }
35
- type LoadBundle = () => Promise<Bundle>
36
- const availableBundles: { [key: string]: LoadBundle } = {}
37
- let bundleNames: string[]
38
- let selectedBaselineBundleName: string
39
- let selectedCurrentBundleName: string
42
+ // For local development mode, use the bundle metadata saved in `LocalStorage`
43
+ let savedBundleMetadataL: BundleMetadata | undefined
44
+ let savedBundleMetadataR: BundleMetadata | undefined
40
45
  // The following value will be injected by `vite-config-for-report.ts`
41
- const baselinesPath = './baselines/*.txt'
42
- if (import.meta.hot && baselinesPath) {
46
+ const bundlesPath = './bundles/*.txt'
47
+ if (import.meta.hot && bundlesPath) {
43
48
  // Restore the previously selected bundles (from before the page was reloaded)
44
- selectedBaselineBundleName = loadBundleName('baseline')
45
- selectedCurrentBundleName = loadBundleName('current')
46
-
47
- // Get the available baseline bundles. The glob pattern part will be replaced
48
- // by Vite (see `vite-config-for-report.ts`). Note that we provide a placeholder
49
- // here that looks like a valid glob pattern, since Vite's dependency resolver will
50
- // report errors if it is invalid (not a literal).
51
- const bundlesGlob = import.meta.glob('./baselines/*.txt', {
52
- eager: false
53
- })
54
- const baselineBundleNames: string[] = []
55
- for (const bundleKey of Object.keys(bundlesGlob)) {
56
- const loadBundle = bundlesGlob[bundleKey]
57
- const bundlePathParts = bundleKey.split('/')
58
- const bundleFileName = bundlePathParts[bundlePathParts.length - 1]
59
- const bundleName = bundleFileName.replace('.js', '')
60
- baselineBundleNames.push(bundleName)
61
- availableBundles[bundleName] = async () => {
62
- const module = (await loadBundle()) as BundleModule
63
- return module.createBundle() as Bundle
64
- }
65
- }
66
-
67
- // Alphabetize (reversed, so that newer dates are at the top of the list)
68
- baselineBundleNames.sort((a, b) => {
69
- return b.toLowerCase().localeCompare(a.toLowerCase())
70
- })
71
-
72
- // Always include the "current" bundle as the first option, followed by
73
- // the alphabetized bundle names
74
- bundleNames = ['current', ...baselineBundleNames]
49
+ savedBundleMetadataL = loadBundleMetadata('left')
50
+ savedBundleMetadataR = loadBundleMetadata('right')
75
51
  }
76
52
 
77
53
  async function initForProduction(): Promise<void> {
@@ -114,29 +90,90 @@ async function initForProduction(): Promise<void> {
114
90
  }
115
91
 
116
92
  async function initForLocal(): Promise<void> {
117
- async function createBundle(requestedName: string | undefined): Promise<[Bundle, string]> {
118
- if (requestedName === undefined) {
119
- requestedName = 'current'
93
+ interface BundleResult {
94
+ bundle: Bundle
95
+ bundleName: string
96
+ bundleUrl: string
97
+ }
98
+
99
+ async function createBundle(bundleMetadata: BundleMetadata | undefined): Promise<BundleResult> {
100
+ if (bundleMetadata === undefined) {
101
+ bundleMetadata = {
102
+ name: 'current',
103
+ url: 'current'
104
+ }
120
105
  }
121
106
 
122
- // See if there is a bundle available for the requested name
123
- let bundle: Bundle
124
- let bundleName: string
125
- if (requestedName in availableBundles) {
126
- // Load the bundle for the requested name
127
- const loadBundle = availableBundles[requestedName]
128
- bundle = await loadBundle()
129
- bundleName = requestedName
130
- } else {
131
- // Load the "current" bundle
132
- bundle = createCurrentBundle()
133
- bundleName = 'current'
107
+ if (bundleMetadata.url.startsWith('http')) {
108
+ // Load remote bundles using dynamic import
109
+ try {
110
+ // Add cache busting parameter
111
+ const cacheBuster = `?cb=${Date.now()}`
112
+ const fullUrl = `${bundleMetadata.url}${cacheBuster}`
113
+ const module = await import(/* @vite-ignore */ fullUrl)
114
+ const bundle = module.createBundle() as Bundle
115
+ return {
116
+ bundle,
117
+ bundleName: bundleMetadata.name,
118
+ bundleUrl: bundleMetadata.url
119
+ }
120
+ } catch (e) {
121
+ console.error(
122
+ `ERROR: Failed to load remote bundle from ${bundleMetadata.url}; will use "current" bundle instead. Cause:`,
123
+ e
124
+ )
125
+ }
126
+ } else if (bundleMetadata.url.startsWith('file://')) {
127
+ // Load local bundles using `import.meta.glob` (since dynamic import isn't
128
+ // available for file URLs due to security restrictions). The glob pattern
129
+ // part will be replaced by Vite (see `vite-config-for-report.ts`). Note
130
+ // that we provide a placeholder here that looks like a valid glob pattern,
131
+ // since Vite's dependency resolver will report errors if it is invalid
132
+ // (not a literal).
133
+ try {
134
+ const bundlesGlob = import.meta.glob('./bundles/*.txt', {
135
+ eager: false
136
+ })
137
+ const remoteBundleUrlParts = bundleMetadata.url.split('/')
138
+ const remoteBundleFileName = remoteBundleUrlParts[remoteBundleUrlParts.length - 1]
139
+ const bundleKey = Object.keys(bundlesGlob).find(key => {
140
+ const bundlePathParts = key.split('/')
141
+ const bundleFileName = bundlePathParts[bundlePathParts.length - 1]
142
+ return bundleFileName === remoteBundleFileName
143
+ })
144
+ if (bundleKey) {
145
+ type BundleModule = {
146
+ createBundle(): Bundle
147
+ }
148
+ const loadBundle = bundlesGlob[bundleKey]
149
+ const module = (await loadBundle()) as BundleModule
150
+ const bundle = module.createBundle() as Bundle
151
+ return {
152
+ bundle,
153
+ bundleName: bundleMetadata.name,
154
+ bundleUrl: bundleMetadata.url
155
+ }
156
+ }
157
+ } catch (e) {
158
+ console.error(
159
+ `ERROR: Failed to load local bundle from ${bundleMetadata.url}; will use "current" bundle instead. Cause:`,
160
+ e
161
+ )
162
+ }
163
+ }
164
+
165
+ // Load the "current" bundle if it was requested or if the other loading
166
+ // processes failed
167
+ const bundle = createCurrentBundle()
168
+ return {
169
+ bundle,
170
+ bundleName: 'current',
171
+ bundleUrl: 'current'
134
172
  }
135
- return [bundle, bundleName]
136
173
  }
137
174
 
138
- const [bundleL, bundleNameL] = await createBundle(selectedBaselineBundleName)
139
- const [bundleR, bundleNameR] = await createBundle(selectedCurrentBundleName)
175
+ const { bundle: bundleL, bundleName: bundleNameL, bundleUrl: bundleUrlL } = await createBundle(savedBundleMetadataL)
176
+ const { bundle: bundleR, bundleName: bundleNameR, bundleUrl: bundleUrlR } = await createBundle(savedBundleMetadataR)
140
177
 
141
178
  // Prepare the model check/comparison configuration
142
179
  const configInitOptions: ConfigInitOptions = {
@@ -155,8 +192,17 @@ async function initForLocal(): Promise<void> {
155
192
  }
156
193
 
157
194
  // Initialize the root Svelte component
195
+ const remoteBundlesUrl = __REMOTE_BUNDLES_URL__
158
196
  initAppShell(configOptions, {
159
- bundleNames
197
+ bundleSelectorConfig: {
198
+ bundleUrlL,
199
+ bundleUrlR,
200
+ remoteBundlesUrl: remoteBundlesUrl !== '' ? remoteBundlesUrl : undefined,
201
+ getLocalBundles: import.meta.hot ? getLocalBundles : undefined,
202
+ downloadBundle: import.meta.hot ? downloadBundle : undefined,
203
+ copyBundle: import.meta.hot ? copyBundle : undefined,
204
+ onBundlesChanged: import.meta.hot ? onBundlesChanged : undefined
205
+ }
160
206
  })
161
207
  }
162
208
 
@@ -188,12 +234,13 @@ if (import.meta.hot) {
188
234
  document.addEventListener('sde-check-bundle', e => {
189
235
  // Change the selected bundle
190
236
  const info = (e as CustomEvent).detail
191
- if (info.kind === 'left') {
192
- saveBundleName('baseline', info.name)
193
- selectedBaselineBundleName = info.name
194
- } else {
195
- saveBundleName('current', info.name)
196
- selectedCurrentBundleName = info.name
237
+ const bundleMetadata = { name: info.name, url: info.url }
238
+ if (info.side === 'left') {
239
+ saveBundleMetadata('left', bundleMetadata)
240
+ savedBundleMetadataL = bundleMetadata
241
+ } else if (info.side === 'right') {
242
+ saveBundleMetadata('right', bundleMetadata)
243
+ savedBundleMetadataR = bundleMetadata
197
244
  }
198
245
 
199
246
  // Reinitialize using the chosen bundles
@@ -207,3 +254,160 @@ if (import.meta.hot) {
207
254
  initBundlesAndUI()
208
255
  })
209
256
  }
257
+
258
+ /**
259
+ * Get the list of locally available bundles (only in development mode with HMR enbaled).
260
+ */
261
+ async function getLocalBundles(): Promise<BundleLocation[]> {
262
+ // Only available in development mode with HMR
263
+ if (!import.meta.hot) {
264
+ throw new Error('getLocalBundles is only available in development mode with HMR enabled')
265
+ }
266
+
267
+ return new Promise((resolve, reject) => {
268
+ // Set up listeners
269
+ const handleSuccess = (data: { bundles: Array<{ name: string; url: string; lastModified: string }> }) => {
270
+ cleanup()
271
+
272
+ // Add the bundles that were found in the Node process
273
+ const bundles: BundleLocation[] = data.bundles.map(b => ({
274
+ name: b.name,
275
+ url: b.url,
276
+ lastModified: b.lastModified
277
+ }))
278
+
279
+ // Add the special "current" bundle that is generated by the builder
280
+ const currentBundleLastModified = __CURRENT_BUNDLE_LAST_MODIFIED__
281
+ bundles.push({
282
+ name: 'current',
283
+ url: 'current',
284
+ lastModified: currentBundleLastModified
285
+ })
286
+
287
+ resolve(bundles)
288
+ }
289
+
290
+ const handleError = (data: { error: string }) => {
291
+ cleanup()
292
+ reject(new Error(data.error))
293
+ }
294
+
295
+ const cleanup = () => {
296
+ import.meta.hot.off('list-bundles-success', handleSuccess)
297
+ import.meta.hot.off('list-bundles-error', handleError)
298
+ }
299
+
300
+ import.meta.hot.on('list-bundles-success', handleSuccess)
301
+ import.meta.hot.on('list-bundles-error', handleError)
302
+
303
+ // Send request to list bundles
304
+ import.meta.hot.send('list-bundles', {})
305
+
306
+ // Timeout after 5 seconds
307
+ setTimeout(() => {
308
+ cleanup()
309
+ reject(new Error('Timeout waiting for bundle list'))
310
+ }, 5000)
311
+ })
312
+ }
313
+
314
+ /**
315
+ * Download a bundle from the network (only in development mode with HMR).
316
+ */
317
+ function downloadBundle(bundle: BundleSpec): void {
318
+ // Only available in development mode with HMR
319
+ if (!import.meta.hot) {
320
+ throw new Error('downloadBundle is only available in development mode with HMR enabled')
321
+ }
322
+
323
+ if (!bundle.remote) {
324
+ throw new Error('Only bundles with a remote URL can be downloaded')
325
+ }
326
+
327
+ const { url, name, lastModified } = bundle.remote
328
+
329
+ // Set up listeners for download result
330
+ const handleSuccess = (data: { name: string; filePath: string }) => {
331
+ cleanup()
332
+ if (data.name === name) {
333
+ console.log(`Successfully downloaded bundle: ${name} to ${data.filePath}`)
334
+ }
335
+ }
336
+
337
+ const handleError = (data: { name: string; error: string }) => {
338
+ cleanup()
339
+ if (data.name === name) {
340
+ console.error(`Failed to download bundle ${name}:`, data.error)
341
+ }
342
+ }
343
+
344
+ const cleanup = () => {
345
+ import.meta.hot.off('download-bundle-success', handleSuccess)
346
+ import.meta.hot.off('download-bundle-error', handleError)
347
+ }
348
+
349
+ import.meta.hot.on('download-bundle-success', handleSuccess)
350
+ import.meta.hot.on('download-bundle-error', handleError)
351
+
352
+ // Send download request
353
+ import.meta.hot.send('download-bundle', { url, name, lastModified })
354
+
355
+ console.log(`Requesting download of bundle: ${name} from ${url}`)
356
+ }
357
+
358
+ /**
359
+ * Copy a local bundle file to a new name.
360
+ */
361
+ function copyBundle(bundle: BundleSpec, newName: string): void {
362
+ // Only available in development mode with HMR
363
+ if (!import.meta.hot) {
364
+ throw new Error('copyBundle is only available in development mode with HMR enabled')
365
+ }
366
+
367
+ if (!bundle.local) {
368
+ throw new Error('Only local bundles can be copied')
369
+ }
370
+
371
+ const { url, name } = bundle.local
372
+
373
+ // Set up listeners for copy result
374
+ const handleSuccess = (data: { name: string; filePath: string }) => {
375
+ cleanup()
376
+ if (data.name === newName) {
377
+ console.log(`Successfully copied bundle: ${name} to ${data.filePath}`)
378
+ }
379
+ }
380
+
381
+ const handleError = (data: { name: string; error: string }) => {
382
+ cleanup()
383
+ if (data.name === name) {
384
+ console.error(`Failed to copy bundle ${name}:`, data.error)
385
+ }
386
+ }
387
+
388
+ const cleanup = () => {
389
+ import.meta.hot.off('copy-bundle-success', handleSuccess)
390
+ import.meta.hot.off('copy-bundle-error', handleError)
391
+ }
392
+
393
+ import.meta.hot.on('copy-bundle-success', handleSuccess)
394
+ import.meta.hot.on('copy-bundle-error', handleError)
395
+
396
+ // Send copy request
397
+ import.meta.hot.send('copy-bundle', { url, name, newName })
398
+
399
+ console.log(`Requesting copy of bundle: ${name} to ${newName}`)
400
+ }
401
+
402
+ /**
403
+ * Add a listener that is notified when there are file system changes detected in the
404
+ * local bundles directory.
405
+ */
406
+ function onBundlesChanged(listener: () => void): void {
407
+ // Only available in development mode with HMR
408
+ if (!import.meta.hot) {
409
+ throw new Error('onBundlesChanged is only available in development mode with HMR enabled')
410
+ }
411
+
412
+ import.meta.hot.on('bundles-changed', listener)
413
+ }