diff --git a/app/ide-desktop/lib/client/src/bin/server.ts b/app/ide-desktop/lib/client/src/bin/server.ts
index 92261fc327f0..ed18a44305cc 100644
--- a/app/ide-desktop/lib/client/src/bin/server.ts
+++ b/app/ide-desktop/lib/client/src/bin/server.ts
@@ -8,6 +8,7 @@ import * as mime from 'mime-types'
import * as portfinder from 'portfinder'
import createServer from 'create-servers'
+import * as common from 'enso-common'
import * as contentConfig from 'enso-content-config'
import * as paths from '../paths'
@@ -105,6 +106,9 @@ export class Server {
resource === '/preload.cjs.map'
? `${paths.APP_PATH}${resource}`
: `${this.config.dir}${resource}`
+ for (const [header, value] of common.COOP_COEP_CORP_HEADERS) {
+ response.setHeader(header, value)
+ }
fs.readFile(resourceFile, (err, data) => {
if (err) {
logger.error(`Resource '${resource}' not found.`)
diff --git a/app/ide-desktop/lib/common/src/index.ts b/app/ide-desktop/lib/common/src/index.ts
index 0b514c5da626..f95ae98b0713 100644
--- a/app/ide-desktop/lib/common/src/index.ts
+++ b/app/ide-desktop/lib/common/src/index.ts
@@ -1,4 +1,5 @@
-/** @file This module contains metadata about the product and distribution.
+/** @file This module contains metadata about the product and distribution,
+ * and various other constants that are needed in multiple sibling packages.
*
* Code in this package is used by two or more sibling packages of this package. The code is defined
* here when it is not possible for a sibling package to own that code without introducing a
@@ -12,3 +13,13 @@ export const DEEP_LINK_SCHEME = 'enso'
/** Name of the product. */
export const PRODUCT_NAME = 'Enso'
+
+/** COOP, COEP, and CORP headers: https://web.dev/coop-coep/
+ *
+ * These are required to increase the resolution of `performance.now()` timers,
+ * making profiling a lot more accurate and consistent. */
+export const COOP_COEP_CORP_HEADERS: [header: string, value: string][] = [
+ ['Cross-Origin-Embedder-Policy', 'require-corp'],
+ ['Cross-Origin-Opener-Policy', 'same-origin'],
+ ['Cross-Origin-Resource-Policy', 'same-origin'],
+]
diff --git a/app/ide-desktop/lib/content/esbuild-config.ts b/app/ide-desktop/lib/content/esbuild-config.ts
index dc67ad28ba1f..eb80ad2b9f50 100644
--- a/app/ide-desktop/lib/content/esbuild-config.ts
+++ b/app/ide-desktop/lib/content/esbuild-config.ts
@@ -26,7 +26,11 @@ import esbuildPluginYaml from 'esbuild-plugin-yaml'
import * as utils from '../../utils'
import BUILD_INFO from '../../build.json' assert { type: 'json' }
-export const THIS_PATH = pathModule.resolve(pathModule.dirname(url.fileURLToPath(import.meta.url)))
+// =================
+// === Constants ===
+// =================
+
+const THIS_PATH = pathModule.resolve(pathModule.dirname(url.fileURLToPath(import.meta.url)))
// =============================
// === Environment variables ===
diff --git a/app/ide-desktop/lib/content/src/index.ts b/app/ide-desktop/lib/content/src/index.ts
index b3f27b68b35c..ea413eb888ff 100644
--- a/app/ide-desktop/lib/content/src/index.ts
+++ b/app/ide-desktop/lib/content/src/index.ts
@@ -20,6 +20,9 @@ const logger = app.log.logger
const ESBUILD_PATH = '/esbuild'
/** SSE event indicating a build has finished. */
const ESBUILD_EVENT_NAME = 'change'
+/** Path to the service worker that resolves all extensionless paths to `/index.html`.
+ * This service worker is required for client-side routing to work when doing local development. */
+const SERVICE_WORKER_PATH = '/serviceWorker.js'
/** One second in milliseconds. */
const SECOND = 1000
/** Time in seconds after which a `fetchTimeout` ends. */
@@ -33,6 +36,7 @@ if (IS_DEV_MODE) {
new EventSource(ESBUILD_PATH).addEventListener(ESBUILD_EVENT_NAME, () => {
location.reload()
})
+ void navigator.serviceWorker.register(SERVICE_WORKER_PATH)
}
// =============
diff --git a/app/ide-desktop/lib/content/src/serviceWorker.ts b/app/ide-desktop/lib/content/src/serviceWorker.ts
new file mode 100644
index 000000000000..332b8849acc8
--- /dev/null
+++ b/app/ide-desktop/lib/content/src/serviceWorker.ts
@@ -0,0 +1,32 @@
+/** @file A service worker that redirects paths without extensions to `/index.html`.
+ * This is required for paths like `/login`, which are handled by client-side routing,
+ * to work when developing locally on `localhost:8080`. */
+// Bring globals and interfaces specific to Web Workers into scope.
+///
+import * as common from 'enso-common'
+
+// =====================
+// === Fetch handler ===
+// =====================
+
+// We `declare` a variable here because Service Workers have a different global scope.
+// eslint-disable-next-line no-restricted-syntax
+declare const self: ServiceWorkerGlobalScope
+
+self.addEventListener('fetch', event => {
+ const url = new URL(event.request.url)
+ if (url.hostname === 'localhost' && url.pathname !== '/esbuild') {
+ event.respondWith(
+ fetch(event.request.url).then(response => {
+ const clonedResponse = new Response(response.body, response)
+ for (const [header, value] of common.COOP_COEP_CORP_HEADERS) {
+ clonedResponse.headers.set(header, value)
+ }
+ return clonedResponse
+ })
+ )
+ return
+ } else {
+ return false
+ }
+})
diff --git a/app/ide-desktop/lib/content/watch.ts b/app/ide-desktop/lib/content/watch.ts
index 44385f47760a..a35afb242d0d 100644
--- a/app/ide-desktop/lib/content/watch.ts
+++ b/app/ide-desktop/lib/content/watch.ts
@@ -1,4 +1,7 @@
/** @file File watch and compile service. */
+import * as path from 'node:path'
+import * as url from 'node:url'
+
import * as esbuild from 'esbuild'
import * as portfinder from 'portfinder'
import chalk from 'chalk'
@@ -12,6 +15,7 @@ import * as dashboardBundler from '../dashboard/esbuild-config'
const PORT = 8080
const HTTP_STATUS_OK = 200
+const THIS_PATH = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)))
// ===============
// === Watcher ===
@@ -29,6 +33,10 @@ async function watch() {
...bundler.argumentsFromEnv(),
devMode: true,
})
+ opts.entryPoints.push({
+ in: path.resolve(THIS_PATH, 'src', 'serviceWorker.ts'),
+ out: 'serviceWorker',
+ })
const builder = await esbuild.context(opts)
await builder.watch()
await builder.serve({
diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx
index a00b416e20e9..d36b439bf229 100644
--- a/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx
+++ b/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx
@@ -254,6 +254,16 @@ export const COMPUTER_ICON = (
)
+/** An icon representing a user without a profile picture. */
+export const DEFAULT_USER_ICON = (
+
+)
+
export interface StopIconProps {
className?: string
}
diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx
index 81fd843c3930..fb1d1329ad04 100644
--- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx
+++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx
@@ -471,10 +471,7 @@ function Dashboard(props: DashboardProps) {
key={user.user.organization_id}
permissions={PERMISSION[user.permission]}
>
-
+ {svg.DEFAULT_USER_ICON}
))}
>
diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/topBar.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/topBar.tsx
index 40f598c01750..41d7f838128f 100644
--- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/topBar.tsx
+++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/topBar.tsx
@@ -120,14 +120,15 @@ function TopBar(props: TopBarProps) {
{/* User profile and menu. */}
)
diff --git a/app/ide-desktop/lib/dashboard/src/serviceWorker.ts b/app/ide-desktop/lib/dashboard/src/serviceWorker.ts
index 295ca28982ee..8c087b349962 100644
--- a/app/ide-desktop/lib/dashboard/src/serviceWorker.ts
+++ b/app/ide-desktop/lib/dashboard/src/serviceWorker.ts
@@ -1,6 +1,9 @@
/** @file A service worker that redirects paths without extensions to `/index.html`.
- * This is only used in the cloud frontend. */
+ * This is required for paths like `/login`, which are handled by client-side routing,
+ * to work when developing locally on `localhost:8081`. */
+// Bring globals and interfaces specific to Web Workers into scope.
///
+import * as common from 'enso-common'
// =====================
// === Fetch handler ===
@@ -12,17 +15,21 @@ declare const self: ServiceWorkerGlobalScope
self.addEventListener('fetch', event => {
const url = new URL(event.request.url)
- if (
- url.hostname === 'localhost' &&
- /\/[^.]+$/.test(event.request.url) &&
- url.pathname !== '/esbuild'
- ) {
- event.respondWith(fetch('/index.html'))
+ if (url.hostname === 'localhost' && url.pathname !== '/esbuild') {
+ const responsePromise = /\/[^.]+$/.test(event.request.url)
+ ? fetch('/index.html')
+ : fetch(event.request.url)
+ event.respondWith(
+ responsePromise.then(response => {
+ const clonedResponse = new Response(response.body, response)
+ for (const [header, value] of common.COOP_COEP_CORP_HEADERS) {
+ clonedResponse.headers.set(header, value)
+ }
+ return clonedResponse
+ })
+ )
return
} else {
return false
}
})
-
-// Required for TypeScript to consider it a module, instead of in window scope.
-export {}