enso/app/ide-desktop/lib/client/watch.ts
somebody1234 37c487f530
Fix restoring cloud projects that have been closed (e.g. from inactivity) (#7584)
Fix issues restoring cloud projects that have been closed.
On current develop, the frontend assumes cloud projects are still open from the last time they were open. If this is not the case, it tries to open WebSocket endpoints that no longer exist

# Important Notes
For a relatively easy way to test:
- Run `Enso.dmg` or `./ide run watch`
- Open a cloud project
- Refresh Electron to make sure restoration is working
- Go to `localhost:8080`
- Close the project
- Refresh Electron to make sure startup + restoration is working

Should also test restoring local projects to make sure that didn't break.

I was unable to properly test cloud projects (the one I tested with opened, but was blank with just the IDE background and no cursor and no nodes, for some reason)
2023-08-25 13:19:31 +00:00

158 lines
5.9 KiB
TypeScript

/** @file This script is for watching the whole IDE and spawning the electron process.
*
* It sets up watchers for the client and content, and spawns the electron process with the IDE.
* The spawned electron process can then use its refresh capability to pull the latest changes
* from the watchers.
*
* If the electron app is closed, the script will restart it, allowing to test the IDE setup.
* To stop, use Ctrl+C. */
import * as childProcess from 'node:child_process'
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import * as url from 'node:url'
import process from 'node:process'
import * as esbuild from 'esbuild'
import * as clientBundler from './esbuild-config'
import * as contentBundler from '../content/esbuild-config'
import * as dashboardBundler from '../dashboard/esbuild-config'
import * as paths from './paths'
// =============
// === Types ===
// =============
/** Set of esbuild watches for the client and content. */
interface Watches {
client: esbuild.BuildResult
dashboard: esbuild.BuildResult
content: esbuild.BuildResult
}
// =================
// === Constants ===
// =================
/** The path of this file. */
const THIS_PATH = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)))
const IDE_DIR_PATH = paths.getIdeDirectory()
const PROJECT_MANAGER_BUNDLE_PATH = paths.getProjectManagerBundlePath()
// =============
// === Watch ===
// =============
console.log('Cleaning IDE dist directory.')
await fs.rm(IDE_DIR_PATH, { recursive: true, force: true })
await fs.mkdir(IDE_DIR_PATH, { recursive: true })
const ALL_BUNDLES_READY = new Promise<Watches>((resolve, reject) => {
void (async () => {
console.log('Bundling client.')
const clientBundlerOpts = clientBundler.bundlerOptionsFromEnv()
clientBundlerOpts.outdir = path.resolve(IDE_DIR_PATH)
// Eslint is wrong here; `clientBundlerOpts.plugins` is actually `undefined`.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
;(clientBundlerOpts.plugins ??= []).push({
name: 'enso-on-rebuild',
setup: build => {
build.onEnd(result => {
if (result.errors.length) {
// We cannot carry on if the client failed to build, because electron
// would immediately exit with an error.
console.error('Client watch bundle failed:', result.errors[0])
reject(result.errors[0])
} else {
console.log('Client bundle updated.')
}
})
},
})
const clientBuilder = await esbuild.context(clientBundlerOpts)
const client = await clientBuilder.rebuild()
console.log('Result of client bundling: ', client)
void clientBuilder.watch()
console.log('Bundling dashboard.')
const dashboardOpts = dashboardBundler.bundleOptions()
dashboardOpts.plugins.push({
name: 'enso-on-rebuild',
setup: build => {
build.onEnd(() => {
console.log('Dashboard bundle updated.')
})
},
})
dashboardOpts.outdir = path.resolve(IDE_DIR_PATH, 'assets')
const dashboardBuilder = await esbuild.context(dashboardOpts)
const dashboard = await dashboardBuilder.rebuild()
console.log('Result of dashboard bundling: ', dashboard)
// We do not need to serve the dashboard as it outputs to the same directory.
// It will not rebuild on request, but it is not intended to rebuild on request anyway.
// This MUST be called before `builder.watch()` as `tailwind.css` must be generated
// before the copy plugin runs.
void dashboardBuilder.watch()
console.log('Bundling content.')
const contentOpts = contentBundler.bundlerOptionsFromEnv({
devMode: process.env.DEV_MODE !== 'false',
supportsLocalBackend: true,
supportsDeepLinks: false,
})
contentOpts.plugins.push({
name: 'enso-on-rebuild',
setup: build => {
build.onEnd(() => {
console.log('Content bundle updated.')
})
},
})
contentOpts.pure.splice(contentOpts.pure.indexOf('assert'), 1)
;(contentOpts.inject = contentOpts.inject ?? []).push(
path.resolve(THIS_PATH, '..', '..', 'debugGlobals.ts')
)
contentOpts.outdir = path.resolve(IDE_DIR_PATH, 'assets')
contentOpts.define.REDIRECT_OVERRIDE = JSON.stringify('http://localhost:8080')
const contentBuilder = await esbuild.context(contentOpts)
const content = await contentBuilder.rebuild()
console.log('Result of content bundling: ', content)
void contentBuilder.watch()
resolve({ client, dashboard, content })
})()
})
await ALL_BUNDLES_READY
console.log('Exposing Project Manager bundle.')
await fs.symlink(
PROJECT_MANAGER_BUNDLE_PATH,
path.join(IDE_DIR_PATH, paths.PROJECT_MANAGER_BUNDLE),
'dir'
)
const ELECTRON_ARGS = [path.join(IDE_DIR_PATH, 'index.cjs'), '--', ...process.argv.slice(2)]
process.on('SIGINT', () => {
console.log('SIGINT received. Exiting.')
// The `esbuild` process seems to remain alive at this point and will keep our process
// from ending. Thus, we exit manually. It seems to terminate the child `esbuild` process
// as well.
process.exit(0)
})
while (true) {
console.log('Spawning Electron process.')
const electronProcess = childProcess.spawn('electron', ELECTRON_ARGS, {
stdio: 'inherit',
shell: true,
})
console.log('Waiting for Electron process to finish.')
const result = await new Promise((resolve, reject) => {
electronProcess.on('close', resolve)
electronProcess.on('error', reject)
})
console.log('Electron process finished. Exit code: ', result)
}