aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--src/runtime.version2
-rw-r--r--src/runtime/hmr.ts169
3 files changed, 139 insertions, 35 deletions
diff --git a/.gitignore b/.gitignore
index 701e36fdf..387fba9b5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,4 +41,5 @@ esbuilddir
parceldist
esbuilddir
outdir/
-outcss \ No newline at end of file
+outcss
+.next \ No newline at end of file
diff --git a/src/runtime.version b/src/runtime.version
index 83772425a..b9f5adbff 100644
--- a/src/runtime.version
+++ b/src/runtime.version
@@ -1 +1 @@
-a1b2ed0e7019e499 \ No newline at end of file
+b735428795c2ee2d \ No newline at end of file
diff --git a/src/runtime/hmr.ts b/src/runtime/hmr.ts
index 583d0b34d..cef229de2 100644
--- a/src/runtime/hmr.ts
+++ b/src/runtime/hmr.ts
@@ -8,13 +8,23 @@ function formatDuration(duration: number) {
return Math.round(duration * 1000) / 1000;
}
+type HTMLStylableElement = HTMLLinkElement | HTMLStyleElement;
type CSSHMRInsertionPoint = {
id: number;
- node: HTMLLinkElement;
+ node?: HTMLStylableElement;
file: string;
bundle_id: number;
+ sheet: CSSStyleSheet;
};
+enum CSSUpdateMethod {
+ // CSS OM allows synchronous style updates
+ cssObjectModel,
+ // Blob URLs allow us to skip converting to JavaScript strings
+ // However, they run asynchronously. Frequent updates cause FOUC
+ blobURL,
+}
+
// How this works
// We keep
export class CSSLoader {
@@ -24,8 +34,20 @@ export class CSSLoader {
bundle_id: 0,
node: null,
file: "",
+ sheet: null,
};
+ updateMethod: CSSUpdateMethod;
+ decoder: TextDecoder;
+
+ constructor() {
+ if ("replaceSync" in CSSStyleSheet.prototype) {
+ this.updateMethod = CSSUpdateMethod.cssObjectModel;
+ } else {
+ this.updateMethod = CSSUpdateMethod.blobURL;
+ }
+ }
+
// This is a separate function because calling a small function 2000 times is more likely to cause it to be JIT'd
// We want it to be JIT'd
// It's possible that returning null may be a de-opt though.
@@ -67,7 +89,8 @@ export class CSSLoader {
if (endFileRegion - startFileRegion <= 0) return null;
CSSLoader.cssLoadId.id = int;
- CSSLoader.cssLoadId.node = sheet.ownerNode as HTMLLinkElement;
+ CSSLoader.cssLoadId.node = sheet.ownerNode as HTMLStylableElement;
+ CSSLoader.cssLoadId.sheet = sheet;
CSSLoader.cssLoadId.file = rule.conditionText.substring(
startFileRegion - 1,
endFileRegion
@@ -86,8 +109,66 @@ export class CSSLoader {
}
private findCSSLinkTag(id: number): CSSHMRInsertionPoint | null {
- const count = document.styleSheets.length;
+ let count = 0;
let match: CSSHMRInsertionPoint = null;
+
+ if (this.updateMethod === CSSUpdateMethod.cssObjectModel) {
+ if (document.adoptedStyleSheets.length > 0) {
+ count = document.adoptedStyleSheets.length;
+
+ for (let i = 0; i < count && match === null; i++) {
+ let cssRules: CSSRuleList;
+ let sheet: CSSStyleSheet;
+ let ruleCount = 0;
+ // Non-same origin stylesheets will potentially throw "Security error"
+ // We will ignore those stylesheets and look at others.
+ try {
+ sheet = document.adoptedStyleSheets[i];
+ cssRules = sheet.rules;
+ ruleCount = sheet.rules.length;
+ } catch (exception) {
+ continue;
+ }
+
+ if (sheet.disabled || sheet.rules.length === 0) {
+ continue;
+ }
+
+ const bundleIdRule = cssRules[0] as CSSSupportsRule;
+ if (
+ bundleIdRule.type !== 12 ||
+ !bundleIdRule.conditionText.startsWith("(hmr-bid:")
+ ) {
+ continue;
+ }
+
+ const bundleIdEnd = bundleIdRule.conditionText.indexOf(
+ ")",
+ "(hmr-bid:".length + 1
+ );
+ if (bundleIdEnd === -1) continue;
+
+ CSSLoader.cssLoadId.bundle_id = parseInt(
+ bundleIdRule.conditionText.substring(
+ "(hmr-bid:".length,
+ bundleIdEnd
+ ),
+ 10
+ );
+
+ for (let j = 1; j < ruleCount && match === null; j++) {
+ match = this.findMatchingSupportsRule(
+ cssRules[j] as CSSSupportsRule,
+ id,
+ sheet
+ );
+ }
+ }
+ }
+ }
+
+ count = document.styleSheets.length;
+
for (let i = 0; i < count && match === null; i++) {
let cssRules: CSSRuleList;
let sheet: CSSStyleSheet;
@@ -102,12 +183,7 @@ export class CSSLoader {
continue;
}
- if (
- sheet.disabled ||
- !sheet.href ||
- sheet.href.length === 0 ||
- sheet.rules.length === 0
- ) {
+ if (sheet.disabled || sheet.rules.length === 0) {
continue;
}
@@ -144,6 +220,7 @@ export class CSSLoader {
CSSLoader.cssLoadId.file = "";
CSSLoader.cssLoadId.bundle_id = CSSLoader.cssLoadId.id = 0;
CSSLoader.cssLoadId.node = null;
+ CSSLoader.cssLoadId.sheet = null;
}
return match;
@@ -156,25 +233,24 @@ export class CSSLoader {
) {
const start = performance.now();
var update = this.findCSSLinkTag(build.id);
+ let bytes =
+ buffer._data.length > buffer._index
+ ? buffer._data.subarray(buffer._index)
+ : new Uint8Array(0);
if (update === null) {
__hmrlog.debug("Skipping unused CSS.");
return;
}
- let blob = new Blob(
- [
- buffer._data.length > buffer._index
- ? buffer._data.subarray(buffer._index)
- : new Uint8Array(0),
- ],
- { type: "text/css" }
- );
- buffer = null;
- const blobURL = URL.createObjectURL(blob);
+ if (bytes.length === 0) {
+ __hmrlog.debug("Skipping empty file");
+ return;
+ }
+
let filepath = update.file;
const _timestamp = timestamp;
const from_timestamp = build.from_timestamp;
- function onLoadHandler(load: Event) {
+ function onLoadHandler() {
const localDuration = formatDuration(performance.now() - start);
const fsDuration = _timestamp - from_timestamp;
__hmrlog.log(
@@ -184,24 +260,51 @@ export class CSSLoader {
filepath
);
- blob = null;
update = null;
filepath = null;
+ }
- if (this.href.includes("blob:")) {
- URL.revokeObjectURL(this.href);
+ // Whenever
+ switch (this.updateMethod) {
+ case CSSUpdateMethod.blobURL: {
+ let blob = new Blob([bytes], { type: "text/css" });
+
+ const blobURL = URL.createObjectURL(blob);
+ // onLoad doesn't fire in Chrome.
+ // I'm not sure why.
+ // Guessing it only triggers when an element is added/removed, not when the href just changes
+ // So we say on the next tick, we're loaded.
+ setTimeout(onLoadHandler.bind(update.node), 0);
+ update.node.setAttribute("href", blobURL);
+ blob = null;
+ URL.revokeObjectURL(blobURL);
+ break;
+ }
+ case CSSUpdateMethod.cssObjectModel: {
+ if (!this.decoder) {
+ this.decoder = new TextDecoder("UTF8");
+ }
+
+ // This is an adoptedStyleSheet, call replaceSync and be done with it.
+ if (!update.node || update.node.tagName === "HTML") {
+ update.sheet.replaceSync(this.decoder.decode(bytes));
+ } else if (
+ update.node.tagName === "LINK" ||
+ update.node.tagName === "STYLE"
+ ) {
+ // This might cause CSS specifity issues....
+ // I'm not 100% sure this is a safe operation
+ const sheet = new CSSStyleSheet();
+ sheet.replaceSync(this.decoder.decode(bytes));
+ update.node.remove();
+ document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
+ }
+ break;
}
}
- // onLoad doesn't fire in Chrome.
- // I'm not sure why.
- // Guessing it only triggers when an element is added/removed, not when the href just changes
- // So we say on the next tick, we're loaded.
- setTimeout(onLoadHandler.bind(update.node), 0);
- if (update.node.href.includes("blob:")) {
- URL.revokeObjectURL(update.node.href);
- }
- update.node.setAttribute("href", blobURL);
- URL.revokeObjectURL(blobURL);
+
+ buffer = null;
+ bytes = null;
}
reload(timestamp: number) {