Bundle CreateJS, instead of loading async

So I finally started looking into the race condition that makes item previews sometimes fail to load, and as expected, it was that we were trying to load the movie before CreateJS had necessarily loaded. Usually the timing worked out, esp after a reload, but not under certain circumstances!

Anyway, I've been wanting for a while to just bundle them instead. That'll help us more eagerly load them when we need them, and not depend on external CDNs, and remove a bunch of loading state!

So yeah, I had to learn how the `easeljs` and `tweenjs` NPM packages did their bundling, and how to use `imports-loader` to let them just register straight onto `window`! But we got there and it's pretty nice tbh!
This commit is contained in:
Emi Matchu 2021-06-16 18:00:25 -07:00
parent bb5ec56752
commit 75ceeba6e2
4 changed files with 41 additions and 65 deletions

View file

@ -29,6 +29,7 @@
"canvas": "^2.6.1", "canvas": "^2.6.1",
"dataloader": "^2.0.0", "dataloader": "^2.0.0",
"dompurify": "^2.2.0", "dompurify": "^2.2.0",
"easeljs": "^1.0.2",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"framer-motion": "^4.1.11", "framer-motion": "^4.1.11",
"graphql": "^15.5.0", "graphql": "^15.5.0",
@ -49,6 +50,7 @@
"react-scripts": "^4.0.1", "react-scripts": "^4.0.1",
"react-transition-group": "^4.3.0", "react-transition-group": "^4.3.0",
"simple-markdown": "^0.7.2", "simple-markdown": "^0.7.2",
"tweenjs": "^1.0.2",
"typescript": "^4.1.3", "typescript": "^4.1.3",
"xmlrpc": "^1.3.2" "xmlrpc": "^1.3.2"
}, },
@ -91,6 +93,7 @@
} }
], ],
"import/first": "off", "import/first": "off",
"import/no-webpack-loader-syntax": "off",
"no-unused-vars": "off", "no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/no-unused-vars": [
"warn", "warn",
@ -128,6 +131,7 @@
"es6-promise-pool": "^2.5.0", "es6-promise-pool": "^2.5.0",
"eslint-plugin-cypress": "^2.11.2", "eslint-plugin-cypress": "^2.11.2",
"husky": "^6.0.0", "husky": "^6.0.0",
"imports-loader": "^3.0.0",
"inquirer": "^7.3.3", "inquirer": "^7.3.3",
"jest-image-snapshot": "^4.3.0", "jest-image-snapshot": "^4.3.0",
"lint-staged": "^10.5.4", "lint-staged": "^10.5.4",

View file

@ -4,6 +4,12 @@ import { useToast } from "@chakra-ui/react";
import { loadImage, logAndCapture, safeImageUrl } from "../util"; import { loadImage, logAndCapture, safeImageUrl } from "../util";
// Import EaselJS and TweenJS directly into the `window` object! The bundled
// scripts are built to attach themselves to `window.createjs`, and
// `window.createjs` is where the Neopets movie libraries expects to find them!
require("imports-loader?wrapper=window!easeljs/lib/easeljs");
require("imports-loader?wrapper=window!tweenjs/lib/tweenjs");
function OutfitMovieLayer({ function OutfitMovieLayer({
libraryUrl, libraryUrl,
width, width,
@ -19,8 +25,6 @@ function OutfitMovieLayer({
const hasShownErrorMessageRef = React.useRef(false); const hasShownErrorMessageRef = React.useRef(false);
const toast = useToast(); const toast = useToast();
const loadingDeps = useEaselDependenciesLoader();
// Set the canvas's internal dimensions to be higher, if the device has high // Set the canvas's internal dimensions to be higher, if the device has high
// DPI like retina. But we'll keep the layout width/height as expected! // DPI like retina. But we'll keep the layout width/height as expected!
const internalWidth = width * window.devicePixelRatio; const internalWidth = width * window.devicePixelRatio;
@ -63,7 +67,7 @@ function OutfitMovieLayer({
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
if (loadingDeps || !canvas) { if (!canvas) {
return; return;
} }
@ -101,15 +105,11 @@ function OutfitMovieLayer({
canvas.height = 0; canvas.height = 0;
} }
}; };
}, [loadingDeps, libraryUrl, toast]); }, [libraryUrl, toast]);
// This effect gives us the `library` and `movieClip`, based on the incoming // This effect gives us the `library` and `movieClip`, based on the incoming
// `libraryUrl`. // `libraryUrl`.
React.useEffect(() => { React.useEffect(() => {
if (loadingDeps) {
return;
}
let canceled = false; let canceled = false;
loadMovieLibrary(libraryUrl) loadMovieLibrary(libraryUrl)
@ -132,7 +132,7 @@ function OutfitMovieLayer({
setLibrary(null); setLibrary(null);
setMovieClip(null); setMovieClip(null);
}; };
}, [loadingDeps, libraryUrl]); }, [libraryUrl]);
// This effect puts the `movieClip` on the `stage`, when both are ready. // This effect puts the `movieClip` on the `stage`, when both are ready.
React.useEffect(() => { React.useEffect(() => {
@ -230,55 +230,6 @@ function OutfitMovieLayer({
); );
} }
/**
* useEaselDependenciesLoader loads the CreateJS scripts we use in OutfitCanvas.
* We load it as part of OutfitCanvas, but callers can also use this to preload
* the scripts and track loading progress.
*/
export function useEaselDependenciesLoader() {
// NOTE: I couldn't find an official NPM source for this that worked with
// Webpack, and I didn't want to rely on random people's ports, and I
// couldn't get a bundled version to work quite right. So we load
// createjs async!
const loadingEaselJS = useScriptTag(
"https://code.createjs.com/1.0.0/easeljs.min.js"
);
const loadingTweenJS = useScriptTag(
"https://code.createjs.com/1.0.0/tweenjs.min.js"
);
const loadingDeps = loadingEaselJS || loadingTweenJS;
return loadingDeps;
}
function useScriptTag(src) {
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
const existingScript = document.querySelector(
`script[src=${CSS.escape(src)}]`
);
if (existingScript) {
setLoading(false);
return;
}
let canceled = false;
loadScriptTag(src).then(() => {
if (!canceled) {
setLoading(false);
}
});
return () => {
canceled = true;
setLoading(true);
};
}, [src, setLoading]);
return loading;
}
function loadScriptTag(src) { function loadScriptTag(src) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const script = document.createElement("script"); const script = document.createElement("script");

View file

@ -16,7 +16,6 @@ import OutfitMovieLayer, {
buildMovieClip, buildMovieClip,
hasAnimations, hasAnimations,
loadMovieLibrary, loadMovieLibrary,
useEaselDependenciesLoader,
} from "./OutfitMovieLayer"; } from "./OutfitMovieLayer";
import HangerSpinner from "./HangerSpinner"; import HangerSpinner from "./HangerSpinner";
import { loadImage, safeImageUrl, useLocalStorage } from "../util"; import { loadImage, safeImageUrl, useLocalStorage } from "../util";
@ -163,13 +162,10 @@ export function OutfitLayers({
false false
); );
const { loading: loadingEasel } = useEaselDependenciesLoader();
const loadingAnything = loading || loadingEasel;
// When we start in a loading state, or re-enter a loading state, start the // When we start in a loading state, or re-enter a loading state, start the
// loading delay timer. // loading delay timer.
React.useEffect(() => { React.useEffect(() => {
if (loadingAnything) { if (loading) {
setLoadingDelayHasPassed(false); setLoadingDelayHasPassed(false);
const t = setTimeout( const t = setTimeout(
() => setLoadingDelayHasPassed(true), () => setLoadingDelayHasPassed(true),
@ -177,7 +173,7 @@ export function OutfitLayers({
); );
return () => clearTimeout(t); return () => clearTimeout(t);
} }
}, [loadingDelayMs, loadingAnything]); }, [loadingDelayMs, loading]);
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
function computeAndSaveCanvasSize() { function computeAndSaveCanvasSize() {
@ -291,7 +287,7 @@ export function OutfitLayers({
// also use a timeout to delay the fade-in by 0.5s, but don't delay the // also use a timeout to delay the fade-in by 0.5s, but don't delay the
// fade-out at all. (The timeout was an awkward choice, it was hard to // fade-out at all. (The timeout was an awkward choice, it was hard to
// find a good CSS way to specify this delay well!) // find a good CSS way to specify this delay well!)
opacity={loadingAnything && loadingDelayHasPassed ? 1 : 0} opacity={loading && loadingDelayHasPassed ? 1 : 0}
transition="opacity 0.2s" transition="opacity 0.2s"
> >
{spinnerVariant === "overlay" && ( {spinnerVariant === "overlay" && (

View file

@ -9226,6 +9226,11 @@ duplexify@^3.4.2, duplexify@^3.6.0:
readable-stream "^2.0.0" readable-stream "^2.0.0"
stream-shift "^1.0.0" stream-shift "^1.0.0"
easeljs@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/easeljs/-/easeljs-1.0.2.tgz#68fdcc69d0f217394e2ebf51ae047428a81240de"
integrity sha512-PQTsiud32vrUIqZCbynjOJjCzoEp0xH+MRusRCdsZ1MzL4LCE2vp4Sa5cr6aShB3mK4vMZ8LFPnts4xuFhjEmg==
ecc-jsbn@~0.1.1: ecc-jsbn@~0.1.1:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@ -11426,6 +11431,16 @@ import-local@^3.0.2:
pkg-dir "^4.2.0" pkg-dir "^4.2.0"
resolve-cwd "^3.0.0" resolve-cwd "^3.0.0"
imports-loader@1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/imports-loader/-/imports-loader-1.2.0.tgz#b06823d0bb42e6f5ff89bc893829000eda46693f"
integrity sha512-zPvangKEgrrPeqeUqH0Uhc59YqK07JqZBi9a9cQ3v/EKUIqrbJHY4CvUrDus2lgQa5AmPyXuGrWP8JJTqzE5RQ==
dependencies:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
source-map "^0.6.1"
strip-comments "^2.0.1"
imurmurhash@^0.1.4: imurmurhash@^0.1.4:
version "0.1.4" version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
@ -17718,6 +17733,11 @@ strip-comments@^1.0.2:
babel-extract-comments "^1.0.0" babel-extract-comments "^1.0.0"
babel-plugin-transform-object-rest-spread "^6.26.0" babel-plugin-transform-object-rest-spread "^6.26.0"
strip-comments@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-2.0.1.tgz#4ad11c3fbcac177a67a40ac224ca339ca1c1ba9b"
integrity sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==
strip-eof@^1.0.0: strip-eof@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
@ -18348,6 +18368,11 @@ tunnel-agent@^0.6.0:
dependencies: dependencies:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
tweenjs@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/tweenjs/-/tweenjs-1.0.2.tgz#768869c4d4ba91fdb4c5ccc661969c58138555af"
integrity sha512-WnFozCNkUkmJtLqJyGrToxVojW2Srzudktr8BzFKQijQRVcmlq7Fc+qfo75ccnwIJGiRWbXKfg7qU67Tzbb1bg==
tweetnacl@^0.14.3, tweetnacl@~0.14.0: tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5" version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"