Closed nicholashamilton closed 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.
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.
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();
Fixing with issue #205