Pageworks / papertrain

Papertrain: a Craft CMS 3 framework
http://download.papertrain.io
MIT License
5 stars 2 forks source link

Add documentation for Safari when library & packages support when file size & length is exceeded #198

Closed nicholashamilton closed 5 years ago

nicholashamilton commented 5 years ago
if (window.safari || navigator.userAgent.toLowerCase().indexOf('safari') !== -1 && navigator.userAgent.toLowerCase().indexOf('chrome') === -1)
codewithkyle commented 5 years ago

This could be moved into a new variable.

{{  craft.papertrain.safari([ { type: 'package', bundle: 'exmaple-package' } ]) }}

We should also test setting proper Content-Type headers on our fetch request to fix the maximum requested file size issue and using the Blob API before accepting the fact we might need to write this inefficient/unoptimized workaround.

codewithkyle commented 5 years ago

After some initial testing using the Blob API to and creating object URLs instead of relying on innerHTML it seems like that might be the best solution to this problem. Click here to view a live prototype where a 600+kb file is loaded using Fetch API before being injected into the documents body as a script tag with the file source being crated by the URL API. Click here to view the source code.

codewithkyle commented 5 years ago

This is a possible solution, it's currently being implemented in Origins. It's a total rewrite of the runtime application script, adds a new build-tools/libraries.config.js script to the bundling process, and requires a new libraries/ directory in the projects root.

runtime.js

interface Window
{
    stylesheets : Array<string>
    packages : Array<string>
    components : Array<string>
    modules : Array<string>
    criticalCss : Array<string>
    libraries : Array<string>
    twig : Function
}

declare var DeviceManager:any;
declare var env:any;
declare var Twig:any;

class Runtime
{
    private _initialFetch : boolean;

    constructor()
    {
        this._initialFetch = true;
        this.init();
    }

    private handleStylesheetsFetchEvent:EventListener = this.getStylesheets.bind(this);
    private handleScriptFetchEvent:EventListener = this.getScripts.bind(this);

    private async fetchFile(element:Element, filename:string, filetype:string, directory:string)
    {
        try
        {
            const request = await fetch(`${ window.location.origin }/automation/${ directory }-${ document.documentElement.dataset.cachebust }/${ filename }.${ filetype }`);
            if (request.ok)
            {
                const response = await request.blob();
                const fileUrl = URL.createObjectURL(response);
                switch (filetype)
                {
                    case 'css':
                        element.setAttribute('rel', 'stylesheet');
                        element.setAttribute('href', fileUrl);
                        break;
                    case 'js':
                        element.setAttribute('type', 'text/javascript');
                        element.setAttribute('src', fileUrl);
                        break;
                }
                return;
            }

            throw `Failed to fetch ${ filename }.${ filetype } server responded with ${ request.status }`;

        }
        catch (error)
        {
            throw error;
        }
    }

    private fetchResources(fileListArray:Array<string>, element:string, filetype:string, directory:string) : Promise<any>
    {
        return new Promise((resolve) => {
            if (fileListArray.length === 0)
            {
                resolve();
            }

            let count = 0;
            const required = fileListArray.length;

            while (fileListArray.length > 0)
            {
                const filename = fileListArray[0].replace(/(\.js)$|(\.css)$/gi, '');
                let el = document.head.querySelector(`${ element }[file="${ filename }.${ filetype }"]`);
                if (!el)
                {
                    el = document.createElement(element);
                    el.setAttribute('file', `${ filename }.${ filetype }`);
                    document.head.append(el);
                    this.fetchFile(el, filename, filetype, directory)
                    .then(() => {
                        el.addEventListener('load', () => {
                            count++;
                            if (count === required)
                            {
                                resolve();
                            }
                        });
                    })
                    .catch(error => {
                        console.error(error);
                        count++;
                        if (count === required)
                        {
                            resolve();
                        }
                    });
                }
                else
                {
                    count++;
                    if (count === required)
                    {
                        resolve();
                    }
                }

                fileListArray.splice(0, 1);
            }
        });
    }

    private criticalCssLoadCallback() : void
    {
        // Do something after the stylesheets finish loading
        const pageLoadingElement = document.body.querySelector('page-loading');
        setTimeout(() => {
            pageLoadingElement.classList.remove('is-loading');
        }, 250);
    }

    private loadingCompleteCallback() : void
    {
        if (this._initialFetch)
        {
            this._initialFetch = false;

            new DeviceManager(env.isDebug, true);

            if (DeviceManager.isIE)
            {
                this.fetchIeComponents();
            }
        }
    }

    private librariesLoadCallback() : void
    {
        try
        {
            window.twig = Twig.twig;
        }
        catch (error)
        {
            console.log(error);
        }
    }

    private async getScripts()
    {
        try
        {
            await this.fetchResources(window.packages, 'script', 'js', 'packages');
            await this.fetchResources(window.libraries, 'script', 'js', 'libraries');
            this.librariesLoadCallback();
            await this.fetchResources(window.modules, 'script', 'js', 'modules');
            await this.fetchResources(window.components, 'script', 'js', 'components');
            this.loadingCompleteCallback();
        }
        catch (error)
        {
            console.error(error);
        }
    }

    private async getStylesheets()
    {
        try
        {
            await this.fetchResources(window.criticalCss, 'link', 'css', 'styles');
            this.criticalCssLoadCallback();
            await this.fetchResources(window.stylesheets, 'link', 'css', 'styles');
        }
        catch (error)
        {
            console.error(error);
        }
    }

    // ======================================

    private fetchIeComponents() : void
    {
        fetch(`${ window.location.origin }/assets/ie-components.js`, {
            method: 'GET',
            credentials: 'include',
            headers: new Headers({
                'X-Requested-With': 'XMLHttpRequest',
                'Accept': 'text/javascript'
            })
        })
        .then(request => request.text())
        .then(response => {
            const ieComponentsScript = document.createElement('script');
            ieComponentsScript.innerHTML = response;
            document.head.appendChild(ieComponentsScript);
        })
        .catch((error)=>{
            if (env.isDebug)
            {
                console.error(error);
            }
        });
    }

    private init() : void
    {
        if ('requestIdleCallback' in window)
        {
            // @ts-ignore
            window.requestIdleCallback(()=>{
                this.getStylesheets();
                this.getScripts();
            });
        }
        else
        {
            console.warn('Idle callback prototype not available in this browser, fetching stylesheets');
            this.getStylesheets();
            this.getScripts();
        }

        document.addEventListener('fetch:stylesheets', this.handleStylesheetsFetchEvent);
        document.addEventListener('fetch:scripts', this.handleScriptFetchEvent);
    }
}

libraries.config.js

const chalk = require('chalk');
const glob = require("glob");
const fs = require("fs");

class LibraryManager
{
    constructor()
    {
        console.log(chalk.white('Cloning 3rd party libraries'));
        this.run();
    }

    async run()
    {
        try
        {
            const timestamp = await this.getTimestamp();
            await this.makeDirectory(timestamp);
            const files = await this.getLibraryFiles();
            await this.moveFiles(files, timestamp);
        }
        catch (error)
        {
            console.log(chalk.hex('#ff6426').bold(error));
            await this.fail();
        }
    }

    getTimestamp()
    {
        return new Promise((resolve, reject)=>{
            fs.readFile('config/papertrain/automation.tmp', (error, buffer)=>{
                if (error)
                {
                    reject(error);
                }

                const data = buffer.toString();

                if (!data.match('continue'))
                {
                    reject('Skipping component bundler due to previous failure, see error above');
                }

                const timestamp = data.match(/\d+/g)[0];
                resolve(timestamp);

            });
        });
    }

    fail()
    {
        return new Promise((resolve, reject)=>{
            fs.readFile('config/papertrain/automation.tmp', (error, buffer)=>{
                if (error)
                {
                    reject(error);
                }

                let data = buffer.toString();
                data = data.replace('continue', 'failed');

                fs.writeFile('config/papertrain/automation.tmp', data, (error)=>{
                    if (error)
                    {
                        reject(error);
                    }

                    resolve();
                });
            });
        });
    }

    makeDirectory(timestamp)
    {
        return new Promise((resolve, reject)=>{
            fs.mkdir(`public/automation/libraries-${ timestamp }`, (err)=>{
                if (err)
                {
                    reject(err);
                }

                resolve();
            });
        });
    }

    getLibraryFiles()
    {
        return new Promise((resolve, reject)=>{
            glob('libraries/*.js', (error, files)=>{
                if (error)
                {
                    reject(error);
                }

                resolve(files);
            });
        });
    }

    moveFiles(files, timestamp)
    {
        return new Promise((resolve, reject)=>{
            if (files.length === 0)
            {
                resolve();
            }

            let count = 0;

            for (let i = 0; i < files.length; i++)
            {
                let filename = files[i].replace(/.*[\/]/, '').trim();
                fs.copyFile(files[i], `public/automation/libraries-${ timestamp }/${ filename }`, (error) => {
                    if (error)
                    {
                        reject(error);
                    }

                    count++;
                    if (count === files.length)
                    {
                        resolve();
                    }
                });
            }
        });
    }
}

new LibraryManager();
codewithkyle commented 5 years ago

Fixing with issue #205