Bläddra i källkod

[monorepo] add integration for GitHub Code Spaces (#32722)

* [web] serve site manifest as application/json

* [monorepo] bail out early from dev-env-setup when inside devcontainer

* [monorepo] move utils for waiting for services into shared bash script

* [monorepo] move domain related env-vars into dedicated .env file

The .env file will be overridden in GitHub Code Spaces.

* [monorepo] remove unused initial_mongo_setup Make target

It was previously used for the mongo replica set setup.
That setup is a docker init script now.

* [monorepo] add flag for silencing docker build output

* [monorepo] add integration for GitHub Code Spaces

* [web] make prettier happy

* [web] restore no use-credentials for manifest outside code spaces

* [monorepo] run make install as part of post-start again

GitOrigin-RevId: 8c2cc3b969b7e3e8f9c5dac778192c9412cbf4df
Jakob Ackermann 4 månader sedan
förälder
incheckning
b893ba36e2

+ 1 - 0
services/web/.eslintrc.js

@@ -308,6 +308,7 @@ module.exports = {
         'scripts/delete_orphaned_chat_threads.mjs',
         'scripts/delete_orphaned_data_helper.mjs',
         'scripts/delete_subscriptions.mjs',
+        'scripts/devcontainer_setup.mjs',
         'scripts/e2e_test_setup.mjs',
         'scripts/ensure_affiliations.mjs',
         'scripts/esm-check-migration.mjs',

+ 5 - 1
services/web/app/views/_metadata.pug

@@ -147,4 +147,8 @@ if metadata && metadata.canonicalURL
 
 //- Manifest
 //- Does not currently contain a start_url to prevent browser installation prompts
-link(rel='manifest' href=buildBaseAssetPath() + 'web.sitemanifest')
+link(
+	rel='manifest'
+	href=buildBaseAssetPath() + 'web.sitemanifest.json'
+	crossorigin=settings.isCodeSpace ? 'use-credentials' : false
+)

+ 2 - 0
services/web/config/settings.defaults.js

@@ -327,6 +327,8 @@ module.exports = {
   // that are sent out, generated links, etc.
   siteUrl: (siteUrl = process.env.PUBLIC_URL || 'http://127.0.0.1:3000'),
 
+  isCodeSpace: process.env.IS_CODE_SPACE === 'true',
+
   lockManager: {
     lockTestInterval: intFromEnv('LOCK_MANAGER_LOCK_TEST_INTERVAL', 50),
     maxTestInterval: intFromEnv('LOCK_MANAGER_MAX_TEST_INTERVAL', 1000),

+ 2 - 2
services/web/package.json

@@ -48,8 +48,8 @@
     "build-storybook": "storybook build",
     "build-unfilled-fonts": "node frontend/fonts/material-symbols/build-unfilled.mjs",
     "precompile-pug": "node app/src/infrastructure/Views.mjs",
-    "local:nodemon": "set -a;. ../../config/dev-environment.env;. ./docker-compose.common.env;. ../../config/local-dev.env;. ./local-dev.env;. ../../config/local.env; set +a; echo $OVERLEAF_CONFIG; WEB_PORT=13000 LISTEN_ADDRESS=0.0.0.0 npm run nodemon",
-    "local:webpack": "set -a;. ../../config/dev-environment.env;. ./docker-compose.common.env;. ../../config/local-dev.env;. ./local-dev.env;. ../../config/local.env; set +a; PORT=13808 OVERLEAF_CONFIG=$(pwd)/config/settings.webpack.js npm run webpack",
+    "local:nodemon": "set -a;. ../../config/dev-environment.env;. ./docker-compose.common.env;. ../../config/local-dev.env;. ./local-dev.env;. ../../config/local.env;. ../../config/domain.env; set +a; echo $OVERLEAF_CONFIG; WEB_PORT=13000 LISTEN_ADDRESS=0.0.0.0 npm run nodemon",
+    "local:webpack": "set -a;. ../../config/dev-environment.env;. ./docker-compose.common.env;. ../../config/local-dev.env;. ./local-dev.env;. ../../config/local.env;. ../../config/domain.env; set +a; PORT=13808 OVERLEAF_CONFIG=$(pwd)/config/settings.webpack.js npm run webpack",
     "local:test:acceptance:run_dir": "set -a;. $(pwd)/docker-compose.common.env;. $(pwd)/local-test.env; set +a; npm run test:acceptance:run_dir",
     "local:test:acceptance:run_app": "OVERLEAF_CONFIG=$(pwd)/test/acceptance/config/settings.test.${OVERLEAF_APP}.js npm run local:test:acceptance:run_dir -- $(pwd)/test/acceptance/src",
     "local:test:acceptance:run_module": "if [ ! -d $(pwd)/modules/${MODULE}/test/acceptance ]; then echo \"Module '${MODULE}' does not have acceptance tests\"; exit 0; fi; OVERLEAF_CONFIG=$(pwd)/modules/${MODULE}/test/acceptance/config/settings.test.js BASE_CONFIG=$(pwd)/test/acceptance/config/settings.test.${OVERLEAF_APP}.js npm run local:test:acceptance:run_dir -- $(pwd)/modules/${MODULE}/test/acceptance",

+ 0 - 18
services/web/public/web.sitemanifest

@@ -1,18 +0,0 @@
-{
-  "name": "Overleaf",
-  "icons": [
-      {
-          "src": "/android-chrome-192x192.png",
-          "sizes": "192x192",
-          "type": "image/png"
-      },
-      {
-          "src": "/android-chrome-512x512.png",
-          "sizes": "512x512",
-          "type": "image/png"
-      }
-  ],
-  "theme_color": "#1b222c",
-  "background_color": "#ffffff",
-  "display": "standalone"
-}

+ 18 - 0
services/web/public/web.sitemanifest.json

@@ -0,0 +1,18 @@
+{
+  "name": "Overleaf",
+  "icons": [
+    {
+      "src": "/android-chrome-192x192.png",
+      "sizes": "192x192",
+      "type": "image/png"
+    },
+    {
+      "src": "/android-chrome-512x512.png",
+      "sizes": "512x512",
+      "type": "image/png"
+    }
+  ],
+  "theme_color": "#1b222c",
+  "background_color": "#ffffff",
+  "display": "standalone"
+}

+ 92 - 0
services/web/scripts/devcontainer_setup.mjs

@@ -0,0 +1,92 @@
+import { promiseMapWithLimit } from '@overleaf/promise-utils'
+import Settings from '@overleaf/settings'
+import { waitForDb, db } from '../app/src/infrastructure/mongodb.mjs'
+import GracefulShutdown from '../app/src/infrastructure/GracefulShutdown.mjs'
+import UserRegistrationHandler from '../app/src/Features/User/UserRegistrationHandler.mjs'
+import minimist from 'minimist'
+import {
+  createProjectWithOldHistoryId,
+  provisionSplitTests,
+} from './e2e_test_setup.mjs'
+
+const { email: USER_EMAIL, password: PASSWORD } = minimist(
+  process.argv.slice(2),
+  { string: ['email', 'password'] }
+)
+
+/**
+ * @param {string} email
+ * @return {Promise<string>}
+ */
+async function createUser(email) {
+  let userId
+  try {
+    const user = await UserRegistrationHandler.promises.registerNewUser({
+      email,
+      password: PASSWORD,
+    })
+    userId = user._id
+  } catch (err) {
+    if (err.message.includes('EmailAlreadyRegistered')) {
+      userId = err.info.userId
+    } else {
+      throw err
+    }
+  }
+  const features = email.startsWith('free')
+    ? Settings.defaultFeatures
+    : Settings.features.professional
+  const isAdmin = email === USER_EMAIL || email === 'admin@overleaf.com'
+  let adminRoles = []
+  if (isAdmin) {
+    adminRoles = ['engineering']
+  }
+  await db.users.updateOne(
+    { _id: userId },
+    {
+      $set: {
+        // Set admin flag.
+        isAdmin,
+        adminRoles,
+        // Override features.
+        features,
+        featuresOverrides: [{ features }],
+        // disable AI features, does not work with custom GH Code Spaces domain.
+        'aiFeatures.enabled': false,
+      },
+    }
+  )
+  return userId.toString()
+}
+
+/**
+ * @param {string} email
+ * @return {Promise<void>}
+ */
+async function provisionUser(email) {
+  const userId = await createUser(email)
+  await createProjectWithOldHistoryId(userId)
+}
+
+async function provisionUsers() {
+  const emails = [
+    USER_EMAIL,
+    'admin@overleaf.com',
+    'free@overleaf.com',
+    'premium@overleaf.com',
+  ]
+  await promiseMapWithLimit(5, emails, provisionUser)
+}
+
+async function main() {
+  if (process.env.NODE_ENV !== 'development') {
+    throw new Error('only available in dev-env')
+  }
+  await waitForDb()
+  await Promise.all([provisionUsers(), provisionSplitTests()])
+}
+
+if (import.meta.main) {
+  await main()
+  await GracefulShutdown.gracefulShutdown()
+}

+ 6 - 11
services/web/scripts/e2e_test_setup.mjs

@@ -85,7 +85,7 @@ async function deleteUser(email) {
   await UserDeleter.promises.expireDeletedUser(user._id)
 }
 
-async function createProjectWithOldHistoryId(userId) {
+export async function createProjectWithOldHistoryId(userId) {
   const projectName = 'old history id'
   const historyId = parseInt(
     await HistoryManager.promises.initializeProject(),
@@ -137,7 +137,7 @@ async function purgeNewUsers() {
   )
 }
 
-async function provisionSplitTests() {
+export async function provisionSplitTests() {
   const backup = Path.join(
     MONOREPO,
     'backup',
@@ -213,12 +213,7 @@ async function main() {
   await Promise.all([purgeNewUsers(), provisionUsers(), provisionSplitTests()])
 }
 
-await main()
-await GracefulShutdown.gracefulShutdown(
-  {
-    close(cb) {
-      cb()
-    },
-  },
-  'SIGTERM'
-)
+if (import.meta.main) {
+  await main()
+  await GracefulShutdown.gracefulShutdown()
+}