tungsten 0.3.1 → 0.3.2
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/Components.d.ts +2 -1
- package/dist/Components.d.ts.map +1 -1
- package/dist/Components.js +10 -1
- package/dist/DomRegion.js +1 -1
- package/dist/FrameworkConfig.d.ts +4 -0
- package/dist/FrameworkConfig.d.ts.map +1 -1
- package/dist/FunctionComponentStandIn.d.ts +2 -0
- package/dist/FunctionComponentStandIn.d.ts.map +1 -1
- package/dist/FunctionComponentStandIn.js +10 -1
- package/dist/FunctionComponentStandIn.sxyt.d.ts +2 -0
- package/dist/FunctionComponentStandIn.sxyt.d.ts.map +1 -0
- package/dist/FunctionComponentStandIn.sxyt.js +52 -0
- package/dist/Servers/DevServer.d.ts +2 -0
- package/dist/Servers/DevServer.d.ts.map +1 -1
- package/dist/Servers/DevServer.js +32 -5
- package/dist/Servers/devErrorPageAutoRetry.d.ts +4 -0
- package/dist/Servers/devErrorPageAutoRetry.d.ts.map +1 -0
- package/dist/Servers/devErrorPageAutoRetry.js +26 -0
- package/dist/Servers/devErrorPageAutoRetry.sxyt.d.ts +2 -0
- package/dist/Servers/devErrorPageAutoRetry.sxyt.d.ts.map +1 -0
- package/dist/Servers/devErrorPageAutoRetry.sxyt.js +117 -0
- package/dist/api/getExport.d.ts.map +1 -1
- package/dist/api/getExport.js +2 -1
- package/dist/babelPlugins/BabelAddDevCacheBust.d.ts +11 -0
- package/dist/babelPlugins/BabelAddDevCacheBust.d.ts.map +1 -0
- package/dist/babelPlugins/BabelAddDevCacheBust.js +68 -0
- package/dist/babelPlugins/BabelAddDevCacheBust.sxyt.d.ts +2 -0
- package/dist/babelPlugins/BabelAddDevCacheBust.sxyt.d.ts.map +1 -0
- package/dist/babelPlugins/BabelAddDevCacheBust.sxyt.js +138 -0
- package/dist/bin/commands/dev.d.ts.map +1 -1
- package/dist/bin/commands/dev.js +2 -0
- package/dist/build/compileUserApp.js +2 -2
- package/dist/build/makeBabelUserDevConfig.d.ts.map +1 -1
- package/dist/build/makeBabelUserDevConfig.js +6 -1
- package/dist/defaultConfig.d.ts +2 -0
- package/dist/defaultConfig.d.ts.map +1 -1
- package/dist/defaultConfig.js +3 -1
- package/dist/env/getEnvSchemas.d.ts.map +1 -1
- package/dist/env/getEnvSchemas.js +2 -1
- package/dist/getComponent.d.ts.map +1 -1
- package/dist/getComponent.js +2 -1
- package/dist/getRoutes.d.ts.map +1 -1
- package/dist/getRoutes.js +2 -1
- package/dist/hmr/applyHmrModule.d.ts +11 -0
- package/dist/hmr/applyHmrModule.d.ts.map +1 -0
- package/dist/hmr/applyHmrModule.js +78 -0
- package/dist/hmr/applyHmrModule.sxyt.d.ts +2 -0
- package/dist/hmr/applyHmrModule.sxyt.d.ts.map +1 -0
- package/dist/hmr/applyHmrModule.sxyt.js +182 -0
- package/dist/hmr/componentIdentity.d.ts +9 -0
- package/dist/hmr/componentIdentity.d.ts.map +1 -0
- package/dist/hmr/componentIdentity.js +17 -0
- package/dist/hmr/instanceRegistry.d.ts +4 -0
- package/dist/hmr/instanceRegistry.d.ts.map +1 -0
- package/dist/hmr/instanceRegistry.js +6 -0
- package/dist/hmr/instanceRegistry.sxyt.d.ts +2 -0
- package/dist/hmr/instanceRegistry.sxyt.d.ts.map +1 -0
- package/dist/hmr/instanceRegistry.sxyt.js +24 -0
- package/dist/hmr/liveReloadHub.d.ts +10 -0
- package/dist/hmr/liveReloadHub.d.ts.map +1 -0
- package/dist/{dev → hmr}/liveReloadHub.js +8 -2
- package/dist/hmr/refreshStylesheets.d.ts +2 -0
- package/dist/hmr/refreshStylesheets.d.ts.map +1 -0
- package/dist/hmr/refreshStylesheets.js +26 -0
- package/dist/hmr/refreshStylesheets.sxyt.d.ts +2 -0
- package/dist/hmr/refreshStylesheets.sxyt.d.ts.map +1 -0
- package/dist/hmr/refreshStylesheets.sxyt.js +46 -0
- package/dist/hydrate/hydrate.d.ts +9 -1
- package/dist/hydrate/hydrate.d.ts.map +1 -1
- package/dist/hydrate/hydrate.js +31 -7
- package/dist/hydrate/hydrate.sxyt.d.ts +2 -0
- package/dist/hydrate/hydrate.sxyt.d.ts.map +1 -0
- package/dist/hydrate/hydrate.sxyt.js +302 -0
- package/dist/hydrate/reconcileHydratedAttributes.d.ts.map +1 -1
- package/dist/hydrate/reconcileHydratedAttributes.js +10 -2
- package/dist/hydrate/reconcileHydratedAttributes.sxyt.js +17 -0
- package/dist/importDevModuleWithRetry.d.ts +3 -0
- package/dist/importDevModuleWithRetry.d.ts.map +1 -0
- package/dist/importDevModuleWithRetry.js +46 -0
- package/dist/importDevModuleWithRetry.sxyt.d.ts +2 -0
- package/dist/importDevModuleWithRetry.sxyt.d.ts.map +1 -0
- package/dist/importDevModuleWithRetry.sxyt.js +104 -0
- package/dist/render/HeadData.d.ts +1 -0
- package/dist/render/HeadData.d.ts.map +1 -1
- package/dist/render/contentToHtml.d.ts.map +1 -1
- package/dist/render/contentToHtml.js +2 -1
- package/dist/render/processHead.d.ts.map +1 -1
- package/dist/render/processHead.js +47 -6
- package/dist/render/processHead.sxyt.d.ts +2 -0
- package/dist/render/processHead.sxyt.d.ts.map +1 -0
- package/dist/render/processHead.sxyt.js +93 -0
- package/dist/update/hotSwap.sxyt.d.ts +2 -0
- package/dist/update/hotSwap.sxyt.d.ts.map +1 -0
- package/dist/update/hotSwap.sxyt.js +252 -0
- package/dist/update/integrateUpdateVdom.d.ts +5 -1
- package/dist/update/integrateUpdateVdom.d.ts.map +1 -1
- package/dist/update/integrateUpdateVdom.js +107 -4
- package/dist/warnOnFrameworkChange.d.ts +2 -0
- package/dist/warnOnFrameworkChange.d.ts.map +1 -0
- package/dist/warnOnFrameworkChange.js +27 -0
- package/package.json +1 -1
- package/dist/dev/liveReloadHub.d.ts +0 -6
- package/dist/dev/liveReloadHub.d.ts.map +0 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
|
|
2
|
+
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
|
|
3
|
+
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
4
|
+
import { expect } from 'chai';
|
|
5
|
+
import { Window } from 'happy-dom';
|
|
6
|
+
import { describe } from 'sxy-test-runner';
|
|
7
|
+
import { TungstenSimpleComponent } from "../Components.js";
|
|
8
|
+
import { Instance } from "../Instance/Instance.js";
|
|
9
|
+
import { initStores } from "../Servers/initStores.js";
|
|
10
|
+
import { UpdateScheduler } from "../update/UpdateScheduler.js";
|
|
11
|
+
import { contentTreeToRenderingTree } from "../render/contentTreeToRenderingTree.js";
|
|
12
|
+
import { renderingNodeToHtml } from "../render/renderingTreeToHtml.js";
|
|
13
|
+
import { DomPointer } from "../DomPointer.js";
|
|
14
|
+
import { hydrateRootComponent } from "./hydrate.js";
|
|
15
|
+
import { WideRenderedContentNodeIsComponent } from "../RenderedContentNode.js";
|
|
16
|
+
import { AnyComponentIsFunctionComponentStandIn } from "../AnyComponent.js";
|
|
17
|
+
import { hotSwapFunctionComponent } from "../update/integrateUpdateVdom.js";
|
|
18
|
+
import { setCurrentInstance } from "../hmr/instanceRegistry.js";
|
|
19
|
+
import { applyHmrModule } from "../hmr/applyHmrModule.js";
|
|
20
|
+
import { jsxs as _jsxs, jsx as _jsx } from "tungsten/jsxRuntime.js";
|
|
21
|
+
const Greeting = ({
|
|
22
|
+
name
|
|
23
|
+
}) => {
|
|
24
|
+
return _jsxs("button", {
|
|
25
|
+
children: ["Hello ", name]
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
Greeting.style = {
|
|
29
|
+
moduleName: 'Greeting',
|
|
30
|
+
tags: {
|
|
31
|
+
button: 'greeting-button'
|
|
32
|
+
},
|
|
33
|
+
classes: {}
|
|
34
|
+
};
|
|
35
|
+
class Root extends TungstenSimpleComponent {
|
|
36
|
+
constructor(...args) {
|
|
37
|
+
super(...args);
|
|
38
|
+
_defineProperty(this, "content", () => _jsx(Greeting, {
|
|
39
|
+
name: "Ada"
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function makeInstance(document, runtimeContext) {
|
|
44
|
+
const location = '/';
|
|
45
|
+
const globalData = {};
|
|
46
|
+
const matchedRouteInfo = {
|
|
47
|
+
key: null,
|
|
48
|
+
params: {},
|
|
49
|
+
content: null
|
|
50
|
+
};
|
|
51
|
+
const {
|
|
52
|
+
internalStore,
|
|
53
|
+
stores,
|
|
54
|
+
storeSubscriptions
|
|
55
|
+
} = initStores(location, null, globalData, matchedRouteInfo);
|
|
56
|
+
const instanceConfig = {
|
|
57
|
+
prod: false,
|
|
58
|
+
useRootComponent: true,
|
|
59
|
+
clientSideNav: false,
|
|
60
|
+
siteBasePath: '/',
|
|
61
|
+
staticSite: false
|
|
62
|
+
};
|
|
63
|
+
const emptyRoutes = {}; // eslint-disable-line no-type-assertion/no-type-assertion -- not exercised by this test
|
|
64
|
+
return new Instance(location, instanceConfig, null, emptyRoutes, emptyRoutes,
|
|
65
|
+
// eslint-disable-line no-type-assertion/no-type-assertion
|
|
66
|
+
internalStore, stores, storeSubscriptions, globalData, matchedRouteInfo, runtimeContext);
|
|
67
|
+
}
|
|
68
|
+
describe('hydrateRootComponent() - function component styling', ({
|
|
69
|
+
it
|
|
70
|
+
}) => {
|
|
71
|
+
it('keeps the server-rendered class attribute on a styled function component after hydration', async () => {
|
|
72
|
+
const window = new Window();
|
|
73
|
+
const document = window.document; // eslint-disable-line no-type-assertion/no-type-assertion -- happy-dom's Document is structurally compatible
|
|
74
|
+
const bridgedGlobals = globalThis; // eslint-disable-line no-type-assertion/no-type-assertion -- bridging happy-dom's globals onto the global scope
|
|
75
|
+
bridgedGlobals.Node = window.Node;
|
|
76
|
+
bridgedGlobals.Text = window.Text;
|
|
77
|
+
bridgedGlobals.Comment = window.Comment;
|
|
78
|
+
bridgedGlobals.Element = window.Element;
|
|
79
|
+
bridgedGlobals.HTMLElement = window.HTMLElement;
|
|
80
|
+
bridgedGlobals.Document = window.Document;
|
|
81
|
+
bridgedGlobals.document = document;
|
|
82
|
+
bridgedGlobals.window = window;
|
|
83
|
+
// navigation: false on both instances - this test never touches routing, and it sidesteps
|
|
84
|
+
// Instance's popstate listener setup, which reaches for the process-wide `window` global
|
|
85
|
+
// when no runtimeContext is given (isClient.ts caches typeof window at first import, and
|
|
86
|
+
// other test files in this same process import happy-dom, so relying on "no runtimeContext
|
|
87
|
+
// means server" here is not reliable the way it is in the real server/client bundle split)
|
|
88
|
+
const ssrScheduler = new UpdateScheduler();
|
|
89
|
+
const ssrInstance = makeInstance(document, {
|
|
90
|
+
document,
|
|
91
|
+
window: document.defaultView,
|
|
92
|
+
scheduler: ssrScheduler,
|
|
93
|
+
navigation: false
|
|
94
|
+
});
|
|
95
|
+
const tree = await contentTreeToRenderingTree(_jsx(Root, {}), null, ssrInstance, false);
|
|
96
|
+
const headData = {
|
|
97
|
+
found: false,
|
|
98
|
+
instance: ssrInstance,
|
|
99
|
+
clientImportAliases: {},
|
|
100
|
+
importAliases: {},
|
|
101
|
+
compilationLocation: '',
|
|
102
|
+
dev: false,
|
|
103
|
+
tungstenPublicEnv: undefined,
|
|
104
|
+
siteBasePath: '',
|
|
105
|
+
hmr: true
|
|
106
|
+
};
|
|
107
|
+
const html = await renderingNodeToHtml(tree, false, headData, null);
|
|
108
|
+
// styleContentSxy prefixes every tag match with componentBumpClass ('__c__')
|
|
109
|
+
expect(html).to.contain('class="__c__ greeting-button"');
|
|
110
|
+
document.body.innerHTML = html;
|
|
111
|
+
// hydrate exactly the way the client does
|
|
112
|
+
const scheduler = new UpdateScheduler();
|
|
113
|
+
const runtimeContext = {
|
|
114
|
+
document,
|
|
115
|
+
window: document.defaultView,
|
|
116
|
+
scheduler,
|
|
117
|
+
navigation: false
|
|
118
|
+
};
|
|
119
|
+
const hydrateInstance = makeInstance(document, runtimeContext);
|
|
120
|
+
const domPointer = new DomPointer(document.body, 0);
|
|
121
|
+
await hydrateRootComponent(hydrateInstance, Root, domPointer, {
|
|
122
|
+
insideForm: false
|
|
123
|
+
});
|
|
124
|
+
const button = document.body.querySelector('button');
|
|
125
|
+
expect(button?.getAttribute('class')).to.equal('__c__ greeting-button');
|
|
126
|
+
scheduler.dispose();
|
|
127
|
+
ssrScheduler.dispose();
|
|
128
|
+
});
|
|
129
|
+
it('sets __renderedNode during hydration, so a hot-swap can find and replace it', async () => {
|
|
130
|
+
// hydrateFunctionComponent builds its own wrapper independently of the reconciler's
|
|
131
|
+
// equivalent functions in integrateUpdateVdom.ts - those set element.__renderedNode, but
|
|
132
|
+
// hydration never did, so hotSwapPrepare() (which requires it) silently bailed out on
|
|
133
|
+
// every hydrated component in the real client, and applyHmrModule() counted the swap as
|
|
134
|
+
// successful anyway because it didn't check the swap functions' return value. Both are
|
|
135
|
+
// fixed; this reproduces the full round trip end to end.
|
|
136
|
+
const window = new Window();
|
|
137
|
+
const document = window.document; // eslint-disable-line no-type-assertion/no-type-assertion -- happy-dom's Document is structurally compatible
|
|
138
|
+
const bridgedGlobals = globalThis; // eslint-disable-line no-type-assertion/no-type-assertion -- bridging happy-dom's globals onto the global scope
|
|
139
|
+
bridgedGlobals.Node = window.Node;
|
|
140
|
+
bridgedGlobals.Text = window.Text;
|
|
141
|
+
bridgedGlobals.Comment = window.Comment;
|
|
142
|
+
bridgedGlobals.Element = window.Element;
|
|
143
|
+
bridgedGlobals.HTMLElement = window.HTMLElement;
|
|
144
|
+
bridgedGlobals.Document = window.Document;
|
|
145
|
+
bridgedGlobals.document = document;
|
|
146
|
+
bridgedGlobals.window = window;
|
|
147
|
+
const ssrScheduler = new UpdateScheduler();
|
|
148
|
+
const ssrInstance = makeInstance(document, {
|
|
149
|
+
document,
|
|
150
|
+
window: document.defaultView,
|
|
151
|
+
scheduler: ssrScheduler,
|
|
152
|
+
navigation: false
|
|
153
|
+
});
|
|
154
|
+
const tree = await contentTreeToRenderingTree(_jsx(Root, {}), null, ssrInstance, false);
|
|
155
|
+
const headData = {
|
|
156
|
+
found: false,
|
|
157
|
+
instance: ssrInstance,
|
|
158
|
+
clientImportAliases: {},
|
|
159
|
+
importAliases: {},
|
|
160
|
+
compilationLocation: '',
|
|
161
|
+
dev: false,
|
|
162
|
+
tungstenPublicEnv: undefined,
|
|
163
|
+
siteBasePath: '',
|
|
164
|
+
hmr: true
|
|
165
|
+
};
|
|
166
|
+
const html = await renderingNodeToHtml(tree, false, headData, null);
|
|
167
|
+
document.body.innerHTML = html;
|
|
168
|
+
const scheduler = new UpdateScheduler();
|
|
169
|
+
const runtimeContext = {
|
|
170
|
+
document,
|
|
171
|
+
window: document.defaultView,
|
|
172
|
+
scheduler,
|
|
173
|
+
navigation: false
|
|
174
|
+
};
|
|
175
|
+
const hydrateInstance = makeInstance(document, runtimeContext);
|
|
176
|
+
const domPointer = new DomPointer(document.body, 0);
|
|
177
|
+
const rootElement = await hydrateRootComponent(hydrateInstance, Root, domPointer, {
|
|
178
|
+
insideForm: false
|
|
179
|
+
});
|
|
180
|
+
const wrapper = rootElement.__currentContent;
|
|
181
|
+
if (!WideRenderedContentNodeIsComponent(wrapper) || !AnyComponentIsFunctionComponentStandIn(wrapper.element)) {
|
|
182
|
+
throw new Error('expected the hydrated root to directly wrap a single function component');
|
|
183
|
+
}
|
|
184
|
+
const hydratedElement = wrapper.element;
|
|
185
|
+
expect(hydratedElement.__renderedNode).to.not.equal(undefined);
|
|
186
|
+
const NewGreeting = ({
|
|
187
|
+
name
|
|
188
|
+
}) => {
|
|
189
|
+
return _jsxs("button", {
|
|
190
|
+
children: ["Hi there ", name]
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
NewGreeting.style = {
|
|
194
|
+
moduleName: 'Greeting',
|
|
195
|
+
tags: {
|
|
196
|
+
button: 'greeting-button'
|
|
197
|
+
},
|
|
198
|
+
classes: {}
|
|
199
|
+
};
|
|
200
|
+
const swapped = await hotSwapFunctionComponent(hydratedElement, NewGreeting);
|
|
201
|
+
expect(swapped).to.not.equal(null);
|
|
202
|
+
expect(document.body.innerHTML).to.contain('Hi there Ada');
|
|
203
|
+
expect(document.body.innerHTML).to.not.contain('Hello Ada');
|
|
204
|
+
scheduler.dispose();
|
|
205
|
+
ssrScheduler.dispose();
|
|
206
|
+
});
|
|
207
|
+
it('end-to-end: SSR -> hydrate -> a real dev-mode edit applies without a full reload', async () => {
|
|
208
|
+
// This is the high-level test for the actual, user-visible bug ("editing a component
|
|
209
|
+
// always triggers a full page reload instead of an in-place swap"). It goes through the
|
|
210
|
+
// real hydrate() -> applyHmrModule() pipeline exactly as the dev-mode reload script does
|
|
211
|
+
// (setCurrentInstance() + applyHmrModule(), not a lower-level helper like
|
|
212
|
+
// hotSwapFunctionComponent() called directly), so it catches applyHmrModule()
|
|
213
|
+
// miscounting a failed swap as successful and hydration never setting __renderedNode in
|
|
214
|
+
// one broad assertion (DOM shows the edited content) rather than two narrow ones.
|
|
215
|
+
//
|
|
216
|
+
// It does NOT cover the third cause (the registration gate checking
|
|
217
|
+
// runtimeContext !== undefined instead of isClient) - this test, like the others in this
|
|
218
|
+
// file, deliberately passes a real runtimeContext to sidestep isClient.ts's module-load-
|
|
219
|
+
// order flakiness in a shared test process, so runtimeContext is always defined here
|
|
220
|
+
// regardless of which gate the source uses. FunctionComponentStandIn.sxyt.ts covers that
|
|
221
|
+
// cause specifically, by forcing isClient directly instead of relying on either detection
|
|
222
|
+
// path.
|
|
223
|
+
const window = new Window();
|
|
224
|
+
const document = window.document; // eslint-disable-line no-type-assertion/no-type-assertion -- happy-dom's Document is structurally compatible
|
|
225
|
+
const bridgedGlobals = globalThis; // eslint-disable-line no-type-assertion/no-type-assertion -- bridging happy-dom's globals onto the global scope
|
|
226
|
+
bridgedGlobals.Node = window.Node;
|
|
227
|
+
bridgedGlobals.Text = window.Text;
|
|
228
|
+
bridgedGlobals.Comment = window.Comment;
|
|
229
|
+
bridgedGlobals.Element = window.Element;
|
|
230
|
+
bridgedGlobals.HTMLElement = window.HTMLElement;
|
|
231
|
+
bridgedGlobals.Document = window.Document;
|
|
232
|
+
bridgedGlobals.document = document;
|
|
233
|
+
bridgedGlobals.window = window;
|
|
234
|
+
const ssrScheduler = new UpdateScheduler();
|
|
235
|
+
const ssrInstance = makeInstance(document, {
|
|
236
|
+
document,
|
|
237
|
+
window: document.defaultView,
|
|
238
|
+
scheduler: ssrScheduler,
|
|
239
|
+
navigation: false
|
|
240
|
+
});
|
|
241
|
+
const tree = await contentTreeToRenderingTree(_jsx(Root, {}), null, ssrInstance, false);
|
|
242
|
+
const headData = {
|
|
243
|
+
found: false,
|
|
244
|
+
instance: ssrInstance,
|
|
245
|
+
clientImportAliases: {},
|
|
246
|
+
importAliases: {},
|
|
247
|
+
compilationLocation: '',
|
|
248
|
+
dev: false,
|
|
249
|
+
tungstenPublicEnv: undefined,
|
|
250
|
+
siteBasePath: '',
|
|
251
|
+
hmr: true
|
|
252
|
+
};
|
|
253
|
+
const html = await renderingNodeToHtml(tree, false, headData, null);
|
|
254
|
+
document.body.innerHTML = html;
|
|
255
|
+
const scheduler = new UpdateScheduler();
|
|
256
|
+
const runtimeContext = {
|
|
257
|
+
document,
|
|
258
|
+
window: document.defaultView,
|
|
259
|
+
scheduler,
|
|
260
|
+
navigation: false
|
|
261
|
+
};
|
|
262
|
+
const hydrateInstance = makeInstance(document, runtimeContext);
|
|
263
|
+
// exactly what hydrate.tsx's real hydrate() does right after constructing its Instance
|
|
264
|
+
setCurrentInstance(hydrateInstance);
|
|
265
|
+
const domPointer = new DomPointer(document.body, 0);
|
|
266
|
+
await hydrateRootComponent(hydrateInstance, Root, domPointer, {
|
|
267
|
+
insideForm: false
|
|
268
|
+
});
|
|
269
|
+
// stands in for "the file was edited and the browser re-imported it fresh" - a
|
|
270
|
+
// same-named export with different markup, exactly like applyHmrModule() receives from
|
|
271
|
+
// the dev-mode reload script
|
|
272
|
+
const EditedGreeting = ({
|
|
273
|
+
name
|
|
274
|
+
}) => {
|
|
275
|
+
return _jsxs("button", {
|
|
276
|
+
children: ["Edited ", name]
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
// identity matching is by name, and a const's inferred .name is its own variable name
|
|
280
|
+
// ("EditedGreeting") - force it to match the export name the real dev-mode reload script
|
|
281
|
+
// would see when re-importing the edited file (the export is still called "Greeting")
|
|
282
|
+
Object.defineProperty(EditedGreeting, 'name', {
|
|
283
|
+
value: 'Greeting'
|
|
284
|
+
});
|
|
285
|
+
EditedGreeting.style = {
|
|
286
|
+
moduleName: 'Greeting',
|
|
287
|
+
tags: {
|
|
288
|
+
button: 'greeting-button'
|
|
289
|
+
},
|
|
290
|
+
classes: {}
|
|
291
|
+
};
|
|
292
|
+
const result = await applyHmrModule({
|
|
293
|
+
Greeting: EditedGreeting
|
|
294
|
+
}, '/test/end-to-end-Greeting.tsx');
|
|
295
|
+
expect(result.matched).to.equal(true);
|
|
296
|
+
expect(result.orphaned).to.equal(false);
|
|
297
|
+
expect(document.body.innerHTML).to.contain('Edited Ada');
|
|
298
|
+
expect(document.body.innerHTML).to.not.contain('Hello Ada');
|
|
299
|
+
scheduler.dispose();
|
|
300
|
+
ssrScheduler.dispose();
|
|
301
|
+
});
|
|
302
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcileHydratedAttributes.d.ts","sourceRoot":"","sources":["../../src/hydrate/reconcileHydratedAttributes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAIxD,MAAM,MAAM,iBAAiB,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,gFAAgF;IAChF,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,CAAA;AA6BD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,2BAA2B,CACvC,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,WAAW,EACpB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,IAAI,GACnD,IAAI,
|
|
1
|
+
{"version":3,"file":"reconcileHydratedAttributes.d.ts","sourceRoot":"","sources":["../../src/hydrate/reconcileHydratedAttributes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAIxD,MAAM,MAAM,iBAAiB,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,gFAAgF;IAChF,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,CAAA;AA6BD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,2BAA2B,CACvC,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,WAAW,EACpB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,IAAI,GACnD,IAAI,CAuCN"}
|
|
@@ -40,9 +40,17 @@ function setAttribute(domNode, name, value) {
|
|
|
40
40
|
*/
|
|
41
41
|
export function reconcileHydratedAttributes(node, domNode, onMismatch) {
|
|
42
42
|
// A copy: processProps() rewrites what it is given, and these props stay in
|
|
43
|
-
// the content tree for the next update to diff against.
|
|
43
|
+
// the content tree for the next update to diff against. children is pulled out
|
|
44
|
+
// first, same as the server renderer (renderingTreeToHtml.ts) and the client
|
|
45
|
+
// update path (integrateUpdateVdom.ts's processAttributes()) both do - it is
|
|
46
|
+
// rendered content, not an HTML attribute, and processProps() was never meant
|
|
47
|
+
// to receive it.
|
|
48
|
+
const {
|
|
49
|
+
children: _children,
|
|
50
|
+
...restProps
|
|
51
|
+
} = node[$props]; // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
44
52
|
const expected = processProps({
|
|
45
|
-
...
|
|
53
|
+
...restProps
|
|
46
54
|
});
|
|
47
55
|
const handled = new Set();
|
|
48
56
|
for (const name in expected) {
|
|
@@ -142,6 +142,23 @@ describe('reconcileHydratedAttributes()', ({
|
|
|
142
142
|
});
|
|
143
143
|
expect(mismatches).to.deep.equal([]);
|
|
144
144
|
});
|
|
145
|
+
// children is rendered content, not an attribute - the server renderer and the client
|
|
146
|
+
// update path both strip it before comparing/writing attributes, and hydration has to
|
|
147
|
+
// match or it writes the stringified child list (e.g. "[object Object],[object Object]")
|
|
148
|
+
// onto every element in the page as a literal "children" attribute.
|
|
149
|
+
it('does not write children as an attribute', () => {
|
|
150
|
+
const {
|
|
151
|
+
attributes,
|
|
152
|
+
mismatches
|
|
153
|
+
} = reconcile('div', {
|
|
154
|
+
id: 'd',
|
|
155
|
+
children: ['a', 'b']
|
|
156
|
+
}, '<div id="d"></div>');
|
|
157
|
+
expect(attributes).to.deep.equal({
|
|
158
|
+
id: 'd'
|
|
159
|
+
});
|
|
160
|
+
expect(mismatches).to.deep.equal([]);
|
|
161
|
+
});
|
|
145
162
|
it('reports what changed, for a dev-mode warning', () => {
|
|
146
163
|
const {
|
|
147
164
|
mismatches
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"importDevModuleWithRetry.d.ts","sourceRoot":"","sources":["../src/importDevModuleWithRetry.ts"],"names":[],"mappings":"AAuBA,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAU7D;AAMD,wBAAsB,wBAAwB,CAC1C,GAAG,EAAE,MAAM,EACX,QAAQ,SAAK,EACb,OAAO,SAAM,GACd,OAAO,CAAC,GAAG,CAAC,CAUd"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// importDevModuleWithRetry
|
|
2
|
+
// exports importDevModuleWithRetry (function)
|
|
3
|
+
//
|
|
4
|
+
// Dev-mode dynamic import wrapper. tsc's own --watch mode emits a multi-file
|
|
5
|
+
// edit (e.g. a rename spanning two files) one recompiled file at a time, so
|
|
6
|
+
// there's a real window where one file has landed and the other hasn't yet.
|
|
7
|
+
// A dev-server request for a component in that window hits a genuine (but
|
|
8
|
+
// self-resolving within a few hundred ms) module-shape error - without this,
|
|
9
|
+
// that's a scary SSR error page for what is, from the developer's
|
|
10
|
+
// perspective, a completely normal edit. Retrying a few times with a fresh
|
|
11
|
+
// cache-busting query per attempt rides out the window; a real, permanent
|
|
12
|
+
// error still surfaces (just slightly slower) once attempts are exhausted.
|
|
13
|
+
// A two-file edit's compile cycle runs tsc's incremental emit followed by a
|
|
14
|
+
// separate downstream babel recompile, sequentially - observed live to take
|
|
15
|
+
// several seconds under load, so the default budget below is generous rather
|
|
16
|
+
// than tuned to a happy-path guess. There's no precise "compile cycle
|
|
17
|
+
// finished" signal plumbed through to here to await instead; this is a
|
|
18
|
+
// bounded heuristic, not a proper synchronization fix.
|
|
19
|
+
// Exported so other dev-only tools (the error page's auto-retry script - see
|
|
20
|
+
// devErrorPageAutoRetry.ts) can recognize the same transient shape without
|
|
21
|
+
// duplicating the heuristic.
|
|
22
|
+
export function isTransientDevModuleError(e) {
|
|
23
|
+
if (!(e instanceof Error)) return false;
|
|
24
|
+
if (e.code === 'ERR_MODULE_NOT_FOUND') return true;
|
|
25
|
+
if (/does not provide an export named/.test(e.message)) return true;
|
|
26
|
+
// A within-file version of the same race: a multi-step edit can transiently save a file
|
|
27
|
+
// whose import line was updated but whose JSX/body still references the old local name -
|
|
28
|
+
// that's a plain runtime ReferenceError, not an ESM linking error, but self-resolves the same
|
|
29
|
+
// way once the file's own recompile catches up. Kept narrow (a bare identifier only) so it
|
|
30
|
+
// doesn't swallow other ReferenceError shapes that are more likely to be genuine bugs.
|
|
31
|
+
return e instanceof ReferenceError && /^\w+ is not defined$/.test(e.message);
|
|
32
|
+
}
|
|
33
|
+
function delay(ms) {
|
|
34
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
35
|
+
}
|
|
36
|
+
export async function importDevModuleWithRetry(url, attempts = 30, delayMs = 250) {
|
|
37
|
+
for (let attempt = 0;; attempt++) {
|
|
38
|
+
try {
|
|
39
|
+
const attemptUrl = attempt === 0 ? url : `${url}${url.includes('?') ? '&' : '?'}retryAttempt=${attempt}`;
|
|
40
|
+
return await import(attemptUrl); // eslint-disable-line @typescript-eslint/no-unsafe-return
|
|
41
|
+
} catch (e) {
|
|
42
|
+
if (attempt >= attempts - 1 || !isTransientDevModuleError(e)) throw e;
|
|
43
|
+
await delay(delayMs);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"importDevModuleWithRetry.sxyt.d.ts","sourceRoot":"","sources":["../src/importDevModuleWithRetry.sxyt.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { describe } from 'sxy-test-runner';
|
|
3
|
+
import { mkdtemp, rm, writeFile } from 'fs/promises';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { importDevModuleWithRetry, isTransientDevModuleError } from "./importDevModuleWithRetry.js";
|
|
7
|
+
function delay(ms) {
|
|
8
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
async function makeFixture() {
|
|
11
|
+
const dir = await mkdtemp(join(tmpdir(), 'tungsten-import-dev-module-retry-'));
|
|
12
|
+
const depPath = join(dir, 'dep.mjs');
|
|
13
|
+
const entryPath = join(dir, 'entry.mjs');
|
|
14
|
+
// dep.mjs never changes - it's the entry file itself that's momentarily
|
|
15
|
+
// stale on disk, exactly like Home.js briefly still reading its
|
|
16
|
+
// pre-rename import statement while Button.js has already been rewritten
|
|
17
|
+
await writeFile(depPath, `export const NewName = 2\n`);
|
|
18
|
+
await writeFile(entryPath, `export { OldName as NewName } from './dep.mjs'\n`);
|
|
19
|
+
return {
|
|
20
|
+
dir,
|
|
21
|
+
depPath,
|
|
22
|
+
entryPath
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
describe('importDevModuleWithRetry()', ({
|
|
26
|
+
it
|
|
27
|
+
}) => {
|
|
28
|
+
it(`rides out a transient module-shape mismatch, succeeding once the entry file itself catches up`, async () => {
|
|
29
|
+
const {
|
|
30
|
+
dir,
|
|
31
|
+
entryPath
|
|
32
|
+
} = await makeFixture();
|
|
33
|
+
try {
|
|
34
|
+
// simulate the entry file's own recompile (e.g. babel finishing
|
|
35
|
+
// its pass over Home.tsx) landing a little after this request
|
|
36
|
+
// started - the exact race this helper exists for
|
|
37
|
+
void delay(150).then(() => writeFile(entryPath, `export { NewName } from './dep.mjs'\n`));
|
|
38
|
+
const module = await importDevModuleWithRetry(`file://${entryPath}`, 10, 50);
|
|
39
|
+
expect(module.NewName).to.equal(2);
|
|
40
|
+
} finally {
|
|
41
|
+
await rm(dir, {
|
|
42
|
+
recursive: true,
|
|
43
|
+
force: true
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
it(`gives up and throws the real error once attempts are exhausted`, async () => {
|
|
48
|
+
const {
|
|
49
|
+
dir,
|
|
50
|
+
entryPath
|
|
51
|
+
} = await makeFixture();
|
|
52
|
+
try {
|
|
53
|
+
let threw = false;
|
|
54
|
+
try {
|
|
55
|
+
await importDevModuleWithRetry(`file://${entryPath}`, 3, 20);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
threw = true;
|
|
58
|
+
expect(String(e)).to.contain('does not provide an export');
|
|
59
|
+
}
|
|
60
|
+
expect(threw).to.equal(true);
|
|
61
|
+
} finally {
|
|
62
|
+
await rm(dir, {
|
|
63
|
+
recursive: true,
|
|
64
|
+
force: true
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
it(`does not retry a genuine, unrelated error - so real bugs still surface immediately`, async () => {
|
|
69
|
+
const dir = await mkdtemp(join(tmpdir(), 'tungsten-import-dev-module-retry-real-error-'));
|
|
70
|
+
try {
|
|
71
|
+
const brokenPath = join(dir, 'broken.mjs');
|
|
72
|
+
await writeFile(brokenPath, `this is not valid javascript {{{\n`);
|
|
73
|
+
const start = Date.now();
|
|
74
|
+
let threw = false;
|
|
75
|
+
try {
|
|
76
|
+
await importDevModuleWithRetry(`file://${brokenPath}`, 10, 200);
|
|
77
|
+
} catch {
|
|
78
|
+
threw = true;
|
|
79
|
+
}
|
|
80
|
+
expect(threw).to.equal(true);
|
|
81
|
+
// a real syntax error must not be retried - if it were, this would
|
|
82
|
+
// take multiple * 200ms delays instead of failing on the first attempt
|
|
83
|
+
expect(Date.now() - start).to.be.lessThan(200);
|
|
84
|
+
} finally {
|
|
85
|
+
await rm(dir, {
|
|
86
|
+
recursive: true,
|
|
87
|
+
force: true
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('isTransientDevModuleError()', ({
|
|
93
|
+
it
|
|
94
|
+
}) => {
|
|
95
|
+
it(`recognizes a bare "X is not defined" ReferenceError - the within-file version of the same race, seen live when a file's JSX still referenced a just-renamed import mid-multi-step-edit`, () => {
|
|
96
|
+
expect(isTransientDevModuleError(new ReferenceError('Button is not defined'))).to.equal(true);
|
|
97
|
+
});
|
|
98
|
+
it('does not match a differently-shaped ReferenceError, to stay a narrow signal', () => {
|
|
99
|
+
expect(isTransientDevModuleError(new ReferenceError('Cannot access X before initialization'))).to.equal(false);
|
|
100
|
+
});
|
|
101
|
+
it('does not match an unrelated TypeError', () => {
|
|
102
|
+
expect(isTransientDevModuleError(new TypeError(`Cannot read properties of undefined (reading 'foo')`))).to.equal(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HeadData.d.ts","sourceRoot":"","sources":["../../src/render/HeadData.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAC9D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAE9D,MAAM,MAAM,YAAY,GAAG,EAAE,GAAG,IAAI,MAAM,EAAE,CAAA;AAE5C,MAAM,MAAM,QAAQ,GAAG;IACnB,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,QAAQ,CAAC;IACnB,mBAAmB,EAAE,aAAa,CAAC;IACnC,aAAa,EAAE,aAAa,CAAC;IAC7B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,GAAG,EAAE,OAAO,CAAC;IACb,iBAAiB,EAAE,sBAAsB,CAAC;IAC1C,YAAY,EAAE,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"HeadData.d.ts","sourceRoot":"","sources":["../../src/render/HeadData.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAC9D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAE9D,MAAM,MAAM,YAAY,GAAG,EAAE,GAAG,IAAI,MAAM,EAAE,CAAA;AAE5C,MAAM,MAAM,QAAQ,GAAG;IACnB,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,QAAQ,CAAC;IACnB,mBAAmB,EAAE,aAAa,CAAC;IACnC,aAAa,EAAE,aAAa,CAAC;IAC7B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,GAAG,EAAE,OAAO,CAAC;IACb,iBAAiB,EAAE,sBAAsB,CAAC;IAC1C,YAAY,EAAE,YAAY,CAAC;IAC3B,GAAG,EAAE,OAAO,GAAG,QAAQ,CAAA;CAC1B,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contentToHtml.d.ts","sourceRoot":"","sources":["../../src/render/contentToHtml.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,KAAK,EAAY,YAAY,EAAE,MAAM,eAAe,CAAA;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAEpD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAMvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAI9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAA;AAGrE,wBAAsB,aAAa,CAC/B,eAAe,EAAE,eAAe,EAAE,mBAAmB,EAAE,aAAa,EACpE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EACxC,MAAM,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,YAAY,EACnE,iBAAiB,GAAE,sBAAkC,EAAE,GAAG,UAAQ,GACnE,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"contentToHtml.d.ts","sourceRoot":"","sources":["../../src/render/contentToHtml.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,KAAK,EAAY,YAAY,EAAE,MAAM,eAAe,CAAA;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAEpD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAMvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAI9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAA;AAGrE,wBAAsB,aAAa,CAC/B,eAAe,EAAE,eAAe,EAAE,mBAAmB,EAAE,aAAa,EACpE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EACxC,MAAM,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,YAAY,EACnE,iBAAiB,GAAE,sBAAkC,EAAE,GAAG,UAAQ,GACnE,OAAO,CAAC,MAAM,CAAC,CAqDjB"}
|
|
@@ -22,7 +22,8 @@ export async function contentToHtml(compilationInfo, clientImportAliases, conten
|
|
|
22
22
|
compilationLocation: compilationInfo.buildCodeLocation,
|
|
23
23
|
dev,
|
|
24
24
|
tungstenPublicEnv,
|
|
25
|
-
siteBasePath: siteBasePathOverride
|
|
25
|
+
siteBasePath: siteBasePathOverride,
|
|
26
|
+
hmr: config.hmr
|
|
26
27
|
};
|
|
27
28
|
//console.log('rendering tree', inspect(tree, { depth: 99 } ))
|
|
28
29
|
let html = await renderingNodeToHtml(tree, false, headData, null);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"processHead.d.ts","sourceRoot":"","sources":["../../src/render/processHead.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAK7C,OAAO,EAAE,oBAAoB,EAAqB,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"processHead.d.ts","sourceRoot":"","sources":["../../src/render/processHead.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAK7C,OAAO,EAAE,oBAAoB,EAAqB,MAAM,qBAAqB,CAAA;AAS7E,wBAAgB,WAAW,CACvB,QAAQ,EAAE,oBAAoB,EAC9B,QAAQ,EAAE,QAAQ,QAwLrB"}
|
|
@@ -3,6 +3,8 @@ import { clientFolder, componentCssFolder, componentCssFile } from "../settings.
|
|
|
3
3
|
import { addImportMap } from "./addImportMap.js";
|
|
4
4
|
import { $props, $type } from "../contentNodeSymbols.js";
|
|
5
5
|
import { windowGlobalEnvStatement } from 'tungsten/env/windowGlobalEnvStatement.js';
|
|
6
|
+
import { frameworkDestLocation } from "../build/frameworkDestLocation.js";
|
|
7
|
+
import { devErrorPageClearRetryScript } from "../Servers/devErrorPageAutoRetry.js";
|
|
6
8
|
// process the tree returned by the root component
|
|
7
9
|
// doing things like ensuring there is are html and head elements,
|
|
8
10
|
// and adding necessary style and script tags
|
|
@@ -12,7 +14,8 @@ export function processHead(headNode, headData) {
|
|
|
12
14
|
importAliases,
|
|
13
15
|
clientImportAliases,
|
|
14
16
|
dev,
|
|
15
|
-
siteBasePath
|
|
17
|
+
siteBasePath,
|
|
18
|
+
hmr
|
|
16
19
|
} = headData;
|
|
17
20
|
//console.log('processing head. dev is', dev)
|
|
18
21
|
if (headNode[$props].children === undefined) headNode[$props].children = [];else if (!Array.isArray(headNode[$props].children)) headNode[$props].children = [headNode[$props].children];
|
|
@@ -59,16 +62,54 @@ export function processHead(headNode, headData) {
|
|
|
59
62
|
};
|
|
60
63
|
cjsHelpersScriptNode[$props].type = 'module';
|
|
61
64
|
headNode[$props].children.unshift(cjsHelpersScriptNode);
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
+
// A successful render means whatever the error page's auto-retry script (see
|
|
66
|
+
// devErrorPageAutoRetry.ts) may have been counting is over - clear it so a later,
|
|
67
|
+
// unrelated transient error gets its own full retry budget instead of inheriting a
|
|
68
|
+
// stale count.
|
|
69
|
+
const clearErrorRetryScriptNode = {
|
|
65
70
|
[$type]: 'script',
|
|
66
71
|
[$props]: {
|
|
67
72
|
'data--t-internal': undefined,
|
|
68
|
-
children:
|
|
73
|
+
children: devErrorPageClearRetryScript
|
|
69
74
|
}
|
|
70
75
|
};
|
|
71
|
-
headNode[$props].children.unshift(
|
|
76
|
+
headNode[$props].children.unshift(clearErrorRetryScriptNode);
|
|
77
|
+
// Auto-reload / HMR. This has to stay a raw inline script rather than a real imported
|
|
78
|
+
// module: it fetches whichever file just recompiled via a computed, cache-busted
|
|
79
|
+
// import() call, and any dynamic import() with a non-literal argument anywhere in the
|
|
80
|
+
// framework's traced dependency graph makes the package-copying step that serves
|
|
81
|
+
// framework code to the browser throw (it requires every import specifier to be
|
|
82
|
+
// statically known so it can be rewritten/copied). Keeping this logic as untraced text
|
|
83
|
+
// sidesteps that. applyHmrModule.js itself has no dynamic imports, so it's safely
|
|
84
|
+
// reachable via a normal static import elsewhere (see hydrate.tsx) and gets copied
|
|
85
|
+
// there, then loaded here by computed URL. See tungsten/docs/hmr.md.
|
|
86
|
+
// Tier 1 (hmr: 'reload', or nothing left for Tier 2 to do): always full-reload. A
|
|
87
|
+
// simple, independent branch from Tier 2's logic - restored as an explicit config value
|
|
88
|
+
// so it's available as a one-line escape hatch if Tier 2 itself is misbehaving.
|
|
89
|
+
const tier1OnlyScript = `(function(){` + `var es=new EventSource('${siteBasePath}/__tungsten_live_reload__');` + `es.addEventListener('css',function(){location.reload()});` + `es.addEventListener('js',function(){location.reload()});` + `})();`;
|
|
90
|
+
// Tier 2: attempt an in-place component/CSS hot-swap before falling back to a full
|
|
91
|
+
// reload. See tungsten/docs/hmr.md.
|
|
92
|
+
const tier2Script = `(function(){` + `var es=new EventSource('${siteBasePath}/__tungsten_live_reload__');` + `function fullReload(reason){console.info('[tungsten-hmr] full reload:',reason);location.reload()}` + `function loadHmr(){return import('${frameworkDestLocation}/hmr/applyHmrModule.js')}`
|
|
93
|
+
// CSS has no state to preserve, so re-applying it is always safe: refreshStylesheets()
|
|
94
|
+
// inserts the new link and only removes the old one once it's loaded, so there's no
|
|
95
|
+
// gap where nothing is applied (a plain href swap can produce a visible flash).
|
|
96
|
+
+ `es.addEventListener('css',function(){` + `console.info('[tungsten-hmr] css change, refreshing stylesheets');` + `loadHmr().then(function(hmr){return Promise.all(hmr.refreshStylesheets())})` + `.catch(function(e){console.error('[tungsten-hmr] stylesheet refresh failed',e)});` + `});` + `es.addEventListener('js',function(ev){` + `var payload;` + `try{payload=JSON.parse(ev.data)}catch(e){fullReload('bad payload');return}` + `var files=(payload&&payload.files)||[];` + `if(files.length===0){fullReload('no files in payload');return}` + `console.info('[tungsten-hmr] fetching',files);` + `loadHmr().then(function(hmr){`
|
|
97
|
+
// a component's own inline style block piggybacks on this same JS pipeline (it
|
|
98
|
+
// lives in the .tsx/.jsx source, not a separate .css file), so refresh stylesheets
|
|
99
|
+
// here too rather than only on a dedicated 'css' event - independently of the
|
|
100
|
+
// component swap below, so a stylesheet load error doesn't force a full reload.
|
|
101
|
+
+ `Promise.all(hmr.refreshStylesheets())` + `.catch(function(e){console.error('[tungsten-hmr] stylesheet refresh failed',e)});` + `return Promise.all(files.map(function(f){return import(f+'?t='+Date.now())}))` + `.then(function(modules){` + `return Promise.all(modules.map(function(m,i){return hmr.applyHmrModule(m,files[i])}))` + `})` + `})` + `.then(function(results){` + `console.info('[tungsten-hmr] results',results);` + `if(results.some(function(r){return r&&r.orphaned})){fullReload('a component export was renamed or removed');return}` + `if(!results.some(function(r){return r&&r.matched}))fullReload('no matching live component found')` + `})` + `.catch(function(e){console.error('[tungsten-hmr] swap failed',e);fullReload('error during swap')});` + `});` + `})();`;
|
|
102
|
+
// hmr === false: no live-reload script at all (auto-refresh fully disabled).
|
|
103
|
+
if (hmr !== false) {
|
|
104
|
+
const liveReloadScriptNode = {
|
|
105
|
+
[$type]: 'script',
|
|
106
|
+
[$props]: {
|
|
107
|
+
'data--t-internal': undefined,
|
|
108
|
+
children: hmr === true ? tier2Script : tier1OnlyScript
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
headNode[$props].children.unshift(liveReloadScriptNode);
|
|
112
|
+
}
|
|
72
113
|
}
|
|
73
114
|
const {
|
|
74
115
|
content: _,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"processHead.sxyt.d.ts","sourceRoot":"","sources":["../../src/render/processHead.sxyt.ts"],"names":[],"mappings":""}
|