@dfosco/storyboard-react 4.0.0-beta.11 → 4.0.0-beta.12
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/package.json +3 -3
- package/src/canvas/widgets/ComponentWidget.module.css +3 -0
- package/src/canvas/widgets/WidgetChrome.module.css +2 -6
- package/src/canvas/widgets/WidgetWrapper.module.css +2 -0
- package/src/canvas/widgets/widgetConfig.js +10 -1
- package/src/vite/data-plugin.js +64 -22
- package/src/vite/data-plugin.test.js +218 -1
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dfosco/storyboard-react",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@dfosco/storyboard-core": "4.0.0-beta.
|
|
7
|
-
"@dfosco/tiny-canvas": "4.0.0-beta.
|
|
6
|
+
"@dfosco/storyboard-core": "4.0.0-beta.12",
|
|
7
|
+
"@dfosco/tiny-canvas": "4.0.0-beta.12",
|
|
8
8
|
"@neodrag/react": "^2.3.1",
|
|
9
9
|
"glob": "^11.0.0",
|
|
10
10
|
"jsonc-parser": "^3.3.1",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
top: calc(100% + 10px);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/* Trigger dot —
|
|
36
|
+
/* Trigger dot — positioned in the toolbar, visible at rest */
|
|
37
37
|
.triggerDot {
|
|
38
38
|
width: 6px;
|
|
39
39
|
height: 6px;
|
|
@@ -41,10 +41,6 @@
|
|
|
41
41
|
background: var(--borderColor-muted, #d0d7de);
|
|
42
42
|
opacity: 0.5;
|
|
43
43
|
transition: opacity 120ms;
|
|
44
|
-
position: absolute;
|
|
45
|
-
left: 50%;
|
|
46
|
-
top: 50%;
|
|
47
|
-
transform: translate(-50%, -50%);
|
|
48
44
|
}
|
|
49
45
|
|
|
50
46
|
:global([data-sb-canvas-theme^='dark']) .triggerDot {
|
|
@@ -235,7 +231,7 @@
|
|
|
235
231
|
.overflowMenu {
|
|
236
232
|
position: absolute;
|
|
237
233
|
top: calc(100% + 10px);
|
|
238
|
-
|
|
234
|
+
left: 0;
|
|
239
235
|
min-width: max-content;
|
|
240
236
|
padding: 4px;
|
|
241
237
|
background: var(--bgColor-default, #ffffff);
|
|
@@ -34,7 +34,16 @@ function resolveFeature(feature) {
|
|
|
34
34
|
if (key === 'items' && Array.isArray(val)) {
|
|
35
35
|
resolved[key] = val.map((item) => {
|
|
36
36
|
const r = {}
|
|
37
|
-
for (const [k, v] of Object.entries(item))
|
|
37
|
+
for (const [k, v] of Object.entries(item)) {
|
|
38
|
+
// Resolve nested alt object inside items
|
|
39
|
+
if (k === 'alt' && v && typeof v === 'object') {
|
|
40
|
+
const altResolved = {}
|
|
41
|
+
for (const [ak, av] of Object.entries(v)) altResolved[ak] = resolveVar(av)
|
|
42
|
+
r[k] = altResolved
|
|
43
|
+
} else {
|
|
44
|
+
r[k] = resolveVar(v)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
38
47
|
return r
|
|
39
48
|
})
|
|
40
49
|
} else if (key === 'alt' && val && typeof val === 'object') {
|
package/src/vite/data-plugin.js
CHANGED
|
@@ -633,6 +633,22 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes }, root)
|
|
|
633
633
|
`export { flows, scenes, objects, records, prototypes, folders, canvases }`,
|
|
634
634
|
`export const index = { flows, scenes, objects, records, prototypes, folders, canvases }`,
|
|
635
635
|
`export default index`,
|
|
636
|
+
'',
|
|
637
|
+
'// Live-patch canvas data on HMR events so SPA navigation shows fresh state',
|
|
638
|
+
'if (import.meta.hot) {',
|
|
639
|
+
' import.meta.hot.on("storyboard:canvas-file-changed", (data) => {',
|
|
640
|
+
' if (!data) return',
|
|
641
|
+
' if (data.removed) {',
|
|
642
|
+
' delete canvases[data.name]',
|
|
643
|
+
' } else if (data.metadata) {',
|
|
644
|
+
' // Merge into existing entry to preserve build-time fields (_jsxModule, _jsxImport, etc.)',
|
|
645
|
+
' canvases[data.name] = canvases[data.name]',
|
|
646
|
+
' ? Object.assign({}, canvases[data.name], data.metadata)',
|
|
647
|
+
' : data.metadata',
|
|
648
|
+
' }',
|
|
649
|
+
' init({ flows, objects, records, prototypes, folders, canvases })',
|
|
650
|
+
' })',
|
|
651
|
+
'}',
|
|
636
652
|
].join('\n')
|
|
637
653
|
}
|
|
638
654
|
|
|
@@ -697,7 +713,7 @@ export default function storyboardDataPlugin() {
|
|
|
697
713
|
const rawHtml = [
|
|
698
714
|
'<!DOCTYPE html>',
|
|
699
715
|
'<html><head>',
|
|
700
|
-
'<style>html,body{margin:0;padding:0;width:100%;height:100
|
|
716
|
+
'<style>html,body{margin:0;padding:0;width:100%;height:100%;background:var(--bgColor-default,transparent)}#root{width:100%;height:100%}</style>',
|
|
701
717
|
'</head><body>',
|
|
702
718
|
'<div id="root"></div>',
|
|
703
719
|
`<script type="module" src="/@fs${isolateEntryPath}"></script>`,
|
|
@@ -730,22 +746,50 @@ export default function storyboardDataPlugin() {
|
|
|
730
746
|
}
|
|
731
747
|
}
|
|
732
748
|
|
|
749
|
+
// Mark the virtual module as stale so the next page load rebuilds it,
|
|
750
|
+
// but do NOT trigger a full-reload (avoids losing canvas editing state).
|
|
751
|
+
const softInvalidate = () => {
|
|
752
|
+
buildResult = null
|
|
753
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
754
|
+
if (mod) server.moduleGraph.invalidateModule(mod)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Read a canvas file and build HMR metadata for the client-side listener.
|
|
758
|
+
const readCanvasMetadata = (filePath, parsed) => {
|
|
759
|
+
try {
|
|
760
|
+
const absPath = path.resolve(root, filePath)
|
|
761
|
+
const raw = fs.readFileSync(absPath, 'utf-8')
|
|
762
|
+
const materialized = materializeFromText(raw)
|
|
763
|
+
const result = { ...materialized }
|
|
764
|
+
// Inject _route and _folder the same way generateModule does
|
|
765
|
+
if (parsed.inferredRoute) result._route = parsed.inferredRoute
|
|
766
|
+
const folderDirMatch = path.relative(root, absPath).replace(/\\/g, '/').match(/(?:^|\/)src\/(?:prototypes|canvas)\/([^/]+)\.folder\//)
|
|
767
|
+
if (folderDirMatch) result._folder = folderDirMatch[1]
|
|
768
|
+
return result
|
|
769
|
+
} catch {
|
|
770
|
+
return null
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
733
774
|
const invalidate = (filePath) => {
|
|
734
775
|
const normalized = filePath.replace(/\\/g, '/')
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
776
|
+
// Canvas .jsonl content changes are mutated at runtime by the canvas
|
|
777
|
+
// server API. A full-reload would create a feedback loop (save →
|
|
778
|
+
// file change → reload → lose editing state). Instead, soft-invalidate
|
|
779
|
+
// the virtual module (so page refresh picks up changes) and send a
|
|
780
|
+
// custom HMR event with updated metadata so the canvas page and
|
|
781
|
+
// viewfinder can react in place.
|
|
740
782
|
if (/\.canvas\.jsonl$/.test(normalized)) {
|
|
741
783
|
const parsed = parseDataFile(filePath)
|
|
742
784
|
if (parsed?.suffix === 'canvas' && parsed?.name) {
|
|
785
|
+
const metadata = readCanvasMetadata(filePath, parsed)
|
|
743
786
|
server.ws.send({
|
|
744
787
|
type: 'custom',
|
|
745
788
|
event: 'storyboard:canvas-file-changed',
|
|
746
|
-
data: { name: parsed.name },
|
|
789
|
+
data: { name: parsed.name, ...(metadata ? { metadata } : {}) },
|
|
747
790
|
})
|
|
748
791
|
}
|
|
792
|
+
softInvalidate()
|
|
749
793
|
return
|
|
750
794
|
}
|
|
751
795
|
|
|
@@ -790,23 +834,27 @@ export default function storyboardDataPlugin() {
|
|
|
790
834
|
server.ws.send({
|
|
791
835
|
type: 'custom',
|
|
792
836
|
event: 'storyboard:canvas-file-changed',
|
|
793
|
-
data: { name },
|
|
837
|
+
data: { name, removed: true },
|
|
794
838
|
})
|
|
839
|
+
softInvalidate()
|
|
795
840
|
}, 1500)
|
|
796
841
|
pendingCanvasUnlinks.set(name, timer)
|
|
797
842
|
return
|
|
798
843
|
}
|
|
799
844
|
|
|
800
845
|
if (eventType === 'add') {
|
|
846
|
+
const metadata = readCanvasMetadata(filePath, parsed)
|
|
801
847
|
const pending = pendingCanvasUnlinks.get(name)
|
|
802
848
|
if (pending) {
|
|
849
|
+
// unlink+add pair = in-place save (atomic write), not a real remove
|
|
803
850
|
clearTimeout(pending)
|
|
804
851
|
pendingCanvasUnlinks.delete(name)
|
|
805
852
|
server.ws.send({
|
|
806
853
|
type: 'custom',
|
|
807
854
|
event: 'storyboard:canvas-file-changed',
|
|
808
|
-
data: { name },
|
|
855
|
+
data: { name, ...(metadata ? { metadata } : {}) },
|
|
809
856
|
})
|
|
857
|
+
softInvalidate()
|
|
810
858
|
return
|
|
811
859
|
}
|
|
812
860
|
|
|
@@ -814,8 +862,9 @@ export default function storyboardDataPlugin() {
|
|
|
814
862
|
server.ws.send({
|
|
815
863
|
type: 'custom',
|
|
816
864
|
event: 'storyboard:canvas-file-changed',
|
|
817
|
-
data: { name },
|
|
865
|
+
data: { name, ...(metadata ? { metadata } : {}) },
|
|
818
866
|
})
|
|
867
|
+
softInvalidate()
|
|
819
868
|
return
|
|
820
869
|
}
|
|
821
870
|
|
|
@@ -823,8 +872,9 @@ export default function storyboardDataPlugin() {
|
|
|
823
872
|
server.ws.send({
|
|
824
873
|
type: 'custom',
|
|
825
874
|
event: 'storyboard:canvas-file-changed',
|
|
826
|
-
data: { name },
|
|
875
|
+
data: { name, ...(metadata ? { metadata } : {}) },
|
|
827
876
|
})
|
|
877
|
+
softInvalidate()
|
|
828
878
|
return
|
|
829
879
|
}
|
|
830
880
|
}
|
|
@@ -859,18 +909,10 @@ export default function storyboardDataPlugin() {
|
|
|
859
909
|
const normalized = ctx.file.replace(/\\/g, '/')
|
|
860
910
|
if (!/\.canvas\.jsonl$/.test(normalized)) return
|
|
861
911
|
|
|
862
|
-
const parsed = parseDataFile(ctx.file)
|
|
863
|
-
if (parsed?.suffix === 'canvas' && parsed?.name) {
|
|
864
|
-
ctx.server.ws.send({
|
|
865
|
-
type: 'custom',
|
|
866
|
-
event: 'storyboard:canvas-file-changed',
|
|
867
|
-
data: { name: parsed.name },
|
|
868
|
-
})
|
|
869
|
-
}
|
|
870
|
-
|
|
871
912
|
// Prevent Vite's default fallback behavior (full page reload) for
|
|
872
|
-
// non-module .canvas.jsonl edits.
|
|
873
|
-
//
|
|
913
|
+
// non-module .canvas.jsonl edits. The watcher 'change' handler
|
|
914
|
+
// (invalidate) already sends the custom HMR event and soft-invalidates
|
|
915
|
+
// the virtual module — no duplicate event needed here.
|
|
874
916
|
return []
|
|
875
917
|
},
|
|
876
918
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'
|
|
1
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { tmpdir } from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import storyboardDataPlugin, { resolveTemplateVars, computeTemplateVars } from './data-plugin.js'
|
|
@@ -829,3 +829,220 @@ describe('template variable integration', () => {
|
|
|
829
829
|
warnSpy.mockRestore()
|
|
830
830
|
})
|
|
831
831
|
})
|
|
832
|
+
|
|
833
|
+
// ── Canvas watcher / HMR tests ──────────────────────────────────────
|
|
834
|
+
|
|
835
|
+
describe('canvas watcher behavior', () => {
|
|
836
|
+
/** Helper: create a mock Vite dev server for configureServer */
|
|
837
|
+
function createMockServer(root) {
|
|
838
|
+
const listeners = {}
|
|
839
|
+
const wsSent = []
|
|
840
|
+
const invalidatedModules = []
|
|
841
|
+
|
|
842
|
+
return {
|
|
843
|
+
wsSent,
|
|
844
|
+
invalidatedModules,
|
|
845
|
+
listeners,
|
|
846
|
+
config: { root, base: '/' },
|
|
847
|
+
watcher: {
|
|
848
|
+
add: vi.fn(),
|
|
849
|
+
on(event, fn) {
|
|
850
|
+
if (!listeners[event]) listeners[event] = []
|
|
851
|
+
listeners[event].push(fn)
|
|
852
|
+
},
|
|
853
|
+
},
|
|
854
|
+
moduleGraph: {
|
|
855
|
+
getModuleById(id) {
|
|
856
|
+
if (id === RESOLVED_ID) return { id: RESOLVED_ID }
|
|
857
|
+
return null
|
|
858
|
+
},
|
|
859
|
+
invalidateModule(mod) {
|
|
860
|
+
invalidatedModules.push(mod.id)
|
|
861
|
+
},
|
|
862
|
+
},
|
|
863
|
+
ws: {
|
|
864
|
+
send(msg) { wsSent.push(msg) },
|
|
865
|
+
},
|
|
866
|
+
middlewares: {
|
|
867
|
+
use: vi.fn(),
|
|
868
|
+
},
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/** Emit a watcher event on the mock server */
|
|
873
|
+
function emit(server, event, filePath) {
|
|
874
|
+
for (const fn of (server.listeners[event] || [])) {
|
|
875
|
+
fn(filePath)
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function writeCanvasFile(dir, name, title) {
|
|
880
|
+
const canvasDir = path.join(dir, 'src', 'canvas')
|
|
881
|
+
mkdirSync(canvasDir, { recursive: true })
|
|
882
|
+
const evt = { event: 'canvas_created', title: title || name, timestamp: Date.now() }
|
|
883
|
+
writeFileSync(path.join(canvasDir, `${name}.canvas.jsonl`), JSON.stringify(evt) + '\n')
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
it('soft-invalidates virtual module on canvas content change (no full-reload)', () => {
|
|
887
|
+
writeCanvasFile(tmpDir, 'test-canvas', 'Original Title')
|
|
888
|
+
const plugin = createPlugin()
|
|
889
|
+
// Force initial buildResult
|
|
890
|
+
plugin.load(RESOLVED_ID)
|
|
891
|
+
|
|
892
|
+
const server = createMockServer(tmpDir)
|
|
893
|
+
plugin.configureServer(server)
|
|
894
|
+
|
|
895
|
+
// Simulate a canvas file content change
|
|
896
|
+
const canvasPath = path.join(tmpDir, 'src', 'canvas', 'test-canvas.canvas.jsonl')
|
|
897
|
+
emit(server, 'change', canvasPath)
|
|
898
|
+
|
|
899
|
+
// Should have sent custom HMR event (not full-reload)
|
|
900
|
+
const customEvents = server.wsSent.filter(m => m.type === 'custom')
|
|
901
|
+
const fullReloads = server.wsSent.filter(m => m.type === 'full-reload')
|
|
902
|
+
|
|
903
|
+
expect(customEvents.length).toBe(1)
|
|
904
|
+
expect(customEvents[0].event).toBe('storyboard:canvas-file-changed')
|
|
905
|
+
expect(customEvents[0].data.name).toBe('test-canvas')
|
|
906
|
+
expect(fullReloads.length).toBe(0)
|
|
907
|
+
|
|
908
|
+
// Should have invalidated the virtual module
|
|
909
|
+
expect(server.invalidatedModules).toContain(RESOLVED_ID)
|
|
910
|
+
})
|
|
911
|
+
|
|
912
|
+
it('includes metadata in HMR event for canvas content changes', () => {
|
|
913
|
+
writeCanvasFile(tmpDir, 'meta-canvas', 'My Canvas Title')
|
|
914
|
+
const plugin = createPlugin()
|
|
915
|
+
plugin.load(RESOLVED_ID)
|
|
916
|
+
|
|
917
|
+
const server = createMockServer(tmpDir)
|
|
918
|
+
plugin.configureServer(server)
|
|
919
|
+
|
|
920
|
+
emit(server, 'change', path.join(tmpDir, 'src', 'canvas', 'meta-canvas.canvas.jsonl'))
|
|
921
|
+
|
|
922
|
+
const event = server.wsSent.find(m => m.type === 'custom')
|
|
923
|
+
expect(event.data.metadata).toBeDefined()
|
|
924
|
+
expect(event.data.metadata.title).toBe('My Canvas Title')
|
|
925
|
+
})
|
|
926
|
+
|
|
927
|
+
it('soft-invalidates on canvas file add (new canvas)', () => {
|
|
928
|
+
const plugin = createPlugin()
|
|
929
|
+
plugin.load(RESOLVED_ID)
|
|
930
|
+
|
|
931
|
+
const server = createMockServer(tmpDir)
|
|
932
|
+
plugin.configureServer(server)
|
|
933
|
+
|
|
934
|
+
// Create the file after the server is configured
|
|
935
|
+
writeCanvasFile(tmpDir, 'new-canvas', 'Brand New')
|
|
936
|
+
emit(server, 'add', path.join(tmpDir, 'src', 'canvas', 'new-canvas.canvas.jsonl'))
|
|
937
|
+
|
|
938
|
+
const customEvents = server.wsSent.filter(m => m.type === 'custom')
|
|
939
|
+
const fullReloads = server.wsSent.filter(m => m.type === 'full-reload')
|
|
940
|
+
|
|
941
|
+
expect(customEvents.length).toBe(1)
|
|
942
|
+
expect(customEvents[0].data.name).toBe('new-canvas')
|
|
943
|
+
expect(customEvents[0].data.metadata).toBeDefined()
|
|
944
|
+
expect(fullReloads.length).toBe(0)
|
|
945
|
+
expect(server.invalidatedModules).toContain(RESOLVED_ID)
|
|
946
|
+
})
|
|
947
|
+
|
|
948
|
+
it('soft-invalidates on canvas file unlink after timeout (true delete)', async () => {
|
|
949
|
+
writeCanvasFile(tmpDir, 'doomed-canvas', 'Gone Soon')
|
|
950
|
+
const plugin = createPlugin()
|
|
951
|
+
plugin.load(RESOLVED_ID)
|
|
952
|
+
|
|
953
|
+
const server = createMockServer(tmpDir)
|
|
954
|
+
plugin.configureServer(server)
|
|
955
|
+
|
|
956
|
+
emit(server, 'unlink', path.join(tmpDir, 'src', 'canvas', 'doomed-canvas.canvas.jsonl'))
|
|
957
|
+
|
|
958
|
+
// Immediately after unlink — no event yet (deferred by 1500ms)
|
|
959
|
+
expect(server.wsSent.length).toBe(0)
|
|
960
|
+
|
|
961
|
+
// Wait for deferred timer
|
|
962
|
+
await new Promise(resolve => setTimeout(resolve, 1600))
|
|
963
|
+
|
|
964
|
+
const customEvents = server.wsSent.filter(m => m.type === 'custom')
|
|
965
|
+
expect(customEvents.length).toBe(1)
|
|
966
|
+
expect(customEvents[0].data.name).toBe('doomed-canvas')
|
|
967
|
+
expect(customEvents[0].data.removed).toBe(true)
|
|
968
|
+
expect(server.invalidatedModules).toContain(RESOLVED_ID)
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
it('cancels deferred unlink on add (atomic write / in-place save)', async () => {
|
|
972
|
+
writeCanvasFile(tmpDir, 'saved-canvas', 'Saved')
|
|
973
|
+
const plugin = createPlugin()
|
|
974
|
+
plugin.load(RESOLVED_ID)
|
|
975
|
+
|
|
976
|
+
const server = createMockServer(tmpDir)
|
|
977
|
+
plugin.configureServer(server)
|
|
978
|
+
|
|
979
|
+
const canvasPath = path.join(tmpDir, 'src', 'canvas', 'saved-canvas.canvas.jsonl')
|
|
980
|
+
|
|
981
|
+
// Simulate atomic write: unlink then add within 1500ms
|
|
982
|
+
emit(server, 'unlink', canvasPath)
|
|
983
|
+
emit(server, 'add', canvasPath)
|
|
984
|
+
|
|
985
|
+
// Should have sent one event immediately (the add cancelling the unlink)
|
|
986
|
+
const customEvents = server.wsSent.filter(m => m.type === 'custom')
|
|
987
|
+
expect(customEvents.length).toBe(1)
|
|
988
|
+
expect(customEvents[0].data.name).toBe('saved-canvas')
|
|
989
|
+
expect(customEvents[0].data.removed).toBeUndefined()
|
|
990
|
+
expect(server.invalidatedModules).toContain(RESOLVED_ID)
|
|
991
|
+
|
|
992
|
+
// Wait past the unlink timer — should NOT get a second event
|
|
993
|
+
await new Promise(resolve => setTimeout(resolve, 1600))
|
|
994
|
+
const allCustom = server.wsSent.filter(m => m.type === 'custom')
|
|
995
|
+
expect(allCustom.length).toBe(1)
|
|
996
|
+
})
|
|
997
|
+
|
|
998
|
+
it('handleHotUpdate returns empty array for canvas files (suppresses full-reload)', () => {
|
|
999
|
+
const plugin = createPlugin()
|
|
1000
|
+
const result = plugin.handleHotUpdate({
|
|
1001
|
+
file: path.join(tmpDir, 'src', 'canvas', 'test.canvas.jsonl'),
|
|
1002
|
+
server: createMockServer(tmpDir),
|
|
1003
|
+
modules: [],
|
|
1004
|
+
})
|
|
1005
|
+
expect(result).toEqual([])
|
|
1006
|
+
})
|
|
1007
|
+
|
|
1008
|
+
it('handleHotUpdate does not send duplicate HMR events', () => {
|
|
1009
|
+
const plugin = createPlugin()
|
|
1010
|
+
const server = createMockServer(tmpDir)
|
|
1011
|
+
plugin.handleHotUpdate({
|
|
1012
|
+
file: path.join(tmpDir, 'src', 'canvas', 'test.canvas.jsonl'),
|
|
1013
|
+
server,
|
|
1014
|
+
modules: [],
|
|
1015
|
+
})
|
|
1016
|
+
// handleHotUpdate should NOT send events (invalidate() handles it)
|
|
1017
|
+
expect(server.wsSent.length).toBe(0)
|
|
1018
|
+
})
|
|
1019
|
+
|
|
1020
|
+
it('generated virtual module includes HMR listener for canvas updates', () => {
|
|
1021
|
+
writeCanvasFile(tmpDir, 'hmr-canvas', 'HMR Test')
|
|
1022
|
+
const plugin = createPlugin()
|
|
1023
|
+
const code = plugin.load(RESOLVED_ID)
|
|
1024
|
+
|
|
1025
|
+
expect(code).toContain('import.meta.hot')
|
|
1026
|
+
expect(code).toContain('storyboard:canvas-file-changed')
|
|
1027
|
+
expect(code).toContain('data.removed')
|
|
1028
|
+
expect(code).toContain('data.metadata')
|
|
1029
|
+
// Should merge into existing entries to preserve build-time fields
|
|
1030
|
+
expect(code).toContain('Object.assign')
|
|
1031
|
+
})
|
|
1032
|
+
|
|
1033
|
+
it('page refresh after canvas add yields updated module with new canvas', () => {
|
|
1034
|
+
const plugin = createPlugin()
|
|
1035
|
+
// First load — no canvases
|
|
1036
|
+
const code1 = plugin.load(RESOLVED_ID)
|
|
1037
|
+
expect(code1).not.toContain('"refresh-canvas"')
|
|
1038
|
+
|
|
1039
|
+
// Simulate adding a canvas and clearing buildResult (what softInvalidate does)
|
|
1040
|
+
writeCanvasFile(tmpDir, 'refresh-canvas', 'After Refresh')
|
|
1041
|
+
|
|
1042
|
+
// Manually clear buildResult by loading a fresh plugin instance with the same root
|
|
1043
|
+
const plugin2 = createPlugin()
|
|
1044
|
+
const code2 = plugin2.load(RESOLVED_ID)
|
|
1045
|
+
expect(code2).toContain('"refresh-canvas"')
|
|
1046
|
+
expect(code2).toContain('After Refresh')
|
|
1047
|
+
})
|
|
1048
|
+
})
|