"use strict"; const assert = require("assert"); const { readFileSync } = require("fs"); const { join: pathJoin, normalize: pathNormalize, } = require("path"); const { fetchURL } = require("./common.js"); const { Meteor } = require("meteor/meteor"); const hasOwn = Object.prototype.hasOwnProperty; require("./security.js"); const client = require("./client.js"); Meteor.startup(() => { if (! Package.webapp) { // If the webapp package is not in use, there's no way for the // dynamic-import package to fetch dynamic modules, so we should // abandon the rest of the logic in this module. // // If api.use("webapp") appeared in dynamic-import/package.js, then // Package.webapp would always be defined here, of course, but that // would be a bad idea, because the dynamic-import package should not // single-handedly force a dependency on webapp if the program does // not otherwise need a web server (e.g., when the program is an // isopacket or build plugin instead of a web application). // // Note that the client.js module (imported above) still defines // Module.prototype.dynamicImport, which will work as long as no // modules need to be fetched. return; } Object.keys(dynamicImportInfo).forEach(setUpPlatform); Package.webapp.WebAppInternals.meteorInternalHandlers.use( fetchURL, middleware ); }); function setUpPlatform(platform) { const info = dynamicImportInfo[platform]; if (info.dynamicRoot) { info.dynamicRoot = pathNormalize(info.dynamicRoot); } if (platform === "server") { client.setSecretKey(info.key = randomId(40)); } } function randomId(n) { let s = ""; while (s.length < n) { s += Math.random().toString(36).slice(2); } return s.slice(0, n); } function middleware(request, response) { // Allow dynamic import() requests from any origin. response.setHeader("Access-Control-Allow-Origin", "*"); if (request.method === "OPTIONS") { const acrh = request.headers["access-control-request-headers"]; response.setHeader('Allow', 'OPTIONS, POST'); response.setHeader('Content-Length', '0'); response.setHeader( "Access-Control-Allow-Headers", typeof acrh === "string" ? acrh : "*" ); response.setHeader("Access-Control-Allow-Methods", "POST"); response.end(); } else if (request.method === "POST") { const chunks = []; request.on("data", chunk => chunks.push(chunk)); request.on("end", () => { try { const tree = JSON.stringify(readTree( JSON.parse(Buffer.concat(chunks)), getPlatform(request) ), null, 2); response.writeHead(200, { "Content-Type": "application/json" }); response.end(tree); } catch (e) { response.writeHead(400, { "Content-Type": "application/json" }); response.end(JSON.stringify( Meteor.isDevelopment && e.message || "bad request" )); } }); } else { const body = `method ${request.method} not allowed`; response.writeHead(405, { Allow: "OPTIONS, POST", 'Content-Length': Buffer.byteLength(body), "Cache-Control": "no-cache" }); response.end(body); } } function getPlatform(request) { // If the __dynamicImport request includes a secret key, and it matches // dynamicImportInfo[platform].key, use platform instead of the default // platform, web.browser. const secretKey = request.query.key; if (typeof secretKey === "string") { for (const p of Object.keys(dynamicImportInfo)) { if (secretKey === dynamicImportInfo[p].key) { return p; } } } return Package.webapp.WebApp.categorizeRequest(request).arch; } function readTree(tree, platform) { const pathParts = []; function walk(node) { if (! node) { return null; } if (typeof node !== "object") { return read(pathParts, platform); } let empty = true; Object.keys(node).forEach(name => { pathParts.push(name); const result = walk(node[name]); if (result === null) { // If the read function returns null, omit this module from the // resulting tree. delete node[name]; } else { node[name] = result; empty = false; } assert.strictEqual(pathParts.pop(), name); }); if (empty) { // If every recursive call to walk(node[name]) returned null, // remove this node from the resulting tree by returning null. return null; } return node; } return walk(tree); } function read(pathParts, platform) { const { dynamicRoot } = dynamicImportInfo[platform]; const absPath = pathNormalize(pathJoin( dynamicRoot, pathJoin(...pathParts).replace(/:/g, "_") )); if (! absPath.startsWith(dynamicRoot)) { console.error("bad dynamic import path:", absPath); return null; } const cache = getCache(platform); if (hasOwn.call(cache, absPath)) { return cache[absPath]; } try { return cache[absPath] = readFileSync(absPath, "utf8"); } catch (e) { console.error(e.stack || e); return null; } } const cachesByPlatform = Object.create(null); function getCache(platform) { return hasOwn.call(cachesByPlatform, platform) ? cachesByPlatform[platform] : cachesByPlatform[platform] = Object.create(null); } const { onMessage } = require("meteor/inter-process-messaging"); onMessage("client-refresh", () => { // The caches for the web.browser[.legacy] platforms need to be // discarded whenever a client-only refresh occurs, so the new client // bundle does not fetch stale module data from dynamic import(). This // message is sent by tools/runners/run-app.js and also consumed by the // autoupdate package. Object.keys(cachesByPlatform).forEach(platform => { delete cachesByPlatform[platform]; }); });