@sdeverywhere/plugin-check 0.3.37 → 0.3.38
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.
- package/dist/index.d.ts +67 -65
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +660 -899
- package/dist/index.js.map +1 -1
- package/package.json +13 -26
- package/template-report/index.html +7 -5
- package/template-report/polyfills/noop-polyfills.ts +8 -5
- package/template-report/src/bundle-metadata.spec.ts +81 -0
- package/template-report/src/bundle-metadata.ts +66 -0
- package/template-report/src/index.ts +17 -78
- package/template-report/src/load-bundle.ts +1 -4
- package/template-report/src/resolve-bundle.spec.ts +109 -0
- package/template-report/src/resolve-bundle.ts +64 -0
- package/dist/index.cjs +0 -972
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -83
- package/template-report/src/bundles/unused.txt +0 -2
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Copyright (c) 2026 Climate Interactive / New Venture Fund
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Identifies one side of the model-check comparison.
|
|
5
|
+
*/
|
|
6
|
+
export type BundleSide = 'left' | 'right'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Describes the bundle that is selected for one side of the comparison.
|
|
10
|
+
*/
|
|
11
|
+
export interface BundleMetadata {
|
|
12
|
+
/** The name of the bundle. */
|
|
13
|
+
name: string
|
|
14
|
+
/** The URL of the bundle, or 'current' for the bundle built into the report app. */
|
|
15
|
+
url: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Return the `LocalStorage` key used to hold the selected bundle for the given side.
|
|
20
|
+
*
|
|
21
|
+
* @param side The side of the comparison.
|
|
22
|
+
* @returns The `LocalStorage` key.
|
|
23
|
+
*/
|
|
24
|
+
function keyForSide(side: BundleSide): string {
|
|
25
|
+
return `sde-check-selected-bundle-${side}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Return the metadata for the bundle that was previously selected for the given side.
|
|
30
|
+
*
|
|
31
|
+
* @param side The side of the comparison.
|
|
32
|
+
* @returns The saved bundle metadata, or undefined if there is no valid saved metadata.
|
|
33
|
+
*/
|
|
34
|
+
export function loadBundleMetadata(side: BundleSide): BundleMetadata | undefined {
|
|
35
|
+
const metadataJson = localStorage.getItem(keyForSide(side))
|
|
36
|
+
if (metadataJson) {
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(metadataJson)
|
|
39
|
+
if (parsed.name && parsed.url) {
|
|
40
|
+
return parsed as BundleMetadata
|
|
41
|
+
}
|
|
42
|
+
} catch (_e) {
|
|
43
|
+
// Treat malformed metadata the same as if no bundle was selected
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Save the metadata for the bundle that is selected for the given side.
|
|
51
|
+
*
|
|
52
|
+
* @param side The side of the comparison.
|
|
53
|
+
* @param metadata The bundle metadata to be saved.
|
|
54
|
+
*/
|
|
55
|
+
export function saveBundleMetadata(side: BundleSide, metadata: BundleMetadata): void {
|
|
56
|
+
localStorage.setItem(keyForSide(side), JSON.stringify(metadata))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Forget the bundle that is selected for the given side.
|
|
61
|
+
*
|
|
62
|
+
* @param side The side of the comparison.
|
|
63
|
+
*/
|
|
64
|
+
export function clearBundleMetadata(side: BundleSide): void {
|
|
65
|
+
localStorage.removeItem(keyForSide(side))
|
|
66
|
+
}
|
|
@@ -6,9 +6,11 @@ import type { BundleLocation, BundleSpec } from '@sdeverywhere/check-ui-shell'
|
|
|
6
6
|
import { initAppShell } from '@sdeverywhere/check-ui-shell'
|
|
7
7
|
import '@sdeverywhere/check-ui-shell/dist/style.css'
|
|
8
8
|
|
|
9
|
-
import type { BundleMetadata
|
|
9
|
+
import type { BundleMetadata } from './bundle-metadata'
|
|
10
|
+
import { loadBundleMetadata, saveBundleMetadata } from './bundle-metadata'
|
|
10
11
|
import { loadBundle } from './load-bundle'
|
|
11
12
|
import { initOverlay } from './overlay'
|
|
13
|
+
import { resolveBundle } from './resolve-bundle'
|
|
12
14
|
|
|
13
15
|
import './global.css'
|
|
14
16
|
|
|
@@ -17,36 +19,6 @@ import { createBundle as createBaselineBundle } from '@_baseline_bundle_'
|
|
|
17
19
|
import { createBundle as createCurrentBundle } from '@_current_bundle_'
|
|
18
20
|
import { getConfigOptions } from '@_test_config_'
|
|
19
21
|
|
|
20
|
-
function loadBundleMetadata(side: 'left' | 'right'): BundleMetadata | undefined {
|
|
21
|
-
if (import.meta.hot) {
|
|
22
|
-
const metadataJson = localStorage.getItem(`sde-check-selected-bundle-${side}`)
|
|
23
|
-
if (metadataJson) {
|
|
24
|
-
const parsed = JSON.parse(metadataJson)
|
|
25
|
-
if (parsed.name && parsed.url) {
|
|
26
|
-
return parsed as BundleMetadata
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return undefined
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function saveBundleMetadata(side: 'left' | 'right', metadata: BundleMetadata): void {
|
|
34
|
-
if (import.meta.hot) {
|
|
35
|
-
localStorage.setItem(`sde-check-selected-bundle-${side}`, JSON.stringify(metadata))
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// For local development mode, use the bundle metadata saved in `LocalStorage`
|
|
40
|
-
let savedBundleMetadataL: BundleMetadata | undefined
|
|
41
|
-
let savedBundleMetadataR: BundleMetadata | undefined
|
|
42
|
-
// The following value will be injected by `vite-config-for-report.ts`
|
|
43
|
-
const bundlesPath = './bundles/**/*.txt'
|
|
44
|
-
if (import.meta.hot && bundlesPath) {
|
|
45
|
-
// Restore the previously selected bundles (from before the page was reloaded)
|
|
46
|
-
savedBundleMetadataL = loadBundleMetadata('left')
|
|
47
|
-
savedBundleMetadataR = loadBundleMetadata('right')
|
|
48
|
-
}
|
|
49
|
-
|
|
50
22
|
async function initForProduction(): Promise<void> {
|
|
51
23
|
// For "production" builds, load the summary from a JSON file that
|
|
52
24
|
// was generated as part of the build process. This makes the
|
|
@@ -87,56 +59,27 @@ async function initForProduction(): Promise<void> {
|
|
|
87
59
|
}
|
|
88
60
|
|
|
89
61
|
async function initForLocal(): Promise<void> {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
): Promise<BundleResult> {
|
|
94
|
-
if (bundleMetadata === undefined) {
|
|
95
|
-
bundleMetadata = {
|
|
96
|
-
name: 'current',
|
|
97
|
-
url: 'current'
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (bundleMetadata.url.startsWith('http') || bundleMetadata.url.startsWith('file://')) {
|
|
102
|
-
// Load bundles (both local and remote) via the Vite dev server
|
|
103
|
-
try {
|
|
104
|
-
console.log(`Loading bundle for ${side} side: name=${bundleMetadata.name} url=${bundleMetadata.url}`)
|
|
105
|
-
const result = await loadBundle(bundleMetadata)
|
|
106
|
-
if (result) {
|
|
107
|
-
return result
|
|
108
|
-
} else {
|
|
109
|
-
console.error(`ERROR: Failed to load bundle ${bundleMetadata.name}; will use "current" bundle instead`)
|
|
110
|
-
}
|
|
111
|
-
} catch (e) {
|
|
112
|
-
console.error(
|
|
113
|
-
`ERROR: Failed to load bundle from ${bundleMetadata.url}; will use "current" bundle instead. Cause:`,
|
|
114
|
-
e
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Load the "current" bundle if it was requested or if the other loading
|
|
120
|
-
// processes failed
|
|
121
|
-
console.log(`Loading current bundle for ${side} side`)
|
|
122
|
-
const bundle = createCurrentBundle()
|
|
123
|
-
return {
|
|
124
|
-
bundle,
|
|
125
|
-
bundleName: 'current',
|
|
126
|
-
bundleUrl: 'current'
|
|
127
|
-
}
|
|
62
|
+
const resolveBundleDeps = {
|
|
63
|
+
loadBundle,
|
|
64
|
+
createCurrentBundle
|
|
128
65
|
}
|
|
129
66
|
|
|
67
|
+
// Restore the bundles that were previously selected (from before the page was reloaded).
|
|
68
|
+
// Note that we read these each time the app is initialized, since `resolveBundle` will
|
|
69
|
+
// clear a saved selection if the bundle cannot be loaded.
|
|
70
|
+
const savedBundleMetadataL = loadBundleMetadata('left')
|
|
71
|
+
const savedBundleMetadataR = loadBundleMetadata('right')
|
|
72
|
+
|
|
130
73
|
const {
|
|
131
74
|
bundle: bundleL,
|
|
132
75
|
bundleName: bundleNameL,
|
|
133
76
|
bundleUrl: bundleUrlL
|
|
134
|
-
} = await
|
|
77
|
+
} = await resolveBundle('left', savedBundleMetadataL, resolveBundleDeps)
|
|
135
78
|
const {
|
|
136
79
|
bundle: bundleR,
|
|
137
80
|
bundleName: bundleNameR,
|
|
138
81
|
bundleUrl: bundleUrlR
|
|
139
|
-
} = await
|
|
82
|
+
} = await resolveBundle('right', savedBundleMetadataR, resolveBundleDeps)
|
|
140
83
|
|
|
141
84
|
// Prepare the model check/comparison configuration
|
|
142
85
|
const configInitOptions: ConfigInitOptions = {
|
|
@@ -197,13 +140,9 @@ if (import.meta.hot) {
|
|
|
197
140
|
document.addEventListener('sde-check-bundle', e => {
|
|
198
141
|
// Change the selected bundle
|
|
199
142
|
const info = (e as CustomEvent).detail
|
|
200
|
-
const bundleMetadata = { name: info.name, url: info.url }
|
|
201
|
-
if (info.side === 'left') {
|
|
202
|
-
saveBundleMetadata(
|
|
203
|
-
savedBundleMetadataL = bundleMetadata
|
|
204
|
-
} else if (info.side === 'right') {
|
|
205
|
-
saveBundleMetadata('right', bundleMetadata)
|
|
206
|
-
savedBundleMetadataR = bundleMetadata
|
|
143
|
+
const bundleMetadata: BundleMetadata = { name: info.name, url: info.url }
|
|
144
|
+
if (info.side === 'left' || info.side === 'right') {
|
|
145
|
+
saveBundleMetadata(info.side, bundleMetadata)
|
|
207
146
|
}
|
|
208
147
|
|
|
209
148
|
// Reinitialize using the chosen bundles
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Copyright (c) 2026 Climate Interactive / New Venture Fund
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import type { Bundle } from '@sdeverywhere/check-core'
|
|
6
|
+
|
|
7
|
+
import type { BundleMetadata } from './bundle-metadata'
|
|
8
|
+
import { loadBundleMetadata, saveBundleMetadata } from './bundle-metadata'
|
|
9
|
+
import type { BundleResult } from './load-bundle'
|
|
10
|
+
import type { ResolveBundleDeps } from './resolve-bundle'
|
|
11
|
+
import { resolveBundle } from './resolve-bundle'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A minimal in-memory implementation of the parts of the `Storage` API that are
|
|
15
|
+
* used by the bundle metadata functions.
|
|
16
|
+
*/
|
|
17
|
+
class FakeStorage {
|
|
18
|
+
private readonly items: Map<string, string> = new Map()
|
|
19
|
+
|
|
20
|
+
getItem(key: string): string | null {
|
|
21
|
+
const value = this.items.get(key)
|
|
22
|
+
return value !== undefined ? value : null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setItem(key: string, value: string): void {
|
|
26
|
+
this.items.set(key, value)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
removeItem(key: string): void {
|
|
30
|
+
this.items.delete(key)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const currentBundle = { version: 1 } as unknown as Bundle
|
|
35
|
+
const remoteBundle = { version: 1 } as unknown as Bundle
|
|
36
|
+
|
|
37
|
+
const remoteMetadata: BundleMetadata = { name: 'remote-2', url: 'http://localhost:9000/remote-2.js' }
|
|
38
|
+
|
|
39
|
+
function deps(loadBundle: ResolveBundleDeps['loadBundle']): ResolveBundleDeps {
|
|
40
|
+
return {
|
|
41
|
+
loadBundle,
|
|
42
|
+
createCurrentBundle: () => currentBundle
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
vi.stubGlobal('localStorage', new FakeStorage())
|
|
48
|
+
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
49
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('resolveBundle', () => {
|
|
53
|
+
it('should use the current bundle if no metadata is provided', async () => {
|
|
54
|
+
const loadBundle = vi.fn()
|
|
55
|
+
const result = await resolveBundle('right', undefined, deps(loadBundle))
|
|
56
|
+
expect(result).toEqual({ bundle: currentBundle, bundleName: 'current', bundleUrl: 'current' })
|
|
57
|
+
expect(loadBundle).not.toHaveBeenCalled()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should use the current bundle if the saved metadata refers to the current bundle', async () => {
|
|
61
|
+
const loadBundle = vi.fn()
|
|
62
|
+
const metadata: BundleMetadata = { name: 'current', url: 'current' }
|
|
63
|
+
const result = await resolveBundle('right', metadata, deps(loadBundle))
|
|
64
|
+
expect(result).toEqual({ bundle: currentBundle, bundleName: 'current', bundleUrl: 'current' })
|
|
65
|
+
expect(loadBundle).not.toHaveBeenCalled()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should use the loaded bundle and keep the saved metadata if the bundle is loaded successfully', async () => {
|
|
69
|
+
saveBundleMetadata('right', remoteMetadata)
|
|
70
|
+
const loaded: BundleResult = {
|
|
71
|
+
bundle: remoteBundle,
|
|
72
|
+
bundleName: remoteMetadata.name,
|
|
73
|
+
bundleUrl: remoteMetadata.url
|
|
74
|
+
}
|
|
75
|
+
const loadBundle = vi.fn(async () => loaded)
|
|
76
|
+
const result = await resolveBundle('right', remoteMetadata, deps(loadBundle))
|
|
77
|
+
expect(result).toEqual(loaded)
|
|
78
|
+
expect(loadBundle).toHaveBeenCalledWith(remoteMetadata)
|
|
79
|
+
expect(loadBundleMetadata('right')).toEqual(remoteMetadata)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should fall back to the current bundle and clear the saved metadata if the bundle cannot be loaded', async () => {
|
|
83
|
+
saveBundleMetadata('right', remoteMetadata)
|
|
84
|
+
const loadBundle = vi.fn(async () => undefined)
|
|
85
|
+
const result = await resolveBundle('right', remoteMetadata, deps(loadBundle))
|
|
86
|
+
expect(result).toEqual({ bundle: currentBundle, bundleName: 'current', bundleUrl: 'current' })
|
|
87
|
+
expect(loadBundleMetadata('right')).toBeUndefined()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should fall back to the current bundle and clear the saved metadata if the load fails with an error', async () => {
|
|
91
|
+
saveBundleMetadata('left', remoteMetadata)
|
|
92
|
+
const loadBundle = vi.fn(async () => {
|
|
93
|
+
throw new Error('fetch failed')
|
|
94
|
+
})
|
|
95
|
+
const result = await resolveBundle('left', remoteMetadata, deps(loadBundle))
|
|
96
|
+
expect(result).toEqual({ bundle: currentBundle, bundleName: 'current', bundleUrl: 'current' })
|
|
97
|
+
expect(loadBundleMetadata('left')).toBeUndefined()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should clear the saved metadata for the failing side only', async () => {
|
|
101
|
+
const localMetadata: BundleMetadata = { name: 'previous', url: 'file:///previous.js' }
|
|
102
|
+
saveBundleMetadata('left', localMetadata)
|
|
103
|
+
saveBundleMetadata('right', remoteMetadata)
|
|
104
|
+
const loadBundle = vi.fn(async () => undefined)
|
|
105
|
+
await resolveBundle('right', remoteMetadata, deps(loadBundle))
|
|
106
|
+
expect(loadBundleMetadata('left')).toEqual(localMetadata)
|
|
107
|
+
expect(loadBundleMetadata('right')).toBeUndefined()
|
|
108
|
+
})
|
|
109
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Copyright (c) 2026 Climate Interactive / New Venture Fund
|
|
2
|
+
|
|
3
|
+
import type { Bundle } from '@sdeverywhere/check-core'
|
|
4
|
+
|
|
5
|
+
import type { BundleMetadata, BundleSide } from './bundle-metadata'
|
|
6
|
+
import { clearBundleMetadata } from './bundle-metadata'
|
|
7
|
+
import type { BundleResult } from './load-bundle'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The functions used by `resolveBundle` to load or create a bundle.
|
|
11
|
+
*/
|
|
12
|
+
export interface ResolveBundleDeps {
|
|
13
|
+
/** Load a bundle (local or remote) via the Vite dev server. */
|
|
14
|
+
loadBundle: (metadata: BundleMetadata) => Promise<BundleResult | undefined>
|
|
15
|
+
/** Create the "current" bundle that is built into the report app. */
|
|
16
|
+
createCurrentBundle: () => Bundle
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the bundle to be used for one side of the comparison.
|
|
21
|
+
*
|
|
22
|
+
* If the given metadata refers to a local or remote bundle, that bundle will be loaded
|
|
23
|
+
* via the Vite dev server. If it cannot be loaded, the saved selection is cleared (so
|
|
24
|
+
* that we don't try and fail to load the same bundle every time the app is reloaded)
|
|
25
|
+
* and the "current" bundle is used instead.
|
|
26
|
+
*
|
|
27
|
+
* @param side The side of the comparison.
|
|
28
|
+
* @param metadata The metadata for the selected bundle, or undefined if no bundle was selected.
|
|
29
|
+
* @param deps The functions used to load or create a bundle.
|
|
30
|
+
* @returns The resolved bundle along with its name and URL.
|
|
31
|
+
*/
|
|
32
|
+
export async function resolveBundle(
|
|
33
|
+
side: BundleSide,
|
|
34
|
+
metadata: BundleMetadata | undefined,
|
|
35
|
+
deps: ResolveBundleDeps
|
|
36
|
+
): Promise<BundleResult> {
|
|
37
|
+
if (metadata && (metadata.url.startsWith('http') || metadata.url.startsWith('file://'))) {
|
|
38
|
+
// Load the bundle (either local or remote) via the Vite dev server
|
|
39
|
+
let failure: string
|
|
40
|
+
try {
|
|
41
|
+
console.log(`Loading bundle for ${side} side: name=${metadata.name} url=${metadata.url}`)
|
|
42
|
+
const result = await deps.loadBundle(metadata)
|
|
43
|
+
if (result) {
|
|
44
|
+
return result
|
|
45
|
+
}
|
|
46
|
+
failure = `ERROR: Failed to load bundle ${metadata.name}; will use "current" bundle instead`
|
|
47
|
+
} catch (e) {
|
|
48
|
+
failure = `ERROR: Failed to load bundle from ${metadata.url}; will use "current" bundle instead. Cause: ${e}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Forget the failed selection, otherwise we would try (and fail) to load the same
|
|
52
|
+
// bundle each time the app is reloaded
|
|
53
|
+
console.error(failure)
|
|
54
|
+
clearBundleMetadata(side)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Use the "current" bundle if it was requested or if the load above failed
|
|
58
|
+
console.log(`Loading current bundle for ${side} side`)
|
|
59
|
+
return {
|
|
60
|
+
bundle: deps.createCurrentBundle(),
|
|
61
|
+
bundleName: 'current',
|
|
62
|
+
bundleUrl: 'current'
|
|
63
|
+
}
|
|
64
|
+
}
|