@sdeverywhere/plugin-check 0.3.19 → 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,72 +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 loadSimplifyScenariosFlag(): boolean {
18
- if (import.meta.hot) {
19
- return localStorage.getItem('sde-check-simplify-scenarios') === '1'
20
- } else {
21
- return false
22
- }
18
+ interface BundleMetadata {
19
+ name: string
20
+ url: string
23
21
  }
24
22
 
25
- function loadBundleName(key: string): string | undefined {
23
+ function loadBundleMetadata(side: 'left' | 'right'): BundleMetadata | undefined {
26
24
  if (import.meta.hot) {
27
- return localStorage.getItem(`sde-check-selected-bundle-${key}`)
28
- } else {
29
- 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
+ }
30
32
  }
33
+ return undefined
31
34
  }
32
35
 
33
- function saveBundleName(key: string, value: string): void {
36
+ function saveBundleMetadata(side: 'left' | 'right', metadata: BundleMetadata): void {
34
37
  if (import.meta.hot) {
35
- localStorage.setItem(`sde-check-selected-bundle-${key}`, value)
38
+ localStorage.setItem(`sde-check-selected-bundle-${side}`, JSON.stringify(metadata))
36
39
  }
37
40
  }
38
41
 
39
- // For local development mode, load the list of available baseline bundles
40
- type BundleModule = {
41
- createBundle(): Bundle
42
- }
43
- type LoadBundle = () => Promise<Bundle>
44
- const availableBundles: { [key: string]: LoadBundle } = {}
45
- let bundleNames: string[]
46
- let selectedBaselineBundleName: string
47
- 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
48
45
  // The following value will be injected by `vite-config-for-report.ts`
49
- const baselinesPath = './baselines/*.txt'
50
- if (import.meta.hot && baselinesPath) {
46
+ const bundlesPath = './bundles/*.txt'
47
+ if (import.meta.hot && bundlesPath) {
51
48
  // Restore the previously selected bundles (from before the page was reloaded)
52
- selectedBaselineBundleName = loadBundleName('baseline')
53
- selectedCurrentBundleName = loadBundleName('current')
54
-
55
- // Get the available baseline bundles. The glob pattern part will be replaced
56
- // by Vite (see `vite-config-for-report.ts`). Note that we provide a placeholder
57
- // here that looks like a valid glob pattern, since Vite's dependency resolver will
58
- // report errors if it is invalid (not a literal).
59
- const bundlesGlob = import.meta.glob('./baselines/*.txt', {
60
- eager: false
61
- })
62
- const baselineBundleNames: string[] = []
63
- for (const bundleKey of Object.keys(bundlesGlob)) {
64
- const loadBundle = bundlesGlob[bundleKey]
65
- const bundlePathParts = bundleKey.split('/')
66
- const bundleFileName = bundlePathParts[bundlePathParts.length - 1]
67
- const bundleName = bundleFileName.replace('.js', '')
68
- baselineBundleNames.push(bundleName)
69
- availableBundles[bundleName] = async () => {
70
- const module = (await loadBundle()) as BundleModule
71
- return module.createBundle() as Bundle
72
- }
73
- }
74
-
75
- // Alphabetize (reversed, so that newer dates are at the top of the list)
76
- baselineBundleNames.sort((a, b) => {
77
- return b.toLowerCase().localeCompare(a.toLowerCase())
78
- })
79
-
80
- // Always include the "current" bundle as the first option, followed by
81
- // the alphabetized bundle names
82
- bundleNames = ['current', ...baselineBundleNames]
49
+ savedBundleMetadataL = loadBundleMetadata('left')
50
+ savedBundleMetadataR = loadBundleMetadata('right')
83
51
  }
84
52
 
85
53
  async function initForProduction(): Promise<void> {
@@ -122,41 +90,119 @@ async function initForProduction(): Promise<void> {
122
90
  }
123
91
 
124
92
  async function initForLocal(): Promise<void> {
125
- async function createBundle(requestedName: string | undefined): Promise<[Bundle, string]> {
126
- if (requestedName === undefined) {
127
- 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
+ }
128
105
  }
129
106
 
130
- // See if there is a bundle available for the requested name
131
- let bundle: Bundle
132
- let bundleName: string
133
- if (requestedName in availableBundles) {
134
- // Load the bundle for the requested name
135
- const loadBundle = availableBundles[requestedName]
136
- bundle = await loadBundle()
137
- bundleName = requestedName
138
- } else {
139
- // Load the "current" bundle
140
- bundle = createCurrentBundle()
141
- 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'
142
172
  }
143
- return [bundle, bundleName]
144
173
  }
145
174
 
146
- const [bundleL, bundleNameL] = await createBundle(selectedBaselineBundleName)
147
- 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)
148
177
 
149
178
  // Prepare the model check/comparison configuration
150
179
  const configInitOptions: ConfigInitOptions = {
151
180
  bundleNameL,
152
- bundleNameR,
153
- simplifyScenarios: loadSimplifyScenariosFlag()
181
+ bundleNameR
154
182
  }
155
183
  const configOptions = await getConfigOptions(bundleL, bundleR, configInitOptions)
156
184
 
185
+ // Override the concurrency setting using the value from LocalStorage
186
+ const concurrencyValue = localStorage.getItem('sde-check-concurrency')
187
+ if (concurrencyValue !== null) {
188
+ const concurrency = parseInt(concurrencyValue)
189
+ configOptions.concurrency = !isNaN(concurrency) ? concurrency : 1
190
+ } else {
191
+ configOptions.concurrency = 1
192
+ }
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,21 +234,180 @@ 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
200
247
  initBundlesAndUI()
201
248
  })
202
249
 
203
- // Reload everything when the user toggles the "Simplify Scenarios" checkbox
204
- document.addEventListener('sde-check-simplify-scenarios-toggled', () => {
205
- // Reinitialize using the new state
250
+ // Reload everything when the user applies updated configuration (e.g., updated filters or
251
+ // concurrency setting)
252
+ document.addEventListener('sde-check-config-changed', () => {
253
+ // Reinitialize using the new configuration
206
254
  initBundlesAndUI()
207
255
  })
208
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
+ }