Skip to content

Commit

Permalink
feat: Add offline support (#866)
Browse files Browse the repository at this point in the history
  • Loading branch information
jespertheend authored Feb 7, 2024
1 parent c9347f0 commit 199d135
Show file tree
Hide file tree
Showing 22 changed files with 1,495 additions and 161 deletions.
2 changes: 1 addition & 1 deletion importmap.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"std/": "https://deno.land/[email protected]/",
"chdir-anywhere": "https://deno.land/x/[email protected]/mod.js",
"fake-imports": "https://deno.land/x/[email protected]/mod.js",
"fake-dom/": "https://deno.land/x/fake_dom@v0.11.0/src/",
"fake-dom/": "https://deno.land/x/fake_dom@v0.12.0/src/",
"puppeteer": "https://raw.githubusercontent.com/jespertheend/deno-puppeteer/7c5ed2e6e9f43d7fc27d4fa49f17db36be220956/mod.ts",
"rollup": "npm:[email protected]",
"rollup-plugin-jscc": "npm:[email protected]",
Expand Down
2 changes: 1 addition & 1 deletion scripts/DevServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class DevServer {
const studioDiscovery = await this.#studioDiscoveryPromise;
return studioDiscovery.webSocketManager.handleRequest(request, connInfo);
}
if (url.pathname == "/studio/internalDiscovery") {
if (url.pathname.endsWith("/internalDiscovery")) {
request = new Request(request.url + ".html", request);
}
const response = await serveDir(request, {
Expand Down
126 changes: 83 additions & 43 deletions scripts/buildStudio.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {rollup} from "rollup";
import {copy, ensureDir} from "std/fs/mod.ts";
import {copy, ensureDir, walk} from "std/fs/mod.ts";
import * as path from "std/path/mod.ts";
import {minify} from "terser";
import {setCwd} from "chdir-anywhere";
Expand All @@ -19,23 +19,24 @@ await dev({
setCwd();
Deno.chdir("../studio");

const outputPath = path.resolve("dist/");
try {
await Deno.remove("dist", {recursive: true});
await Deno.remove(outputPath, {recursive: true});
} catch {
// Already removed
}
await ensureDir("dist");
await ensureDir(outputPath);

await copy("index.html", "dist/index.html");
await copy("internalDiscovery.html", "dist/internalDiscovery.html");
await copy("static/", "dist/static/");
await copy("builtInAssets/", "dist/builtInAssets/");
await copy("index.html", path.resolve(outputPath, "index.html"));
await copy("internalDiscovery.html", path.resolve(outputPath, "internalDiscovery.html"));
await copy("static/", path.resolve(outputPath, "static/"));
await copy("builtInAssets/", path.resolve(outputPath, "builtInAssets/"));

const engineSource = await buildEngine();
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(engineSource));
const hashString = toHashString(hash).slice(0, 8);
const engineFileName = `renda-${hashString}.js`;
await Deno.writeTextFile("dist/" + engineFileName, engineSource);
await Deno.writeTextFile(path.resolve(outputPath, engineFileName), engineSource);

/**
* Replaces the value of an attribute in a html file.
Expand Down Expand Up @@ -77,24 +78,6 @@ function overrideDefines(definesFilePath, defines) {
};
}

/**
* A rollup plugin for minifying builds.
* @param {import("terser").MinifyOptions} minifyOptions
* @returns {import("rollup").Plugin}
*/
function terser(minifyOptions = {}) {
return {
name: "terser",
async renderChunk(code, chunk, outputOptions) {
const output = await minify(code, minifyOptions);
if (!output.code) return null;
return {
code: output.code,
};
},
};
}

/**
* A rollup plugin for remapping url() paths in css files.
* @param {object} options
Expand Down Expand Up @@ -167,6 +150,7 @@ const studioDefines = {
};
const STUDIO_ENTRY_POINT_PATH = "src/main.js";
const INTERNAL_DISCOVERY_ENTRY_POINT_PATH = "src/network/studioConnections/internalDiscovery/internalDiscoveryIframeEntryPoint.js";
const SERVICE_WORKER_ENTRY_POINT_PATH = "sw.js";
const bundle = await rollup({
input: [
STUDIO_ENTRY_POINT_PATH,
Expand All @@ -175,9 +159,8 @@ const bundle = await rollup({
plugins: [
overrideDefines("/studio/src/studioDefines.js", studioDefines),
resolveUrlObjects(),
terser(),
rebaseCssUrl({
outputPath: path.resolve("dist/"),
outputPath,
}),
importAssertionsPlugin(),
],
Expand All @@ -189,30 +172,87 @@ const bundle = await rollup({
preserveEntrySignatures: false,
});
const {output} = await bundle.write({
dir: "dist/",
dir: outputPath,
format: "esm",
entryFileNames: "[name]-[hash].js",
});

const studioEntryPointPath = path.resolve(STUDIO_ENTRY_POINT_PATH);
const internalDiscoveryEntryPointPath = path.resolve(INTERNAL_DISCOVERY_ENTRY_POINT_PATH);
let bundleEntryPoint;
let internalDiscoveryEntryPoint;
/** @type {Map<string, string>} */
const entryPointPaths = new Map();
/** @type {string[]} */
const createdChunkFiles = [];
for (const chunkOrAsset of output) {
if (chunkOrAsset.type != "chunk") {
throw new Error("Assertion failed, unexpected type: " + chunkOrAsset.type);
}
if (chunkOrAsset.facadeModuleId == studioEntryPointPath) {
bundleEntryPoint = chunkOrAsset.fileName;
} else if (chunkOrAsset.facadeModuleId == internalDiscoveryEntryPointPath) {
internalDiscoveryEntryPoint = chunkOrAsset.fileName;
if (chunkOrAsset.facadeModuleId) {
entryPointPaths.set(chunkOrAsset.facadeModuleId, chunkOrAsset.fileName);
}
createdChunkFiles.push(chunkOrAsset.fileName);
}

/**
* Takes a path to a JavaScript source file and returns the path which it was converted to when bundling.
* @param {string} sourceEntryPointPath
*/
function getEntryPoint(sourceEntryPointPath) {
const bundleEntryPoint = entryPointPaths.get(path.resolve(sourceEntryPointPath));
if (!bundleEntryPoint) {
throw new Error(`Assertion failed: no entry point chunk was found for "${sourceEntryPointPath}"`);
}
return bundleEntryPoint;
}
if (!bundleEntryPoint) {
throw new Error("Assertion failed: no studio entry point chunk was found");

// Insert entry points into html files
const bundleEntryPoint = getEntryPoint(STUDIO_ENTRY_POINT_PATH);
await setHtmlAttribute(path.resolve(outputPath, "index.html"), "studio script tag", bundleEntryPoint);

const internalDiscoveryEntryPoint = getEntryPoint(INTERNAL_DISCOVERY_ENTRY_POINT_PATH);
await setHtmlAttribute(path.resolve(outputPath, "internalDiscovery.html"), "discovery script tag", internalDiscoveryEntryPoint);

// Insert all generated files into the service worker script
const serviceWorkerEntryPoint = getEntryPoint(SERVICE_WORKER_ENTRY_POINT_PATH);
const fullServiceWorkerEntryPointPath = path.resolve(outputPath, serviceWorkerEntryPoint);
const swCacheFiles = [
"./",
"./internalDiscovery",
];
for await (const entry of walk(outputPath)) {
if (entry.name.endsWith(".html")) continue;
if (!entry.isFile) continue;

// We should exclude the service worker from the list of files to cache,
// because we don't want the service worker to cache itself.
// Besides, later on we will rename this file back to 'sw.js'
if (entry.path == fullServiceWorkerEntryPointPath) continue;

const entryName = path.relative(outputPath, entry.path);
swCacheFiles.push("./" + entryName);
}
if (!internalDiscoveryEntryPoint) {
throw new Error("Assertion failed: no internal discovery entry point chunk was found");

let serviceWorkerContent = await Deno.readTextFile(fullServiceWorkerEntryPointPath);
serviceWorkerContent = serviceWorkerContent.replace("/* GENERATED_FILES_INSERTION_TAG */", `"${swCacheFiles.join(`", "`)}",`);
serviceWorkerContent = serviceWorkerContent.replace('/* GIT_COMMIT_INSERTION_TAG */""', `"${gitCommit}"`);
await Deno.writeTextFile(fullServiceWorkerEntryPointPath, serviceWorkerContent);

// Minify all JavaScript files
for (const chunk of createdChunkFiles) {
const chunkPath = path.resolve(outputPath, chunk);
let fileContent = await Deno.readTextFile(chunkPath);

// We need to replace any occurrences of the service worker because we will be renaming it below
fileContent = fileContent.replaceAll(serviceWorkerEntryPoint, "sw.js");

const output = await minify(fileContent);
if (!output.code) {
throw new Error(`Failed to minify "${chunkPath}"`);
}
await Deno.writeTextFile(chunkPath, output.code);
}
await setHtmlAttribute("dist/index.html", "studio script tag", bundleEntryPoint);
await setHtmlAttribute("dist/internalDiscovery.html", "discovery script tag", internalDiscoveryEntryPoint);

// We rename the service worker file to always be 'sw.js'.
// If we don't do this, the service worker will get a different name with different versions.
// The browser will check the contents of the currently installed service worker to see if there are any updates.
// If the file name is changed the browser will check an old service worker, resulting in a 404 response.
// As a result, the service worker would never be updated and the user would stay stuck with a past version forever.
await Deno.rename(fullServiceWorkerEntryPointPath, path.resolve(outputPath, "sw.js"));
Loading

0 comments on commit 199d135

Please sign in to comment.