diff --git a/dev/browser/start.js b/dev/browser/start.js index f2794ac..f378c10 100644 --- a/dev/browser/start.js +++ b/dev/browser/start.js @@ -2,7 +2,16 @@ import { exec } from 'node:child_process'; import os from 'node:os'; export default async function start(server) { - const url = server ? `https://${server.host}:${server.port}` : 'https://localhost:3000'; + const protocol = server?.protocol || 'https'; + const url = server ? `${protocol}://${server.host}:${server.port}` : 'https://localhost:3000'; + + console.log(`-> Dev server: ${url}`); + + // A remote or headless workstation has no browser to open; printing the URL + // is all that is useful there. + if (server && server.headless) { + return; + } if (os.platform() === 'darwin') { exec(`open ${url}`); diff --git a/dev/start.js b/dev/start.js index 368beb6..a0bfd15 100644 --- a/dev/start.js +++ b/dev/start.js @@ -7,6 +7,14 @@ const args = process.argv.slice(2); const platform = args.find((arg) => /(android|ios|browser)/i.test(arg)) || "android"; const isRelease = args.includes("--release") || args.includes("-r") || false; const noServer = args.includes("--no-server"); +// Bind and serve overrides, needed whenever the dev server is not reached over +// the LAN: a remote workstation (VS Code Remote port forwarding) wants +// `--host localhost --http`, and `--headless` keeps a headless box from trying +// to launch a browser. +const hostOverride = getArgValue("--host") || process.env.SHELLULAR_DEV_HOST; +const portOverride = getArgValue("--port") || process.env.SHELLULAR_DEV_PORT; +const noHttps = args.includes("--http"); +const headless = args.includes("--headless"); const { default: start } = await import(`./${platform}/start.js`); @@ -37,10 +45,11 @@ async function main() { } else if (noServer) { command = `webpack --mode development --env platform=${platform}`; } else { - const host = getIp(); - const port = getPort(); - devServer = { host, port }; - command = `webpack serve --mode development --env platform=${platform} host=${host} port=${port}`; + const host = hostOverride || getIp(); + const port = portOverride || getPort(); + const protocol = platform === "browser" && !noHttps ? "https" : "http"; + devServer = { host, port, protocol, headless }; + command = `webpack serve --mode development --env platform=${platform} host=${host} port=${port}${noHttps ? " https=false" : ""}`; } console.log(command); @@ -131,6 +140,21 @@ function printToStdOut(error, stdout, stderr) { } } +/** + * Reads the value of a `--flag value` or `--flag=value` argument. + * @param {string} flag - The flag to look for. + * @returns {string|undefined} The value, or undefined when the flag is absent. + */ +function getArgValue(flag) { + const inline = args.find((arg) => arg.startsWith(`${flag}=`)); + if (inline) { + return inline.slice(flag.length + 1); + } + + const index = args.indexOf(flag); + return index !== -1 ? args[index + 1] : undefined; +} + function getPort() { if (platform === "browser") { return 7977; diff --git a/webpack.config.js b/webpack.config.js index f47ef42..3b8b9de 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -34,8 +34,14 @@ export default (_, { env = {}, mode = "development" }) => { platform = "android", host, port, + https = "true", console: compileConsole = false, } = env; + // The browser target is served over https so the phone gets a secure + // context on the LAN. Behind a localhost tunnel (VS Code Remote port + // forwarding) http is both a secure context and one less certificate + // warning, so `https=false` turns TLS off. + const useHttps = platform === "browser" && https !== "false"; const outputPath = platform === "browser" ? BROWSER_BUNDLE @@ -45,11 +51,16 @@ export default (_, { env = {}, mode = "development" }) => { let alias; if (isDev) { - console.log(`dev server: ${host}:${port}`); - alias = { - // assuming app & packages are in the same monorepo, adjust as needed - "@shellular/protocol": resolve("../packages/protocol/dist/index.js"), - }; + // Only alias the protocol package when this checkout really sits next to + // it in the monorepo. A standalone checkout has no ../packages, and an + // alias pointing at a missing file breaks every dev build. + const localProtocol = resolve("../packages/protocol/dist/index.js"); + if (existsSync(localProtocol)) { + console.log(`using local protocol: ${localProtocol}`); + alias = { "@shellular/protocol": localProtocol }; + } else { + console.log(`no local protocol found. using the one from node_modules`); + } } else if (!compileConsole) { clearOutputDir(outputPath); } @@ -125,7 +136,7 @@ export default (_, { env = {}, mode = "development" }) => { webSocketURL: { hostname: host, port: Number(port), - protocol: platform === "browser" ? "wss" : "ws", + protocol: useHttps ? "wss" : "ws", pathname: "/ws", }, }, @@ -138,7 +149,7 @@ export default (_, { env = {}, mode = "development" }) => { : {}), ...(platform === "browser" ? { - server: "https", + server: useHttps ? "https" : "http", headers: (request) => { const isAuthCallback = new URL( @@ -255,7 +266,10 @@ export default (_, { env = {}, mode = "development" }) => { DISPLAY_NAME: packageJson.displayName, HOST: isDev && host ? host : null, PORT: isDev && port ? port : null, - ORIGIN: isDev && host && port ? `http://${host}:${port}` : null, + ORIGIN: + isDev && host && port + ? `${useHttps ? "https" : "http"}://${host}:${port}` + : null, }), ], optimization: {