diff --git a/content/en/tutorial/01-vdom.md b/content/en/tutorial/01-vdom.md
index e278e086f..16232424d 100644
--- a/content/en/tutorial/01-vdom.md
+++ b/content/en/tutorial/01-vdom.md
@@ -196,7 +196,7 @@ useResult(function(result) {
var p = result.output.querySelector('p');
var hasColor = p && p.style && p.style.color === 'purple';
if (hasEm && hasColor) {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
}
}, []);
```
diff --git a/content/en/tutorial/02-events.md b/content/en/tutorial/02-events.md
index f92f5374e..136a138eb 100644
--- a/content/en/tutorial/02-events.md
+++ b/content/en/tutorial/02-events.md
@@ -61,7 +61,7 @@ useRealm(function (realm) {
var win = realm.globalThis;
var prevConsoleLog = win.console.log;
win.console.log = function() {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
return prevConsoleLog.apply(win.console, arguments);
};
diff --git a/content/en/tutorial/03-components.md b/content/en/tutorial/03-components.md
index b85b086b6..297dc4027 100644
--- a/content/en/tutorial/03-components.md
+++ b/content/en/tutorial/03-components.md
@@ -254,7 +254,7 @@ useRealm(function (realm) {
win.console.log = function() {
if (hasComponent && check) {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
}
return prevConsoleLog.apply(win.console, arguments);
};
diff --git a/content/en/tutorial/04-state.md b/content/en/tutorial/04-state.md
index ce0e2a0d3..b65e111ba 100644
--- a/content/en/tutorial/04-state.md
+++ b/content/en/tutorial/04-state.md
@@ -161,7 +161,7 @@ useResult(function () {
}
if (Number(text2[1]) === Number(text[1]) + 1) {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
}
}, 10);
}
diff --git a/content/en/tutorial/05-refs.md b/content/en/tutorial/05-refs.md
index a696d7e84..e720773a4 100644
--- a/content/en/tutorial/05-refs.md
+++ b/content/en/tutorial/05-refs.md
@@ -133,7 +133,7 @@ function patch(input) {
input.__patched = true;
var old = input.focus;
input.focus = function() {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
return old.call(this);
};
}
diff --git a/content/en/tutorial/07-side-effects.md b/content/en/tutorial/07-side-effects.md
index efe1ca2b3..12f91a9c8 100644
--- a/content/en/tutorial/07-side-effects.md
+++ b/content/en/tutorial/07-side-effects.md
@@ -138,7 +138,7 @@ useRealm(function (realm) {
var prevConsoleLog = win.console.log;
win.console.log = function(m, s) {
if (/Count is now/.test(m) && s === 1) {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
}
return prevConsoleLog.apply(win.console, arguments);
};
diff --git a/content/en/tutorial/08-keys.md b/content/en/tutorial/08-keys.md
index 9e6259a86..4a0c3fd3c 100644
--- a/content/en/tutorial/08-keys.md
+++ b/content/en/tutorial/08-keys.md
@@ -307,7 +307,7 @@ useRealm(function (realm) {
/learn preact/i.test(c[0].textContent) &&
/make an awesome app/i.test(c[1].textContent)
) {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
}
}
diff --git a/content/en/tutorial/09-error-handling.md b/content/en/tutorial/09-error-handling.md
index 99ccc1680..86e42f582 100644
--- a/content/en/tutorial/09-error-handling.md
+++ b/content/en/tutorial/09-error-handling.md
@@ -98,7 +98,7 @@ useResult(function(result) {
oe.apply(this, arguments);
setTimeout(function() {
if (result.output.textContent.match(/error/i)) {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
}
}, 10);
};
diff --git a/content/kr/tutorial/01-vdom.md b/content/kr/tutorial/01-vdom.md
index 44cadba4c..2e200bf9f 100644
--- a/content/kr/tutorial/01-vdom.md
+++ b/content/kr/tutorial/01-vdom.md
@@ -145,7 +145,7 @@ useResult(function(result) {
var p = result.output.querySelector('p');
var hasColor = p && p.style && p.style.color === 'purple';
if (hasEm && hasColor) {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
}
}, []);
```
diff --git a/content/kr/tutorial/02-events.md b/content/kr/tutorial/02-events.md
index 8314f6395..cea550d09 100644
--- a/content/kr/tutorial/02-events.md
+++ b/content/kr/tutorial/02-events.md
@@ -54,7 +54,7 @@ useRealm(function (realm) {
var win = realm.globalThis;
var prevConsoleLog = win.console.log;
win.console.log = function() {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
return prevConsoleLog.apply(win.console, arguments);
};
diff --git a/content/ru/tutorial/01-vdom.md b/content/ru/tutorial/01-vdom.md
index 69a7e8617..86b7270bf 100644
--- a/content/ru/tutorial/01-vdom.md
+++ b/content/ru/tutorial/01-vdom.md
@@ -152,7 +152,7 @@ useResult(function(result) {
var p = result.output.querySelector('p');
var hasColor = p && p.style && p.style.color === 'purple';
if (hasEm && hasColor) {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
}
}, []);
```
diff --git a/content/ru/tutorial/02-events.md b/content/ru/tutorial/02-events.md
index 247e05f6d..eeb8460b3 100644
--- a/content/ru/tutorial/02-events.md
+++ b/content/ru/tutorial/02-events.md
@@ -48,7 +48,7 @@ useRealm(function (realm) {
var win = realm.globalThis;
var prevConsoleLog = win.console.log;
win.console.log = function() {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
return prevConsoleLog.apply(win.console, arguments);
};
diff --git a/content/ru/tutorial/03-components.md b/content/ru/tutorial/03-components.md
index b2e2137b8..06dbff203 100644
--- a/content/ru/tutorial/03-components.md
+++ b/content/ru/tutorial/03-components.md
@@ -188,7 +188,7 @@ useRealm(function (realm) {
win.console.log = function() {
if (hasComponent && check) {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
}
return prevConsoleLog.apply(win.console, arguments);
};
diff --git a/content/ru/tutorial/04-state.md b/content/ru/tutorial/04-state.md
index 859c3b857..f9ed7ab94 100644
--- a/content/ru/tutorial/04-state.md
+++ b/content/ru/tutorial/04-state.md
@@ -113,7 +113,7 @@ useResult(function () {
}
if (Number(text2[1]) === Number(text[1]) + 1) {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
}
}, 10);
}
diff --git a/content/ru/tutorial/05-refs.md b/content/ru/tutorial/05-refs.md
index aa5118eba..f11749c83 100644
--- a/content/ru/tutorial/05-refs.md
+++ b/content/ru/tutorial/05-refs.md
@@ -103,7 +103,7 @@ function patch(input) {
input.__patched = true;
var old = input.focus;
input.focus = function() {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
return old.call(this);
};
}
diff --git a/content/ru/tutorial/07-side-effects.md b/content/ru/tutorial/07-side-effects.md
index 600232439..698c499c3 100644
--- a/content/ru/tutorial/07-side-effects.md
+++ b/content/ru/tutorial/07-side-effects.md
@@ -113,7 +113,7 @@ useRealm(function (realm) {
var prevConsoleLog = win.console.log;
win.console.log = function(m, s) {
if (/Счётчик: /.test(m) && s === 1) {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
}
return prevConsoleLog.apply(win.console, arguments);
};
diff --git a/content/ru/tutorial/08-keys.md b/content/ru/tutorial/08-keys.md
index 869f2f06b..9b2761357 100644
--- a/content/ru/tutorial/08-keys.md
+++ b/content/ru/tutorial/08-keys.md
@@ -240,7 +240,7 @@ useRealm(function (realm) {
/изучить preact/i.test(c[0].textContent) &&
/сделать крутое приложение/i.test(c[1].textContent)
) {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
}
}
diff --git a/content/ru/tutorial/09-error-handling.md b/content/ru/tutorial/09-error-handling.md
index 532a81b38..3d4975576 100644
--- a/content/ru/tutorial/09-error-handling.md
+++ b/content/ru/tutorial/09-error-handling.md
@@ -73,7 +73,7 @@ useResult(function(result) {
oe.apply(this, arguments);
setTimeout(function() {
if (result.output.textContent.match(/ошибка/i)) {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
}
}, 10);
};
diff --git a/content/zh/tutorial/01-vdom.md b/content/zh/tutorial/01-vdom.md
index 041f224cd..e63f65c3e 100644
--- a/content/zh/tutorial/01-vdom.md
+++ b/content/zh/tutorial/01-vdom.md
@@ -161,7 +161,7 @@ useResult(function(result) {
var p = result.output.querySelector('p');
var hasColor = p && p.style && p.style.color === 'purple';
if (hasEm && hasColor) {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
}
}, []);
```
diff --git a/content/zh/tutorial/02-events.md b/content/zh/tutorial/02-events.md
index db46d89ff..774f2d9e2 100644
--- a/content/zh/tutorial/02-events.md
+++ b/content/zh/tutorial/02-events.md
@@ -48,7 +48,7 @@ useRealm(function (realm) {
var win = realm.globalThis;
var prevConsoleLog = win.console.log;
win.console.log = function() {
- store.setState({ solved: true });
+ solutionCtx.setSolved(true);
return prevConsoleLog.apply(win.console, arguments);
};
diff --git a/jsconfig.json b/jsconfig.json
index caaa651a3..e401488e3 100644
--- a/jsconfig.json
+++ b/jsconfig.json
@@ -1,8 +1,8 @@
{
"compilerOptions": {
"target": "ESNext",
- "module": "NodeNext",
- "moduleResolution": "NodeNext",
+ "module": "ESNext",
+ "moduleResolution": "Node",
"resolveJsonModule": true,
"jsx": "react-jsx",
"jsxImportSource": "preact",
diff --git a/package-lock.json b/package-lock.json
index a672460fa..c208ae948 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4,6 +4,7 @@
"requires": true,
"packages": {
"": {
+ "hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@preact/signals": "^1.1.3",
@@ -20,12 +21,11 @@
"node-fetch": "^2.6.1",
"preact": "10.15.1",
"preact-custom-element": "^4.3.0",
+ "preact-iso": "2.2.0",
"preact-markup": "^2.1.1",
"preact-render-to-string": "^5.2.6",
- "preact-router": "^3.1.0",
"rollup": "^2.79.1",
"sucrase": "^3.32.0",
- "unistore": "^3.5.1",
"yaml": "^1.7.2"
},
"devDependencies": {
@@ -38,6 +38,7 @@
"jsdom": "^15.2.1",
"lint-staged": "^15.2.0",
"netlify-lambda": "^2.0.16",
+ "patch-package": "^8.0.0",
"postcss-custom-properties": "^13.3.2",
"postcss-import": "^14.0.0",
"postcss-nesting": "^12.0.1",
@@ -3305,6 +3306,12 @@
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true
},
+ "node_modules/@yarnpkg/lockfile": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
+ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
+ "dev": true
+ },
"node_modules/abab": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
@@ -8270,6 +8277,15 @@
"url": "https://github.com/avajs/find-cache-dir?sponsor=1"
}
},
+ "node_modules/find-yarn-workspace-root": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
+ "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
+ "dev": true,
+ "dependencies": {
+ "micromatch": "^4.0.2"
+ }
+ },
"node_modules/flat": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
@@ -8504,18 +8520,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/fork-ts-checker-webpack-plugin/node_modules/jsonfile": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
- "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
- "dev": true,
- "dependencies": {
- "universalify": "^2.0.0"
- },
- "optionalDependencies": {
- "graceful-fs": "^4.1.6"
- }
- },
"node_modules/fork-ts-checker-webpack-plugin/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -8685,6 +8689,21 @@
"safe-buffer": "~5.1.0"
}
},
+ "node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -10781,12 +10800,36 @@
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
+ "node_modules/json-stable-stringify": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz",
+ "integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "isarray": "^2.0.5",
+ "jsonify": "^0.0.1",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
"dev": true
},
+ "node_modules/json-stable-stringify/node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true
+ },
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
@@ -10805,6 +10848,27 @@
"json5": "lib/cli.js"
}
},
+ "node_modules/jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dev": true,
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsonify": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
+ "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/jsonpointer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz",
@@ -10857,6 +10921,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/klaw-sync": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
+ "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.11"
+ }
+ },
"node_modules/kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -11752,10 +11825,13 @@
}
},
"node_modules/minimist": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
- "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
- "dev": true
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
},
"node_modules/minipass": {
"version": "7.0.4",
@@ -13642,6 +13718,124 @@
"node": ">=0.10.0"
}
},
+ "node_modules/patch-package": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz",
+ "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==",
+ "dev": true,
+ "dependencies": {
+ "@yarnpkg/lockfile": "^1.1.0",
+ "chalk": "^4.1.2",
+ "ci-info": "^3.7.0",
+ "cross-spawn": "^7.0.3",
+ "find-yarn-workspace-root": "^2.0.0",
+ "fs-extra": "^9.0.0",
+ "json-stable-stringify": "^1.0.2",
+ "klaw-sync": "^6.0.0",
+ "minimist": "^1.2.6",
+ "open": "^7.4.2",
+ "rimraf": "^2.6.3",
+ "semver": "^7.5.3",
+ "slash": "^2.0.0",
+ "tmp": "^0.0.33",
+ "yaml": "^2.2.2"
+ },
+ "bin": {
+ "patch-package": "index.js"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">5"
+ }
+ },
+ "node_modules/patch-package/node_modules/ci-info": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+ "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/patch-package/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/patch-package/node_modules/open": {
+ "version": "7.4.2",
+ "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
+ "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
+ "dev": true,
+ "dependencies": {
+ "is-docker": "^2.0.0",
+ "is-wsl": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/patch-package/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/patch-package/node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/patch-package/node_modules/slash": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
+ "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/patch-package/node_modules/yaml": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz",
+ "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/path-browserify": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz",
@@ -14694,6 +14888,15 @@
"preact": "10.x"
}
},
+ "node_modules/preact-iso": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/preact-iso/-/preact-iso-2.2.0.tgz",
+ "integrity": "sha512-BIUOYJWnfmOcgFiuAqGTLkqvC2Rqlpn3aLP/ik8kT8afKdLrG3CvBEpSQivMrFY1o2InGX/jEL44ofmoQdoieA==",
+ "peerDependencies": {
+ "preact": ">=10",
+ "preact-render-to-string": ">=5"
+ }
+ },
"node_modules/preact-markup": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/preact-markup/-/preact-markup-2.1.1.tgz",
@@ -14713,14 +14916,6 @@
"preact": ">=10"
}
},
- "node_modules/preact-router": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/preact-router/-/preact-router-3.2.1.tgz",
- "integrity": "sha512-KEN2VN1DxUlTwzW5IFkF13YIA2OdQ2OvgJTkQREF+AA2NrHRLaGbB68EjS4IeZOa1shvQ1FvEm3bSLta4sXBhg==",
- "peerDependencies": {
- "preact": ">=10"
- }
- },
"node_modules/prelude-ls": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
@@ -18225,19 +18420,6 @@
"node": ">=8"
}
},
- "node_modules/unistore": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/unistore/-/unistore-3.5.2.tgz",
- "integrity": "sha512-2Aa4eX0Ua1umyiI3Eai6Li+wXYOHgaDBGOPB3Hvw7PAVuD30TAyh5kS4yNKb2fLDbQgizvPhKQRcYnOdfsm4VQ==",
- "peerDependenciesMeta": {
- "preact": {
- "optional": true
- },
- "react": {
- "optional": true
- }
- }
- },
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@@ -19685,21 +19867,6 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
- "node_modules/workbox-build/node_modules/fs-extra": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
- "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
- "dev": true,
- "dependencies": {
- "at-least-node": "^1.0.0",
- "graceful-fs": "^4.2.0",
- "jsonfile": "^6.0.1",
- "universalify": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/workbox-build/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -19735,18 +19902,6 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
- "node_modules/workbox-build/node_modules/jsonfile": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
- "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
- "dev": true,
- "dependencies": {
- "universalify": "^2.0.0"
- },
- "optionalDependencies": {
- "graceful-fs": "^4.1.6"
- }
- },
"node_modules/workbox-build/node_modules/rollup-plugin-terser": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz",
@@ -22674,6 +22829,12 @@
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true
},
+ "@yarnpkg/lockfile": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
+ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
+ "dev": true
+ },
"abab": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
@@ -26488,6 +26649,15 @@
"pkg-dir": "^4.1.0"
}
},
+ "find-yarn-workspace-root": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
+ "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
+ "dev": true,
+ "requires": {
+ "micromatch": "^4.0.2"
+ }
+ },
"flat": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
@@ -26661,16 +26831,6 @@
"resolve-from": "^4.0.0"
}
},
- "jsonfile": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
- "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
- "dev": true,
- "requires": {
- "graceful-fs": "^4.1.6",
- "universalify": "^2.0.0"
- }
- },
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -26800,6 +26960,18 @@
}
}
},
+ "fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "requires": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ }
+ },
"fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -28363,6 +28535,26 @@
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
+ "json-stable-stringify": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz",
+ "integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.5",
+ "isarray": "^2.0.5",
+ "jsonify": "^0.0.1",
+ "object-keys": "^1.1.1"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true
+ }
+ }
+ },
"json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
@@ -28384,6 +28576,22 @@
"minimist": "^1.2.0"
}
},
+ "jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.6",
+ "universalify": "^2.0.0"
+ }
+ },
+ "jsonify": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
+ "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
+ "dev": true
+ },
"jsonpointer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz",
@@ -28424,6 +28632,15 @@
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"dev": true
},
+ "klaw-sync": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
+ "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.11"
+ }
+ },
"kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -29113,9 +29330,9 @@
}
},
"minimist": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
- "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true
},
"minipass": {
@@ -30633,6 +30850,86 @@
"integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==",
"dev": true
},
+ "patch-package": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz",
+ "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==",
+ "dev": true,
+ "requires": {
+ "@yarnpkg/lockfile": "^1.1.0",
+ "chalk": "^4.1.2",
+ "ci-info": "^3.7.0",
+ "cross-spawn": "^7.0.3",
+ "find-yarn-workspace-root": "^2.0.0",
+ "fs-extra": "^9.0.0",
+ "json-stable-stringify": "^1.0.2",
+ "klaw-sync": "^6.0.0",
+ "minimist": "^1.2.6",
+ "open": "^7.4.2",
+ "rimraf": "^2.6.3",
+ "semver": "^7.5.3",
+ "slash": "^2.0.0",
+ "tmp": "^0.0.33",
+ "yaml": "^2.2.2"
+ },
+ "dependencies": {
+ "ci-info": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+ "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+ "dev": true
+ },
+ "lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "open": {
+ "version": "7.4.2",
+ "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
+ "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
+ "dev": true,
+ "requires": {
+ "is-docker": "^2.0.0",
+ "is-wsl": "^2.1.1"
+ }
+ },
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ },
+ "slash": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
+ "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
+ "dev": true
+ },
+ "yaml": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz",
+ "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==",
+ "dev": true
+ }
+ }
+ },
"path-browserify": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz",
@@ -31308,6 +31605,12 @@
"integrity": "sha512-5hG7nQhU4e7RNfCEQklaUqYQiiyibLuJ2wbhR+E2v1m8m9NDsJok5MykW/Nx0YLLBcXr8xnkap6DwByGy2TzDA==",
"requires": {}
},
+ "preact-iso": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/preact-iso/-/preact-iso-2.2.0.tgz",
+ "integrity": "sha512-BIUOYJWnfmOcgFiuAqGTLkqvC2Rqlpn3aLP/ik8kT8afKdLrG3CvBEpSQivMrFY1o2InGX/jEL44ofmoQdoieA==",
+ "requires": {}
+ },
"preact-markup": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/preact-markup/-/preact-markup-2.1.1.tgz",
@@ -31322,12 +31625,6 @@
"pretty-format": "^3.8.0"
}
},
- "preact-router": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/preact-router/-/preact-router-3.2.1.tgz",
- "integrity": "sha512-KEN2VN1DxUlTwzW5IFkF13YIA2OdQ2OvgJTkQREF+AA2NrHRLaGbB68EjS4IeZOa1shvQ1FvEm3bSLta4sXBhg==",
- "requires": {}
- },
"prelude-ls": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
@@ -34096,11 +34393,6 @@
"crypto-random-string": "^2.0.0"
}
},
- "unistore": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/unistore/-/unistore-3.5.2.tgz",
- "integrity": "sha512-2Aa4eX0Ua1umyiI3Eai6Li+wXYOHgaDBGOPB3Hvw7PAVuD30TAyh5kS4yNKb2fLDbQgizvPhKQRcYnOdfsm4VQ=="
- },
"universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@@ -35237,18 +35529,6 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
- "fs-extra": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
- "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
- "dev": true,
- "requires": {
- "at-least-node": "^1.0.0",
- "graceful-fs": "^4.2.0",
- "jsonfile": "^6.0.1",
- "universalify": "^2.0.0"
- }
- },
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -35278,16 +35558,6 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
- "jsonfile": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
- "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
- "dev": true,
- "requires": {
- "graceful-fs": "^4.1.6",
- "universalify": "^2.0.0"
- }
- },
"rollup-plugin-terser": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz",
diff --git a/package.json b/package.json
index 4c754f882..0aacadebf 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,8 @@
"prebuild": "rimraf build/assets && rimraf build/content",
"lint": "eslint src test",
"format": "prettier --write \"{src,test}/**/*.{css,js,json}\"",
- "prepare": "husky install"
+ "prepare": "husky install",
+ "postinstall": "patch-package"
},
"eslintConfig": {
"extends": "developit",
@@ -64,6 +65,7 @@
"jsdom": "^15.2.1",
"lint-staged": "^15.2.0",
"netlify-lambda": "^2.0.16",
+ "patch-package": "^8.0.0",
"postcss-custom-properties": "^13.3.2",
"postcss-import": "^14.0.0",
"postcss-nesting": "^12.0.1",
@@ -88,12 +90,11 @@
"node-fetch": "^2.6.1",
"preact": "10.15.1",
"preact-custom-element": "^4.3.0",
+ "preact-iso": "2.2.0",
"preact-markup": "^2.1.1",
"preact-render-to-string": "^5.2.6",
- "preact-router": "^3.2.1",
"rollup": "^2.79.1",
"sucrase": "^3.32.0",
- "unistore": "^3.5.1",
"yaml": "^1.7.2"
}
}
diff --git a/patches/preact-cli+4.0.0-next.6.patch b/patches/preact-cli+4.0.0-next.6.patch
new file mode 100644
index 000000000..2836e08b2
--- /dev/null
+++ b/patches/preact-cli+4.0.0-next.6.patch
@@ -0,0 +1,57 @@
+diff --git a/node_modules/preact-cli/src/lib/webpack/prerender.js b/node_modules/preact-cli/src/lib/webpack/prerender.js
+index 945a4e0..e8323d3 100644
+--- a/node_modules/preact-cli/src/lib/webpack/prerender.js
++++ b/node_modules/preact-cli/src/lib/webpack/prerender.js
+@@ -2,7 +2,7 @@ const { red, yellow } = require('kleur');
+ const { resolve } = require('path');
+ const { readFileSync } = require('fs');
+ const stackTrace = require('stack-trace');
+-const URL = require('url');
++const { URL } = require('url');
+ const { SourceMapConsumer } = require('source-map');
+
+ module.exports = async function (config, params) {
+@@ -11,14 +11,14 @@ module.exports = async function (config, params) {
+ let entry = resolve(config.dest, './ssr-build/ssr-bundle.js');
+ let url = params.url || '/';
+
+- global.history = {};
+- global.location = { ...URL.parse(url) };
++ global.history = /** @type {object} */ ({});
++ global.location = /** @type {object} */ (new URL(url, 'http://localhost'));
+
+ try {
+ let m = require(entry),
+- app = (m && m.default) || m;
++ vnode = (m && m.default) || m;
+
+- if (typeof app !== 'function') {
++ if (typeof vnode !== 'function') {
+ // eslint-disable-next-line no-console
+ console.warn(
+ 'Entry does not export a Component function/class, aborting prerendering.'
+@@ -29,7 +29,23 @@ module.exports = async function (config, params) {
+ const renderToString = require(require.resolve('preact-render-to-string', {
+ paths: [config.cwd],
+ }));
+- return renderToString(preact.h(app, { ...params, url }));
++
++ vnode = preact.h(vnode, { ...params, url });
++
++ // Slightly modified version of preact-iso's `prerender()`
++ let tries;
++ const maxDepth = 10;
++ const render = () => {
++ if (++tries > maxDepth) return;
++ try {
++ return renderToString(vnode);
++ } catch (e) {
++ if (e && e.then) return e.then(render);
++ throw e;
++ }
++ };
++
++ return await render();
+ } catch (err) {
+ let stack = stackTrace.parse(err).filter(s => s.getFileName() === entry)[0];
+ if (!stack) {
diff --git a/src/components/app.js b/src/components/app.js
index caf615d1d..fa588e000 100644
--- a/src/components/app.js
+++ b/src/components/app.js
@@ -1,46 +1,24 @@
-import { Component } from 'preact';
-import { Provider } from 'unistore/preact';
-import createStore from '../store';
-import Routes from './routes';
+import { LocationProvider, ErrorBoundary } from 'preact-iso';
+import { LanguageProvider } from '../lib/i18n';
+// TODO: SolutionProvider should really just wrap the tutorial,
+// but that requires a bit of refactoring
+import { SolutionProvider } from './controllers/tutorial/index.js';
import Header from './header';
-import { storeCtx } from './store-adapter';
-import { getCurrentDocVersion } from '../lib/docs';
-
-export default class App extends Component {
- store = createStore({
- url: this.props.url || location.pathname + location.search,
- lang: 'en',
- preactVersion: this.props.preactVersion || '10.11.3',
- docVersion: getCurrentDocVersion(location.pathname),
- toc: null
- });
-
- handleUrlChange = ({ url }) => {
- let prev = this.store.getState().url || '/';
- if (url !== prev) {
- this.store.setState({
- ...this.store.getState(),
- toc: null,
- url,
- docVersion: getCurrentDocVersion(url)
- });
- if (typeof ga === 'function') {
- ga('send', 'pageview', url);
- }
- }
- };
+import Routes from './routes';
- render() {
- const { url } = this.store.getState();
- return (
-
-
-
-
-
-
-
-
- );
- }
+export default function App({ preactVersion }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
}
diff --git a/src/components/blog-meta/index.js b/src/components/blog-meta/index.js
new file mode 100644
index 000000000..70100d986
--- /dev/null
+++ b/src/components/blog-meta/index.js
@@ -0,0 +1,62 @@
+import { Time } from '../time';
+import config from '../../config.json';
+import style from './style.module.css';
+
+export default function BlogMeta({ meta }) {
+ return (
+
+ {meta.date &&
}
+ {Array.isArray(meta.authors) && meta.authors.length > 0 && (
+ <>
+ , written by{' '}
+
+ {meta.authors.map((author, i, arr) => {
+ const authorData = config.blogAuthors.find(
+ data => data.name === author
+ );
+ return (
+
+ );
+ })}
+ {(meta.translation_by || []).map((author, i, arr) => {
+ const authorData = config.blogAuthors.find(
+ data => data.name === author
+ );
+ return (
+ <>
+ {', translated by '}
+
+ >
+ );
+ })}
+
+ >
+ )}
+
+ );
+}
+
+function AuthorLinks({ authorData, author, i, arr }) {
+ return (
+
+ {authorData ? (
+
+ {author}
+
+ ) : (
+ {author}
+ )}
+ {i < arr.length - 2 ? ', ' : i === arr.length - 2 ? ' and ' : null}
+
+ );
+}
diff --git a/src/components/blog-meta/style.module.css b/src/components/blog-meta/style.module.css
new file mode 100644
index 000000000..cd7ccb07e
--- /dev/null
+++ b/src/components/blog-meta/style.module.css
@@ -0,0 +1,12 @@
+.blogMeta {
+ /* Negative bottom margin negates content-region's top margin */
+ margin: 2.5rem auto -3rem;
+ z-index: 10;
+ padding: 0.5em 1rem 0.25rem;
+ width: 100%;
+ max-width: var(--content-width);
+
+ .authors {
+ display: inline-block;
+ }
+}
diff --git a/src/components/blog-overview/index.js b/src/components/blog-overview/index.js
index 435a1be26..f09723d90 100644
--- a/src/components/blog-overview/index.js
+++ b/src/components/blog-overview/index.js
@@ -1,12 +1,11 @@
import config from '../../config.json';
-import { useTranslation } from '../../lib/i18n';
+import { useLanguage, useTranslation } from '../../lib/i18n';
import { getRouteName } from '../header';
-import { useStore } from '../store-adapter';
import { Time } from '../time';
import s from './style.module.css';
export default function BlogOverview() {
- const { lang } = useStore(['lang']).state;
+ const [lang] = useLanguage();
const continueReading = useTranslation('continueReading');
return (
diff --git a/src/components/branding/index.js b/src/components/branding/index.js
index c10c9878b..d292ca407 100644
--- a/src/components/branding/index.js
+++ b/src/components/branding/index.js
@@ -21,10 +21,18 @@ function LogoVariation({ name, alt }) {
height="64"
/>
diff --git a/src/components/code-block/index.js b/src/components/code-block/index.js
index 303c6f32c..26e85b9da 100644
--- a/src/components/code-block/index.js
+++ b/src/components/code-block/index.js
@@ -1,72 +1,29 @@
-import { useState, useMemo, useRef, useEffect } from 'preact/hooks';
-import { Link } from 'preact-router';
+import { useMemo } from 'preact/hooks';
import * as Comlink from 'comlink';
-import cx from '../../lib/cx';
-
-let highlight;
-(async function initHighlight() {
- ({ highlight } = PRERENDER
- ? import('./prism.worker.js')
- : Comlink.wrap(
- new Worker(
- /* webpackChunkName: "prism-worker" */ new URL(
- './prism.worker.js',
- import.meta.url
- )
+import { FakeSuspense, useResource } from '../../lib/use-resource';
+
+const { highlight } = PRERENDER
+ ? require('./prism.worker.js')
+ : Comlink.wrap(
+ new Worker(
+ /* webpackChunkName: "prism-worker" */ new URL(
+ './prism.worker.js',
+ import.meta.url
)
- ));
-})();
-
-function useFuture(initializer, params) {
- const getInitialState = () => {
- try {
- const value = initializer();
- if (value && value.then) {
- if ('_value' in value) return [value._value];
- if ('_error' in value) return [undefined, value._error];
- return [undefined, undefined, value];
- }
- return [value];
- } catch (err) {
- return [undefined, err];
- }
- };
-
- const [pair, setValue] = useState(getInitialState);
-
- // only run on changes, not initial mount
- const isFirstRun = useRef(true);
- useEffect(() => {
- if (isFirstRun.current) return (isFirstRun.current = false);
- setValue(getInitialState());
- }, params || []);
-
- const pending = pair[2];
- if (pending) {
- if (!pending._processing) {
- pending._processing = true;
- pending
- .then(value => {
- pending._value = value;
- setValue([value]);
- })
- .catch(err => {
- pending._error = err;
- setValue([undefined, err]);
- });
- }
- }
- return pair;
-}
+ )
+ );
+
+/**
+ * @param {{ code: string, lang: string }} props
+ */
+function HighlightedCode({ code, lang }) {
+ const highlighted = useResource(() => highlight(code, lang), [code, lang]);
-const CACHE = {};
-function cachedHighlight(code, lang) {
- const id = lang + '\n' + code;
- return CACHE[id] || (CACHE[id] = highlight(code, lang));
+ const htmlObj = useMemo(() => ({ __html: highlighted }), [highlighted]);
+ return
;
}
-function HighlightedCodeBlock({ code, lang, ...props }) {
- let repl = false;
+function processRepl(code, repl) {
let source = code;
if (code.startsWith('// --repl')) {
repl = true;
@@ -102,29 +59,32 @@ function HighlightedCodeBlock({ code, lang, ...props }) {
}
}
- const [highlighted, error, pending] = useFuture(
- () => cachedHighlight(code, lang),
- [code, lang]
- );
+ return [code, source, repl];
+}
+
+/**
+ * @param {{ code: string, lang: string, repl?: string }} props
+ */
+function HighlightedCodeBlock({ code, lang }) {
+ let repl = false,
+ source = code;
+
+ [code, source, repl] = processRepl(source, repl);
- const canHighlight = !!pending || !error;
- const html =
- (canHighlight && highlighted) ||
- code.replace(//g, '>');
- const htmlObj = useMemo(() => ({ __html: html }), [html]);
+ // Show unhighlighted code as a fallback until we're ready
+ const fallback = {code}
;
return (
-
+
);
@@ -143,14 +103,7 @@ const CodeBlock = props => {
)[1];
const firstChild = getChild(child.props);
const code = String(firstChild || '').replace(/(^\s+|\s+$)/g, '');
- return (
-
- );
+ return
;
}
return
;
diff --git a/src/components/code-block/prism.worker.js b/src/components/code-block/prism.worker.js
index 05f1342d0..0354a1c92 100644
--- a/src/components/code-block/prism.worker.js
+++ b/src/components/code-block/prism.worker.js
@@ -7,7 +7,8 @@ export function highlight(code, lang) {
if (prism.languages[lang] != null) {
return prism.highlight(code, prism.languages[lang], lang);
}
- throw Error(`Unknown language: ${lang}`);
+ //console.error(`Unknown language: ${lang}`);
+ return code;
}
// .expose will throw in SSR env
diff --git a/src/components/content-region/index.js b/src/components/content-region/index.js
index 995d001b2..d9fdde6be 100644
--- a/src/components/content-region/index.js
+++ b/src/components/content-region/index.js
@@ -2,6 +2,7 @@ import Markup from 'preact-markup';
import widgets from '../widgets';
import style from './style.module.css';
import { useTranslation } from '../../lib/i18n';
+import { TocContext } from '../table-of-contents';
const COMPONENTS = {
...widgets,
@@ -60,13 +61,15 @@ export default function ContentRegion({ content, components, ...props }) {
return (
{content && (
-
+
+
+
)}
{hasNav && (
diff --git a/src/components/controllers/blog-page.js b/src/components/controllers/blog-page.js
new file mode 100644
index 000000000..dc23d7d8b
--- /dev/null
+++ b/src/components/controllers/blog-page.js
@@ -0,0 +1,40 @@
+import { useRoute, useLocation } from 'preact-iso';
+import { useContent } from '../../lib/use-resource';
+import { useTitle, useDescription } from './utils';
+import { NotFound } from './not-found';
+import { useLanguage } from '../../lib/i18n';
+import { MarkdownRegion } from './markdown-region';
+import Footer from '../footer/index';
+import { blogRoutes } from '../../lib/route-utils';
+import style from './style.module.css';
+
+export default function BlogPage() {
+ const { params } = useRoute();
+ const { slug } = params;
+
+ if (!blogRoutes[`/blog/${slug}`]) {
+ return
;
+ }
+
+ return
;
+}
+
+function BlogLayout() {
+ const { url } = useLocation();
+ const [lang] = useLanguage();
+
+ const { html, meta } = useContent([lang, url === '/' ? 'index' : url]);
+ useTitle(meta.title);
+ useDescription(meta.description);
+
+ return (
+
+ );
+}
diff --git a/src/components/controllers/doc-page.js b/src/components/controllers/doc-page.js
new file mode 100644
index 000000000..e24f89e49
--- /dev/null
+++ b/src/components/controllers/doc-page.js
@@ -0,0 +1,85 @@
+import { useRoute, useLocation } from 'preact-iso';
+import { useContent } from '../../lib/use-resource';
+import { useTitle, useDescription } from './utils';
+import config from '../../config.json';
+import { NotFound } from './not-found';
+import cx from '../../lib/cx';
+import { LATEST_MAJOR, isDocPage } from '../../lib/docs';
+import { useLanguage } from '../../lib/i18n';
+import { MarkdownRegion } from './markdown-region';
+import Sidebar from '../sidebar';
+import Footer from '../footer/index';
+import { docRoutes } from '../../lib/route-utils';
+import style from './style.module.css';
+
+export function DocPage() {
+ const { params } = useRoute();
+ const { version, name } = params;
+
+ if (!docRoutes[version]['/' + name]) {
+ return
;
+ }
+
+ return
;
+}
+
+export function DocLayout() {
+ const { url } = useLocation();
+ const [lang] = useLanguage();
+
+ const { html, meta } = useContent([lang, url === '/' ? 'index' : url]);
+ useTitle(meta.title);
+ useDescription(meta.description);
+
+ const hasSidebar = meta.toc !== false && isDocPage(url);
+
+ return (
+
+
+ {hasSidebar && (
+
+ )}
+
+
+
+ );
+}
+
+function OldDocsWarning() {
+ const { name, version } = useRoute().params;
+ const { url } = useLocation();
+
+ if (!isDocPage(url) || version === LATEST_MAJOR) {
+ return null;
+ }
+
+ const latestExists = config.docs[LATEST_MAJOR].some(section =>
+ section.routes.some(route => route.path === '/' + name)
+ );
+
+ return (
+
+ You are viewing the documentation for an older version of Preact.
+ {latestExists ? (
+ <>
+ {' '}
+ Switch to the{' '}
+
current version .
+ >
+ ) : (
+ <>
+ {' '}
+ Get started with the{' '}
+
current version
+ .
+ >
+ )}
+
+ );
+}
diff --git a/src/components/controllers/index.js b/src/components/controllers/index.js
deleted file mode 100644
index b80fe0cc2..000000000
--- a/src/components/controllers/index.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import Page from './page';
-import Repl from 'async!./repl';
-import Tutorial from 'async!./tutorial';
-
-export default {
- default: Page,
- error: Page,
- Repl,
- Blog: Page,
- Tutorial
-};
diff --git a/src/components/controllers/markdown-region.js b/src/components/controllers/markdown-region.js
new file mode 100644
index 000000000..2c0328214
--- /dev/null
+++ b/src/components/controllers/markdown-region.js
@@ -0,0 +1,30 @@
+import { useLocation } from 'preact-iso';
+import EditThisPage from '../edit-button';
+import ContentRegion from '../content-region';
+import BlogMeta from '../blog-meta';
+
+/**
+ * @param {object} props
+ * @propery {string} html
+ * @propery {any} meta
+ * @propery {any} [components]
+ */
+export function MarkdownRegion({ html, meta, components }) {
+ const { url } = useLocation();
+
+ const canEdit = url !== '/' && !url.startsWith('/tutorial');
+ const isBlogArticle = url.startsWith('/blog/');
+
+ return (
+ <>
+ {canEdit &&
}
+ {isBlogArticle &&
}
+
+ >
+ );
+}
diff --git a/src/components/controllers/not-found.js b/src/components/controllers/not-found.js
new file mode 100644
index 000000000..6a64acbc9
--- /dev/null
+++ b/src/components/controllers/not-found.js
@@ -0,0 +1,25 @@
+import { useLanguage } from '../../lib/i18n';
+import { useContent } from '../../lib/use-resource';
+import { useTitle, useDescription } from './utils';
+import Footer from '../footer';
+import { MarkdownRegion } from './markdown-region';
+import style from './style.module.css';
+
+export function NotFound() {
+ const [lang] = useLanguage();
+
+ const { html, meta } = useContent([lang, '404']);
+ useTitle(meta.title);
+ useDescription(meta.description);
+
+ return (
+
+ );
+}
diff --git a/src/components/controllers/page.js b/src/components/controllers/page.js
new file mode 100644
index 000000000..72744473e
--- /dev/null
+++ b/src/components/controllers/page.js
@@ -0,0 +1,14 @@
+import { useRoute } from 'preact-iso';
+import { navRoutes } from '../../lib/route-utils';
+import { NotFound } from './not-found';
+import { DocLayout } from './doc-page';
+
+export function Page() {
+ const { path } = useRoute();
+
+ if (!navRoutes[path]) {
+ return
;
+ }
+
+ return
;
+}
diff --git a/src/components/controllers/page/index.js b/src/components/controllers/page/index.js
deleted file mode 100644
index 9c81244aa..000000000
--- a/src/components/controllers/page/index.js
+++ /dev/null
@@ -1,175 +0,0 @@
-import { useEffect, useMemo } from 'preact/hooks';
-import cx from '../../../lib/cx';
-import ContentRegion from '../../content-region';
-import config from '../../../config.json';
-import style from './style.module.css';
-import Footer from '../../footer';
-import Sidebar from './sidebar';
-import Hydrator from '../../../lib/hydrator';
-import EditThisPage from '../../edit-button';
-import { InjectPrerenderData } from '../../../lib/prerender-data';
-import { isDocPage } from '../../../lib/docs';
-import { useStore } from '../../store-adapter';
-import { AVAILABLE_DOCS } from '../../doc-version';
-import { Time } from '../../time';
-import { usePage, getContentId } from '../utils';
-
-export default function Page({ route, prev, next }, ctx) {
- const store = useStore(['url', 'lang', 'docVersion']);
- const { loading, meta, content, html, current, isFallback } = usePage(
- route,
- store.state.lang
- );
- const urlState = store.state;
- const url = useMemo(() => urlState.url, [current]);
-
- const docsUrl = useMemo(
- () => url.replace(/(v\d{1,2})/, `v${AVAILABLE_DOCS[0]}`),
- [url]
- );
-
- const layout = `${meta.layout || 'default'}Layout`;
- const name = getContentId(route);
-
- const isReady = !loading;
-
- // workaround: we toc in the store in order for
to pick it up.
- if (meta.toc && ctx.store.getState().toc !== meta.toc) {
- ctx.store.setState({
- toc: meta.toc
- });
- }
-
- // Note:
- // "name" is the exact page ID from the URL
- // "current" is the currently *displayed* page ID.
-
- const canEdit = current !== 'index';
- const isBlogArticle = current.startsWith('/blog/');
- const hasSidebar = meta.toc !== false && isDocPage(url);
-
- useEffect(() => {
- if (location.hash) {
- const anchor = document.querySelector(location.hash);
- if (anchor) {
- // Do not use scrollIntoView as it will cause
- // the heading to be covered by the header
- scrollTo({ top: anchor.offsetTop });
- }
- }
- }, [html]);
-
- return (
-
-
-
-
-
- {isDocPage(url) && +store.state.docVersion !== AVAILABLE_DOCS[0] && (
-
- )}
-
- {isBlogArticle &&
}
-
-
-
-
-
-
- );
-}
-
-function BlogMeta({ meta }) {
- return (
-
- {meta.date &&
}
- {Array.isArray(meta.authors) && meta.authors.length > 0 && (
- <>
- , written by{' '}
-
- {meta.authors.map((author, i, arr) => {
- const authorData = config.blogAuthors.find(
- data => data.name === author
- );
- return (
-
- {authorData ? (
-
- {author}
-
- ) : (
- {author}
- )}
- {i < arr.length - 2
- ? ', '
- : i === arr.length - 2
- ? ' and '
- : null}
-
- );
- })}
- {(meta.translation_by || []).map((author, i, arr) => {
- const authorData = config.blogAuthors.find(
- data => data.name === author
- );
- return (
- <>
- {', translated by '}
-
- {authorData ? (
-
- {author}
-
- ) : (
- {author}
- )}
- {i < arr.length - 2
- ? ', '
- : i === arr.length - 2
- ? ' and '
- : null}
-
- >
- );
- })}
-
- >
- )}
-
- );
-}
diff --git a/src/components/controllers/page/sidebar.js b/src/components/controllers/page/sidebar.js
deleted file mode 100644
index 41ed32fdb..000000000
--- a/src/components/controllers/page/sidebar.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import style from './sidebar.module.css';
-import DocVersion from './../../doc-version';
-import SidebarNav from './sidebar-nav';
-import { useCallback } from 'preact/hooks';
-import config from '../../../config.json';
-import { useStore } from '../../store-adapter';
-import { useOverlayToggle } from '../../../lib/toggle-overlay';
-import { getRouteName } from '../../header';
-
-export default function Sidebar() {
- const [open, setOpen] = useOverlayToggle(false);
- const toggle = useCallback(() => setOpen(!open), [open]);
- const close = useCallback(() => setOpen(false), []);
- const { docVersion, lang } = useStore(['docVersion', 'lang']).state;
-
- // Get menu items for the current version of the docs (guides)
- // TODO: allow multiple sections - config[meta.section]
- const docNav = config.docs
- .filter(item => {
- // We know that nested routes are part of the same
- // doc version, so we just need to check the first
- // route.
- if (item.routes) {
- item = item.routes[0];
- }
-
- return item.path.indexOf(`/v${docVersion}`) > -1;
- })
- .reduce((acc, item) => {
- if (item.routes) {
- acc.push({
- text: getRouteName(item, lang),
- level: 2,
- href: null,
- routes: item.routes.map(nested => ({
- text: getRouteName(nested, lang),
- level: 3,
- href: nested.path
- }))
- });
- } else {
- acc.push({
- text: getRouteName(item, lang),
- level: 2,
- href: item.path
- });
- }
- return acc;
- }, []);
-
- // TODO: use URL match instead of .content
- const guide = config.nav.filter(item => item.content === 'guide')[0];
- const sectionName = getRouteName(guide, lang);
-
- return (
-
-
- {sectionName}
-
-
-
- );
-}
diff --git a/src/components/controllers/page/style.module.css b/src/components/controllers/style.module.css
similarity index 87%
rename from src/components/controllers/page/style.module.css
rename to src/components/controllers/style.module.css
index 8c2491d19..dfc89912c 100644
--- a/src/components/controllers/page/style.module.css
+++ b/src/components/controllers/style.module.css
@@ -7,7 +7,7 @@
margin-bottom: 4rem;
}
- content-region:not([name='index']) :global(.markup) {
+ content-region:not([name='/']) :global(.markup) {
margin-top: 2.5rem;
@media (max-width: 600px) {
@@ -45,14 +45,6 @@
text-align: center;
}
-.blogMeta {
- /* Negative bottom margin negates content-region's top margin */
- margin: 2.5rem auto -3rem;
- padding: 0.5em 1rem 0.25rem;
- width: 100%;
- max-width: var(--content-width);
-}
-
content-region h1 {
padding: 0.5rem 1rem 0.25rem !important;
font-weight: 200;
@@ -64,10 +56,6 @@ content-region h1 {
}
}
-.authors {
- display: inline-block;
-}
-
.inner {
display: flex;
flex-direction: column;
diff --git a/src/components/controllers/tutorial/index.js b/src/components/controllers/tutorial/index.js
index 10e55edb7..aca963b5d 100644
--- a/src/components/controllers/tutorial/index.js
+++ b/src/components/controllers/tutorial/index.js
@@ -8,19 +8,42 @@ import {
useMemo,
useCallback
} from 'preact/hooks';
+import { useLocation, useRoute } from 'preact-iso';
import linkState from 'linkstate';
import cx from '../../../lib/cx';
import style from './style.module.css';
import { ErrorOverlay } from '../repl/error-overlay';
import { parseStackTrace } from '../repl/errors';
-import ContentRegion from '../../content-region';
import widgets from '../../widgets';
-import { usePage } from '../utils';
-import { useStore, storeCtx } from '../../store-adapter';
import { InjectPrerenderData } from '../../../lib/prerender-data';
-import { getContent } from '../../../lib/content';
+//import { getContent } from '../../../lib/content';
+import { useLanguage } from '../../../lib/i18n';
+import { useContent } from '../../../lib/use-resource';
+import { useTitle, useDescription } from '../utils';
import { Splitter } from '../../splitter';
import config from '../../../config.json';
+import { MarkdownRegion } from '../markdown-region.js';
+
+/**
+ * @typedef SolutionContext
+ * @property {boolean} solved
+ * @property {(boolean) => void} setSolved
+ */
+
+/**
+ * @type {import('preact').Context}
+ */
+const SolutionContext = createContext(/** @type {SolutionContext} */ ({}));
+
+export function SolutionProvider({ children }) {
+ const [solved, setSolved] = useState(false);
+
+ return (
+
+ {children}
+
+ );
+}
const IS_PRERENDERING = typeof window === 'undefined';
@@ -43,7 +66,7 @@ export default class Tutorial extends Component {
content = createRef();
runner = createRef();
- static contextType = storeCtx;
+ static contextType = SolutionContext;
resultHandlers = new Set();
realmHandlers = new Set();
@@ -79,7 +102,7 @@ export default class Tutorial extends Component {
'repl-initial': '',
'repl-final': ''
});
- this.context.setState({ solved: false });
+ this.context.setSolved(false);
}
}
@@ -138,9 +161,8 @@ export default class Tutorial extends Component {
this.setState({ error: null });
};
- render({ route, step }, { loading, code, error }) {
+ render({ step }, { loading, code, error }) {
const state = {
- route,
step,
loading,
code,
@@ -158,7 +180,6 @@ export default class Tutorial extends Component {
function TutorialView({
step,
- route,
loading,
code,
error,
@@ -166,35 +187,45 @@ function TutorialView({
CodeEditor,
clearError
}) {
- const content = useRef();
+ const content = useRef(null);
const tutorial = useContext(TutorialContext);
const [showCodeOverride, toggleCode] = useReducer(s => !s, true);
- const { lang, solved } = useStore(['lang', 'solved']).state;
- const fullPath = route.path.replace(':step?', step || route.first);
- const page = usePage({ path: fullPath }, lang);
- const solvable = page && page.meta.solvable === true;
- const hasCode = page && page.meta.code !== false && step && step !== 'index';
+ const { url } = useLocation();
+ const { params } = useRoute();
+ const [lang] = useLanguage();
+ const { solved } = useContext(SolutionContext);
+
+ const { html, meta } = useContent([
+ lang,
+ !params.step ? 'tutorial/index' : url
+ ]);
+ useTitle(meta.title);
+ useDescription(meta.description);
+
+ const solvable = meta.solvable === true;
+ const hasCode = meta.code !== false && step && step !== 'index';
const showCode = showCodeOverride && hasCode;
- loading =
- !page.html || (showCode && (!!page.loading || !Runner || !CodeEditor));
- const initialLoad = !page.html || !Runner || !CodeEditor;
+ loading = !html || (showCode && (!Runner || !CodeEditor));
+ const initialLoad = !html || !Runner || !CodeEditor;
// Scroll to the top after loading
useEffect(() => {
if (!loading && !initialLoad) {
content.current.scrollTo(0, 0);
}
- }, [fullPath, loading, initialLoad]);
+ }, [url, loading, initialLoad]);
// Preload the next chapter
- useEffect(() => {
- if (page.meta && page.meta.next) {
- getContent([lang, page.meta.next]);
- }
- }, [page.meta && page.meta.next, fullPath]);
+ // TODO: Webpack creates a circular dependency that
+ // it cannot resolve. Temporarily disabled
+ //useEffect(() => {
+ // if (meta && meta.next) {
+ // getContent([lang, meta.next]);
+ // }
+ //}, [meta && meta.next, url]);
const reRun = useCallback(() => {
let code = tutorial.state.code;
@@ -206,7 +237,6 @@ function TutorialView({
return (
{!initialLoad && (
-
@@ -313,10 +340,10 @@ function TutorialView({
@@ -335,7 +362,6 @@ const REPL_CSS = `
function ReplWrapper({
loading,
- subtleLoading,
solvable,
solved,
initialLoad,
@@ -344,7 +370,7 @@ function ReplWrapper({
}) {
return (
-
+
tutorial.runner.current.realm.globalThis._require(m);
const fn = new Function(
@@ -419,11 +445,10 @@ function TutorialSetupBlock({ code }) {
'useEffect',
'useRef',
'useMemo',
- 'useStore',
'useResult',
'useRealm',
'useError',
- 'store',
+ 'solutionCtx',
'realm',
'require',
code
@@ -437,11 +462,10 @@ function TutorialSetupBlock({ code }) {
useEffect,
useRef,
useMemo,
- useStore,
tutorial.useResult,
tutorial.useRealm,
tutorial.useError,
- store,
+ solutionCtx,
tutorial.runner.current && tutorial.runner.current.realm,
require
);
@@ -454,8 +478,8 @@ function TutorialSetupBlock({ code }) {
/** Shows a solution banner when the chapter is solved */
function Solution({ children }) {
- const { solved } = useStore(['solved']).state;
- const ref = useRef();
+ const { solved } = useContext(SolutionContext);
+ const ref = useRef(null);
useEffect(() => {
if (solved) {
diff --git a/src/components/controllers/utils.js b/src/components/controllers/utils.js
index 5eb2663e6..288451e16 100644
--- a/src/components/controllers/utils.js
+++ b/src/components/controllers/utils.js
@@ -1,7 +1,5 @@
-import { useEffect, useRef, useState } from 'preact/hooks';
+import { useEffect } from 'preact/hooks';
-import { getContentOnServer, getContent } from '../../lib/content';
-import { getPrerenderData } from '../../lib/prerender-data';
import { createTitle } from '../../lib/page-title';
/**
@@ -28,86 +26,3 @@ export function useDescription(text) {
}
}, [text]);
}
-
-export const getContentId = route => route.content || route.path;
-export function usePage(route, lang) {
- // on the server, pass data down through the tree to avoid repeated FS lookups
- if (PRERENDER) {
- const { content, html, meta } = getContentOnServer(route.path, lang);
- return {
- current: getContentId(route),
- content,
- html,
- meta,
- loading: true // this is important since the client will initialize in a loading state.
- };
- }
-
- const [current, setCurrent] = useState(getContentId(route));
-
- const bootData = getPrerenderData(current);
-
- const [hydrated, setHydrated] = useState(!!bootData);
- const [content, setContent] = useState(
- hydrated && bootData && bootData.content
- );
- const [html, setHtml] = useState(hydrated && bootData && bootData.html);
-
- const [loading, setLoading] = useState(true);
- const [isFallback, setFallback] = useState(false);
- let [meta, setMeta] = useState(hydrated ? bootData.meta : undefined);
- if (!meta) meta = (hydrated && bootData.meta) || {};
-
- const lock = useRef();
- useEffect(() => {
- if (!didLoad) {
- setLoading(true);
- }
- const contentId = getContentId(route);
- lock.current = contentId;
- getContent([lang, contentId]).then(data => {
- // Discard old load events
- if (lock.current !== contentId) return;
- onLoad(data);
- });
- }, [getContentId(route), lang]);
-
- useTitle(meta.title);
- useDescription(meta.description);
-
- let didLoad = false;
- function onLoad(data) {
- const { content, html, meta, fallback } = data;
- didLoad = true;
-
- // Don't show loader forever in case of an error
- if (!meta) return;
-
- setContent(content);
- setMeta(meta);
- setHtml(html);
- setLoading(false);
- setFallback(fallback);
- const current = getContentId(route);
- const bootData = getPrerenderData(current);
- setHydrated(!!bootData);
- setCurrent(current);
- // content was loaded. if this was a forward route transition, animate back to top
- if (window.nextStateToTop) {
- window.nextStateToTop = false;
- scrollTo({
- top: 0,
- left: 0
- });
- }
- }
-
- return {
- current,
- content,
- html,
- meta,
- loading,
- isFallback
- };
-}
diff --git a/src/components/doc-version/index.js b/src/components/doc-version/index.js
index 0399e251a..cbb9804bf 100644
--- a/src/components/doc-version/index.js
+++ b/src/components/doc-version/index.js
@@ -1,11 +1,7 @@
+import { useCallback } from 'preact/hooks';
+import { useLocation, useRoute } from 'preact-iso';
+import config from '../../config.json';
import style from './style.module.css';
-import { getCurrentUrl, route } from 'preact-router';
-import { useStore } from '../store-adapter';
-
-function onChange(e) {
- const url = getCurrentUrl().replace(/(v\d{1,2})/, `v${e.target.value}`);
- route(url);
-}
export const AVAILABLE_DOCS = [10, 8];
@@ -13,16 +9,28 @@ export const AVAILABLE_DOCS = [10, 8];
* Select box to switch the currently displayed docs version
*/
export default function DocVersion() {
- const { docVersion } = useStore(['docVersion']).state;
+ const { path, route } = useLocation();
+ const { version, name } = useRoute().params;
+
+ const onChange = useCallback(
+ e => {
+ const version = e.currentTarget.value;
+ const url = config.docs[version]?.[name]
+ ? path.replace(/(v\d{1,2})/, version)
+ : `/guide/${version}/getting-started`;
+ route(url);
+ },
+ [path, route]
+ );
return (
Version:{' '}
-
+
{AVAILABLE_DOCS.map(v => {
const suffix = v === 10 ? ' (current)' : '';
return (
-
+
{v}.x{suffix}
);
diff --git a/src/components/edit-button/index.js b/src/components/edit-button/index.js
index 7acf7b248..09488dd29 100644
--- a/src/components/edit-button/index.js
+++ b/src/components/edit-button/index.js
@@ -1,35 +1,33 @@
-import { useStore } from '../store-adapter';
+import { useLocation } from 'preact-iso';
+import { useLanguage } from '../../lib/i18n';
import style from './style.module.css';
-export default function EditThisPage({ show, isFallback }) {
- const store = useStore(['lang', 'url']);
- const { url, lang } = store.state;
+export default function EditThisPage({ isFallback }) {
+ const { url } = useLocation();
+ const [lang] = useLanguage();
+
let path = url.replace(/\/$/, '') || '/index';
path = !isFallback ? path + '.md' : '';
const editUrl = `https://github.com/preactjs/preact-www/tree/master/content/${lang}${path}`;
return (
- show && (
- <>
-
-
- {!isFallback ? 'Edit this Page' : 'Add translation'}
-
+
);
}
diff --git a/src/components/footer/index.js b/src/components/footer/index.js
index bfd16766c..193762919 100644
--- a/src/components/footer/index.js
+++ b/src/components/footer/index.js
@@ -1,6 +1,6 @@
import config from '../../config.json';
import { useState, useEffect, useCallback } from 'preact/hooks';
-import { useStore } from '../store-adapter';
+import { useLocation } from 'preact-iso';
import style from './style.module.css';
import { useLanguage } from '../../lib/i18n';
@@ -42,7 +42,7 @@ function useContributors(deps) {
}
export default function Footer() {
- const { url } = useStore(['url']).state;
+ const { url } = useLocation();
const contrib = useContributors([url]);
const [lang, setLang] = useLanguage();
@@ -72,7 +72,7 @@ export default function Footer() {
rel="noopener noreferrer"
>
lovely people
-
+ {' '}
{contrib && [
' like ',
setOpen(!open), [open]);
useEffect(() => {
@@ -178,7 +177,8 @@ class NavMenu extends Component {
// depending on the type of nav link, use
const NavLink = ({ to, isOpen, route, ...props }) => {
- const { lang } = useStore(['lang']).state;
+ const location = useLocation();
+ const [lang] = useLanguage();
let Flair = to.flair && LINK_FLAIR[to.flair];
if (to.skipHeader) return;
@@ -199,7 +199,7 @@ const NavLink = ({ to, isOpen, route, ...props }) => {
function BrandingRedirect(e) {
if (to.href == '/' || to.path == '/') {
e.preventDefault();
- reroute('/branding', false);
+ location.route('/branding');
}
}
diff --git a/src/components/logo.js b/src/components/logo.js
index e771e7478..660677b20 100644
--- a/src/components/logo.js
+++ b/src/components/logo.js
@@ -1,8 +1,9 @@
import { Component } from 'preact';
-import { route } from 'preact-router';
+import { useLocation } from 'preact-iso';
export default class Logo extends Component {
state = { i: 0, hover: false };
+ route = null;
hover = () => {
this.setState({ hover: true });
@@ -26,7 +27,7 @@ export default class Logo extends Component {
contextMenu = e => {
e.preventDefault();
- route('/branding', false);
+ this.route('/branding');
};
componentDidMount() {
@@ -79,6 +80,7 @@ export default class Logo extends Component {
let touch =
typeof navigator !== 'undefined' && navigator.maxTouchPoints > 1;
if (inverted) [bg, fg] = [fg, bg];
+ this.route = useLocation().route;
return (
{
- pushState.call(history, a, b, url);
- if (url.indexOf('#') < 0) {
- // next time content loads, scroll to top:
- window.nextStateToTop = true;
- // scrollTo(0, 0);
- }
-};
-
-function isValidSiblingRoute(sibling, route) {
- const idx = route.path.lastIndexOf('/');
- const common = idx > 1 ? route.path.slice(0, idx) : route.path;
- return sibling && sibling.path.substring(0, common.length) === common;
-}
-
-export default class Routes extends Component {
- /**
- * Gets fired when the route changes.
- * @param {Object} event "change" event from [preact-router](https://github.com/preactjs/preact-router)
- * @param {string} event.url The newly routed URL
- */
- handleRoute = event => {
- let { onChange } = this.props;
- if (onChange) onChange(event);
- };
-
- shouldComponentUpdate() {
- return false;
- }
-
- getNavRoutes(nav) {
- const routes = [];
- const stack = [...nav];
- let route;
- while ((route = stack.pop())) {
- if (route.routes) {
- stack.push(...route.routes);
- } else {
- routes.push(route);
- }
+import { useState } from 'preact/hooks';
+import { Router, Route, lazy } from 'preact-iso';
+import { Page } from './controllers/page';
+import { DocPage } from './controllers/doc-page';
+import { NotFound } from './controllers/not-found';
+import { navRoutes } from '../lib/route-utils';
+
+if (typeof history !== 'undefined') {
+ let { pushState } = history;
+ history.pushState = (a, b, url) => {
+ pushState.call(history, a, b, url);
+ if (url.indexOf('#') < 0) {
+ // next time content loads, scroll to top:
+ window.nextStateToTop = true;
+ // scrollTo(0, 0);
}
+ };
+}
- return routes.reverse().reduce((out, route, i, routes) => {
- if (route.path) {
- const skip = route.path === '/' || /^\/about/.test(route.path);
- const prev = !skip && i - 1 > 0 ? routes[i - 1] : null;
- const next = !skip && i + 1 < routes.length ? routes[i + 1] : null;
-
- const view = this.buildRoute(
- route,
- isValidSiblingRoute(prev, route) ? prev : null,
- isValidSiblingRoute(next, route) ? next : null
- );
- out.push(view);
- }
- return out;
- }, []);
- }
-
- buildRoute(route, prev, next) {
- let Ctrl = controllers.default;
- if (route.controller) {
- // eslint-disable-next-line no-unused-vars
- for (let i in controllers) {
- if (i.toLowerCase() === route.controller.toLowerCase()) {
- Ctrl = controllers[i];
+const Repl = lazy(() => import('./controllers/repl'));
+const Tutorial = lazy(() => import('./controllers/tutorial'));
+const BlogPage = lazy(() => import('./controllers/blog-page'));
+
+export default function Routes() {
+ const [loading, setLoading] = useState(false);
+ return (
+
+
+ setLoading(true)}
+ onLoadEnd={() => setLoading(false)}
+ onRouteChange={url =>
+ typeof ga === 'function' && ga('send', 'pageview', url)
}
- }
- }
- return (
-
- );
- }
-
- render({ url }) {
- return (
-
-
- {this.getNavRoutes(config.docs)}
- {this.getNavRoutes(config.nav)}
- {this.getNavRoutes(config.blog)}
-
-
-
- );
- }
+ >
+ {Object.keys(navRoutes)
+ .filter(route => !route.startsWith('/guide'))
+ .map(route => {
+ let component;
+ if (route === '/repl') {
+ component = Repl;
+ } else if (route.startsWith('/tutorial')) {
+ component = Tutorial;
+ } else {
+ component = Page;
+ }
+ return ;
+ })}
+
+
+
+
+
+
+ );
}
diff --git a/src/components/sidebar/index.js b/src/components/sidebar/index.js
new file mode 100644
index 000000000..e5978e5ee
--- /dev/null
+++ b/src/components/sidebar/index.js
@@ -0,0 +1,60 @@
+import { useRoute } from 'preact-iso';
+import DocVersion from '../doc-version';
+import SidebarNav from './sidebar-nav';
+import config from '../../config.json';
+import { useOverlayToggle } from '../../lib/toggle-overlay';
+import { getRouteName } from '../header';
+import { useLanguage } from '../../lib/i18n';
+import style from './style.module.css';
+
+export default function Sidebar() {
+ const { version } = useRoute().params;
+ const [lang] = useLanguage();
+ const [open, setOpen] = useOverlayToggle();
+
+ const navItems = [];
+ const routes = config.docs[version];
+ for (let i = 0; i < routes.length; i++) {
+ const item = routes[i];
+ if (item.routes) {
+ navItems.push({
+ text: getRouteName(item, lang),
+ level: 2,
+ href: null,
+ routes: item.routes.map(nested => ({
+ text: getRouteName(nested, lang),
+ level: 3,
+ href: `/guide/${version}${nested.path}`
+ }))
+ });
+ } else {
+ navItems.push({
+ text: getRouteName(item, lang),
+ level: 2,
+ href: `/guide/${version}${item.path}`
+ });
+ }
+ }
+
+ // TODO: use URL match instead of .content
+ const guide = config.nav.filter(item => item.content === 'guide')[0];
+ const sectionName = getRouteName(guide, lang);
+
+ return (
+
+ setOpen(v => !v)}
+ value="sidebar"
+ >
+ {sectionName}
+
+
+
+ );
+}
diff --git a/src/components/controllers/page/sidebar-nav.js b/src/components/sidebar/sidebar-nav.js
similarity index 91%
rename from src/components/controllers/page/sidebar-nav.js
rename to src/components/sidebar/sidebar-nav.js
index 961aa6cd8..8955c55a8 100644
--- a/src/components/controllers/page/sidebar-nav.js
+++ b/src/components/sidebar/sidebar-nav.js
@@ -1,6 +1,6 @@
-import cx from '../../../lib/cx';
+import { useLocation } from 'preact-iso';
+import cx from '../../lib/cx';
import style from './sidebar-nav.module.css';
-import { getCurrentUrl } from 'preact-router';
/**
* @typedef {object} SidebarNavProps
@@ -13,17 +13,17 @@ import { getCurrentUrl } from 'preact-router';
* @param {SidebarNavProps} props
*/
export default function SidebarNav({ items, onClick }) {
+ let { url } = useLocation();
+
// Remove trailing slash to fix activeCss check below.
// Note that netlify will always append a slash to the url so that we end
// up with something like "foo/bar/?lang=de". That's why we first remove
// the search params before removing the trailing slash.
- const url = getCurrentUrl()
- .replace(/\?.*/, '')
- .replace(/\/$/, '');
+ url = url.replace(/\?.*/, '').replace(/\/$/, '');
return (
1) && style.disabled)}
>
{items.map(({ text, level, href, routes }) => {
diff --git a/src/components/controllers/page/sidebar-nav.module.css b/src/components/sidebar/sidebar-nav.module.css
similarity index 100%
rename from src/components/controllers/page/sidebar-nav.module.css
rename to src/components/sidebar/sidebar-nav.module.css
diff --git a/src/components/controllers/page/sidebar.module.css b/src/components/sidebar/style.module.css
similarity index 100%
rename from src/components/controllers/page/sidebar.module.css
rename to src/components/sidebar/style.module.css
diff --git a/src/components/table-of-contents/index.js b/src/components/table-of-contents/index.js
index f3aa357f7..37cd02877 100644
--- a/src/components/table-of-contents/index.js
+++ b/src/components/table-of-contents/index.js
@@ -1,13 +1,17 @@
-import cx from '../../lib/cx';
-import style from './style.module.css';
-import { useStore } from '../store-adapter';
-import { useMemo, useRef } from 'preact/hooks';
+import { createContext } from 'preact';
+import { useContext, useMemo, useRef } from 'preact/hooks';
+import { useLocation } from 'preact-iso';
+
+/** @type {import('preact').Context<{ toc: any}} */
+export const TocContext = createContext({ toc: null });
export default function Toc() {
const ref = useRef(null);
const cache = useRef([]);
- const { toc, url } = useStore(['url', 'toc']).state;
+ const { url } = useLocation();
+ const { toc } = useContext(TocContext);
+
// eslint-disable-next-line
const items = useMemo(() => {
return toc !== null
@@ -20,8 +24,8 @@ export default function Toc() {
const loc = url.slice(url.indexOf('#') >>> 0);
return (
- 1) && style.disabled)}>
-
+
+
{items.map(entry => (
@@ -58,17 +62,11 @@ export function listToTree(arr) {
}
export function TocItem(props) {
- const { id, text, level, children, loc } = props;
- let activeCss = loc ? style.linkActive : undefined;
+ const { id, text, children } = props;
return (
-
- {text}
-
+ {text}
{children.length > 0 && (
{children.map(entry => (
diff --git a/src/config.json b/src/config.json
index f63afe111..68a3eba7d 100644
--- a/src/config.json
+++ b/src/config.json
@@ -76,16 +76,14 @@
"skipHeader": true
},
{
- "path": "/tutorial/:step?",
- "href": "/tutorial/",
- "first": "index",
+ "path": "/tutorial/:step",
+ "href": "/tutorial",
"name": {
"en": "Tutorial",
"zh": "教程",
"kr": "튜토리얼",
"ru": "Учебник"
- },
- "controller": "Tutorial"
+ }
},
{
"name": {
@@ -189,428 +187,428 @@
"zh": "博客",
"kr": "블로그",
"ru": "Блог"
- },
- "first": "index",
- "content": "blog",
- "controller": "Blog"
+ }
},
{
"path": "/repl",
"name": {
"en": "REPL",
"zh": "在线试用"
- },
- "controller": "Repl"
+ }
}
],
- "docs": [
- {
- "path": "/guide/v8/getting-started",
- "name": {
- "en": "Getting Started",
- "pt-br": "Começando",
- "de": "Los geht's",
- "zh": "开始上手",
- "ru": "Первые шаги"
- }
- },
- {
- "path": "/guide/v8/differences-to-react",
- "name": {
- "en": "Differences to React",
- "pt-br": "Diferenças do React",
- "de": "Unterschiede zu React",
- "zh": "与 React 的区别",
- "ru": "Отличия от React"
- }
- },
- {
- "path": "/guide/v8/switching-to-preact",
- "name": {
- "en": "Switching to Preact",
- "pt-br": "Mudando para Preact",
- "de": "Wechsel zu Preact",
- "zh": "从 React 转到 Preact",
- "ru": "Переход на Preact"
- }
- },
- {
- "path": "/guide/v8/types-of-components",
- "name": {
- "en": "Types of Components",
- "pt-br": "Tipos de Componentes",
- "de": "Komponenten-Typen",
- "zh": "组件类型",
- "ru": "Типы компонентов"
- }
- },
- {
- "path": "/guide/v8/api-reference",
- "name": {
- "en": "API Reference",
- "pt-br": "Referência da API",
- "de": "API Referenz",
- "zh": "API 参考",
- "ru": "Справочник по API"
- }
- },
- {
- "path": "/guide/v8/forms",
- "name": {
- "en": "Forms",
- "pt-br": "Formulários",
- "de": "Formulare",
- "zh": "表单",
- "ru": "Формы"
- }
- },
- {
- "path": "/guide/v8/linked-state",
- "name": {
- "en": "Linked State",
- "zh": "关联状态",
- "ru": "Связанное состояние"
- }
- },
- {
- "path": "/guide/v8/external-dom-mutations",
- "name": {
- "en": "External DOM Mutations",
- "pt-br": "Mutações do DOM externas",
- "de": "Externe DOM Mutationen",
- "zh": "外部 DOM 修改",
- "ru": "Внешние мутации DOM"
- }
- },
- {
- "path": "/guide/v8/extending-component",
- "name": {
- "en": "Extending Component",
- "pt-br": "Extendendo Componentes",
- "de": "Komponente Erweitern",
- "zh": "扩展组件",
- "ru": "Расширение компонентов"
- }
- },
- {
- "path": "/guide/v8/unit-testing-with-enzyme",
- "name": {
- "en": "Unit Testing with Enzyme",
- "pt-br": "Teste unitado com Enzyme",
- "de": "Unit-Tests mit Enzyme",
- "zh": "使用 Enzyme 进行单元测试",
- "ru": "Модульное тестирование с помощью Enzyme"
- }
- },
- {
- "path": "/guide/v8/progressive-web-apps",
- "name": {
- "en": "Progressive Web Apps",
- "zh": "渐进式 Web 应用",
- "ru": "Прогрессивные веб-приложения"
- }
- },
- {
- "name": {
- "en": "Introduction",
- "de": "Einführung",
- "zh": "介绍",
- "kr": "개요",
- "ru": "Введение"
- },
- "routes": [
- {
- "path": "/guide/v10/getting-started",
- "name": {
- "en": "Getting Started",
- "pt-br": "Começando",
- "ja": "はじめに",
- "de": "Los geht's",
- "zh": "开始上手",
- "kr": "시작하기",
- "ru": "Первые шаги"
- }
- },
- {
- "path": "/guide/v10/whats-new",
- "name": {
- "en": "What's new?",
- "ja": "Preact Xの新機能",
- "pt-br": "o que há de novo",
- "zh": "新鲜功能",
- "kr": "새로운 기능",
- "ru": "Что нового?"
- }
- },
- {
- "path": "/guide/v10/upgrade-guide",
- "name": {
- "en": "Upgrading from 8.x",
- "pt-br": "Fazendo upgrade do 8.x",
- "ja": "Preact 8.xからのアップグレード",
- "de": "Migration von 8.x",
- "zh": "从 8.x 版本更新",
- "kr": "8.x에서 업그레이드하기",
- "ru": "Обновление с 8.x"
- }
+ "docs": {
+ "v10": [
+ {
+ "name": {
+ "en": "Introduction",
+ "de": "Einführung",
+ "zh": "介绍",
+ "kr": "개요",
+ "ru": "Введение"
},
- {
- "path": "/guide/v10/tutorial",
- "name": {
- "en": "Tutorial",
- "pt-br": "Tutorial",
- "ja": "チュートリアル",
- "de": "Tutorial",
- "zh": "教程",
- "kr": "튜토리얼",
- "ru": "Учебник"
+ "routes": [
+ {
+ "path": "/getting-started",
+ "name": {
+ "en": "Getting Started",
+ "pt-br": "Começando",
+ "ja": "はじめに",
+ "de": "Los geht's",
+ "zh": "开始上手",
+ "kr": "시작하기",
+ "ru": "Первые шаги"
+ }
+ },
+ {
+ "path": "/whats-new",
+ "name": {
+ "en": "What's new?",
+ "ja": "Preact Xの新機能",
+ "pt-br": "o que há de novo",
+ "zh": "新鲜功能",
+ "kr": "새로운 기능",
+ "ru": "Что нового?"
+ }
+ },
+ {
+ "path": "/upgrade-guide",
+ "name": {
+ "en": "Upgrading from 8.x",
+ "pt-br": "Fazendo upgrade do 8.x",
+ "ja": "Preact 8.xからのアップグレード",
+ "de": "Migration von 8.x",
+ "zh": "从 8.x 版本更新",
+ "kr": "8.x에서 업그레이드하기",
+ "ru": "Обновление с 8.x"
+ }
+ },
+ {
+ "path": "/tutorial",
+ "name": {
+ "en": "Tutorial",
+ "pt-br": "Tutorial",
+ "ja": "チュートリアル",
+ "de": "Tutorial",
+ "zh": "教程",
+ "kr": "튜토리얼",
+ "ru": "Учебник"
+ }
}
- }
- ]
- },
- {
- "name": {
- "en": "Essentials",
- "zh": "基础",
- "kr": "필수 항목",
- "ru": "Основы"
+ ]
},
- "routes": [
- {
- "path": "/guide/v10/components",
- "name": {
- "en": "Components",
- "pt-br": "Componentes",
- "ja": "コンポーネント",
- "de": "Komponenten",
- "zh": "组件",
- "kr": "컴포넌트",
- "ru": "Компоненты"
- }
- },
- {
- "path": "/guide/v10/hooks",
- "name": {
- "en": "Hooks",
- "pt-br": "Hooks",
- "ja": "フック(Hooks)",
- "de": "Hooks",
- "zh": "钩子",
- "kr": "훅",
- "ru": "Хуки"
- }
+ {
+ "name": {
+ "en": "Essentials",
+ "zh": "基础",
+ "kr": "필수 항목",
+ "ru": "Основы"
},
- {
- "path": "/guide/v10/signals",
- "name": {
- "en": "Signals",
- "zh": "信号",
- "kr": "시그널",
- "ru": "Сигналы"
+ "routes": [
+ {
+ "path": "/components",
+ "name": {
+ "en": "Components",
+ "pt-br": "Componentes",
+ "ja": "コンポーネント",
+ "de": "Komponenten",
+ "zh": "组件",
+ "kr": "컴포넌트",
+ "ru": "Компоненты"
+ }
+ },
+ {
+ "path": "/hooks",
+ "name": {
+ "en": "Hooks",
+ "pt-br": "Hooks",
+ "ja": "フック(Hooks)",
+ "de": "Hooks",
+ "zh": "钩子",
+ "kr": "훅",
+ "ru": "Хуки"
+ }
+ },
+ {
+ "path": "/signals",
+ "name": {
+ "en": "Signals",
+ "zh": "信号",
+ "kr": "시그널",
+ "ru": "Сигналы"
+ }
+ },
+ {
+ "path": "/forms",
+ "name": {
+ "en": "Forms",
+ "pt-br": "Formulários",
+ "ja": "フォーム",
+ "de": "Formulare",
+ "zh": "表单",
+ "kr": "폼",
+ "ru": "Формы"
+ }
+ },
+ {
+ "path": "/refs",
+ "name": {
+ "en": "References",
+ "pt-br": "Referências",
+ "ja": "リファレンス(Ref)",
+ "de": "Referenzen",
+ "zh": "引用",
+ "kr": "레퍼런스",
+ "ru": "Ссылки"
+ }
+ },
+ {
+ "path": "/context",
+ "name": {
+ "en": "Context",
+ "pt-br": "Contexto",
+ "ja": "コンテキスト(Context)",
+ "de": "Kontext",
+ "zh": "上下文",
+ "kr": "컨텍스트",
+ "ru": "Контекст"
+ }
}
+ ]
+ },
+ {
+ "name": {
+ "en": "Debug & Test",
+ "zh": "调试与测试",
+ "kr": "디버그 & 테스트",
+ "ru": "Отладка и тестирование"
},
- {
- "path": "/guide/v10/forms",
- "name": {
- "en": "Forms",
- "pt-br": "Formulários",
- "ja": "フォーム",
- "de": "Formulare",
- "zh": "表单",
- "kr": "폼",
- "ru": "Формы"
+ "routes": [
+ {
+ "path": "/debugging",
+ "name": {
+ "en": "Debugging Tools",
+ "pt-br": "Ferramentas de depuração",
+ "ja": "デバッグツール",
+ "de": "Entwickler-Tools",
+ "zh": "调试工具",
+ "kr": "디버깅 도구",
+ "ru": "Инструменты отладки"
+ }
+ },
+ {
+ "path": "/preact-testing-library",
+ "name": {
+ "en": "Preact Testing Library",
+ "pt-br": "Preact Testing Library",
+ "ja": "Preact Testing Library",
+ "de": "Preact Testing Library",
+ "zh": "Preact 测试库",
+ "kr": "Preact 테스팅 라이브러리",
+ "ru": "Preact Testing Library"
+ }
+ },
+ {
+ "path": "/unit-testing-with-enzyme",
+ "name": {
+ "en": "Unit Testing with Enzyme",
+ "pt-br": "Teste unitario com Enzyme",
+ "ja": "Enzymeを使った単体テスト",
+ "de": "Unit-Tests mit Enzyme",
+ "kr": "Enzyme를 이용한 유닛 테스트",
+ "zh": "使用 Enzyme 进行单元测试",
+ "ru": "Модульное тестирование с помощью Enzyme"
+ }
}
+ ]
+ },
+ {
+ "name": {
+ "en": "React compatibility",
+ "zh": "React 兼容性",
+ "kr": "React 호환성",
+ "ru": "Совместимость с React"
},
- {
- "path": "/guide/v10/refs",
- "name": {
- "en": "References",
- "pt-br": "Referências",
- "ja": "リファレンス(Ref)",
- "de": "Referenzen",
- "zh": "引用",
- "kr": "레퍼런스",
- "ru": "Ссылки"
+ "routes": [
+ {
+ "path": "/differences-to-react",
+ "name": {
+ "en": "Differences to React",
+ "pt-br": "Diferenças do React",
+ "ja": "Reactとの違い",
+ "de": "Unterschiede zu React",
+ "kr": "React와의 차이점",
+ "zh": "与 React 的区别",
+ "ru": "Отличия от React"
+ }
+ },
+ {
+ "path": "/switching-to-preact",
+ "name": {
+ "en": "Switching to Preact",
+ "pt-br": "Mudando para Preact",
+ "ja": "Preactへの移行",
+ "de": "Wechsel zu Preact",
+ "kr": "Preact로 전환",
+ "zh": "从 React 转到 Preact",
+ "ru": "Переход на Preact"
+ }
}
+ ]
+ },
+ {
+ "name": {
+ "en": "Advanced",
+ "de": "Fortgeschritten",
+ "zh": "进阶",
+ "kr": "고급",
+ "ru": "Дополнительно"
},
- {
- "path": "/guide/v10/context",
- "name": {
- "en": "Context",
- "pt-br": "Contexto",
- "ja": "コンテキスト(Context)",
- "de": "Kontext",
- "zh": "上下文",
- "kr": "컨텍스트",
- "ru": "Контекст"
+ "routes": [
+ {
+ "path": "/api-reference",
+ "name": {
+ "en": "API Reference",
+ "pt-br": "Referência da API",
+ "ja": "APIリファレンス",
+ "de": "API Referenz",
+ "zh": "API 参考",
+ "kr": "API 참조",
+ "ru": "Справочник по API"
+ }
+ },
+ {
+ "path": "/web-components",
+ "name": {
+ "en": "Web Components",
+ "pt-br": "Web Components",
+ "ja": "Webコンポーネント",
+ "de": "Web Components",
+ "zh": "Web 组件",
+ "kr": "웹 컴포넌트",
+ "ru": "Веб-компоненты"
+ }
+ },
+ {
+ "path": "/progressive-web-apps",
+ "name": {
+ "en": "Progressive Web Apps",
+ "zh": "渐进式 Web 应用",
+ "kr": "점진적 웹 앱 (PWA)",
+ "ru": "Прогрессивные веб-приложения"
+ }
+ },
+ {
+ "path": "/server-side-rendering",
+ "name": {
+ "en": "Server-Side Rendering",
+ "pt-br": "Server-Side Rendering",
+ "ja": "サーバーサイドレンダリング",
+ "de": "Server-Side Rendering",
+ "zh": "服务端渲染",
+ "kr": "서버 측 렌더링",
+ "ru": "Рендеринг на стороне сервера"
+ }
+ },
+ {
+ "path": "/external-dom-mutations",
+ "name": {
+ "en": "External DOM Mutations",
+ "pt-br": "Mutações do DOM externas",
+ "ja": "Preactのステート管理の範囲外のDOMとの連携",
+ "de": "Externe DOM Mutationen",
+ "zh": "外部 DOM 修改",
+ "kr": "외부 DOM 변경",
+ "ru": "Внешние мутации DOM"
+ }
+ },
+ {
+ "path": "/options",
+ "name": {
+ "en": "Option Hooks",
+ "ja": "オプションフック",
+ "de": "Optionen",
+ "zh": "选项钩子",
+ "kr": "옵션 훅",
+ "ru": "Опционные хуки"
+ }
+ },
+ {
+ "path": "/typescript",
+ "name": "TypeScript"
}
+ ]
+ }
+ ],
+ "v8": [
+ {
+ "path": "/getting-started",
+ "name": {
+ "en": "Getting Started",
+ "pt-br": "Começando",
+ "de": "Los geht's",
+ "zh": "开始上手",
+ "ru": "Первые шаги"
}
- ]
- },
- {
- "name": {
- "en": "Debug & Test",
- "zh": "调试与测试",
- "kr": "디버그 & 테스트",
- "ru": "Отладка и тестирование"
},
- "routes": [
- {
- "path": "/guide/v10/debugging",
- "name": {
- "en": "Debugging Tools",
- "pt-br": "Ferramentas de depuração",
- "ja": "デバッグツール",
- "de": "Entwickler-Tools",
- "zh": "调试工具",
- "kr": "디버깅 도구",
- "ru": "Инструменты отладки"
- }
- },
- {
- "path": "/guide/v10/preact-testing-library",
- "name": {
- "en": "Preact Testing Library",
- "pt-br": "Preact Testing Library",
- "ja": "Preact Testing Library",
- "de": "Preact Testing Library",
- "zh": "Preact 测试库",
- "kr": "Preact 테스팅 라이브러리",
- "ru": "Preact Testing Library"
- }
- },
- {
- "path": "/guide/v10/unit-testing-with-enzyme",
- "name": {
- "en": "Unit Testing with Enzyme",
- "pt-br": "Teste unitario com Enzyme",
- "ja": "Enzymeを使った単体テスト",
- "de": "Unit-Tests mit Enzyme",
- "kr": "Enzyme를 이용한 유닛 테스트",
- "zh": "使用 Enzyme 进行单元测试",
- "ru": "Модульное тестирование с помощью Enzyme"
- }
+ {
+ "path": "/differences-to-react",
+ "name": {
+ "en": "Differences to React",
+ "pt-br": "Diferenças do React",
+ "de": "Unterschiede zu React",
+ "zh": "与 React 的区别",
+ "ru": "Отличия от React"
}
- ]
- },
- {
- "name": {
- "en": "React compatibility",
- "zh": "React 兼容性",
- "kr": "React 호환성",
- "ru": "Совместимость с React"
},
- "routes": [
- {
- "path": "/guide/v10/differences-to-react",
- "name": {
- "en": "Differences to React",
- "pt-br": "Diferenças do React",
- "ja": "Reactとの違い",
- "de": "Unterschiede zu React",
- "kr": "React와의 차이점",
- "zh": "与 React 的区别",
- "ru": "Отличия от React"
- }
- },
- {
- "path": "/guide/v10/switching-to-preact",
- "name": {
- "en": "Switching to Preact",
- "pt-br": "Mudando para Preact",
- "ja": "Preactへの移行",
- "de": "Wechsel zu Preact",
- "kr": "Preact로 전환",
- "zh": "从 React 转到 Preact",
- "ru": "Переход на Preact"
- }
+ {
+ "path": "/switching-to-preact",
+ "name": {
+ "en": "Switching to Preact",
+ "pt-br": "Mudando para Preact",
+ "de": "Wechsel zu Preact",
+ "zh": "从 React 转到 Preact",
+ "ru": "Переход на Preact"
}
- ]
- },
- {
- "name": {
- "en": "Advanced",
- "de": "Fortgeschritten",
- "zh": "进阶",
- "kr": "고급",
- "ru": "Дополнительно"
},
- "routes": [
- {
- "path": "/guide/v10/api-reference",
- "name": {
- "en": "API Reference",
- "pt-br": "Referência da API",
- "ja": "APIリファレンス",
- "de": "API Referenz",
- "zh": "API 参考",
- "kr": "API 참조",
- "ru": "Справочник по API"
- }
- },
- {
- "path": "/guide/v10/web-components",
- "name": {
- "en": "Web Components",
- "pt-br": "Web Components",
- "ja": "Webコンポーネント",
- "de": "Web Components",
- "zh": "Web 组件",
- "kr": "웹 컴포넌트",
- "ru": "Веб-компоненты"
- }
- },
- {
- "path": "/guide/v10/progressive-web-apps",
- "name": {
- "en": "Progressive Web Apps",
- "zh": "渐进式 Web 应用",
- "kr": "점진적 웹 앱 (PWA)",
- "ru": "Прогрессивные веб-приложения"
- }
- },
- {
- "path": "/guide/v10/server-side-rendering",
- "name": {
- "en": "Server-Side Rendering",
- "pt-br": "Server-Side Rendering",
- "ja": "サーバーサイドレンダリング",
- "de": "Server-Side Rendering",
- "zh": "服务端渲染",
- "kr": "서버 측 렌더링",
- "ru": "Рендеринг на стороне сервера"
- }
- },
- {
- "path": "/guide/v10/external-dom-mutations",
- "name": {
- "en": "External DOM Mutations",
- "pt-br": "Mutações do DOM externas",
- "ja": "Preactのステート管理の範囲外のDOMとの連携",
- "de": "Externe DOM Mutationen",
- "zh": "外部 DOM 修改",
- "kr": "외부 DOM 변경",
- "ru": "Внешние мутации DOM"
- }
- },
- {
- "path": "/guide/v10/options",
- "name": {
- "en": "Option Hooks",
- "ja": "オプションフック",
- "de": "Optionen",
- "zh": "选项钩子",
- "kr": "옵션 훅",
- "ru": "Опционные хуки"
- }
- },
- {
- "path": "/guide/v10/typescript",
- "name": "TypeScript"
+ {
+ "path": "/types-of-components",
+ "name": {
+ "en": "Types of Components",
+ "pt-br": "Tipos de Componentes",
+ "de": "Komponenten-Typen",
+ "zh": "组件类型",
+ "ru": "Типы компонентов"
}
- ]
- }
- ],
+ },
+ {
+ "path": "/api-reference",
+ "name": {
+ "en": "API Reference",
+ "pt-br": "Referência da API",
+ "de": "API Referenz",
+ "zh": "API 参考",
+ "ru": "Справочник по API"
+ }
+ },
+ {
+ "path": "/forms",
+ "name": {
+ "en": "Forms",
+ "pt-br": "Formulários",
+ "de": "Formulare",
+ "zh": "表单",
+ "ru": "Формы"
+ }
+ },
+ {
+ "path": "/linked-state",
+ "name": {
+ "en": "Linked State",
+ "zh": "关联状态",
+ "ru": "Связанное состояние"
+ }
+ },
+ {
+ "path": "/external-dom-mutations",
+ "name": {
+ "en": "External DOM Mutations",
+ "pt-br": "Mutações do DOM externas",
+ "de": "Externe DOM Mutationen",
+ "zh": "外部 DOM 修改",
+ "ru": "Внешние мутации DOM"
+ }
+ },
+ {
+ "path": "/extending-component",
+ "name": {
+ "en": "Extending Component",
+ "pt-br": "Extendendo Componentes",
+ "de": "Komponente Erweitern",
+ "zh": "扩展组件",
+ "ru": "Расширение компонентов"
+ }
+ },
+ {
+ "path": "/unit-testing-with-enzyme",
+ "name": {
+ "en": "Unit Testing with Enzyme",
+ "pt-br": "Teste unitado com Enzyme",
+ "de": "Unit-Tests mit Enzyme",
+ "zh": "使用 Enzyme 进行单元测试",
+ "ru": "Модульное тестирование с помощью Enzyme"
+ }
+ },
+ {
+ "path": "/progressive-web-apps",
+ "name": {
+ "en": "Progressive Web Apps",
+ "zh": "渐进式 Web 应用",
+ "ru": "Прогрессивные веб-приложения"
+ }
+ }
+ ]
+ },
"blogAuthors": [
{
"name": "Jason Miller",
diff --git a/src/lib/content.js b/src/lib/content.js
index 3fe0d8dc4..e67582e8c 100644
--- a/src/lib/content.js
+++ b/src/lib/content.js
@@ -5,26 +5,18 @@ const FRONT_MATTER_REG = /^\s*---\n\s*([\s\S]*?)\s*\n---\n/i;
const MARKDOWN_TITLE = /(?:^|\n\n)\s*(#{1,6})\s+(.+)\n+/g;
-// Store URL-keyed cached Promise return values from getContent in production.
-const CACHE = {};
-
/**
* Fetch and parse a markdown document with optional JSON FrontMatter.
- * @returns {{ content: string, meta: {toc:{text:string, id:string, level:number}[], title: string} }}
+ * @returns {Promise<{ content: string, meta: {toc:{text:string, id:string, level:number}[], title: string}, html: string }>}
*/
-export function getContent([lang, name]) {
+export async function getContent([lang, name]) {
let path = `/content/${lang}`,
url = `${path}/${name.replace(/^\//, '')}`,
ext = (url.match(/\.([a-z]+)$/i) || [])[1];
if (!ext) url += '.md';
- // In prod, never re-fetch the content (url is just a convenient compound cache key)
- if (process.env.NODE_ENV === 'production' && url in CACHE) {
- return CACHE[url];
- }
-
let fallback = false;
- const res = fetch(url)
+ return await fetch(url)
.then(r => {
// fall back to english
if (!r.ok && lang != 'en') {
@@ -46,7 +38,6 @@ export function getContent([lang, name]) {
data.fallback = fallback;
return data;
});
- return (CACHE[url] = res);
}
/**
@@ -54,11 +45,11 @@ export function getContent([lang, name]) {
* Note: noop on the client to avoid pulling in libs.
*/
export const getContentOnServer = PRERENDER
- ? (route, lang) => {
- if (route == '/') route = '/index';
+ ? ([lang, name]) => {
+ if (name == '/') name = '/index';
const fs = __non_webpack_require__('fs');
- let sourceData = fs.readFileSync(`content/${lang}/${route}.md`, 'utf8');
+ let sourceData = fs.readFileSync(`content/${lang}/${name}.md`, 'utf8');
// convert frontmatter from yaml to json:
const yaml = __non_webpack_require__('yaml');
@@ -67,14 +58,14 @@ export const getContentOnServer = PRERENDER
return '---\n' + JSON.stringify(meta) + '\n---\n';
});
- const data = parseContent(sourceData, 'md');
+ const data = parseContent(sourceData);
const marked = __non_webpack_require__('marked');
data.html = marked(data.content);
return data;
}
- : (route, lang) => {};
+ : ([lang, name]) => {};
/**
* Parse Markdown with "JSON FrontMatter" (think YAML FrontMatter, with less YAML)
diff --git a/src/lib/docs.js b/src/lib/docs.js
index 25daf334f..f95ead300 100644
--- a/src/lib/docs.js
+++ b/src/lib/docs.js
@@ -1,6 +1,4 @@
-// `preact-router` doesn't support url paths like `/docs/:version/*`
-// so we'll just use a plain regex for now.
-const LATEST_MAJOR = 10;
+export const LATEST_MAJOR = 'v10';
const DOC_REG = /^\/guide\/v(\d+)\//;
/**
diff --git a/src/lib/i18n.js b/src/lib/i18n.js
index 17b843283..9f369d5ee 100644
--- a/src/lib/i18n.js
+++ b/src/lib/i18n.js
@@ -1,24 +1,64 @@
import config from '../config.json';
-import { useCallback } from 'preact/hooks';
-import { useStore } from '../components/store-adapter';
+import { useContext, useState } from 'preact/hooks';
+import { createContext } from 'preact';
+import { useLocation } from 'preact-iso';
/**
- * Handles all logic related to language settings
+ * @typedef LanguageContext
+ * @property {string} lang
+ * @property {(string) => void} setLang
*/
-export function useLanguage() {
- const store = useStore(['lang', 'url']);
- const { lang, url } = store.state;
-
- const setLang = useCallback(
- next => {
- if (typeof document !== 'undefined' && document.documentElement) {
- document.documentElement.lang = next;
- }
- store.update({ lang: next });
- },
- [url]
+
+/**
+ * @type {import('preact').Context}
+ */
+const LanguageContext = createContext(/** @type {LanguageContext} */ ({}));
+
+/**
+ * Get the default language based on the preferred preference of the browser
+ * @param {Record} available All available languages
+ * @param {string} [override]
+ * @returns {string | undefined}
+ */
+export function getDefaultLanguage(available, override) {
+ if (typeof navigator === 'undefined') return;
+
+ // Override via `?lang=foo` parameter
+ if (override && config.languages[override]) {
+ return override;
+ }
+
+ let langs = [navigator.language].concat(navigator.languages);
+ for (let i = 0; i < langs.length; i++) {
+ if (langs[i]) {
+ let lang = String(langs[i]).toLowerCase();
+ if (available[lang]) return lang;
+ // Respect order of `navigator.languages` by returning if the fallback language `English` is found
+ if (lang === 'en') return;
+ }
+ }
+}
+
+export function LanguageProvider({ children }) {
+ const location = useLocation();
+
+ const [lang, setLang] = useState(
+ getDefaultLanguage(config.languages, location.query.lang) || 'en'
+ );
+
+ return (
+
+ {children}
+
);
+}
+/**
+ * Handles all logic related to language settings
+ * @returns {[string, (v: string) => void]}
+ */
+export function useLanguage() {
+ const { lang, setLang } = useContext(LanguageContext);
return [lang, setLang];
}
diff --git a/src/lib/route-utils.js b/src/lib/route-utils.js
new file mode 100644
index 000000000..4fe77ad5a
--- /dev/null
+++ b/src/lib/route-utils.js
@@ -0,0 +1,38 @@
+import config from '../config.json';
+
+/**
+ * @typedef RouteInfo
+ * @property {string} path
+ * @property {string | Record} name
+ *
+ */
+
+/**
+ * @returns {Record}
+ */
+function flattenRoutes(routes) {
+ let out = {};
+
+ const stack = [...routes];
+ let item;
+ while ((item = stack.pop())) {
+ if (item.routes) {
+ for (let i = item.routes.length - 1; i >= 0; i--) {
+ stack.push(item.routes[i]);
+ }
+ } else {
+ out[item.path] = item;
+ }
+ }
+
+ return out;
+}
+
+export const navRoutes = flattenRoutes(config.nav);
+
+export const docRoutes = {};
+for (const k in config.docs) {
+ docRoutes[k] = flattenRoutes(config.docs[k]);
+}
+
+export const blogRoutes = flattenRoutes(config.blog);
diff --git a/src/lib/toggle-overlay.js b/src/lib/toggle-overlay.js
index 3cc627192..5d109faeb 100644
--- a/src/lib/toggle-overlay.js
+++ b/src/lib/toggle-overlay.js
@@ -26,7 +26,7 @@ export function useOverlayToggle() {
return () => window.removeEventListener('resize', onResize);
}, [open]);
- return [open, setOpen];
+ return /** @type {const} */ ([open, setOpen]);
}
function setHeight() {
diff --git a/src/lib/use-resource.js b/src/lib/use-resource.js
new file mode 100644
index 000000000..e82c75fac
--- /dev/null
+++ b/src/lib/use-resource.js
@@ -0,0 +1,67 @@
+import { useEffect } from 'preact/hooks';
+import { getContent, getContentOnServer } from './content.js';
+
+/** @type {Map>} */
+const CACHE = new Map();
+
+/**
+ * @param {[ lang: string, url: string ]} args
+ * @returns {{ html: string, meta: any }}
+ */
+export function useContent([lang, url]) {
+ return useResource(
+ () => (PRERENDER ? getContentOnServer : getContent)([lang, url]),
+ [lang, url]
+ );
+}
+
+export function useResource(fn, deps) {
+ const cacheKey = '' + fn + JSON.stringify(deps);
+
+ let state = CACHE.get(cacheKey);
+ if (!state) {
+ state = { promise: null, status: 'pending', result: undefined, users: 0 };
+
+ state.promise = fn();
+ if (state.promise.then) {
+ state.promise
+ .then(r => {
+ state.status = 'success';
+ state.result = r;
+ })
+ .catch(err => {
+ state.status = 'error';
+ state.result = err;
+ });
+ } else {
+ state.status = 'success';
+ state.result = state.promise;
+ }
+
+ CACHE.set(cacheKey, state);
+ }
+
+ useEffect(() => {
+ state.users++;
+
+ return () => {
+ // Delete cached Promise if nobody uses it anymore
+ if (state.users-- <= 0) {
+ CACHE.delete(cacheKey);
+ }
+ };
+ }, [cacheKey, state]);
+
+ if (state.status === 'success') return state.result;
+ else if (state.status === 'error') throw state.result;
+ throw state.promise;
+}
+
+export function FakeSuspense(props) {
+ this.__c = childDidSuspend;
+ return this.state && this.state.suspended ? props.fallback : props.children;
+}
+
+function childDidSuspend(err) {
+ err.then(() => this.forceUpdate());
+}
diff --git a/src/prerender.js b/src/prerender.js
index 586617aec..5af2389ad 100644
--- a/src/prerender.js
+++ b/src/prerender.js
@@ -2,7 +2,7 @@ const fs = require('fs');
const path = require('path');
const marked = require('marked');
const yaml = require('yaml');
-const config = require('./config.json');
+const { navRoutes, docRoutes, blogRoutes } = require('./lib/route-utils');
const { createTitle } = require('./lib/page-title');
const { fetchRelease } = require('./lambda/release');
@@ -12,12 +12,17 @@ const FRONT_MATTER_REG = /^\s*---\n\s*([\s\S]*?)\s*\n---\n/i;
const MAX_DESCRIPTION_LENGTH = 200;
module.exports = async () => {
- const routes = config.nav
- .concat(config.docs)
- .concat(config.blog)
- .flatMap(arr =>
- arr.path ? { path: arr.path, name: arr.name } : arr.routes
- );
+ const guideRoutes = [];
+ for (const version of Object.keys(docRoutes)) {
+ for (const route of Object.values(docRoutes[version])) {
+ route.path = `/guide/${version}${route.path}`;
+ guideRoutes.push(route);
+ }
+ }
+ const routes = Object.values(navRoutes)
+ .concat(Object.values(blogRoutes))
+ .concat(guideRoutes)
+ .sort((a, b) => a.path.localeCompare(b.path));
let preactVersion;
try {
@@ -36,10 +41,12 @@ module.exports = async () => {
.readdirSync(dir)
.filter(rep => rep[0] !== '.' && rep.match(/\.md$/i))
.map(rep => rep.replace(/(^index)?\.md$/i, ''));
- return paths.flatMap(rep => {
- let path = url.replace(FIELD, '/' + rep).replace(/\/$/, '');
- return map(Object.assign({}, route, { path }));
- });
+ return paths
+ .flatMap(rep => {
+ let path = url.replace(FIELD, '/' + rep).replace(/\/$/, '');
+ return map(Object.assign({}, route, { path }));
+ })
+ .sort((a, b) => a.url.localeCompare(b.url));
}
const content = fs.readFileSync(
path.resolve(__dirname, `../content/en/${url == '/' ? 'index' : url}.md`),
diff --git a/src/pwa.js b/src/pwa.js
index 352e252e5..1939f6ffe 100644
--- a/src/pwa.js
+++ b/src/pwa.js
@@ -3,6 +3,8 @@ let hasInteracted, shouldReload;
function sw() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistration().then(reg => {
+ if (!reg) return;
+
reg.onupdatefound = () => {
const w = reg.installing;
w.onstatechange = () => {
diff --git a/src/types.d.ts b/src/types.d.ts
index 42a24ec54..6db1a7272 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -4,3 +4,4 @@ declare module '*.module.css' {
}
declare var PRERENDER: boolean;
+declare var __non_webpack_require__: (id: string) => any;