From 1bdb7d6345971b2503113f4dddfb373e0a17689e Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Sun, 2 Sep 2018 09:01:51 -0700 Subject: [PATCH] Inject lazy dependencies (#639) Still TODO Find a nice way to expose WebampLazy as: ```JavaScript import WebampLazy from 'webamp/lazy'; ``` --- config/webpack.library.js | 7 +- js/actionCreators/files.js | 21 +++- js/fileUtils.js | 38 +++--- js/index.js | 40 ++++++- js/skinParser.js | 18 +-- js/store.js | 13 +-- js/webamp.js | 226 ++---------------------------------- js/webampLazy.js | 231 +++++++++++++++++++++++++++++++++++++ package.json | 3 +- 9 files changed, 324 insertions(+), 273 deletions(-) create mode 100644 js/webampLazy.js diff --git a/config/webpack.library.js b/config/webpack.library.js index 5d114ac4..dd7fe8e9 100644 --- a/config/webpack.library.js +++ b/config/webpack.library.js @@ -7,7 +7,8 @@ module.exports = { node: { // Consider suggesting jsmediatags use: https://github.com/feross/is-buffer // Cuts 22k - Buffer: false + Buffer: false, + fs: "empty" }, module: { rules: [ @@ -64,7 +65,9 @@ module.exports = { ], entry: { bundle: "./js/webamp.js", - "bundle.min": "./js/webamp.js" + "bundle.min": "./js/webamp.js", + "lazy-bundle": "./js/webampLazy.js", + "lazy-bundle.min": "./js/webampLazy.js" }, output: { path: path.resolve(__dirname, "../built"), diff --git a/js/actionCreators/files.js b/js/actionCreators/files.js index e0aaf14e..f5c0fdca 100644 --- a/js/actionCreators/files.js +++ b/js/actionCreators/files.js @@ -80,10 +80,23 @@ export function loadFilesFromReferences( } export function setSkinFromArrayBuffer(arrayBuffer) { - return async dispatch => { + return async (dispatch, getState, { requireJSZip }) => { + if (!requireJSZip) { + alert("Webamp has not been configured to support custom skins."); + return; + } dispatch({ type: LOADING }); + let JSZip; try { - const skinData = await skinParser(arrayBuffer); + JSZip = await requireJSZip(); + } catch (e) { + console.error(e); + dispatch({ type: LOADED }); + alert("Failed to load the skin parser."); + return; + } + try { + const skinData = await skinParser(arrayBuffer, JSZip); dispatch({ type: SET_SKIN_DATA, skinImages: skinData.images, @@ -250,10 +263,10 @@ function queueFetchingMediaTags(id) { } export function fetchMediaTags(file, id) { - return async dispatch => { + return async (dispatch, getState, { requireJSMediaTags }) => { dispatch({ type: MEDIA_TAG_REQUEST_INITIALIZED, id }); try { - const data = await genMediaTags(file); + const data = await genMediaTags(file, await requireJSMediaTags()); // There's more data here, but we don't have a use for it yet: // https://github.com/aadsm/jsmediatags#shortcuts const { artist, title, picture } = data.tags; diff --git a/js/fileUtils.js b/js/fileUtils.js index 84e85482..7fc18c4f 100644 --- a/js/fileUtils.js +++ b/js/fileUtils.js @@ -1,6 +1,6 @@ import invariant from "invariant"; -export function genMediaTags(file) { +export function genMediaTags(file, jsmediatags) { invariant( file != null, "Attempted to get the tags of media file without passing a file" @@ -9,26 +9,22 @@ export function genMediaTags(file) { if (typeof file === "string" && !/^[a-z]+:\/\//i.test(file)) { file = `${location.protocol}//${location.host}${location.pathname}${file}`; } - return new Promise((resolve, reject) => { - require.ensure( - ["jsmediatags/dist/jsmediatags"], - require => { - const jsmediatags = require("jsmediatags/dist/jsmediatags"); - try { - jsmediatags.read(file, { onSuccess: resolve, onError: reject }); - } catch (e) { - // Possibly jsmediatags could not find a parser for this file? - // Nothing to do. - // Consider removing this after https://github.com/aadsm/jsmediatags/issues/83 is resolved. - reject(e); - } - }, - () => { - // The dependency failed to load - }, - "jsmediatags" - ); - }); + return new Promise( + (resolve, reject) => { + try { + jsmediatags.read(file, { onSuccess: resolve, onError: reject }); + } catch (e) { + // Possibly jsmediatags could not find a parser for this file? + // Nothing to do. + // Consider removing this after https://github.com/aadsm/jsmediatags/issues/83 is resolved. + reject(e); + } + }, + () => { + // The dependency failed to load + }, + "jsmediatags" + ); } export function genMediaDuration(url) { diff --git a/js/index.js b/js/index.js index 491a4e51..92153d39 100644 --- a/js/index.js +++ b/js/index.js @@ -12,7 +12,7 @@ import zaxon from "../skins/ZaxonRemake1-0.wsz"; import green from "../skins/Green-Dimension-V2.wsz"; import MilkdropWindow from "./components/MilkdropWindow"; import screenshotInitialState from "./screenshotInitialState"; -import Webamp from "./webamp"; +import WebampLazy from "./webampLazy"; import { STEP_MARQUEE, UPDATE_TIME_ELAPSED, @@ -34,6 +34,38 @@ import { disableMarquee } from "./config"; +const requireJSZip = () => { + return new Promise((resolve, reject) => { + require.ensure( + ["jszip/dist/jszip"], + require => { + resolve(require("jszip/dist/jszip")); + }, + e => { + console.error("Error loading JSZip", e); + reject(e); + }, + "jszip" + ); + }); +}; + +const requireJSMediaTags = () => { + return new Promise((resolve, reject) => { + require.ensure( + ["jsmediatags/dist/jsmediatags"], + require => { + resolve(require("jsmediatags/dist/jsmediatags")); + }, + e => { + console.error("Error loading jsmediatags", e); + reject(e); + }, + "jsmediatags" + ); + }); +}; + const NOISY_ACTION_TYPES = new Set([ STEP_MARQUEE, UPDATE_TIME_ELAPSED, @@ -122,7 +154,7 @@ Raven.context(() => { if (screenshot) { document.getElementsByClassName("about")[0].style.visibility = "hidden"; } - if (!Webamp.browserIsSupported()) { + if (!WebampLazy.browserIsSupported()) { document.getElementById("browser-compatibility").style.display = "block"; document.getElementById("app").style.visibility = "hidden"; return; @@ -165,7 +197,7 @@ Raven.context(() => { const initialSkin = !skinUrl ? null : { url: skinUrl }; - const webamp = new Webamp({ + const webamp = new WebampLazy({ initialSkin, initialTracks: screenshot ? null : initialTracks, availableSkins: [ @@ -191,6 +223,8 @@ Raven.context(() => { } ], enableHotkeys: true, + requireJSZip, + requireJSMediaTags, __extraWindows, __initialWindowLayout, __initialState: screenshot ? screenshotInitialState : initialState, diff --git a/js/skinParser.js b/js/skinParser.js index fb3c5d0f..b6e27a1e 100644 --- a/js/skinParser.js +++ b/js/skinParser.js @@ -3,21 +3,6 @@ import regionParser from "./regionParser"; import { LETTERS, DEFAULT_SKIN } from "./constants"; import { parseViscolors, parseIni, getFileExtension } from "./utils"; -const getJSZip = () => { - return new Promise(resolve => { - require.ensure( - ["jszip/dist/jszip"], - require => { - resolve(require("jszip/dist/jszip")); - }, - e => { - console.error("Error loading JSZip", e); - }, - "jszip" - ); - }); -}; - const shallowMerge = objs => objs.reduce((prev, img) => Object.assign(prev, img), {}); @@ -249,8 +234,7 @@ async function genGenTextSprites(zip) { } // A promise that, given an array buffer returns a skin style object -async function skinParser(zipFileBuffer) { - const JSZip = await getJSZip(); +async function skinParser(zipFileBuffer, JSZip) { const zip = await JSZip.loadAsync(zipFileBuffer); const [ colors, diff --git a/js/store.js b/js/store.js index f1913bd9..2e51367b 100644 --- a/js/store.js +++ b/js/store.js @@ -10,12 +10,13 @@ const compose = composeWithDevTools({ actionsBlacklist: [UPDATE_TIME_ELAPSED, STEP_MARQUEE] }); -const getStore = ( +export default function( media, actionEmitter, customMiddlewares = [], - stateOverrides -) => { + stateOverrides, + extras +) { let initialState; if (stateOverrides) { initialState = merge( @@ -36,7 +37,7 @@ const getStore = ( compose( applyMiddleware( ...[ - thunk, + thunk.withExtraArgument(extras), mediaMiddleware(media), emitterMiddleware, ...customMiddlewares @@ -44,6 +45,4 @@ const getStore = ( ) ) ); -}; - -export default getStore; +} diff --git a/js/webamp.js b/js/webamp.js index 1fd8bbc2..e065f809 100644 --- a/js/webamp.js +++ b/js/webamp.js @@ -1,224 +1,14 @@ -import React from "react"; -import { render } from "react-dom"; -import { Provider } from "react-redux"; - -import getStore from "./store"; -import App from "./components/App"; -import Hotkeys from "./hotkeys"; -import Media from "./media"; -import { getTrackCount, getTracks } from "./selectors"; -import { - setSkinFromUrl, - loadMediaFiles, - setWindowSize, - loadFilesFromReferences -} from "./actionCreators"; -import { LOAD_STYLE } from "./constants"; -import { uniqueId, objectMap, objectForEach } from "./utils"; - -import { - SET_AVAILABLE_SKINS, - NETWORK_CONNECTED, - NETWORK_DISCONNECTED, - CLOSE_WINAMP, - MINIMIZE_WINAMP, - ADD_GEN_WINDOW, - UPDATE_WINDOW_POSITIONS, - LOADED, - REGISTER_VISUALIZER, - SET_Z_INDEX, - SET_MEDIA -} from "./actionTypes"; -import Emitter from "./emitter"; - -import "../css/base-skin.min.css"; - -// Return a promise that resolves when the store matches a predicate. -const storeHas = (store, predicate) => - new Promise(resolve => { - if (predicate(store.getState())) { - resolve(); - return; - } - const unsubscribe = store.subscribe(() => { - if (predicate(store.getState())) { - resolve(); - unsubscribe(); - } - }); - }); - -class Winamp { - static browserIsSupported() { - const supportsAudioApi = !!( - window.AudioContext || window.webkitAudioContext - ); - const supportsCanvas = !!window.document.createElement("canvas").getContext; - const supportsPromises = typeof Promise !== "undefined"; - return supportsAudioApi && supportsCanvas && supportsPromises; - } +import JSZip from "jszip"; +import jsmediatags from "jsmediatags"; +import WebampLazy from "./webampLazy"; +class Winamp extends WebampLazy { constructor(options) { - this._actionEmitter = new Emitter(); - this.options = options; - const { - initialTracks, - initialSkin, - avaliableSkins, // Old misspelled name - availableSkins, - enableHotkeys = false, - zIndex, - __extraWindows - } = this.options; - - this.media = new Media(); - this.store = getStore( - this.media, - this._actionEmitter, - this.options.__customMiddlewares, - this.options.__initialState - ); - this.store.dispatch({ - type: navigator.onLine ? NETWORK_CONNECTED : NETWORK_DISCONNECTED + super({ + ...options, + requireJSZip: () => JSZip, + requireJSMediaTags: () => jsmediatags }); - - if (true) { - const fileInput = document.createElement("input"); - fileInput.id = "webamp-file-input"; - fileInput.style.display = "none"; - fileInput.type = "file"; - fileInput.value = null; - fileInput.addEventListener("change", e => { - this.store.dispatch(loadFilesFromReferences(e.target.files)); - }); - document.body.appendChild(fileInput); - } - - if (zIndex != null) { - this.store.dispatch({ type: SET_Z_INDEX, zIndex }); - } - - this.genWindows = []; - if (__extraWindows) { - this.genWindows = __extraWindows.map(genWindow => ({ - id: genWindow.id || `${genWindow.title}-${uniqueId()}`, - ...genWindow - })); - - __extraWindows.forEach(genWindow => { - if (genWindow.isVisualizer) { - this.store.dispatch({ type: REGISTER_VISUALIZER, id: genWindow.id }); - } - }); - } - - this.genWindows.forEach(genWindow => { - this.store.dispatch({ - type: ADD_GEN_WINDOW, - windowId: genWindow.id, - title: genWindow.title, - open: genWindow.open - }); - }); - - window.addEventListener("online", () => - this.store.dispatch({ type: NETWORK_CONNECTED }) - ); - window.addEventListener("offline", () => - this.store.dispatch({ type: NETWORK_DISCONNECTED }) - ); - - if (initialSkin) { - this.store.dispatch(setSkinFromUrl(initialSkin.url)); - } else { - // We are using the default skin. - this.store.dispatch({ type: LOADED }); - } - - if (initialTracks) { - this.appendTracks(initialTracks); - } - - if (avaliableSkins != null) { - console.warn( - "The misspelled option `avaliableSkins` is deprecated. Please use `availableSkins` instead." - ); - this.store.dispatch({ type: SET_AVAILABLE_SKINS, skins: avaliableSkins }); - } else if (availableSkins != null) { - this.store.dispatch({ type: SET_AVAILABLE_SKINS, skins: availableSkins }); - } - - const layout = options.__initialWindowLayout; - if (layout != null) { - objectForEach(layout, (w, windowId) => { - if (w.size != null) { - this.store.dispatch(setWindowSize(windowId, w.size)); - } - }); - this.store.dispatch({ - type: UPDATE_WINDOW_POSITIONS, - positions: objectMap(layout, w => w.position) - }); - } - - if (enableHotkeys) { - new Hotkeys(this.store.dispatch); - } - } - - // Append this array of tracks to the end of the current playlist. - appendTracks(tracks) { - const nextIndex = getTrackCount(this.store.getState()); - this.store.dispatch(loadMediaFiles(tracks, LOAD_STYLE.BUFFER, nextIndex)); - } - - // Replace any existing tracks with this array of tracks, and begin playing. - setTracksToPlay(tracks) { - this.store.dispatch(loadMediaFiles(tracks, LOAD_STYLE.PLAY)); - } - - onClose(cb) { - return this._actionEmitter.on(CLOSE_WINAMP, cb); - } - - onTrackDidChange(cb) { - return this._actionEmitter.on(SET_MEDIA, action => { - const tracks = getTracks(this.store.getState()); - const track = tracks[action.id]; - if (track == null) { - return; - } - cb({ url: track.url }); - }); - } - - onMinimize(cb) { - return this._actionEmitter.on(MINIMIZE_WINAMP, cb); - } - - async skinIsLoaded() { - // Wait for the skin to load. - return storeHas(this.store, state => !state.display.loading); - } - - async renderWhenReady(node) { - await this.skinIsLoaded(); - const genWindowComponents = {}; - this.genWindows.forEach(w => { - genWindowComponents[w.id] = w.Component; - }); - - render( - - - , - node - ); } } diff --git a/js/webampLazy.js b/js/webampLazy.js new file mode 100644 index 00000000..753f9acf --- /dev/null +++ b/js/webampLazy.js @@ -0,0 +1,231 @@ +import React from "react"; +import { render } from "react-dom"; +import { Provider } from "react-redux"; + +import getStore from "./store"; +import App from "./components/App"; +import Hotkeys from "./hotkeys"; +import Media from "./media"; +import { getTrackCount, getTracks } from "./selectors"; +import { + setSkinFromUrl, + loadMediaFiles, + setWindowSize, + loadFilesFromReferences +} from "./actionCreators"; +import { LOAD_STYLE } from "./constants"; +import { uniqueId, objectMap, objectForEach } from "./utils"; + +import { + SET_AVAILABLE_SKINS, + NETWORK_CONNECTED, + NETWORK_DISCONNECTED, + CLOSE_WINAMP, + MINIMIZE_WINAMP, + ADD_GEN_WINDOW, + UPDATE_WINDOW_POSITIONS, + LOADED, + REGISTER_VISUALIZER, + SET_Z_INDEX, + SET_MEDIA +} from "./actionTypes"; +import Emitter from "./emitter"; + +import "../css/base-skin.min.css"; + +// Return a promise that resolves when the store matches a predicate. +const storeHas = (store, predicate) => + new Promise(resolve => { + if (predicate(store.getState())) { + resolve(); + return; + } + const unsubscribe = store.subscribe(() => { + if (predicate(store.getState())) { + resolve(); + unsubscribe(); + } + }); + }); + +class Winamp { + static browserIsSupported() { + const supportsAudioApi = !!( + window.AudioContext || window.webkitAudioContext + ); + const supportsCanvas = !!window.document.createElement("canvas").getContext; + const supportsPromises = typeof Promise !== "undefined"; + return supportsAudioApi && supportsCanvas && supportsPromises; + } + + constructor(options) { + this._actionEmitter = new Emitter(); + this.options = options; + const { + initialTracks, + initialSkin, + avaliableSkins, // Old misspelled name + availableSkins, + enableHotkeys = false, + zIndex, + requireJSZip, + requireJSMediaTags, + __extraWindows + } = this.options; + + // TODO: Validate required options + + this.media = new Media(); + this.store = getStore( + this.media, + this._actionEmitter, + this.options.__customMiddlewares, + this.options.__initialState, + { requireJSZip, requireJSMediaTags } + ); + this.store.dispatch({ + type: navigator.onLine ? NETWORK_CONNECTED : NETWORK_DISCONNECTED + }); + + if (true) { + const fileInput = document.createElement("input"); + fileInput.id = "webamp-file-input"; + fileInput.style.display = "none"; + fileInput.type = "file"; + fileInput.value = null; + fileInput.addEventListener("change", e => { + this.store.dispatch(loadFilesFromReferences(e.target.files)); + }); + document.body.appendChild(fileInput); + } + + if (zIndex != null) { + this.store.dispatch({ type: SET_Z_INDEX, zIndex }); + } + + this.genWindows = []; + if (__extraWindows) { + this.genWindows = __extraWindows.map(genWindow => ({ + id: genWindow.id || `${genWindow.title}-${uniqueId()}`, + ...genWindow + })); + + __extraWindows.forEach(genWindow => { + if (genWindow.isVisualizer) { + this.store.dispatch({ type: REGISTER_VISUALIZER, id: genWindow.id }); + } + }); + } + + this.genWindows.forEach(genWindow => { + this.store.dispatch({ + type: ADD_GEN_WINDOW, + windowId: genWindow.id, + title: genWindow.title, + open: genWindow.open + }); + }); + + window.addEventListener("online", () => + this.store.dispatch({ type: NETWORK_CONNECTED }) + ); + window.addEventListener("offline", () => + this.store.dispatch({ type: NETWORK_DISCONNECTED }) + ); + + if (initialSkin) { + this.store.dispatch(setSkinFromUrl(initialSkin.url)); + } else { + // We are using the default skin. + this.store.dispatch({ type: LOADED }); + } + + if (initialTracks) { + this.appendTracks(initialTracks); + } + + if (avaliableSkins != null) { + console.warn( + "The misspelled option `avaliableSkins` is deprecated. Please use `availableSkins` instead." + ); + this.store.dispatch({ type: SET_AVAILABLE_SKINS, skins: avaliableSkins }); + } else if (availableSkins != null) { + this.store.dispatch({ type: SET_AVAILABLE_SKINS, skins: availableSkins }); + } + + const layout = options.__initialWindowLayout; + if (layout != null) { + objectForEach(layout, (w, windowId) => { + if (w.size != null) { + this.store.dispatch(setWindowSize(windowId, w.size)); + } + }); + this.store.dispatch({ + type: UPDATE_WINDOW_POSITIONS, + positions: objectMap(layout, w => w.position) + }); + } + + if (enableHotkeys) { + new Hotkeys(this.store.dispatch); + } + } + + // Append this array of tracks to the end of the current playlist. + appendTracks(tracks) { + const nextIndex = getTrackCount(this.store.getState()); + this.store.dispatch(loadMediaFiles(tracks, LOAD_STYLE.BUFFER, nextIndex)); + } + + // Replace any existing tracks with this array of tracks, and begin playing. + setTracksToPlay(tracks) { + this.store.dispatch(loadMediaFiles(tracks, LOAD_STYLE.PLAY)); + } + + onClose(cb) { + return this._actionEmitter.on(CLOSE_WINAMP, cb); + } + + onTrackDidChange(cb) { + return this._actionEmitter.on(SET_MEDIA, action => { + const tracks = getTracks(this.store.getState()); + const track = tracks[action.id]; + if (track == null) { + return; + } + cb({ url: track.url }); + }); + } + + onMinimize(cb) { + return this._actionEmitter.on(MINIMIZE_WINAMP, cb); + } + + async skinIsLoaded() { + // Wait for the skin to load. + return storeHas(this.store, state => !state.display.loading); + } + + async renderWhenReady(node) { + await this.skinIsLoaded(); + const genWindowComponents = {}; + this.genWindows.forEach(w => { + genWindowComponents[w.id] = w.Component; + }); + + render( + + + , + node + ); + } +} + +export default Winamp; +module.exports = Winamp; diff --git a/package.json b/package.json index b6e8e228..2bd98b5c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "files": [ "built/webamp.bundle.js", "built/webamp.bundle.min.js", - "index.d.ts" + "built/webamp-lazy.bundle.js", + "built/webamp-lazy.bundle.min.js" ], "scripts": { "lint": "eslint .",