From fcad05997ed45bd74ef26e82aa8fa509bc661ca3 Mon Sep 17 00:00:00 2001 From: Arun Date: Sun, 30 Apr 2023 19:55:40 +0530 Subject: [PATCH 1/2] fix: live preview in safari/mac --- package-lock.json | 4 +- src/phoenix/virtualServer/config.js | 5 +- src/phoenix/virtualServer/content-type.js | 62 ++-- src/phoenix/virtualServer/html-formatter.js | 236 ++++++------ src/phoenix/virtualServer/mime-types.js | 305 ++++++++-------- src/phoenix/virtualServer/webserver.js | 375 ++++++++++---------- 6 files changed, 496 insertions(+), 491 deletions(-) diff --git a/package-lock.json b/package-lock.json index 759daa37e2..a8f7a845fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "phoenix", - "version": "3.1.21-0", + "version": "3.1.22-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "phoenix", - "version": "3.1.21-0", + "version": "3.1.22-0", "dependencies": { "@bugsnag/js": "^7.18.0", "@floating-ui/dom": "^0.5.4", diff --git a/src/phoenix/virtualServer/config.js b/src/phoenix/virtualServer/config.js index a1279fdb67..d99b90f398 100644 --- a/src/phoenix/virtualServer/config.js +++ b/src/phoenix/virtualServer/config.js @@ -33,7 +33,6 @@ if(!self.Config){ * * `debug`: if present (i.e., `Boolean`), enable workbox debug logging */ - const url = new URL(location); /** * Given a route string, make sure it follows the pattern we expect: @@ -47,7 +46,7 @@ if(!self.Config){ * @param {String} route */ function getNormalizeRoute() { - let route = url.searchParams.get('route') || 'fs'; + let route = (new URL(location)).searchParams.get('route') || 'fs'; // Only a single / at the front of the route route = route.replace(/^\/*/, ''); @@ -59,7 +58,7 @@ if(!self.Config){ self.Config = { route: getNormalizeRoute(), - disableIndexes: url.searchParams.get('disableIndexes') !== null, + disableIndexes: (new URL(location)).searchParams.get('disableIndexes') !== null, debug: false // this is set via sw messages from phoenix }; } diff --git a/src/phoenix/virtualServer/content-type.js b/src/phoenix/virtualServer/content-type.js index 4b6ec53a01..775a622c45 100644 --- a/src/phoenix/virtualServer/content-type.js +++ b/src/phoenix/virtualServer/content-type.js @@ -17,46 +17,48 @@ * */ -/* global lookup, importScripts*/ +/* global mime, importScripts*/ importScripts('phoenix/virtualServer/mime-types.js'); if(!self.ContentType){ - function getMimeType(path) { - return lookup(path) || 'application/octet-stream'; - } - - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#Audio_and_video_types - function isMedia(path) { - let mimeType = lookup(path); - if (!mimeType) { - return false; + (function () { + function getMimeType(path) { + return mime.lookup(path) || 'application/octet-stream'; } - mimeType = mimeType.toLowerCase(); + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#Audio_and_video_types + function isMedia(path) { + let mimeType = mime.lookup(path); + if (!mimeType) { + return false; + } - // Deal with OGG special case - if (mimeType === 'application/ogg') { - return true; - } + mimeType = mimeType.toLowerCase(); - // Anything else with `audio/*` or `video/*` is "media" - return mimeType.startsWith('audio/') || mimeType.startsWith('video/'); - } + // Deal with OGG special case + if (mimeType === 'application/ogg') { + return true; + } - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#Image_types - function isImage(path) { - const mimeType = lookup(path); - if (!mimeType) { - return false; + // Anything else with `audio/*` or `video/*` is "media" + return mimeType.startsWith('audio/') || mimeType.startsWith('video/'); } - return mimeType.toLowerCase().startsWith('image/'); - } + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#Image_types + function isImage(path) { + const mimeType = mime.lookup(path); + if (!mimeType) { + return false; + } + + return mimeType.toLowerCase().startsWith('image/'); + } - self.ContentType = { - isMedia, - isImage, - getMimeType - }; + self.ContentType = { + isMedia, + isImage, + getMimeType + }; + }()); } diff --git a/src/phoenix/virtualServer/html-formatter.js b/src/phoenix/virtualServer/html-formatter.js index b8dada03ba..59ff3643a4 100644 --- a/src/phoenix/virtualServer/html-formatter.js +++ b/src/phoenix/virtualServer/html-formatter.js @@ -23,44 +23,45 @@ importScripts('phoenix/virtualServer/content-type.js'); importScripts('phoenix/virtualServer/icons.js'); if(!self.HtmlFormatter){ -// 20-Apr-2004 17:14 - const formatDate = d => { - const day = d.getDate(); - const month = d.toLocaleString('en-us', {month: 'short'}); - const year = d.getFullYear(); - const hours = d.getHours(); - const mins = d.getMinutes(); - return `${day}-${month}-${year} ${hours}:${mins}`; - }; - - const formatSize = s => { - const units = ['', 'K', 'M']; - if (!s) { - return '-'; - } - const i = Math.floor(Math.log(s) / Math.log(1024)); - return Math.round(s / Math.pow(1024, i), 2) + units[i]; - }; - - const formatRow = ( - icon, - alt = '[ ]', - href, - name, - modified, - size - ) => `${alt} + (function () { + // 20-Apr-2004 17:14 + const formatDate = d => { + const day = d.getDate(); + const month = d.toLocaleString('en-us', {month: 'short'}); + const year = d.getFullYear(); + const hours = d.getHours(); + const mins = d.getMinutes(); + return `${day}-${month}-${year} ${hours}:${mins}`; + }; + + const formatSize = s => { + const units = ['', 'K', 'M']; + if (!s) { + return '-'; + } + const i = Math.floor(Math.log(s) / Math.log(1024)); + return Math.round(s / Math.pow(1024, i), 2) + units[i]; + }; + + const formatRow = ( + icon, + alt = '[ ]', + href, + name, + modified, + size + ) => `${alt} ${name} ${formatDate(new Date(modified))} ${formatSize(size)} `; - const footerClose = '
nohost (Web Browser Server)
'; + const footerClose = '
nohost (Web Browser Server)
'; - /** - * Send an Apache-style 404 - */ - function format404(url) { - const body = ` + /** + * Send an Apache-style 404 + */ + function format404(url) { + const body = ` 404 Not Found @@ -69,21 +70,21 @@ if(!self.HtmlFormatter){

The requested URL ${url} was not found on this server.


${footerClose}`; - return { - body, - config: { - status: 404, - statusText: 'Not Found', - headers: {'Content-Type': 'text/html'} - } - }; - } + return { + body, + config: { + status: 404, + statusText: 'Not Found', + headers: {'Content-Type': 'text/html'} + } + }; + } - /** - * Send an Apache-style 500 - */ - function format500(path, err) { - const body = ` + /** + * Send an Apache-style 500 + */ + function format500(path, err) { + const body = ` 500 Internal Server Error @@ -93,24 +94,24 @@ if(!self.HtmlFormatter){

The error was: ${err.message}.


${footerClose}`; - return { - body, - config: { - status: 500, - statusText: 'Internal Error', - headers: {'Content-Type': 'text/html'} - } - }; - } - - /** - * Send an Apache-style directory listing - */ - function formatDir(route, dirPath, entries) { - const parent = self.path.dirname(dirPath) || '/'; - // Maintain path sep, but deal with things like spaces in filenames - const url = encodeURI(route + parent); - const header = ` + return { + body, + config: { + status: 500, + statusText: 'Internal Error', + headers: {'Content-Type': 'text/html'} + } + }; + } + + /** + * Send an Apache-style directory listing + */ + function formatDir(route, dirPath, entries) { + const parent = self.path.dirname(dirPath) || '/'; + // Maintain path sep, but deal with things like spaces in filenames + const url = encodeURI(route + parent); + const header = ` Index of ${dirPath}

Index of ${dirPath}

@@ -121,59 +122,60 @@ if(!self.HtmlFormatter){ [DIR] Parent Directory  -  `; - const footer = `
${footerClose}`; - - const rows = entries.map(entry => { - let entryName = entry.name || entry; - const ext = self.path.extname(entryName); - // Maintain path sep, but deal with things like spaces in filenames - const href = encodeURI(`${route}${self.path.join(dirPath, entryName)}`); - let icon; - let alt; - - // TODO: switch this to entry.isDirectory() if possible - if (ContentType.isImage(ext)) { - icon = icons.image2; - alt = '[IMG]'; - } else if (ContentType.isMedia(ext)) { - icon = icons.movie; - alt = '[MOV]'; - } else if (!ext) { - icon = icons.folder; - alt = '[DIR]'; - } else { - icon = icons.text; - alt = '[TXT]'; - } + const footer = `
${footerClose}`; + + const rows = entries.map(entry => { + let entryName = entry.name || entry; + const ext = self.path.extname(entryName); + // Maintain path sep, but deal with things like spaces in filenames + const href = encodeURI(`${route}${self.path.join(dirPath, entryName)}`); + let icon; + let alt; + + // TODO: switch this to entry.isDirectory() if possible + if (ContentType.isImage(ext)) { + icon = icons.image2; + alt = '[IMG]'; + } else if (ContentType.isMedia(ext)) { + icon = icons.movie; + alt = '[MOV]'; + } else if (!ext) { + icon = icons.folder; + alt = '[DIR]'; + } else { + icon = icons.text; + alt = '[TXT]'; + } + + return formatRow(icon, alt, href, entryName, entry.mtime, entry.size); + }).join('\n'); + + return { + body: header + rows + footer, + config: { + status: 200, + statusText: 'OK', + headers: {'Content-Type': 'text/html'} + } + }; + } - return formatRow(icon, alt, href, entryName, entry.mtime, entry.size); - }).join('\n'); + function formatFile(path, content) { + return { + body: content, + config: { + status: 200, + statusText: 'OK', + headers: {'Content-Type': ContentType.getMimeType(path)} + } + }; + } - return { - body: header + rows + footer, - config: { - status: 200, - statusText: 'OK', - headers: {'Content-Type': 'text/html'} - } - }; - } - - function formatFile(path, content) { - return { - body: content, - config: { - status: 200, - statusText: 'OK', - headers: {'Content-Type': ContentType.getMimeType(path)} - } + self.HtmlFormatter = { + format404, + format500, + formatDir, + formatFile }; - } - - self.HtmlFormatter = { - format404, - format500, - formatDir, - formatFile - }; + }()); } diff --git a/src/phoenix/virtualServer/mime-types.js b/src/phoenix/virtualServer/mime-types.js index 74917da792..cb2951be6b 100644 --- a/src/phoenix/virtualServer/mime-types.js +++ b/src/phoenix/virtualServer/mime-types.js @@ -19,7 +19,7 @@ * */ -/*globals path, MIME_TYPE_DATABASE_REFERENCE*/ +/*globals MIME_TYPE_DATABASE_REFERENCE*/ importScripts('phoenix/virtualServer/mime-db.js'); /** @@ -27,191 +27,192 @@ importScripts('phoenix/virtualServer/mime-db.js'); * based on mime-types lib https://github.com/jshttp/mime-types */ if(!self.mime){ - self.mime ={}; - let db = MIME_TYPE_DATABASE_REFERENCE; - - if (!self.path) { - console.error("Phoenix fs lib should be loaded before mime type."); - } - let extname = path.extname; - /** - * Module variables. - * @private - */ - - var EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/; - var TEXT_TYPE_REGEXP = /^text\//i; - - /** - * Module exports. - * @public - */ - - self.mime.charset = charset; - self.mime.charsets = {lookup: charset}; - self.mime.contentType = contentType; - self.mime.extension = extension; - self.mime.extensions = { - "text/html": [ - "html", - "htm", - "shtml" - ] - }; - self.mime.lookup = lookup; - self.mime.types = { - htm: "text/html", - html: "text/html", - shtml: "text/html" - }; - - /** - * Get the default charset for a MIME type. - * - * @param {string} type - * @return {boolean|string} - */ - - function charset(type) { - if (!type || typeof type !== 'string') { - return false; - } - - // TODO: use media-typer - var match = EXTRACT_TYPE_REGEXP.exec(type); - var mime = match && db[match[1].toLowerCase()]; + (function() { + self.mime ={}; + let db = MIME_TYPE_DATABASE_REFERENCE; - if (mime && mime.charset) { - return mime.charset; + if (!self.path) { + console.error("Phoenix fs lib should be loaded before mime type."); } + /** + * Module variables. + * @private + */ + + var EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/; + var TEXT_TYPE_REGEXP = /^text\//i; + + /** + * Module exports. + * @public + */ + + self.mime.charset = charset; + self.mime.charsets = {lookup: charset}; + self.mime.contentType = contentType; + self.mime.extension = extension; + self.mime.extensions = { + "text/html": [ + "html", + "htm", + "shtml" + ] + }; + self.mime.lookup = lookup; + self.mime.types = { + htm: "text/html", + html: "text/html", + shtml: "text/html" + }; + + /** + * Get the default charset for a MIME type. + * + * @param {string} type + * @return {boolean|string} + */ + + function charset(type) { + if (!type || typeof type !== 'string') { + return false; + } - // default text/* to utf-8 - if (match && TEXT_TYPE_REGEXP.test(match[1])) { - return 'UTF-8'; - } + // TODO: use media-typer + var match = EXTRACT_TYPE_REGEXP.exec(type); + var mime = match && db[match[1].toLowerCase()]; - return false; - } + if (mime && mime.charset) { + return mime.charset; + } - /** - * Create a full Content-Type header given a MIME type or extension. - * - * @param {string} str - * @return {boolean|string} - */ + // default text/* to utf-8 + if (match && TEXT_TYPE_REGEXP.test(match[1])) { + return 'UTF-8'; + } - function contentType(str) { - // TODO: should this even be in this module? - if (!str || typeof str !== 'string') { return false; } - var mime = str.indexOf('/') === -1 - ? self.mime.lookup(str) - : str; + /** + * Create a full Content-Type header given a MIME type or extension. + * + * @param {string} str + * @return {boolean|string} + */ + + function contentType(str) { + // TODO: should this even be in this module? + if (!str || typeof str !== 'string') { + return false; + } - if (!mime) { - return false; - } + var mime = str.indexOf('/') === -1 + ? self.mime.lookup(str) + : str; + + if (!mime) { + return false; + } + + // TODO: use content-type or other module + if (mime.indexOf('charset') === -1) { + var charset = self.mime.charset(mime); + if (charset) { mime += '; charset=' + charset.toLowerCase(); } + } - // TODO: use content-type or other module - if (mime.indexOf('charset') === -1) { - var charset = self.mime.charset(mime); - if (charset) { mime += '; charset=' + charset.toLowerCase(); } + return mime; } - return mime; - } + /** + * Get the default extension for a MIME type. + * + * @param {string} type + * @return {boolean|string} + */ - /** - * Get the default extension for a MIME type. - * - * @param {string} type - * @return {boolean|string} - */ + function extension(type) { + if (!type || typeof type !== 'string') { + return false; + } - function extension(type) { - if (!type || typeof type !== 'string') { - return false; - } + // TODO: use media-typer + var match = EXTRACT_TYPE_REGEXP.exec(type); - // TODO: use media-typer - var match = EXTRACT_TYPE_REGEXP.exec(type); + // get extensions + var exts = match && self.mime.extensions[match[1].toLowerCase()]; - // get extensions - var exts = match && self.mime.extensions[match[1].toLowerCase()]; + if (!exts || !exts.length) { + return false; + } - if (!exts || !exts.length) { - return false; + return exts[0]; } - return exts[0]; - } + /** + * Lookup the MIME type for a file path/extension. + * + * @param {string} path + * @return {boolean|string} + */ - /** - * Lookup the MIME type for a file path/extension. - * - * @param {string} path - * @return {boolean|string} - */ + function lookup(path) { + if (!path || typeof path !== 'string') { + return false; + } - function lookup(path) { - if (!path || typeof path !== 'string') { - return false; - } + // get the extension ("ext" or ".ext" or full path) + var extension = self.path.extname('x.' + path) + .toLowerCase() + .substr(1); - // get the extension ("ext" or ".ext" or full path) - var extension = extname('x.' + path) - .toLowerCase() - .substr(1); + if (!extension) { + return false; + } - if (!extension) { - return false; + return self.mime.types[extension] || false; } - return self.mime.types[extension] || false; - } + /** + * Populate the extensions and types maps. + * @private + */ - /** - * Populate the extensions and types maps. - * @private - */ + function populateMaps(extensions, types) { + // source preference (least -> most) + var preference = ['nginx', 'apache', undefined, 'iana']; - function populateMaps(extensions, types) { - // source preference (least -> most) - var preference = ['nginx', 'apache', undefined, 'iana']; + Object.keys(db).forEach(function forEachMimeType(type) { + var mime = db[type]; + var exts = mime.extensions; - Object.keys(db).forEach(function forEachMimeType(type) { - var mime = db[type]; - var exts = mime.extensions; - - if (!exts || !exts.length) { - return; - } + if (!exts || !exts.length) { + return; + } - // mime -> extensions - extensions[type] = exts; + // mime -> extensions + extensions[type] = exts; - // extension -> mime - for (var i = 0; i < exts.length; i++) { - var extension = exts[i]; + // extension -> mime + for (var i = 0; i < exts.length; i++) { + var extension = exts[i]; - if (types[extension]) { - var from = preference.indexOf(db[types[extension]].source); - var to = preference.indexOf(mime.source); + if (types[extension]) { + var from = preference.indexOf(db[types[extension]].source); + var to = preference.indexOf(mime.source); - if (types[extension] !== 'application/octet-stream' && - (from > to || (from === to && types[extension].substr(0, 12) === 'application/'))) { - // skip the remapping - continue; + if (types[extension] !== 'application/octet-stream' && + (from > to || (from === to && types[extension].substr(0, 12) === 'application/'))) { + // skip the remapping + continue; + } } - } - // set the extension -> mime - types[extension] = type; - } - }); - } + // set the extension -> mime + types[extension] = type; + } + }); + } - populateMaps(self.mime.extensions, self.mime.types); + populateMaps(self.mime.extensions, self.mime.types); + }()); } diff --git a/src/phoenix/virtualServer/webserver.js b/src/phoenix/virtualServer/webserver.js index b205c3ccc8..adb7754315 100644 --- a/src/phoenix/virtualServer/webserver.js +++ b/src/phoenix/virtualServer/webserver.js @@ -23,221 +23,222 @@ importScripts('phoenix/virtualServer/html-formatter.js'); importScripts('phoenix/virtualServer/config.js'); if(!self.Serve){ - const _serverBroadcastChannel = new BroadcastChannel("virtual_server_broadcast"); - const fs = self.fs; - const Path = self.path; - let instrumentedURLs = {}, - responseListeners = {}; - - function _getNewRequestID() { - return Math.round( Math.random()*1000000000000); - } - - function _getAllInstrumentedFullPaths() { - let allURLs = []; - for(let rootPaths of Object.keys(instrumentedURLs)){ - for(let subPath of instrumentedURLs[rootPaths]){ - allURLs.push(Path.normalize(rootPaths + subPath)); - } + (function(){ + const _serverBroadcastChannel = new BroadcastChannel("virtual_server_broadcast"); + const fs = self.fs; + const Path = self.path; + let instrumentedURLs = {}, + responseListeners = {}; + + function _getNewRequestID() { + return Math.round( Math.random()*1000000000000); } - return allURLs; - } - - // https://tools.ietf.org/html/rfc2183 - function formatContentDisposition(path, stats) { - const filename = Path.basename(path); - const modified = stats.mtime.toUTCString(); - return `attachment; filename="${filename}"; modification-date="${modified}"; size=${stats.size};`; - } - - async function _wait(timeMs) { - return new Promise((resolve)=>{ - setTimeout(resolve, timeMs); - }); - } - - // fs read that always resolves even if there is error - async function _resolvingRead(path, encoding) { - return new Promise((resolve)=>{ - fs.readFile(path, encoding, function (error, contents) { - resolve({error, contents}); - }); - }); - } - // fs stat that always resolves even if there is error - async function _resolvingStat(path) { - return new Promise((resolve)=>{ - fs.stat(path, function (error, stats) { - resolve({error, stats}); - }); - }); - } - const FILE_READ_RETRY_COUNT = 5, - BACKOFF_TIME_MS = 10; - - const serve = async function (path, download, phoenixInstanceID) { - path = Path.normalize(path); - return new Promise(async (resolve, reject) => { // eslint-disable-line - function buildResponse(responseData) { - return new Response(responseData.body, responseData.config); - } - function serveError(path, err) { - if (err.code === 'ENOENT') { - return resolve(buildResponse(HtmlFormatter.format404(path))); + function _getAllInstrumentedFullPaths() { + let allURLs = []; + for(let rootPaths of Object.keys(instrumentedURLs)){ + for(let subPath of instrumentedURLs[rootPaths]){ + allURLs.push(Path.normalize(rootPaths + subPath)); } - resolve(buildResponse(HtmlFormatter.format500(path, err))); } + return allURLs; + } - function serveInstrumentedFile(path) { - let allURLs = _getAllInstrumentedFullPaths(); - // html and htm files are always served by phoenix to prevent non instrumented transient content - if(!phoenixInstanceID || - (!allURLs.includes(path) && !path.endsWith("htm") && !path.endsWith("html"))){ - return false; - } - self._debugLivePreviewLog("Service worker: serving instrumented file", path); - const requestID = _getNewRequestID(); - _serverBroadcastChannel.postMessage({ - type: "getInstrumentedContent", - path, - requestID, - phoenixInstanceID + // https://tools.ietf.org/html/rfc2183 + function formatContentDisposition(path, stats) { + const filename = Path.basename(path); + const modified = stats.mtime.toUTCString(); + return `attachment; filename="${filename}"; modification-date="${modified}"; size=${stats.size};`; + } + + async function _wait(timeMs) { + return new Promise((resolve)=>{ + setTimeout(resolve, timeMs); + }); + } + + // fs read that always resolves even if there is error + async function _resolvingRead(path, encoding) { + return new Promise((resolve)=>{ + fs.readFile(path, encoding, function (error, contents) { + resolve({error, contents}); }); - responseListeners[requestID] = function (response) { - if(response.contents !== "" && !response.contents){ - self._debugLivePreviewLog( - "Service worker: no instrumented file received from phoenix!", path); - return resolve(buildResponse(HtmlFormatter.format404(path))); - } - const responseData = HtmlFormatter.formatFile(path, response.contents); - const headers = response.headers || {}; - responseData.config.headers = { ...responseData.config.headers, ...headers}; - resolve(new Response(responseData.body, responseData.config)); - }; - return true; - } + }); + } + // fs stat that always resolves even if there is error + async function _resolvingStat(path) { + return new Promise((resolve)=>{ + fs.stat(path, function (error, stats) { + resolve({error, stats}); + }); + }); + } + const FILE_READ_RETRY_COUNT = 5, + BACKOFF_TIME_MS = 10; + + const serve = async function (path, download, phoenixInstanceID) { + path = Path.normalize(path); + return new Promise(async (resolve, reject) => { // eslint-disable-line + function buildResponse(responseData) { + return new Response(responseData.body, responseData.config); + } - async function serveFile(path, stats) { - let err = null; - for(let i = 1; i <= FILE_READ_RETRY_COUNT; i++){ - // sometimes there is read after write contention in native fs between main thread and worker. - // so we retry - let fileResponse = await _resolvingRead(path, fs.BYTE_ARRAY_ENCODING); - if(fileResponse.error){ - err = fileResponse.error; - await _wait(i * BACKOFF_TIME_MS); - continue; + function serveError(path, err) { + if (err.code === 'ENOENT') { + return resolve(buildResponse(HtmlFormatter.format404(path))); } - const responseData = HtmlFormatter.formatFile(path, fileResponse.contents); + resolve(buildResponse(HtmlFormatter.format500(path, err))); + } - // If we are supposed to serve this file or download, add headers - if (responseData.config.status === 200 && download) { - responseData.config.headers['Content-Disposition'] = - formatContentDisposition(path, stats); + function serveInstrumentedFile(path) { + let allURLs = _getAllInstrumentedFullPaths(); + // html and htm files are always served by phoenix to prevent non instrumented transient content + if(!phoenixInstanceID || + (!allURLs.includes(path) && !path.endsWith("htm") && !path.endsWith("html"))){ + return false; } - - resolve(new Response(responseData.body, responseData.config)); - return; + self._debugLivePreviewLog("Service worker: serving instrumented file", path); + const requestID = _getNewRequestID(); + _serverBroadcastChannel.postMessage({ + type: "getInstrumentedContent", + path, + requestID, + phoenixInstanceID + }); + responseListeners[requestID] = function (response) { + if(response.contents !== "" && !response.contents){ + self._debugLivePreviewLog( + "Service worker: no instrumented file received from phoenix!", path); + return resolve(buildResponse(HtmlFormatter.format404(path))); + } + const responseData = HtmlFormatter.formatFile(path, response.contents); + const headers = response.headers || {}; + responseData.config.headers = { ...responseData.config.headers, ...headers}; + resolve(new Response(responseData.body, responseData.config)); + }; + return true; } - serveError(path, err); - } - // Either serve /index.html (default index) or / (directory listing) - function serveDir(path) { + async function serveFile(path, stats) { + let err = null; + for(let i = 1; i <= FILE_READ_RETRY_COUNT; i++){ + // sometimes there is read after write contention in native fs between main thread and worker. + // so we retry + let fileResponse = await _resolvingRead(path, fs.BYTE_ARRAY_ENCODING); + if(fileResponse.error){ + err = fileResponse.error; + await _wait(i * BACKOFF_TIME_MS); + continue; + } + const responseData = HtmlFormatter.formatFile(path, fileResponse.contents); - function maybeServeIndexFile() { - if(path.endsWith("//")){ - // this is for us to override and show the directory listing if the path ends with // - serveDirListing(); + // If we are supposed to serve this file or download, add headers + if (responseData.config.status === 200 && download) { + responseData.config.headers['Content-Disposition'] = + formatContentDisposition(path, stats); + } + + resolve(new Response(responseData.body, responseData.config)); return; } - - const indexPath = Path.join(path, 'index.html'); - fs.stat(indexPath, function (err, stats) { - if (err) { - if (err.code === 'ENOENT' && !Config.disableIndexes) { - // Fallback to a directory listing instead - serveDirListing(); - } else { - // Let the error (likely 404) pass through instead - serveError(path, err); - } - } else { - // Index file found, serve that instead - serveFile(indexPath, stats); - } - }); + serveError(path, err); } - function serveDirListing() { - fs.readdir(path, function (err, entries) { - if (err) { - return serveError(path, err); + // Either serve /index.html (default index) or / (directory listing) + function serveDir(path) { + + function maybeServeIndexFile() { + if(path.endsWith("//")){ + // this is for us to override and show the directory listing if the path ends with // + serveDirListing(); + return; } - const responseData = HtmlFormatter.formatDir(virtualServerBaseURL, path, entries); - resolve(new Response(responseData.body, responseData.config)); - }); - } + const indexPath = Path.join(path, 'index.html'); + fs.stat(indexPath, function (err, stats) { + if (err) { + if (err.code === 'ENOENT' && !Config.disableIndexes) { + // Fallback to a directory listing instead + serveDirListing(); + } else { + // Let the error (likely 404) pass through instead + serveError(path, err); + } + } else { + // Index file found, serve that instead + serveFile(indexPath, stats); + } + }); + } - maybeServeIndexFile(); - } + function serveDirListing() { + fs.readdir(path, function (err, entries) { + if (err) { + return serveError(path, err); + } + + const responseData = HtmlFormatter.formatDir(virtualServerBaseURL, path, entries); + resolve(new Response(responseData.body, responseData.config)); + }); + } - let err = null; - try{ - if(serveInstrumentedFile(path)){ - return; + maybeServeIndexFile(); } - for(let i = 1; i <= FILE_READ_RETRY_COUNT; i++){ - let fileStat = await _resolvingStat(path); - if(fileStat.error){ - err = fileStat.error; - await _wait(i * BACKOFF_TIME_MS); - continue; + + let err = null; + try{ + if(serveInstrumentedFile(path)){ + return; } - if (fileStat.stats.isDirectory()) { - return serveDir(path); + for(let i = 1; i <= FILE_READ_RETRY_COUNT; i++){ + let fileStat = await _resolvingStat(path); + if(fileStat.error){ + err = fileStat.error; + await _wait(i * BACKOFF_TIME_MS); + continue; + } + if (fileStat.stats.isDirectory()) { + return serveDir(path); + } + return serveFile(path, fileStat.stats); + } - return serveFile(path, fileStat.stats); + return serveError(path, err); + } catch (e) { + reject(e); + } + }); + }; + + async function setInstrumentedURLs(event) { + const data = event.data; + const root = data.root, + paths = data.paths; + self._debugLivePreviewLog("Service worker: setInstrumentedURLs", data); + instrumentedURLs[root] = paths; + event.ports[0].postMessage(true);// acknowledge for the other side to resolve promise + } + console.log("service worker init"); + + function processVirtualServerMessage(event) { + let eventType = event.data && event.data.type; + switch (eventType) { + case 'REQUEST_RESPONSE': + const requestID = event.data.requestID; + if(event.data.requestID && responseListeners[requestID]){ + responseListeners[requestID](event.data); + delete responseListeners[requestID]; + return true; } - return serveError(path, err); - } catch (e) { - reject(e); - } - }); - }; - - async function setInstrumentedURLs(event) { - const data = event.data; - const root = data.root, - paths = data.paths; - self._debugLivePreviewLog("Service worker: setInstrumentedURLs", data); - instrumentedURLs[root] = paths; - event.ports[0].postMessage(true);// acknowledge for the other side to resolve promise - } - - console.log("service worker init"); - - function processVirtualServerMessage(event) { - let eventType = event.data && event.data.type; - switch (eventType) { - case 'REQUEST_RESPONSE': - const requestID = event.data.requestID; - if(event.data.requestID && responseListeners[requestID]){ - responseListeners[requestID](event.data); - delete responseListeners[requestID]; - return true; } } - } - - _serverBroadcastChannel.onmessage = processVirtualServerMessage; - self.Serve = { - serve, - setInstrumentedURLs - }; + _serverBroadcastChannel.onmessage = processVirtualServerMessage; + self.Serve = { + serve, + setInstrumentedURLs + }; + }()); } From bdfe79e9569f0b302803888853d8d247d5da82c4 Mon Sep 17 00:00:00 2001 From: abose Date: Sun, 30 Apr 2023 20:58:01 +0530 Subject: [PATCH 2/2] test: live preview inetgration tests working --- test/spec/LiveDevelopmentMultiBrowser-test.js | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/test/spec/LiveDevelopmentMultiBrowser-test.js b/test/spec/LiveDevelopmentMultiBrowser-test.js index 00515e6632..29b2169755 100644 --- a/test/spec/LiveDevelopmentMultiBrowser-test.js +++ b/test/spec/LiveDevelopmentMultiBrowser-test.js @@ -179,6 +179,16 @@ define(function (require, exports, module) { await endPreviewSession(); }); + function _isRelatedStyleSheet(liveDoc, fileName) { + let relatedSheets = Object.keys(liveDoc.getRelated().stylesheets); + for(let relatedPath of relatedSheets){ + if(relatedPath.endsWith(fileName)) { + return true; + } + } + return false; + } + it("should send notifications for added/removed stylesheets through link nodes", async function () { let liveDoc; await awaitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), @@ -188,21 +198,12 @@ define(function (require, exports, module) { liveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); let curDoc = DocumentManager.getCurrentDocument(); - curDoc.replaceRange('\n', {line: 8, ch: 0}); - - await awaitsFor( - function relatedDocsReceived() { - return (Object.getOwnPropertyNames(liveDoc.getRelated().stylesheets).length === 3); - }, - "relatedDocuments.done.received", - 10000 - ); curDoc.replaceRange('\n', {line: 8, ch: 0}); await awaitsFor( function relatedDocsReceived() { - return (Object.getOwnPropertyNames(liveDoc.getRelated().stylesheets).length === 4); + return _isRelatedStyleSheet(liveDoc, "blank.css"); }, "relatedDocuments.done.received", 10000