Remove ensogl-pack (#9407)

This PR removes enso-pack (ensogl-pack) crate.
It still keeps the `enso-runner` JS package, as it is used for CLI argument parser and logger. The runner should be probably refactored (and possible removed altogether).

# Important Notes
I've temporarily extracted the `enso-runner` to `lib/js` directory, as I wanted to avoid keeping pure JS library under `lib/rust`. Attempts at integrating this with `app/ide-desktop` and family caused too much trouble for this PR. The expectation is that the package will be removed or moved elsewhere soon anyway.
This commit is contained in:
Michał Wawrzyniec Urbańczyk 2024-03-18 13:18:18 +01:00 committed by GitHub
parent 703cafa6d9
commit de9f2764f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 288 additions and 2800 deletions

27
Cargo.lock generated
View File

@ -1199,7 +1199,6 @@ dependencies = [
"enso-build-macros-lib",
"enso-enso-font",
"enso-font",
"enso-pack",
"futures",
"futures-util",
"glob",
@ -1414,20 +1413,6 @@ dependencies = [
"serde",
]
[[package]]
name = "enso-pack"
version = "0.1.0"
dependencies = [
"enso-prelude",
"futures",
"ide-ci",
"manifest-dir-macros",
"serde",
"serde_json",
"tokio",
"walkdir",
]
[[package]]
name = "enso-parser"
version = "0.1.0"
@ -2513,18 +2498,6 @@ dependencies = [
"tokio-stream",
]
[[package]]
name = "manifest-dir-macros"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f08150cf2bab1fc47c2196f4f41173a27fcd0f684165e5458c0046b53a472e2f"
dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 1.0.107",
]
[[package]]
name = "matchers"
version = "0.1.0"

View File

@ -21,7 +21,6 @@ members = [
"lib/rust/parser/generate-java",
"lib/rust/parser/schema",
"lib/rust/parser/debug",
"lib/rust/enso-pack",
"tools/language-server/logstat",
"tools/language-server/wstest",
]

View File

@ -41,6 +41,8 @@
"esbuild": "^0.19.3",
"fast-glob": "^3.2.12",
"portfinder": "^1.0.32",
"sharp": "^0.31.2",
"to-ico": "^1.1.5",
"tsx": "^4.7.1"
},
"optionalDependencies": {
@ -53,7 +55,6 @@
"typecheck": "npm run --workspace=enso-gui2 compile-server && tsc --build",
"start": "tsx start.ts",
"build": "tsx bundle.ts",
"dist": "tsx dist.ts",
"watch": "tsx watch.ts"
"dist": "tsx dist.ts"
}
}

View File

@ -1,150 +0,0 @@
/** @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 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 {
readonly client: esbuild.BuildResult
readonly dashboard: esbuild.BuildResult
readonly content: esbuild.BuildResult
}
// =================
// === Constants ===
// =================
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({
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.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)
}

View File

@ -1,16 +0,0 @@
/** @file Entry point for the bundler. */
import * as esbuild from 'esbuild'
import * as bundler from './esbuild-config'
// =======================
// === Generate bundle ===
// =======================
try {
const options = bundler.bundleOptions({ supportsLocalBackend: true, supportsDeepLinks: true })
void esbuild.build(options)
} catch (error) {
console.error(error)
throw error
}

View File

@ -1,198 +0,0 @@
/** @file Configuration for the esbuild bundler and build/watch commands.
*
* The bundler processes each entry point into a single file, each with no external dependencies and
* minified. This primarily involves resolving all imports, along with some other transformations
* (like TypeScript compilation).
*
* See the bundlers documentation for more information:
* https://esbuild.github.io/getting-started/#bundling-for-node. */
import * as childProcess from 'node:child_process'
import * as fs from 'node:fs/promises'
import * as fsSync from 'node:fs'
import * as pathModule from 'node:path'
import * as url from 'node:url'
import type * as esbuild from 'esbuild'
import * as esbuildPluginNodeGlobals from '@esbuild-plugins/node-globals-polyfill'
import * as esbuildPluginNodeModules from '@esbuild-plugins/node-modules-polyfill'
import esbuildPluginCopyDirectories from 'esbuild-plugin-copy-directories'
import esbuildPluginTime from 'esbuild-plugin-time'
import esbuildPluginYaml from 'esbuild-plugin-yaml'
import * as appConfig from 'enso-common/src/appConfig'
import * as buildUtils from 'enso-common/src/buildUtils'
import BUILD_INFO from '../../../../build.json' assert { type: 'json' }
// =================
// === Constants ===
// =================
const THIS_PATH = pathModule.resolve(pathModule.dirname(url.fileURLToPath(import.meta.url)))
// ====================
// === Global setup ===
// ====================
await appConfig.readEnvironmentFromFile()
// =============================
// === Environment variables ===
// =============================
/** Arguments that must always be supplied, because they are not defined as
* environment variables. */
export interface PassthroughArguments {
/** Whether the application may have the local backend running. */
readonly supportsLocalBackend: boolean
/** Whether the application supports deep links. This is only true when using
* the installed app on macOS and Windows. */
readonly supportsDeepLinks: boolean
}
/** Mandatory build options. */
export interface Arguments extends PassthroughArguments {
/** List of files to be copied from WASM artifacts. */
readonly wasmArtifacts: string
/** Directory with assets. Its contents are to be copied. */
readonly assetsPath: string
/** Path where bundled files are output. */
readonly outputPath: string
}
/** Get arguments from the environment. */
export function argumentsFromEnv(passthroughArguments: PassthroughArguments): Arguments {
const wasmArtifacts = buildUtils.requireEnv('ENSO_BUILD_GUI_WASM_ARTIFACTS')
const assetsPath = buildUtils.requireEnv('ENSO_BUILD_GUI_ASSETS')
const outputPath = pathModule.resolve(buildUtils.requireEnv('ENSO_BUILD_GUI'), 'assets')
return { ...passthroughArguments, wasmArtifacts, assetsPath, outputPath }
}
// ===================
// === Git process ===
// ===================
/** Get output of a git command.
* @param command - Command line following the `git` program.
* @returns Output of the command. */
function git(command: string): string {
// TODO [mwu] Eventually this should be removed, data should be provided by the build script
// through `BUILD_INFO`. The bundler configuration should not invoke git,
// it is not its responsibility.
return childProcess.execSync(`git ${command}`, { encoding: 'utf8' }).trim()
}
// ================
// === Bundling ===
// ================
/** Generate the builder options. */
export function bundlerOptions(args: Arguments) {
const { outputPath, wasmArtifacts, assetsPath, supportsLocalBackend, supportsDeepLinks } = args
const buildOptions = {
// The names come from a third-party API and cannot be changed.
/* eslint-disable @typescript-eslint/naming-convention */
absWorkingDir: THIS_PATH,
bundle: true,
loader: {
'.html': 'copy',
'.css': 'copy',
'.map': 'copy',
'.wasm': 'copy',
'.svg': 'dataurl',
'.png': 'file',
'.jpg': 'file',
'.ttf': 'copy',
},
entryPoints: [
pathModule.resolve(THIS_PATH, 'src', 'entrypoint.ts'),
pathModule.resolve(THIS_PATH, 'src', 'index.html'),
pathModule.resolve(THIS_PATH, 'src', 'run.js'),
pathModule.resolve(THIS_PATH, 'src', 'style.css'),
pathModule.resolve(THIS_PATH, 'src', 'serviceWorker.ts'),
...wasmArtifacts.split(pathModule.delimiter),
...fsSync
.readdirSync(assetsPath)
.map(fileName => pathModule.resolve(assetsPath, fileName)),
].map(path => ({ in: path, out: pathModule.basename(path, pathModule.extname(path)) })),
outdir: outputPath,
outbase: 'src',
plugins: [
{
name: 'override-loaders',
setup: build => {
// This file MUST be in CommonJS format because it is loaded using `Function()`
// in `ensogl/pack/js/src/runner/index.ts`.
// All other files are ESM because of `"type": "module"` in `package.json`.
build.onLoad({ filter: /[/\\]pkg\.js$/ }, async info => {
const { path } = info
return {
contents: await fs.readFile(path),
loader: 'copy',
}
})
// `.png` and `.svg` files not in the `assets` module should not use the `file`
// loader.
build.onLoad({ filter: /(?:\.png|\.svg)$/ }, async info => {
const { path } = info
if (!/[/\\]assets[/\\][^/\\]*(?:\.png|\.svg)$/.test(path)) {
return {
contents: await fs.readFile(path),
loader: 'copy',
}
} else {
return
}
})
},
},
esbuildPluginCopyDirectories(),
esbuildPluginYaml.yamlPlugin({}),
esbuildPluginNodeModules.NodeModulesPolyfillPlugin(),
esbuildPluginNodeGlobals.NodeGlobalsPolyfillPlugin({ buffer: true, process: true }),
esbuildPluginTime(),
],
define: {
GIT_HASH: JSON.stringify(git('rev-parse HEAD')),
GIT_STATUS: JSON.stringify(git('status --short --porcelain')),
BUILD_INFO: JSON.stringify(BUILD_INFO),
SUPPORTS_LOCAL_BACKEND: JSON.stringify(supportsLocalBackend),
SUPPORTS_DEEP_LINKS: JSON.stringify(supportsDeepLinks),
...appConfig.getDefines(),
},
pure: ['assert'],
sourcemap: true,
metafile: true,
format: 'esm',
platform: 'browser',
color: true,
logOverride: {
// Happens in Emscripten-generated MSDF (msdfgen_wasm.js):
// 1 │ ...typeof module!=="undefined"){module["exports"]=Module}process["o...
'commonjs-variable-in-esm': 'silent',
// Happens in Emscripten-generated MSDF (msdfgen_wasm.js):
// 1 │ ...y{table.grow(1)}catch(err){if(!err instanceof RangeError){throw ...
'suspicious-boolean-not': 'silent',
},
/* eslint-enable @typescript-eslint/naming-convention */
} satisfies esbuild.BuildOptions
// The narrower type is required to avoid non-null assertions elsewhere.
// The intersection with `esbuild.BuildOptions` is required to allow mutation.
const correctlyTypedBuildOptions: esbuild.BuildOptions & typeof buildOptions = buildOptions
return correctlyTypedBuildOptions
}
/** The basic, common settings for the bundler, based on the environment variables.
*
* Note that they should be further customized as per the needs of the specific workflow
* (e.g. watch vs. build). */
export function bundlerOptionsFromEnv(passthroughArguments: PassthroughArguments) {
return bundlerOptions(argumentsFromEnv(passthroughArguments))
}
/** esbuild options for bundling the package for a one-off build.
*
* Relies on the environment variables to be set. */
export function bundleOptions(passthroughArguments: PassthroughArguments) {
return bundlerOptionsFromEnv(passthroughArguments)
}

View File

@ -1,19 +0,0 @@
/** @file Globals defined only in this module. */
// =====================================
// === Global namespace augmentation ===
// =====================================
declare global {
// These are top-level constants, and therefore should be `CONSTANT_CASE`.
/* eslint-disable @typescript-eslint/naming-convention */
/** Whether the */
/** Whether the application may have the local backend running. */
const SUPPORTS_LOCAL_BACKEND: boolean
/** Whether the application supports deep links. This is only true when using
* the installed app on macOS and Windows. */
const SUPPORTS_DEEP_LINKS: boolean
/* eslint-enable @typescript-eslint/naming-convention */
}
export {}

View File

@ -1,57 +0,0 @@
{
"name": "enso-content",
"version": "1.0.0",
"type": "module",
"author": {
"name": "Enso Team",
"email": "contact@enso.org"
},
"homepage": "https://github.com/enso-org/ide",
"repository": {
"type": "git",
"url": "git@github.com:enso-org/ide.git"
},
"bugs": {
"url": "https://github.com/enso-org/ide/issues"
},
"scripts": {
"typecheck": "tsc --build",
"build": "tsx bundle.ts",
"watch": "tsx watch.ts",
"start": "tsx start.ts"
},
"dependencies": {
"@types/semver": "^7.3.9",
"enso-content-config": "^1.0.0",
"react-toastify": "^9.1.3"
},
"devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@eslint/js": "^8.49.0",
"@types/connect": "^3.4.35",
"@types/morgan": "^1.9.4",
"@types/serve-static": "^1.15.1",
"@types/sharp": "^0.31.1",
"@types/to-ico": "^1.1.1",
"@types/ws": "^8.5.4",
"@typescript-eslint/eslint-plugin": "^6.7.2",
"@typescript-eslint/parser": "^6.7.2",
"enso-dashboard": "^0.1.0",
"esbuild": "^0.19.3",
"esbuild-plugin-copy-directories": "^1.0.0",
"esbuild-plugin-time": "^1.0.0",
"esbuild-plugin-yaml": "^0.0.1",
"eslint": "^8.49.0",
"eslint-plugin-jsdoc": "^46.8.1",
"globals": "^13.20.0",
"portfinder": "^1.0.32",
"tsx": "^4.7.1",
"typescript": "~5.2.2"
},
"optionalDependencies": {
"@esbuild/darwin-x64": "^0.17.15",
"@esbuild/linux-x64": "^0.17.15",
"@esbuild/windows-x64": "^0.17.15"
}
}

View File

@ -1,64 +0,0 @@
/** @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.
/// <reference lib="WebWorker" />
import * as common from 'enso-common'
import * as constants from './serviceWorkerConstants'
// =====================
// === 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('install', event => {
event.waitUntil(
caches.open(constants.CACHE_NAME).then(cache => {
void cache.addAll(constants.DEPENDENCIES)
return
})
)
})
self.addEventListener('fetch', event => {
const url = new URL(event.request.url)
if (
(url.hostname === 'localhost' || url.hostname === '127.0.0.1') &&
url.pathname === '/esbuild'
) {
return false
} else if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
const responsePromise = caches
.open(constants.CACHE_NAME)
.then(cache => cache.match(event.request))
.then(
response =>
response ??
(/\/[^.]+$/.test(url.pathname)
? 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 {
event.respondWith(
caches
.open(constants.CACHE_NAME)
.then(cache => cache.match(event.request))
.then(response => response ?? fetch(event.request))
)
return
}
})

View File

@ -1,348 +0,0 @@
/** @file This module is responsible for loading the WASM binary, its dependencies, and providing
* the user with a visual representation of this process (welcome screen). It also implements a view
* allowing to choose a debug rendering test from. */
import * as semver from 'semver'
import * as toastify from 'react-toastify'
import * as app from 'enso-runner/src/runner'
import * as common from 'enso-common'
import * as contentConfig from 'enso-content-config'
import * as dashboard from 'enso-dashboard'
import * as detect from 'enso-common/src/detect'
import * as gtag from 'enso-common/src/gtag'
import * as remoteLog from './remoteLog'
import GLOBAL_CONFIG from '../../../../gui2/config.yaml' assert { type: 'yaml' }
const logger = app.log.logger
// =================
// === Constants ===
// =================
/** The name of the `localStorage` key storing the initial URL of the app. */
const INITIAL_URL_KEY = `${common.PRODUCT_NAME.toLowerCase()}-initial-url`
/** Path to the SSE endpoint over which esbuild sends events. */
const ESBUILD_PATH = './esbuild'
/** SSE event indicating a build has finished. */
const ESBUILD_EVENT_NAME = 'change'
/** Path to the serice worker that caches assets for offline usage.
* In development, it also resolves all extensionless paths to `/index.html`.
* This is required for client-side routing to work when doing `./run gui watch`.
*/
const SERVICE_WORKER_PATH = './serviceWorker.js'
/** One second in milliseconds. */
const SECOND = 1000
/** Time in seconds after which a `fetchTimeout` ends. */
const FETCH_TIMEOUT = 300
// ===================
// === Live reload ===
// ===================
if (detect.IS_DEV_MODE && !detect.isOnElectron()) {
new EventSource(ESBUILD_PATH).addEventListener(ESBUILD_EVENT_NAME, () => {
// This acts like `location.reload`, but it preserves the query-string.
// The `toString()` is to bypass a lint without using a comment.
location.href = location.href.toString()
})
}
void (async () => {
// `navigator.serviceWorker` may be disabled in certain situations, for example in Private mode
// on Safari.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const registration = await navigator.serviceWorker?.getRegistration()
await registration?.unregister()
await navigator.serviceWorker.register(SERVICE_WORKER_PATH)
})()
// =============
// === Fetch ===
// =============
/** Returns an `AbortController` that aborts after the specified number of seconds. */
function timeout(timeSeconds: number) {
const controller = new AbortController()
setTimeout(() => {
controller.abort()
}, timeSeconds * SECOND)
return controller
}
/** A version of `fetch` which times out after the provided time. */
async function fetchTimeout(url: string, timeoutSeconds: number): Promise<unknown> {
return fetch(url, { signal: timeout(timeoutSeconds).signal }).then(response => {
const statusCodeOK = 200
if (response.status === statusCodeOK) {
return response.json()
} else {
throw new Error(`Failed to fetch '${url}'. Response status: ${response.status}.`)
}
})
}
/** Return `true` if the current application version is still supported and `false` otherwise.
*
* Function downloads the application config containing the minimum supported version from GitHub
* and compares it with the version of the `client` js package. When the function is unable to
* download the application config, or one of the compared versions does not match the semver
* scheme, it returns `true`. */
async function checkMinSupportedVersion(config: typeof contentConfig.OPTIONS) {
let supported = false
if (config.groups.engine.options.skipMinVersionCheck.value) {
supported = true
} else {
try {
const appConfig = await fetchTimeout(
config.groups.engine.options.configUrl.value,
FETCH_TIMEOUT
)
if (
typeof appConfig === 'object' &&
appConfig != null &&
'minimumSupportedVersion' in appConfig
) {
const minSupportedVersion = appConfig.minimumSupportedVersion
if (typeof minSupportedVersion !== 'string') {
logger.error('The minimum supported version is not a string.')
} else {
const comparator = new semver.Comparator(`>=${minSupportedVersion}`)
supported = comparator.test(contentConfig.VERSION.ide)
}
} else {
logger.error('The application config is not an object.')
}
} catch (e) {
console.error('Minimum version check failed.', e)
supported = true
}
}
return supported
}
/** Display information that the current app version is deprecated. */
function displayDeprecatedVersionDialog() {
const versionCheckText = document.createTextNode(
'This version is no longer supported. Please download a new one.'
)
const root = document.getElementById('root')
const versionCheckDiv = document.createElement('div')
versionCheckDiv.id = 'version-check'
versionCheckDiv.className = 'auth-info'
versionCheckDiv.style.display = 'block'
versionCheckDiv.appendChild(versionCheckText)
if (root == null) {
console.error('Cannot find the root DOM element.')
} else {
root.appendChild(versionCheckDiv)
}
}
// ========================
// === Main entry point ===
// ========================
/** Nested configuration options with `string` values. */
export interface StringConfig {
[key: string]: StringConfig | string
}
/** Configuration options for the authentication flow and dashboard. */
interface AuthenticationConfig {
readonly supportsVibrancy: boolean
readonly projectManagerUrl: string | null
readonly isInAuthenticationFlow: boolean
readonly shouldUseAuthentication: boolean
readonly initialProjectName: string | null
}
/** Contains the entrypoint into the IDE. */
class Main implements AppRunner {
app: app.App | null = null
toast = toastify.toast
/** Stop an app instance, if one is running. */
stopApp() {
this.app?.stop()
}
/** Run an app instance with the specified configuration.
* This includes the scene to run and the WebSocket endpoints to the backend. */
async runApp(
inputConfig: StringConfig | null,
accessToken: string | null,
loggingMetadata?: object
) {
this.stopApp()
/** FIXME: https://github.com/enso-org/enso/issues/6475
* Default values names are out of sync with values used in code.
* Rather than setting fixed values here we need to fix default values in config. */
const config = Object.assign(
{
loader: {
wasmUrl: 'pkg-opt.wasm',
},
},
inputConfig
)
const configOptions = contentConfig.OPTIONS.clone()
const newApp = new app.App({
config,
configOptions,
packageInfo: {
version: BUILD_INFO.version,
engineVersion: BUILD_INFO.engineVersion,
},
})
// We override the remote logger stub with the "real" one. Eventually the runner should not
// be aware of the remote logger at all, and it should be integrated with our logging infrastructure.
const remoteLogger = accessToken != null ? new remoteLog.RemoteLogger(accessToken) : null
newApp.remoteLog = (message: string, metadata: unknown) => {
const metadataObject =
typeof metadata === 'object' && metadata != null ? metadata : { metadata }
const actualMetadata =
loggingMetadata == null ? metadata : { ...loggingMetadata, ...metadataObject }
if (newApp.config.options.dataCollection.value && remoteLogger != null) {
// FIXME [sb]: https://github.com/enso-org/cloud-v2/issues/735
// The current GUI sends a lot of logs (over 300) every time a project is opened.
// This severely degrades performance, and the logs generated do not appear to be
// useful. This should be re-enabled when the GUI no longer sends a large amount
// of logs.
// await remoteLogger.remoteLog(message, actualMetadata)
} else {
const logMessage = [
'Not sending log to remote server. Data collection is disabled.',
`Message: "${message}"`,
`Metadata: ${JSON.stringify(actualMetadata)}`,
].join(' ')
logger.log(logMessage)
}
return Promise.resolve()
}
this.app = newApp
if (!this.app.initialized) {
console.error('Failed to initialize the application.')
} else {
if (!(await checkMinSupportedVersion(configOptions))) {
displayDeprecatedVersionDialog()
} else {
const email = configOptions.groups.authentication.options.email.value
// The default value is `""`, so a truthiness check is most appropriate here.
if (email) {
logger.log(`User identified as '${email}'.`)
}
void this.app.run()
}
}
}
/** The entrypoint into the IDE. */
main(inputConfig?: StringConfig) {
/** Note: Signing out always redirects to `/`. It is impossible to make this work,
* as it is not possible to distinguish between having just logged out, and explicitly
* opening a page with no URL parameters set.
*
* Client-side routing endpoints are explicitly not supported for live-reload, as they are
* transitional pages that should not need live-reload when running `gui watch`. */
const url = new URL(location.href)
const isInAuthenticationFlow = url.searchParams.has('code') && url.searchParams.has('state')
const authenticationUrl = location.href
if (isInAuthenticationFlow) {
gtag.gtag('event', 'cloud_sign_in_redirect')
history.replaceState(null, '', localStorage.getItem(INITIAL_URL_KEY))
}
const configOptions = contentConfig.OPTIONS.clone()
const parseOk = configOptions.loadAllAndDisplayHelpIfUnsuccessful([app.urlParams()])
if (isInAuthenticationFlow) {
history.replaceState(null, '', authenticationUrl)
} else {
localStorage.setItem(INITIAL_URL_KEY, location.href)
}
if (parseOk) {
const supportsVibrancy = configOptions.groups.window.options.vibrancy.value
const shouldUseAuthentication = configOptions.options.authentication.value
const isOpeningMainEntryPoint =
configOptions.groups.startup.options.entry.value ===
configOptions.groups.startup.options.entry.default
const initialProjectName = configOptions.groups.startup.options.project.value || null
// This does not need to be removed from the URL, but only because local projects
// also use the Project Manager URL, and remote (cloud) projects remove the URL
// completely.
const projectManagerUrl =
configOptions.groups.engine.options.projectManagerUrl.value || null
// This MUST be removed as it would otherwise override the `startup.project` passed
// explicitly in `ide.tsx`.
if (isOpeningMainEntryPoint && url.searchParams.has('startup.project')) {
url.searchParams.delete('startup.project')
history.replaceState(null, '', url.toString())
}
if (shouldUseAuthentication && isOpeningMainEntryPoint) {
this.runAuthentication({
supportsVibrancy,
isInAuthenticationFlow,
projectManagerUrl,
shouldUseAuthentication,
initialProjectName,
})
} else {
void this.runApp(inputConfig ?? null, null)
}
}
}
/** Begins the authentication UI flow. */
runAuthentication(config: AuthenticationConfig) {
const ideElement = document.getElementById('root')
if (ideElement) {
ideElement.style.top = '-100vh'
ideElement.style.position = 'fixed'
}
const ide2Element = document.getElementById('app')
if (ide2Element) {
ide2Element.style.display = 'none'
}
/** TODO [NP]: https://github.com/enso-org/cloud-v2/issues/345
* `content` and `dashboard` packages **MUST BE MERGED INTO ONE**. The IDE
* should only have one entry point. Right now, we have two. One for the cloud
* and one for the desktop. */
/** FIXME [PB]: https://github.com/enso-org/cloud-v2/issues/366
* React hooks rerender themselves multiple times. It is resulting in multiple
* Enso main scene being initialized. As a temporary workaround we check whether
* appInstance was already ran. Target solution should move running appInstance
* where it will be called only once. */
dashboard.run({
appRunner: this,
logger,
vibrancy: config.supportsVibrancy,
supportsLocalBackend: SUPPORTS_LOCAL_BACKEND,
supportsDeepLinks: SUPPORTS_DEEP_LINKS,
projectManagerUrl: config.projectManagerUrl,
isAuthenticationDisabled: !config.shouldUseAuthentication,
shouldShowDashboard: true,
initialProjectName: config.initialProjectName,
onAuthenticated: () => {
if (config.isInAuthenticationFlow) {
const initialUrl = localStorage.getItem(INITIAL_URL_KEY)
if (initialUrl != null) {
// This is not used past this point, however it is set to the initial URL
// to make refreshing work as expected.
history.replaceState(null, '', initialUrl)
}
}
},
})
}
}
// @ts-expect-error `globalConfig.windowAppScopeName` is not known at typecheck time.
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
window[GLOBAL_CONFIG.windowAppScopeName] = new Main()

View File

@ -1,62 +0,0 @@
<!--
FIXME [NP]: https://github.com/enso-org/cloud-v2/issues/345
This file is used by both the `content` and `dashboard` packages. The `dashboard` package uses it
via a symlink. This is temporary, while the `content` and `dashboard` have separate entrypoints
for cloud and desktop. Once they are merged, the symlink must be removed.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<!-- FIXME https://github.com/validator/validator/issues/917 -->
<!-- FIXME Security Vulnerabilities: https://github.com/enso-org/ide/issues/226 -->
<!-- NOTE `frame-src` section of `http-equiv` required only for authorization -->
<!-- NOTE [NP]: https://stripe.com/docs/security/guide#content-security-policy for Stripe.js -->
<meta
http-equiv="Content-Security-Policy"
content="
default-src 'self';
frame-src 'self' data: https://js.stripe.com;
script-src 'self' 'unsafe-eval' data: https://*;
style-src 'self' 'unsafe-inline' data: https://*;
connect-src 'self' data: ws://localhost:* ws://127.0.0.1:* http://localhost:* https://* wss://*;
worker-src 'self' blob:;
img-src 'self' blob: data: https://*;
font-src 'self' data: https://*"
/>
<meta
name="viewport"
content="
width=device-width,
initial-scale = 1.0,
maximum-scale = 1.0,
user-scalable = no"
/>
<title>Enso</title>
<link rel="stylesheet" href="./tailwind.css" />
<link rel="stylesheet" href="./style.css" />
<!-- Generated by the build script based on the Enso Font package. -->
<link rel="stylesheet" href="./ensoFont.css" />
<script type="module" src="./entrypoint.js" defer></script>
<script type="module" src="./run.js" defer></script>
</head>
<body>
<div id="root"></div>
<div id="enso-dashboard" class="enso-dashboard"></div>
<div id="enso-chat" class="enso-chat"></div>
<noscript>
This page requires JavaScript to run. Please enable it in your browser.
</noscript>
<script
src="https://cdn.jsdelivr.net/npm/@twemoji/api@14.1.2/dist/twemoji.min.js"
integrity="sha384-D6GSzpW7fMH86ilu73eB95ipkfeXcMPoOGVst/L04yqSSe+RTUY0jXcuEIZk0wrT"
crossorigin="anonymous"
></script>
<!-- Google tag (gtag.js) -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-CLTBJ37MDM"
></script>
</body>
</html>

View File

@ -1,77 +0,0 @@
/** @file Defines the {@link RemoteLogger} class and {@link remoteLog} function for sending logs to a remote server.
* {@link RemoteLogger} provides a convenient way to manage remote logging with access token authorization. */
import * as app from 'enso-runner/src/runner'
const logger = app.log.logger
// =================
// === Constants ===
// =================
/** URL address where remote logs should be sent. */
const REMOTE_LOG_URL =
process.env.ENSO_CLOUD_API_URL == null
? null
: new URL(`${process.env.ENSO_CLOUD_API_URL}/logs`)
// ====================
// === RemoteLogger ===
// ====================
// === Class ===
/** Helper class facilitating sending logs to a remote. */
export class RemoteLogger {
/** Initialize a new instance.
* @param accessToken - JWT token used to authenticate within the cloud. */
constructor(public accessToken: string) {
this.accessToken = accessToken
}
/** Sends a log message to a remote.
* @param message - The log message to send.
* @param metadata - Additional metadata to send along with the log.
* @returns Promise which resolves when the log message has been sent. */
async remoteLog(message: string, metadata: unknown): Promise<void> {
await remoteLog(this.accessToken, message, metadata)
}
}
// === Underlying logic ===
/** Sends a log message to a remote server using the provided access token.
* @param accessToken - The access token for authentication.
* @param message - The message to be logged on the server.
* @param metadata - Additional metadata to include in the log.
* @throws Will throw an error if the response from the server is not okay (response status is not 200).
* @returns Returns a promise that resolves when the log message is successfully sent. */
export async function remoteLog(
accessToken: string,
message: string,
metadata: unknown
): Promise<void> {
if (REMOTE_LOG_URL != null) {
try {
const headers: HeadersInit = [
['Content-Type', 'application/json'],
['Authorization', `Bearer ${accessToken}`],
]
const body = JSON.stringify({ message, metadata })
const response = await fetch(REMOTE_LOG_URL, { method: 'POST', headers, body })
if (response.ok) {
return
} else {
const errorMessage = `Error while sending log to a remote: Status ${response.status}.`
const text = await response.text().catch(error => {
throw new Error(`${errorMessage} Failed to read response: ${String(error)}.`)
})
throw new Error(`${errorMessage} Response: ${text}.`)
}
} catch (error) {
logger.error(error)
// eslint-disable-next-line no-restricted-syntax
throw error
}
}
}

View File

@ -1,10 +0,0 @@
/** @file This file is used to simply run the IDE. It can be not invoked if the IDE needs to be used
* as a library. */
// ===============
// === Run IDE ===
// ===============
// This `void` is used to explicitly not `await` a promise, not to produce an `undefined`.
// eslint-disable-next-line no-restricted-syntax
void window.enso?.main()

View File

@ -1,38 +0,0 @@
/** @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.
/// <reference lib="WebWorker" />
import * as constants from './serviceWorkerConstants'
// =====================
// === 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('install', event => {
event.waitUntil(
caches.open(constants.CACHE_NAME).then(cache => {
void cache.addAll(constants.DEPENDENCIES)
return
})
)
})
self.addEventListener('fetch', event => {
const url = new URL(event.request.url)
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
return false
} else {
event.respondWith(
caches
.open(constants.CACHE_NAME)
.then(cache => cache.match(event.request))
.then(response => response ?? fetch(event.request))
)
return
}
})

View File

@ -1,59 +0,0 @@
/** @file Constants shared between all service workers (development and production). */
import * as common from 'enso-common'
// =================
// === Constants ===
// =================
/** The name of the cache under which offline assets are stored. */
export const CACHE_NAME = common.PRODUCT_NAME.toLowerCase()
/** The numbers after each font loaded by the "M PLUS 1" font. */
const M_PLUS_1_SECTIONS = [
/* eslint-disable @typescript-eslint/no-magic-numbers */
0, 1, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 53,
54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77,
78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100,
101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119,
/* eslint-enable @typescript-eslint/no-magic-numbers */
]
/** The complete list of assets to cache for offline use. */
export const DEPENDENCIES = [
// app/gui/view/graph-editor/src/builtin/visualization/java_script/heatmap.js
// app/gui/view/graph-editor/src/builtin/visualization/java_script/histogram.js
// app/gui/view/graph-editor/src/builtin/visualization/java_script/scatterPlot.js
'https://d3js.org/d3.v4.min.js',
'https://fonts.cdnfonts.com/css/dejavu-sans-mono',
// Loaded by https://fonts.cdnfonts.com/css/dejavu-sans-mono
'https://fonts.cdnfonts.com/s/108/DejaVuSansMono.woff',
'https://fonts.cdnfonts.com/s/108/DejaVuSansMono-Oblique.woff',
'https://fonts.cdnfonts.com/s/108/DejaVuSansMono-Bold.woff',
'https://fonts.cdnfonts.com/s/108/DejaVuSansMono-BoldOblique.woff',
// app/gui/view/graph-editor/src/builtin/visualization/java_script/geoMap.js
'https://unpkg.com/deck.gl@8.4/dist.min.js',
'https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.js',
'https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.css',
// Loaded by https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.js
'https://api.mapbox.com/styles/v1/mapbox/light-v9?access_token=pk.' +
'eyJ1IjoiZW5zby1vcmciLCJhIjoiY2tmNnh5MXh2MGlyOTJ5cWdubnFxbXo4ZSJ9.3KdAcCiiXJcSM18nwk09-Q',
'https://api.mapbox.com/styles/v1/mapbox/light-v9/sprite.json?access_token=pk.' +
'eyJ1IjoiZW5zby1vcmciLCJhIjoiY2tmNnh5MXh2MGlyOTJ5cWdubnFxbXo4ZSJ9.3KdAcCiiXJcSM18nwk09-Q',
'https://api.mapbox.com/styles/v1/mapbox/light-v9/sprite.png?access_token=pk.' +
'eyJ1IjoiZW5zby1vcmciLCJhIjoiY2tmNnh5MXh2MGlyOTJ5cWdubnFxbXo4ZSJ9.3KdAcCiiXJcSM18nwk09-Q',
// app/gui/view/graph-editor/src/builtin/visualization/java_script/sql.js
'https://cdnjs.cloudflare.com/ajax/libs/sql-formatter/4.0.2/sql-formatter.min.js',
// app/gui/view/graph-editor/src/builtin/visualization/java_script/table.js
'https://cdn.jsdelivr.net/npm/ag-grid-community/dist/ag-grid-community.min.js',
'https://cdn.jsdelivr.net/npm/ag-grid-community/styles/ag-grid.css',
'https://cdn.jsdelivr.net/npm/ag-grid-community/styles/ag-theme-alpine.css',
// app/ide-desktop/lib/dashboard/src/tailwind.css
'https://fonts.googleapis.com/css2?family=M+PLUS+1:wght@500;700&display=swap',
// Loaded by https://fonts.googleapis.com/css2?family=M+PLUS+1:wght@500;700&display=swap
...M_PLUS_1_SECTIONS.map(
number =>
'https://fonts.gstatic.com/s/mplus1/v6/' +
`R70ZjygA28ymD4HgBVu92j6eR1mYP_TX-Bb-rTg93gHfHe9F4Q.${number}.woff2`
),
]

View File

@ -1,262 +0,0 @@
/* Fonts */
@font-face {
font-family: "M PLUS 1";
src: url("/MPLUS1[wght].ttf") format("truetype");
font-weight: 100;
font-display: block;
}
@font-face {
font-family: "M PLUS 1";
src: url("/MPLUS1[wght].ttf") format("truetype");
font-style: normal;
font-weight: 200;
font-display: block;
}
@font-face {
font-family: "M PLUS 1";
src: url("/MPLUS1[wght].ttf") format("truetype");
font-style: normal;
font-weight: 300;
font-display: block;
}
@font-face {
font-family: "M PLUS 1";
src: url("/MPLUS1[wght].ttf") format("truetype");
font-style: normal;
font-weight: 400;
font-display: block;
}
@font-face {
font-family: "M PLUS 1";
src: url("/MPLUS1[wght].ttf") format("truetype");
font-style: normal;
font-weight: 500;
font-display: block;
}
@font-face {
font-family: "M PLUS 1";
src: url("/MPLUS1[wght].ttf") format("truetype");
font-style: normal;
font-weight: 600;
font-display: block;
}
@font-face {
font-family: "M PLUS 1";
src: url("/MPLUS1[wght].ttf") format("truetype");
font-style: normal;
font-weight: 700;
font-display: block;
}
@font-face {
font-family: "M PLUS 1";
src: url("/MPLUS1[wght].ttf") format("truetype");
font-style: normal;
font-weight: 800;
font-display: block;
}
@font-face {
font-family: "M PLUS 1";
src: url("/MPLUS1[wght].ttf") format("truetype");
font-style: normal;
font-weight: 900;
font-display: block;
}
/* End of fonts */
html,
body {
height: 100vh;
}
body {
margin: 0;
overscroll-behavior: none;
}
#root {
height: 100vh;
width: 100vw;
margin: 0;
position: absolute;
overflow: hidden;
}
.visualization {
z-index: 2;
border-radius: 14px;
}
.auth-header {
font-family: sans-serif;
text-align: center;
margin: 24px auto;
}
.auth-text {
text-align: justify;
font-family: sans-serif;
color: #454545;
width: 50%;
margin: 12px auto;
}
.auth-info {
text-align: center;
font-family: sans-serif;
color: #454545;
margin: 24px auto;
display: none;
}
#crash-banner {
background: DarkSalmon;
color: #2c1007;
font-family: sans-serif;
line-height: 1.5;
position: absolute;
/* Put the banner in front of the "root" node which has index 1 */
z-index: 2;
/* Center the banner horizontally */
left: 0;
right: 0;
margin: auto;
width: fit-content;
padding: 1em;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
#crash-banner button {
border-radius: 4px;
border: none;
font: inherit;
/* Balance padding with negative margin to make the label fit with other text */
padding: 2px;
margin: -2px;
padding-left: 0.5em;
padding-right: 0.5em;
}
#crash-banner button:focus {
/* Show a 2px outline, following the button's shape, instead of the standard
rectangular outline */
outline: none;
box-shadow: 0 0 0 2px #fbeee9;
}
#crash-banner #crash-banner-close-button {
float: right;
margin-left: 0.75em;
color: #2c1007;
background: none;
}
#crash-banner #crash-banner-close-button:hover {
color: #58210e;
}
#crash-banner #crash-banner-close-button:active {
color: #843115;
}
#crash-banner #crash-report-button {
float: right;
margin-left: 1em;
color: DarkSalmon;
background: #2c1007;
}
#crash-banner #crash-report-button:hover {
background-color: #58210e;
}
#crash-banner #crash-report-button:active {
background-color: #843115;
}
#crash-banner-content {
display: inline;
}
#crash-banner hr {
height: 1px;
border: none;
background: #b96a50;
margin: 0.8em -1em;
}
#debug-root {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
margin: 0;
overflow: hidden;
pointer-events: none;
display: none;
}
#debug-enable-checkbox {
position: absolute;
bottom: 0px;
right: 0px;
color: white;
font-size: 12px;
font-family: "M PLUS 1";
padding: 3px 6px;
background: rgba(15, 0, 77, 0.7);
cursor: none;
user-select: none;
}
#debug-enable-checkbox:has(input:checked) + #debug-root {
display: initial;
}
#debug-root > .debug-layer {
position: absolute;
top: 50vh;
left: 50vw;
width: 0px;
height: 0px;
transform-origin: 0 0;
}
#debug-root > .debug-layer * {
position: absolute;
background: rgba(0, 0, 0, 0.05);
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
transform-origin: 0 0;
}
#debug-root > .debug-layer [data-name="Text"] > div {
background: rgba(0, 160, 60, 0.15);
}
#debug-root > .debug-layer [data-name*="compound::rectangle::shape"] {
background: rgba(0, 20, 180, 0.15);
}
#debug-root > .debug-layer .hidden {
display: none;
}
#debug-root > .debug-layer[data-layer-name="DETACHED"] {
display: none;
}

View File

@ -1,11 +0,0 @@
/** @file This module exports wasm Rust glue code generated by EnsoGL Pack. */
import * as wasmRustGlue from 'wasm_rust_glue'
// =============================
// === Export WASM Rust glue ===
// =============================
// Eslint is not (and should not be) set up to check CommonJS.
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
exports.init = wasmRustGlue.default

View File

@ -1,36 +0,0 @@
/** @file Start the file watch service. */
import * as esbuild from 'esbuild'
import * as portfinder from 'portfinder'
import * as bundler from './esbuild-config.js'
// =================
// === Constants ===
// =================
const PORT = 8080
const HTTP_STATUS_OK = 200
// ===============
// === Watcher ===
// ===============
/** Start the esbuild watcher. */
async function watch() {
const options = bundler.bundleOptions({ supportsLocalBackend: true, supportsDeepLinks: false })
const builder = await esbuild.context(options)
await builder.watch()
await builder.serve({
port: await portfinder.getPortPromise({ port: PORT }),
servedir: options.outdir,
/** This function is called on every request.
* It is used here to show an error if the file to serve was not found. */
onRequest(args) {
if (args.status !== HTTP_STATUS_OK) {
console.error(`HTTP error ${args.status} when serving path '${args.path}'.`)
}
},
})
}
void watch()

View File

@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["../types", "../../../../build.json", "."],
"references": [{ "path": "../dashboard" }]
}

View File

@ -1,61 +0,0 @@
/** @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'
import * as bundler from './esbuild-config'
import * as dashboardBundler from '../dashboard/esbuild-config'
// =================
// === Constants ===
// =================
/** The path of this file. */
const THIS_PATH = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)))
const PORT = 8080
const HTTP_STATUS_OK = 200
// ===============
// === Watcher ===
// ===============
/** Starts the esbuild watcher. */
async function watch() {
const dashboardOpts = dashboardBundler.bundleOptions()
const dashboardBuilder = await esbuild.context(dashboardOpts)
// 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.
await dashboardBuilder.watch()
const args = bundler.argumentsFromEnv({ supportsLocalBackend: true, supportsDeepLinks: false })
const options = bundler.bundlerOptions(args)
options.pure.splice(options.pure.indexOf('assert'), 1)
options.define.REDIRECT_OVERRIDE = JSON.stringify('http://localhost:8080')
// This is safe as this entry point is statically known.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const serviceWorkerEntryPoint = options.entryPoints.find(
entryPoint => entryPoint.out === 'serviceWorker'
)!
serviceWorkerEntryPoint.in = path.resolve(THIS_PATH, 'src', 'devServiceWorker.ts')
const builder = await esbuild.context(options)
await builder.watch()
await builder.serve({
port: await portfinder.getPortPromise({ port: PORT }),
servedir: options.outdir,
/** This function is called on every request.
* It is used here to show an error if the file to serve was not found. */
onRequest(request) {
if (request.status !== HTTP_STATUS_OK) {
console.error(
chalk.red(`HTTP error ${request.status} when serving path '${request.path}'.`)
)
}
},
})
}
void watch()

View File

@ -66,6 +66,7 @@
"esbuild": "^0.19.3",
"esbuild-plugin-inline-image": "^0.0.9",
"esbuild-plugin-time": "^1.0.0",
"esbuild-plugin-yaml": "^0.0.1",
"eslint": "^8.49.0",
"eslint-plugin-jsdoc": "^46.8.1",
"eslint-plugin-react": "^7.32.1",

View File

@ -18,7 +18,9 @@
},
"devDependencies": {
"sharp": "^0.31.2",
"to-ico": "^1.1.5"
"to-ico": "^1.1.5",
"@types/sharp": "^0.31.1",
"@types/to-ico": "^1.1.1"
},
"type": "module"
}

View File

@ -25,7 +25,6 @@ heck = "0.4.0"
enso-build-base = { path = "../base" }
enso-enso-font = { path = "../../lib/rust/enso-font" }
enso-font = { path = "../../lib/rust/font" }
enso-pack = { path = "../../lib/rust/enso-pack" }
ide-ci = { path = "../ci_utils" }
mime = "0.3.16"
new_mime_guess = "4.0.1"

View File

@ -1,7 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "enso-pack"
version = "0.1.0"

View File

@ -1,18 +0,0 @@
[package]
name = "enso-pack"
version = "0.1.0"
authors = ["Enso Team <contact@enso.org>"]
edition = "2021"
[lib]
crate-type = ["rlib"]
[dependencies]
futures = { version = "0.3" }
ide-ci = { path = "../../../build/ci_utils" }
manifest-dir-macros = "0.1.16"
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
walkdir = "2"
enso-prelude = { path = "../prelude" }

View File

@ -1,117 +0,0 @@
/** @file A simple argument parser. */
import * as util from 'node:util'
// ==========================
// === Naming Conversions ===
// ==========================
/** Converts a camel case string to a kebab case string. */
function camelToKebabCase(name: string) {
return name
.split('')
.map((letter, idx) => {
return letter.toUpperCase() === letter
? `${idx !== 0 ? '-' : ''}${letter.toLowerCase()}`
: letter
})
.join('')
}
// ==============
// === Option ===
// ==============
class Option<T> {
'default': T | undefined
value: T | undefined
description: string
type: 'string' | 'boolean'
constructor(description: string, def?: T) {
this.default = def
this.description = description
if (def === true || def === false) {
this.type = 'boolean'
} else {
this.type = 'string'
}
}
}
// =================
// === ArgParser ===
// =================
interface ParseArgsOptionConfig {
type: 'string' | 'boolean'
multiple?: boolean | undefined
short?: string | undefined
default?: string | boolean | string[] | boolean[] | undefined
}
export class Args {
[key: string]: Option<string | boolean>
help = new Option('Print help message.', false)
outDir = new Option<string>('The directory the extracted asset sources will be written to.')
}
export class ArgParser {
args = new Args()
parse() {
const optionToFieldNameMap = new Map<string, string>()
const options: Record<string, ParseArgsOptionConfig> = {}
for (const [fieldName, option] of Object.entries(this.args)) {
const optionName = camelToKebabCase(fieldName)
optionToFieldNameMap.set(optionName, fieldName)
options[optionName] = { type: option.type, default: option.default }
}
try {
const out = util.parseArgs({ options })
for (const [optionName, optionValue] of Object.entries(out.values)) {
const fieldName = optionToFieldNameMap.get(optionName)
if (fieldName) {
// @ts-expect-error
this.args[fieldName].value = optionValue
} else {
console.error(`Unknown option: ${optionName}`)
process.exit(1)
}
}
} catch (error) {
const msg = error instanceof Error ? `${error.message}. ` : ''
console.error(`${msg}Use --help to learn about possible options.`)
process.exit(1)
}
if (this.args.help.value) {
this.printHelpAndExit(0)
}
}
printHelp() {
console.log(`Options:`)
for (const [fieldName, option] of Object.entries(this.args)) {
const optionName = camelToKebabCase(fieldName)
let header = `--${optionName}`
if (option.type == 'string') {
const def = option.default != null ? `[${option.default}]` : '<value>'
header += `=${def}`
}
console.log()
console.log(header)
console.log(option.description)
}
}
printHelpAndExit(exitCode: number) {
this.printHelp()
process.exit(exitCode)
}
}
/** Parse the command line arguments. */
export function parse(): ArgParser {
const argParser = new ArgParser()
argParser.parse()
return argParser
}

View File

@ -1,70 +0,0 @@
/** @file Tool for extracting sources of dynamic assets from compiled WASM binaries. */
import path from 'path'
import * as args from './args'
import * as fs from './fs'
import * as log from '../runner/log'
import * as runner from '../runner/index'
// ===========
// === App ===
// ===========
/** The main application. It loads the WASM file from disk, runs before main entry points, extract
* asset sources and saves them to files. */
class App extends runner.App {
override async loadWasm() {
const mainJsUrl = path.join(__dirname, this.config.groups.loader.options.jsUrl.value)
const mainWasmUrl = path.join(__dirname, this.config.groups.loader.options.wasmUrl.value)
const mainJs = await fs.readFile(mainJsUrl, 'utf8')
const mainWasm = await fs.readFile(mainWasmUrl)
this.wasm = await this.compileAndRunWasm(mainJs, mainWasm)
}
async extractAssets(outDir: string) {
await log.Task.asyncRun('Extracting dynamic assets source code.', async () => {
// Clear the extracted-sources directory before getting new sources.
// If getting sources fails we leave the directory empty, not outdated.
await fs.rm(outDir, { recursive: true, force: true })
await fs.mkdir(outDir)
const assetsMap = this.getAssetSources()
if (assetsMap) {
await log.Task.asyncRun(`Writing assets to '${outDir}'.`, async () => {
for (const [builder, asset] of assetsMap) {
for (const [key, files] of asset) {
const dirPath = path.join(outDir, builder, key)
await fs.mkdir(dirPath, { recursive: true })
for (const [name, data] of files) {
const filePath = path.join(dirPath, name)
await fs.writeFile(`${filePath}`, Buffer.from(data))
}
}
}
})
}
})
}
override async run(): Promise<void> {
const parser = args.parse()
const outDir = parser.args.outDir.value
if (outDir) {
await log.Task.asyncRun('Running the program.', async () => {
await app.loadAndInitWasm()
const r = app.runBeforeMainEntryPoints().then(() => {
return app.extractAssets(outDir)
})
await r
})
} else {
parser.printHelpAndExit(1)
}
}
}
// ============
// === Main ===
// ============
const app = new App()
void app.run()

View File

@ -1,130 +0,0 @@
/** @file This module redefines some `node:fs` functions with embedded logging, so it is easy to
* track what they do. */
import {
MakeDirectoryOptions,
Mode,
ObjectEncodingOptions,
OpenMode,
PathLike,
RmDirOptions,
RmOptions,
} from 'node:fs'
import { FileHandle } from 'fs/promises'
import { Abortable } from 'node:events'
import { promises as fs } from 'fs'
import * as log from '../runner/log'
import { Stream } from 'node:stream'
// ================
// === readFile ===
// ================
export async function readFile(
path: PathLike | FileHandle,
options?:
| ({
encoding?: null | undefined
flag?: OpenMode | undefined
} & Abortable)
| null
): Promise<Buffer>
export async function readFile(
path: PathLike | FileHandle,
options:
| ({
encoding: BufferEncoding
flag?: OpenMode | undefined
} & Abortable)
| BufferEncoding
): Promise<string>
/** Read a file and log the operation. */
export async function readFile(
path: PathLike | FileHandle,
options?:
| (ObjectEncodingOptions &
Abortable & {
flag?: OpenMode | undefined
})
| BufferEncoding
| null
): Promise<string | Buffer> {
return log.Task.asyncRun(`Reading file '${String(path)}'.`, async () => {
return await fs.readFile(path, options)
})
}
// ==============
// === unlink ===
// ==============
/** Unlink a file and log the operation. */
export async function unlink(path: PathLike): Promise<void> {
return log.Task.asyncRun(`Removing file '${String(path)}'.`, async () => {
return await fs.unlink(path)
})
}
// =============
// === rmdir ===
// =============
/** Remove a directory and log the operation. */
export async function rmdir(path: PathLike, options?: RmDirOptions): Promise<void> {
return log.Task.asyncRun(`Removing directory '${String(path)}'.`, async () => {
return await fs.rmdir(path, options)
})
}
// =============
// === mkdir ===
// =============
/** Make a directory and log the operation. */
export async function mkdir(
path: PathLike,
options?: Mode | MakeDirectoryOptions | null
): Promise<string | undefined> {
return log.Task.asyncRun(`Creating directory '${String(path)}'.`, async () => {
return await fs.mkdir(path, options)
})
}
// ==========
// === rm ===
// ==========
/** Remove a file or directory and log the operation. */
export async function rm(path: PathLike, options?: RmOptions): Promise<void> {
return log.Task.asyncRun(`Removing '${String(path)}'.`, async () => {
return await fs.rm(path, options)
})
}
// =================
// === writeFile ===
// =================
/** Write a file and log the operation. */
export async function writeFile(
file: PathLike | FileHandle,
data:
| string
| NodeJS.ArrayBufferView
| Iterable<string | NodeJS.ArrayBufferView>
| AsyncIterable<string | NodeJS.ArrayBufferView>
| Stream,
options?:
| (ObjectEncodingOptions & {
mode?: Mode | undefined
flag?: OpenMode | undefined
} & Abortable)
| BufferEncoding
| null
): Promise<void> {
return log.Task.asyncRun(`Writing file '${String(file)}'.`, async () => {
return await fs.writeFile(file, data, options)
})
}

View File

@ -1,8 +0,0 @@
/** @file Contains {@link spector} function which is a wrapper for
* [spectorjs]{@link https://github.com/BabylonJS/Spector.js}. */
/* eslint @typescript-eslint/no-unsafe-return: "off" */
/** Spectorjs function wrapper. */
export function spector() {
return require('spectorjs')
}

View File

@ -1,11 +0,0 @@
/** @file @{link pkg} init export. */
// Following imports are only valid in context of the built package. Here, those are guaranteed to
// not resolve correctly, so we need to disable the type checking.
// @ts-expect-error
import init from './pkg.js'
// @ts-expect-error
export * from './runtime-libs'
export { init }

View File

@ -1,401 +0,0 @@
//! Building dynamic assets (assets which require the application to be run to generate their
//! sources).
//!
//! The essential operation, producing a directory of outputs from a directory of inputs, is
//! implemented by each builder (e.g. [`Builder::Shader`], [`Builder::Font`]).
//!
//! As builders can take some time to run, a caching mechanism is used to avoid unnecessary
//! rebuilds. Caching is achieved by making populating-the-output-directory an idempotent process:
//! Paths within the output directory are dependent on the *content* of the corresponding input
//! files, so that if a calculated output path already exists, it is already up-to-date; otherwise,
//! it must be built. This design may be familiar to users of the Nix or Guix package managers.
use ide_ci::prelude::*;
use crate::Paths;
use enso_prelude::anyhow;
use ide_ci::programs::shaderc::Glslc;
use ide_ci::programs::shaderc::SpirvOpt;
use ide_ci::programs::spirv_cross::SpirvCross;
use std::hash::Hasher;
// =============
// === Build ===
// =============
/// Bring the dynamic assets up-to-date, for the current asset sources. This consists of:
/// - Scan the asset source directory tree, hashing the input files.
/// - Update the assets:
/// - For each asset-source directory, determine an output directory based on the inputs name and
/// the hashes of its files.
/// - If that output directory doesn't exist, run the builder (determined by the top-level
/// directory the in which the asset was found, e.g. `shader`) and populate the directory.
/// - Generate a manifest, identifying the current assets and paths to their sources.
pub async fn build(paths: &Paths) -> Result<()> {
info!("Building dynamic assets.");
let sources = survey_asset_sources(paths)?;
let assets = update_assets(paths, &sources).await?;
let manifest = serde_json::to_string(&assets)?;
ide_ci::fs::tokio::write(&paths.target.enso_pack.dist.dynamic_assets.manifest, manifest)
.await?;
gc_assets(paths, &assets)?;
Ok(())
}
// ===============
// === Builder ===
// ===============
/// Identifies an asset type, which determines how it is built.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "lowercase")]
enum Builder {
Font,
Shader,
}
impl Builder {
fn dir_name<'a>(self) -> &'a str {
self.into()
}
async fn build_asset(
self,
input_dir: &Path,
input_files: &[String],
output_dir: &Path,
tmp: &Path,
) -> Result<()> {
match self {
Builder::Font => build_font(input_dir, input_files, output_dir).await,
Builder::Shader => build_shader(input_dir, input_files, output_dir, tmp).await,
}
}
}
impl TryFrom<&str> for Builder {
type Error = anyhow::Error;
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
match value {
"font" => Ok(Builder::Font),
"shader" => Ok(Builder::Shader),
other => Err(anyhow!("Unknown builder: {other:?}")),
}
}
}
impl From<Builder> for &'static str {
fn from(value: Builder) -> Self {
match value {
Builder::Font => "font",
Builder::Shader => "shader",
}
}
}
// ====================
// === Build Inputs ===
// ====================
/// The inputs to a builder.
struct AssetSources {
asset_key: String,
input_files: Vec<String>,
inputs_hash: u64,
}
impl AssetSources {
/// The output directory name for the asset.
fn dir_name(&self) -> String {
let key = &self.asset_key;
let hash = self.inputs_hash;
format!("{key}-{hash:x}")
}
}
// =====================
// === Build Outputs ===
// =====================
/// The outputs of a builder.
#[derive(Serialize)]
struct Asset {
dir: String,
files: Vec<String>,
}
/// The outputs of all builders.
type AssetManifest = BTreeMap<Builder, BTreeMap<String, Asset>>;
// ================
// === Building ===
// ================
/// Scan the sources found in the asset sources directory.
///
/// Returns, for each [`Builder`] (e.g. shader or font), for each asset directory found, an
/// [`AssetSources`] object identifying the asset key (i.e. the name of its directory), its input
/// files, and a hash covering all its input files.
fn survey_asset_sources(paths: &Paths) -> Result<HashMap<Builder, Vec<AssetSources>>> {
let dir = ide_ci::fs::read_dir(&paths.target.enso_pack.dynamic_assets)?;
let mut asset_sources: HashMap<_, Vec<_>> = HashMap::new();
let mut buf = Vec::new();
for entry in dir {
let entry = entry?;
let builder = Builder::try_from(entry.file_name().to_string_lossy().as_ref())?;
let builder_dir = ide_ci::fs::read_dir(entry.path())?;
let builder_sources = asset_sources.entry(builder).or_default();
for entry in builder_dir {
let entry = entry?;
let asset_key = entry.file_name().to_string_lossy().to_string();
let dir = ide_ci::fs::read_dir(entry.path())?;
let mut file_hashes = BTreeMap::new();
for entry in dir {
let entry = entry?;
let file_name = entry.file_name().to_string_lossy().to_string();
let path = entry.path();
buf.clear();
ide_ci::fs::open(path)?.read_to_end(&mut buf)?;
let mut file_hasher = std::collections::hash_map::DefaultHasher::new();
buf.hash(&mut file_hasher);
file_hashes.insert(file_name, file_hasher.finish());
}
let mut asset_hasher = std::collections::hash_map::DefaultHasher::new();
file_hashes.hash(&mut asset_hasher);
let inputs_hash = asset_hasher.finish();
let input_files = file_hashes.into_keys().collect();
builder_sources.push(AssetSources { asset_key, input_files, inputs_hash });
}
}
Ok(asset_sources)
}
/// Generate any assets not found up-to-date in the cache.
///
/// If an output directory already exists, it can be assumed to be up-to-date (because output path
/// is dependent on the input data), and is used as-is. Otherwise, [`build_asset`] runs the
/// appropriate builder to generate the output directory. In either case, a summary of the files
/// present in the output directory is produced; these summaries are assembled into an
/// [`AssetManifest`].
///
/// When asset builders need to be invoked, they are all run in parallel.
async fn update_assets(
paths: &Paths,
sources: &HashMap<Builder, Vec<AssetSources>>,
) -> Result<AssetManifest> {
let out = &paths.target.enso_pack.dist.dynamic_assets;
ide_ci::fs::create_dir_if_missing(out)?;
let mut assets: AssetManifest = BTreeMap::new();
let mut deferred_assets: BTreeMap<Builder, Vec<_>> = BTreeMap::new();
for (&builder, builder_sources) in sources {
let out = out.join(builder.dir_name());
ide_ci::fs::create_dir_if_missing(&out)?;
for source_specification in builder_sources {
let out = out.join(source_specification.dir_name());
let key = source_specification.asset_key.clone();
match std::fs::try_exists(&out)? {
false => {
info!("Rebuilding asset: `{}`.", out.display());
let builder_assets = deferred_assets.entry(builder).or_default();
let build = build_asset(paths, builder, source_specification);
builder_assets.push(async move { Ok((key, build.await?)) });
}
true => {
debug!("Skipping clean asset: `{}`.", out.display());
let builder_assets = assets.entry(builder).or_default();
let asset = survey_asset(paths, builder, source_specification)?;
builder_assets.insert(key, asset);
}
};
}
}
for (builder, deferred_assets) in deferred_assets.into_iter() {
let deferred_assets =
futures::stream::iter(deferred_assets).buffer_unordered(50).collect::<Vec<_>>().await;
let deferred_assets: Result<Vec<_>> = deferred_assets.into_iter().collect();
assets.entry(builder).or_default().extend(deferred_assets?);
}
Ok(assets)
}
/// Generate an asset from the given sources.
///
/// Set up paths (as described in the [`crate`] docs): run the appropriate [`Builder`]; move its
/// output from a temporary path into its final location (note that outputs are not built directly
/// in their final location, because directories found in the output tree are assumed to
/// accurately represent the results of running the specified builder for the specified inputs;
/// creating the output directory in its complete state ensures that if a build process is
/// interrupted, incomplete artifacts are never used).
async fn build_asset(
paths: &Paths,
builder: Builder,
source_specification: &AssetSources,
) -> Result<Asset> {
let input_dir = paths
.target
.enso_pack
.dynamic_assets
.join(builder.dir_name())
.join(&source_specification.asset_key);
let tmp_output_dir = paths
.target
.enso_pack
.dist
.dynamic_assets
.join(builder.dir_name())
.join(&source_specification.asset_key);
tokio::fs::create_dir(&tmp_output_dir).await?;
let work_path = paths
.target
.enso_pack
.dynamic_assets
.join(builder.dir_name())
.join(format!("{}.work", source_specification.asset_key));
builder
.build_asset(&input_dir, &source_specification.input_files, &tmp_output_dir, &work_path)
.await?;
let output_dir = paths
.target
.enso_pack
.dist
.dynamic_assets
.join(builder.dir_name())
.join(source_specification.dir_name());
tokio::fs::rename(tmp_output_dir, output_dir).await?;
survey_asset(paths, builder, source_specification)
}
/// Identify the files present in an asset directory.
fn survey_asset(
paths: &Paths,
builder: Builder,
source_specification: &AssetSources,
) -> Result<Asset> {
let dir = source_specification.dir_name();
let path = paths.target.enso_pack.dist.dynamic_assets.join(builder.dir_name()).join(&dir);
let mut files = Vec::new();
for entry in ide_ci::fs::read_dir(&path)? {
files.push(entry?.file_name().to_string_lossy().to_string());
}
Ok(Asset { dir, files })
}
/// Remove any assets not present in the manifest.
fn gc_assets(paths: &Paths, assets: &AssetManifest) -> Result<()> {
let is_not_manifest = |entry: &std::io::Result<std::fs::DirEntry>| {
entry
.as_ref()
.map(|entry| entry.path() != paths.target.enso_pack.dist.dynamic_assets.manifest)
.unwrap_or(true)
};
for entry in paths.target.enso_pack.dist.dynamic_assets.read_dir()?.filter(is_not_manifest) {
let entry = entry?;
let path = entry.path();
let builder = Builder::try_from(entry.file_name().to_string_lossy().as_ref()).ok();
let assets = builder.and_then(|builder| assets.get(&builder));
match assets {
Some(assets) => {
let assets: HashSet<_> = assets.values().map(|asset| asset.dir.as_ref()).collect();
for entry in path.read_dir()? {
let entry = entry?;
let path = entry.path();
if !assets.contains(entry.file_name().to_string_lossy().as_ref()) {
info!("Cleaning unused asset at `{}`.", path.display());
ide_ci::fs::remove_if_exists(path)?;
}
}
}
_ => {
info!("Cleaning unused builder at `{}`.", path.display());
ide_ci::fs::remove_if_exists(path)?;
}
}
}
Ok(())
}
// =============
// === Fonts ===
// =============
async fn build_font(input_dir: &Path, input_files: &[String], output_dir: &Path) -> Result<()> {
for file_name in input_files {
crate::copy(input_dir.join(file_name), output_dir.join(file_name))?;
}
Ok(())
}
// ===============
// === Shaders ===
// ===============
/// Build optimized shaders by using `glslc`, `spirv-opt` and `spirv-cross`.
async fn build_shader(
input_dir: &Path,
input_files: &[String],
output_dir: &Path,
work_dir: &Path,
) -> Result<()> {
ide_ci::fs::tokio::create_dir_if_missing(work_dir).await?;
info!("Optimizing `{}`.", input_dir.file_name().unwrap_or_default().to_string_lossy());
for glsl_file_name in input_files {
let glsl_path = input_dir.join(glsl_file_name);
let work_path = work_dir.join(glsl_file_name);
let stage_path = work_path.with_extension("");
let stage =
stage_path.file_name().ok_or_else(|| anyhow!("Empty stage path."))?.to_string_lossy();
let spv_path = stage_path.with_appended_extension("spv");
let spv_opt_path = stage_path.with_appended_extension("opt.spv");
let glsl_opt_path = stage_path.with_appended_extension("opt.glsl");
let glsl_opt_dist_path = output_dir.join(glsl_file_name);
let spv_path = spv_path.as_str();
let glsl_path = glsl_path.as_str();
let shader_stage = &format!("-fshader-stage={stage}");
let glslc_args = ["--target-env=opengl", shader_stage, "-o", spv_path, glsl_path];
let spirv_opt_args = ["-O", "-o", spv_opt_path.as_str(), spv_path.as_str()];
let spirv_cross_args = ["--output", glsl_opt_path.as_str(), spv_opt_path.as_str()];
Glslc.cmd()?.args(glslc_args).run_ok().await?;
SpirvOpt.cmd()?.args(spirv_opt_args).run_ok().await?;
SpirvCross.cmd()?.args(spirv_cross_args).run_ok().await?;
let content =
ide_ci::fs::tokio::read_to_string(&glsl_opt_path).await?.replace("\r\n", "\n");
let extract_err = || format!("Failed to process shader '{}'.", glsl_opt_path.as_str());
let code = extract_main_shader_code(&content).with_context(extract_err)?;
ide_ci::fs::tokio::write(&glsl_opt_dist_path, code).await?;
}
Ok(())
}
/// Read the optimized shader code, extract the main function body and preserve all top-level
/// variable declarations.
fn extract_main_shader_code(code: &str) -> Result<String> {
let main_start_str = "void main()\n{";
let main_end_str = "}";
let main_fn_find_err = "Failed to find main function.";
let main_start = code.find(main_start_str).with_context(|| main_fn_find_err)?;
let main_end = code.rfind(main_end_str).with_context(|| main_fn_find_err)?;
let before_main = &code[..main_start];
let declarations: Vec<&str> = before_main
.lines()
.filter_map(|line| {
let version_def = line.starts_with("#version ");
let precision_def = line.starts_with("precision ");
let layout_def = line.starts_with("layout(");
let def = version_def || precision_def || layout_def;
(!def).then_some(line)
})
.collect();
let declarations = declarations.join("\n");
let main_content = &code[main_start + main_start_str.len()..main_end];
Ok(format!("{declarations}\n{main_content}"))
}

View File

@ -1,464 +0,0 @@
//! EnsoGL Pack compiles Rust sources, builds the dynamic assets of the EnsoGL app (including
//! optimized shaders and pre-seeded caches), and outputs the JS WASM loader, additional JS runtime
//! utilities, and a set of optimized dynamic assets. It is a wrapper for `wasm-pack` tool.
//!
//! # Compilation process.
//! When run, the following file tree will be created/used. The files/directories marked with '*'
//! are required to be included with your final application code. The files marked with '**' are
//! recommended to be included.
//!
//! ```text
//! workspace | The main workspace directory (repo root).
//! ├─ ... / this_crate | This crate's directory.
//! │ ╰─ js | This crate's JS sources.
//! │ ├─ runner | Runner of WASM app. Used as a package by the app.
//! │ ├─ runtime-libs | Additional libs bundled with app. E.g. SpectorJS.
//! │ ├─ shader-extractor | App to extract shaders from WASM.
//! │ ╰─ wasm-pack-bundle | Glue for `wasm-pack` artifacts.
//! │ ╰─ index.ts | Copied to `target/ensogl-pack/wasm-pack/index.ts`.
//! ╰─ target | Directory where Rust and wasm-pack store build artifacts.
//! ╰─ ensogl-pack | Directory where ensogl-pack stores its build artifacts.
//! ├─ wasm-pack | Wasm-pack artifacts, re-created on every run.
//! │ ├─ pkg.js | Wasm-pack JS file to load WASM and glue it with snippets.
//! │ ├─ pkg_bg.wasm | Wasm-pack WASM bundle.
//! │ ├─ index.ts | Main file, copied from `this_crate/js/wasm-pack-bundle`.
//! │ ├─ runtime-libs.js | Bundled `this_crate/js/runtime-libs`.
//! │ ╰─ snippets | Rust-extracted JS snippets.
//! │ ╰─ <name>.js | A single Rust-extracted JS snippet.
//! ├─ dynamic-assets | Dynamic asset sources extracted from WASM bundle.
//! │ ├─ shader | Pre-compiled shaders.
//! │ │ ├─ <key> | Asset sources (the GLSL file).
//! │ │ ├─ <key>.work | Intermediate files produced by the shader compiler.
//! │ │ ╰─ ...
//! │ ├─ font | Pre-generated MSDF data.
//! │ │ ├─ <key> | Asset sources (the glyph atlas image, and metadata).
//! │ │ ╰─ ...
//! │ ╰─ <type>...
//! ├─ runtime-libs
//! │ ╰─ runtime-libs.js
//! ╰─ dist | Final build artifacts of ensogl-pack.
//! * ├─ index.js | The main JS bundle to load WASM and JS wasm-pack bundles.
//! ├─ index.js.map | The sourcemap mapping to sources in TypeScript.
//! ** ├─ index.d.ts | TypeScript types interface file.
//! ├─ asset-extractor.cjs | Node program to extract asset sources from WASM.
//! ├─ asset-extractor.cjs.map | The sourcemap mapping to sources in TypeScript.
//! * ├─ pkg.js | The `pks.js` artifact of wasm-pack WITH bundled snippets.
//! ├─ pkg.js.map | The sourcemap mapping to `pkg.js` generated by wasm-pack.
//! * ├─ pkg.wasm | The `pks_bg.wasm` artifact of wasm-pack.
//! * ╰─ dynamic-assets | Built dynamic assets.
//! ├─ manifest.json | An index of all the assets and their files.
//! ├─ shader | Pre-compiled shaders.
//! │ ├─ <key> | A subdirectory for each asset.
//! │ ╰─ ...
//! ├─ font | Pre-generated MSDF data.
//! │ ├─ <key> | A subdirectory for each asset.
//! │ ╰─ ...
//! ╰─ <type>...
//! ```
//!
//! The high-level app compilation process is summarized below:
//!
//! 1. If the `dist/index.js` file does not exist, or its modification date is older than
//! `this_crate/js` sources:
//!
//! 1. `npm install` is assumed to have been already run in the `this_crate/js` directory.
//!
//! 2. The `this_crate/js/runner` is compiled to `target/ensogl-pack/dist/index.cjs`. This is the
//! main file which is capable of loading WASM file, displaying a loading screen, running
//! before-main entry points, and running the main entry point of the application.
//!
//! 3. The `this_crate/js/shader-extractor` is compiled to
//! `target/ensogl-pack/dist/shader-extractor.js`. This is a node program that extracts
//! non-optimized shaders from the WASM file.
//!
//! 4. The `this_crate/js/runtime-libs` is compiled to
//! `target/ensogl-pack/runtime-libs/runtime-libs.js`. This is a bundle containing additional JS
//! libs, such as SpectorJS.
//!
//! 2. The rust sources are build with `wasm-pack`, which produces the following artifacts:
//! `target/ensogl-pack/wasm-pack/{pkg.js, pkg_bg.wasm, snippets}`. The file `pkg_bg.wasm` is copied
//! to `target/ensogl-pack/dist/pkg.wasm`.
//!
//! 3. The file `this_crate/js/wasm-pack-bundle/index.ts` is copied to
//! `target/ensogl-pack/wasm-pack/index.ts`. This is the main file which when compiled glues
//! `pkg.js`, `snippets`, and `runtime-libs.js` into a single bundle.
//!
//! 4. The program `target/ensogl-pack/dist/asset-extractor.cjs` is run. It loads
//! `target/dist/pkg.wasm` and writes asset sources to `target/ensogl-pack/dynamic-assets`.
//!
//! 5. For each asset, its inputs are hashed and an output directory is determined based on its
//! name and input hash. If the output directory doesn't already exist, the asset is built, and the
//! result is written to `dist/dynamic-assets`. The manifest is rebuilt to reflect the current set
//! of asset outputs, and any outdated output directories are removed.
//!
//! 6. The `target/ensogl-pack/wasm-pack/index.ts` is compiled to
//! `target/ensogl-pack/dist/index.js`.
//!
//!
//!
//! # Runtime process.
//! When `target/dist/index.js` is run:
//!
//! 1. The following files are downloaded from a server:
//! `target/dist/{pkg.js, pkg.wasm, dynamic-assets}`.
//! 2. The code from `pkg.js` is run to compile the WASM file.
//! 3. All before-main entry points are run.
//! 4. Optimized shaders are uploaded to the EnsoGL application.
//! 5. The main entry point is run.
// === Features ===
#![feature(async_closure)]
#![feature(fs_try_exists)]
// === Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]
#![allow(clippy::bool_to_int_with_if)]
#![allow(clippy::let_and_return)]
// === Non-Standard Linter Configuration ===
#![warn(missing_docs)]
use ide_ci::prelude::*;
use ide_ci::program::EMPTY_ARGS;
use ide_ci::programs::wasm_pack::WasmPackCommand;
use manifest_dir_macros::path;
use std::env;
use std::path::Path;
use std::path::PathBuf;
use walkdir::WalkDir;
// ==============
// === Export ===
// ==============
pub mod assets;
pub use ide_ci::prelude;
// =================
// === Hot Fixes ===
// =================
/// A hot-fix for a bug on macOS, where `std::fs::copy` causes cargo-watch to loop infinitely.
/// See: https://github.com/watchexec/cargo-watch/issues/242
pub fn copy(source_file: impl AsRef<Path>, destination_file: impl AsRef<Path>) -> Result {
if env::consts::OS == "macos" {
Command::new("cp").arg(source_file.as_ref()).arg(destination_file.as_ref()).spawn()?;
Ok(())
} else {
ide_ci::fs::copy(source_file, destination_file)
}
}
// =============
// === Paths ===
// =============
/// Paths of the directories and files used by `ensogl-pack`. This struct maps to variables the
/// directory layout described in the docs of this module.
#[derive(Debug, Default)]
#[allow(missing_docs)]
pub struct Paths {
pub workspace: PathBuf,
pub this_crate: paths::ThisCrate,
pub target: paths::Target,
}
macro_rules! define_paths {
($(
$name:ident {
$($field:ident : $field_ty:ty),* $(,)?
}
)*) => {$(
#[derive(Debug, Default)]
#[allow(missing_docs)]
pub struct $name {
pub root: PathBuf,
$( pub $field: $field_ty ),*
}
impl Deref for $name {
type Target = PathBuf;
fn deref(&self) -> &Self::Target {
&self.root
}
}
impl AsRef<std::path::Path> for $name {
fn as_ref(&self) -> &std::path::Path {
&self.root
}
}
impl AsRef<OsStr> for $name {
fn as_ref(&self) -> &OsStr {
self.root.as_ref()
}
}
)*};
}
/// Paths used during build.
pub mod paths {
use super::*;
define_paths! {
ThisCrate {
js: ThisCrateJs,
}
ThisCrateJs {
wasm_pack_bundle: ThisCrateJsWasmPackBundle,
}
ThisCrateJsWasmPackBundle {
index: PathBuf
}
Target {
enso_pack: TargetEnsoglPack,
}
TargetEnsoglPack {
wasm_pack: TargetEnsoglPackWasmPack,
dynamic_assets: PathBuf,
runtime_libs: TargetEnsoglPackRuntimeLibs,
dist: TargetEnsoglPackDist,
}
TargetEnsoglPackRuntimeLibs {
runtime_libs: PathBuf,
}
TargetEnsoglPackWasmPack {
index: PathBuf,
pkg_bg: PathBuf,
pkg_js: PathBuf,
runtime_libs: PathBuf,
}
TargetEnsoglPackDist {
asset_extractor: PathBuf,
pkg_js: PathBuf,
main_wasm: PathBuf,
dynamic_assets: TargetEnsoglPackDistDynamicAssets,
}
TargetEnsoglPackDistDynamicAssets {
manifest: PathBuf,
}
}
}
const WASM_PACK_OUT_NAME: &str = "pkg";
impl Paths {
/// Create a set of paths values.
pub async fn new() -> Result<Self> {
let mut p = Paths::default();
let current_cargo_path = Path::new(path!("Cargo.toml"));
p.this_crate.root = current_cargo_path.try_parent()?.into();
p.this_crate.js.root = p.this_crate.join("js");
p.this_crate.js.wasm_pack_bundle.root =
p.this_crate.js.root.join("src").join("wasm-pack-bundle");
p.this_crate.js.wasm_pack_bundle.index = p.this_crate.js.wasm_pack_bundle.join("index.ts");
p.workspace = workspace_dir().await?;
p.target.root = p.workspace.join("target");
p.target.enso_pack.root = p.target.join("ensogl-pack");
p.target.enso_pack.wasm_pack.root = p.target.enso_pack.join("wasm-pack");
let pkg_wasm = format!("{WASM_PACK_OUT_NAME}_bg.wasm");
let pkg_js = format!("{WASM_PACK_OUT_NAME}.js");
p.target.enso_pack.wasm_pack.index = p.target.enso_pack.wasm_pack.join("index.ts");
p.target.enso_pack.wasm_pack.pkg_bg = p.target.enso_pack.wasm_pack.join(pkg_wasm);
p.target.enso_pack.wasm_pack.pkg_js = p.target.enso_pack.wasm_pack.join(pkg_js);
p.target.enso_pack.wasm_pack.runtime_libs =
p.target.enso_pack.wasm_pack.join("runtime-libs.js");
p.target.enso_pack.dynamic_assets = p.target.enso_pack.join("dynamic-assets");
p.target.enso_pack.runtime_libs.root = p.target.enso_pack.join("runtime-libs");
p.target.enso_pack.runtime_libs.runtime_libs =
p.target.enso_pack.runtime_libs.join("runtime-libs.js");
p.target.enso_pack.dist.root = p.target.enso_pack.join("dist");
p.target.enso_pack.dist.asset_extractor =
p.target.enso_pack.dist.join("asset-extractor.cjs");
p.target.enso_pack.dist.pkg_js = p.target.enso_pack.dist.join("pkg.js");
p.target.enso_pack.dist.main_wasm = p.target.enso_pack.dist.join("pkg.wasm");
p.target.enso_pack.dist.dynamic_assets.root =
p.target.enso_pack.dist.join("dynamic-assets");
p.target.enso_pack.dist.dynamic_assets.manifest =
p.target.enso_pack.dist.dynamic_assets.join("manifest.json");
Ok(p)
}
}
/// Returns the workspace directory (repo root).
pub async fn workspace_dir() -> Result<PathBuf> {
use ide_ci::programs::cargo;
use ide_ci::programs::Cargo;
let output = Cargo
.cmd()?
.apply(&cargo::Command::LocateProject)
.apply(&cargo::LocateProjectOption::Workspace)
.apply(&cargo::LocateProjectOption::MessageFormat(cargo::MessageFormat::Plain))
.output_ok()
.await?
.into_stdout_string()?;
let cargo_path = Path::new(output.trim());
Ok(cargo_path.try_parent()?.to_owned())
}
// =============
// === Build ===
// =============
/// The arguments to `wasm-pack build` that `ensogl-pack` wants to customize.
pub struct WasmPackOutputs {
/// Value to passed as `--out-dir` to `wasm-pack`.
pub out_dir: PathBuf,
/// Value to passed as `--out-name` to `wasm-pack`.
pub out_name: String,
}
/// Check the modification time of all files in this crate's `js` directory and compare them with
/// the modification time of dist artifacts, if any. Do not traverse `node_modules` directory.
fn check_if_ts_needs_rebuild(paths: &Paths) -> Result<bool> {
let walk = WalkDir::new(&paths.this_crate.js).into_iter();
let walk_no_node_modules = walk.filter_entry(|e| e.file_name() != "node_modules");
let mut newest_mod_time: Option<std::time::SystemTime> = None;
for opt_entry in walk_no_node_modules {
let entry = opt_entry?;
if entry.file_type().is_file() {
let metadata = entry.metadata()?;
let mod_time = metadata.modified()?;
newest_mod_time = Some(newest_mod_time.map_or(mod_time, |t| t.max(mod_time)));
}
}
if let Ok(app_js_metadata) = std::fs::metadata(&paths.target.enso_pack.dist.asset_extractor) {
let app_js_mod_time = app_js_metadata.modified()?;
Ok(newest_mod_time.map_or(true, |t| t > app_js_mod_time))
} else {
Ok(true)
}
}
/// Compile TypeScript sources of this crate in case they were not compiled yet.
pub async fn compile_this_crate_ts_sources(paths: &Paths) -> Result<()> {
println!("compile_this_crate_ts_sources");
if check_if_ts_needs_rebuild(paths)? {
info!("EnsoGL Pack TypeScript sources changed, recompiling.");
let run_script = async move |script_name, script_args: &[&str]| {
ide_ci::programs::Npm
.cmd()?
.run(script_name)
.args(script_args)
.current_dir(&paths.this_crate.js)
.run_ok()
.await
};
info!("Linting TypeScript sources.");
run_script("lint", &EMPTY_ARGS).await?;
info!("Building TypeScript sources.");
let args = ["--", &format!("--out-dir={}", paths.target.enso_pack.dist.display())];
run_script("build-asset-extractor", &args).await?;
println!("BUILD build-runtime-libs");
let args = ["--", &format!("--outdir={}", paths.target.enso_pack.runtime_libs.display())];
run_script("build-runtime-libs", &args).await?;
} else {
println!("NO BUILD");
}
Ok(())
}
/// Run wasm-pack to build the wasm artifact.
#[context("Failed to run wasm-pack.")]
pub async fn run_wasm_pack(
paths: &Paths,
provider: impl FnOnce(WasmPackOutputs) -> Result<WasmPackCommand>,
) -> Result<()> {
info!("Obtaining and running the wasm-pack command.");
let replaced_args = WasmPackOutputs {
out_dir: paths.target.enso_pack.wasm_pack.root.clone(),
out_name: WASM_PACK_OUT_NAME.to_string(),
};
let mut command = provider(replaced_args).context("Failed to obtain wasm-pack command.")?;
command.run_ok().await?;
copy(&paths.this_crate.js.wasm_pack_bundle.index, &paths.target.enso_pack.wasm_pack.index)?;
copy(
&paths.target.enso_pack.runtime_libs.runtime_libs,
&paths.target.enso_pack.wasm_pack.runtime_libs,
)?;
compile_wasm_pack_artifacts(
&paths.target.enso_pack.wasm_pack,
&paths.target.enso_pack.wasm_pack.index,
&paths.target.enso_pack.dist.pkg_js,
)
.await?;
ide_ci::fs::copy(
&paths.target.enso_pack.wasm_pack.pkg_bg,
&paths.target.enso_pack.dist.main_wasm,
)
}
/// Compile wasm-pack artifacts (JS sources and snippets) to a single bundle.
async fn compile_wasm_pack_artifacts(pwd: &Path, pkg_js: &Path, out: &Path) -> Result {
info!("Compiling {}.", pkg_js.display());
ide_ci::programs::Npx
.cmd()?
.args([
"--yes",
"esbuild",
pkg_js.display().to_string().as_str(),
"--format=cjs",
"--bundle",
"--sourcemap",
"--platform=node",
&format!("--outfile={}", out.display()),
])
.current_dir(pwd)
.run_ok()
.await
}
/// Extract asset sources from the WASM artifact.
async fn extract_assets(paths: &Paths) -> Result<()> {
info!("Extracting asset sources from generated WASM file.");
ide_ci::programs::Node
.cmd()?
.arg(&paths.target.enso_pack.dist.asset_extractor)
.arg("--out-dir")
.arg(&paths.target.enso_pack.dynamic_assets)
.run_ok()
.await
}
/// Just builds the TypeScript sources.
pub async fn build_ts_sources_only() -> Result {
let paths = Paths::new().await?;
compile_this_crate_ts_sources(&paths).await
}
/// Wrapper over `wasm-pack build` command.
///
/// # Arguments
/// * `outputs` - The outputs that'd be usually given to `wasm-pack build` command.
/// * `provider` - Function that generates an invocation of the `wasm-pack build` command that has
/// applied given (customized) output-related arguments.
pub async fn build(
outputs: WasmPackOutputs,
provider: impl FnOnce(WasmPackOutputs) -> Result<WasmPackCommand>,
) -> Result {
let paths = Paths::new().await?;
compile_this_crate_ts_sources(&paths).await?;
run_wasm_pack(&paths, provider).await?;
extract_assets(&paths).await?;
assets::build(&paths).await?;
let out_dir = Path::new(&outputs.out_dir);
ide_ci::fs::copy(&paths.target.enso_pack.dist, out_dir)
}

346
package-lock.json generated
View File

@ -8,8 +8,8 @@
"workspaces": [
"app/ide-desktop",
"app/ide-desktop/lib/*",
"app/gui2",
"lib/rust/enso-pack/js"
"lib/js/runner",
"app/gui2"
],
"dependencies": {
"chromedriver": "^106.0.1",
@ -190,6 +190,8 @@
"esbuild": "^0.19.3",
"fast-glob": "^3.2.12",
"portfinder": "^1.0.32",
"sharp": "^0.31.2",
"to-ico": "^1.1.5",
"tsx": "^4.7.1"
},
"optionalDependencies": {
@ -206,6 +208,7 @@
"app/ide-desktop/lib/content": {
"name": "enso-content",
"version": "1.0.0",
"extraneous": true,
"dependencies": {
"@types/semver": "^7.3.9",
"enso-content-config": "^1.0.0",
@ -286,6 +289,7 @@
"esbuild": "^0.19.3",
"esbuild-plugin-inline-image": "^0.0.9",
"esbuild-plugin-time": "^1.0.0",
"esbuild-plugin-yaml": "^0.0.1",
"eslint": "^8.49.0",
"eslint-plugin-jsdoc": "^46.8.1",
"eslint-plugin-react": "^7.32.1",
@ -316,10 +320,56 @@
"name": "enso-icons",
"version": "1.0.0",
"devDependencies": {
"@types/sharp": "^0.31.1",
"@types/to-ico": "^1.1.1",
"sharp": "^0.31.2",
"to-ico": "^1.1.5"
}
},
"app/ide-desktop/lib/js": {
"name": "enso-runner",
"version": "1.0.0",
"extraneous": true,
"dependencies": {
"spectorjs": "^0.9.27"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.7.2",
"@typescript-eslint/parser": "^6.7.2",
"esbuild": "^0.19.3",
"eslint": "^8.49.0",
"eslint-plugin-jsdoc": "^46.8.1",
"tsup": "^7.2.0",
"typescript": "~5.2.2"
},
"optionalDependencies": {
"esbuild-darwin-64": "^0.15.18",
"esbuild-linux-64": "^0.15.18",
"esbuild-windows-64": "^0.15.18"
}
},
"app/ide-desktop/lib/runner": {
"name": "enso-runner",
"version": "1.0.0",
"extraneous": true,
"dependencies": {
"spectorjs": "^0.9.27"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.7.2",
"@typescript-eslint/parser": "^6.7.2",
"esbuild": "^0.19.3",
"eslint": "^8.49.0",
"eslint-plugin-jsdoc": "^46.8.1",
"tsup": "^7.2.0",
"typescript": "~5.2.2"
},
"optionalDependencies": {
"esbuild-darwin-64": "^0.15.18",
"esbuild-linux-64": "^0.15.18",
"esbuild-windows-64": "^0.15.18"
}
},
"app/ide-desktop/lib/ts-plugin-namespace-auto-import": {
"version": "1.0.0",
"devDependencies": {
@ -368,6 +418,69 @@
"zen-observable-ts": "0.8.19"
}
},
"app/ide-desktop/node_modules/@eslint/eslintrc": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
"integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^9.6.0",
"globals": "^13.19.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"app/ide-desktop/node_modules/@eslint/eslintrc/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"app/ide-desktop/node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"app/ide-desktop/node_modules/@eslint/js": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz",
"integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"app/ide-desktop/node_modules/@types/sharp": {
"version": "0.31.1",
"resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.31.1.tgz",
"integrity": "sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"app/ide-desktop/node_modules/ajv": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
@ -395,6 +508,143 @@
"js-cookie": "^2.2.1"
}
},
"app/ide-desktop/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"app/ide-desktop/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"app/ide-desktop/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"app/ide-desktop/node_modules/eslint": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz",
"integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.2",
"@eslint/js": "8.49.0",
"@humanwhocodes/config-array": "^0.11.11",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
"debug": "^4.3.2",
"doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^7.2.2",
"eslint-visitor-keys": "^3.4.3",
"espree": "^9.6.1",
"esquery": "^1.4.2",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^6.0.1",
"find-up": "^5.0.0",
"glob-parent": "^6.0.2",
"globals": "^13.19.0",
"graphemer": "^1.4.0",
"ignore": "^5.2.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"is-path-inside": "^3.0.3",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
"lodash.merge": "^4.6.2",
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
"optionator": "^0.9.3",
"strip-ansi": "^6.0.1",
"text-table": "^0.2.0"
},
"bin": {
"eslint": "bin/eslint.js"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"app/ide-desktop/node_modules/eslint/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"app/ide-desktop/node_modules/eslint/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"app/ide-desktop/node_modules/eslint/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"app/ide-desktop/node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"app/ide-desktop/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@ -405,9 +655,31 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"lib/js/runner": {
"name": "enso-runner",
"version": "1.0.0",
"dependencies": {
"spectorjs": "^0.9.27"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.7.2",
"@typescript-eslint/parser": "^6.7.2",
"esbuild": "^0.19.3",
"eslint": "^8.49.0",
"eslint-plugin-jsdoc": "^46.8.1",
"tsup": "^7.2.0",
"typescript": "~5.2.2"
},
"optionalDependencies": {
"esbuild-darwin-64": "^0.15.18",
"esbuild-linux-64": "^0.15.18",
"esbuild-windows-64": "^0.15.18"
}
},
"lib/rust/enso-pack/js": {
"name": "enso-runner",
"version": "1.0.0",
"extraneous": true,
"dependencies": {
"spectorjs": "^0.9.27"
},
@ -1966,15 +2238,6 @@
"node": ">=16"
}
},
"node_modules/@esbuild-plugins/node-globals-polyfill": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz",
"integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==",
"dev": true,
"peerDependencies": {
"esbuild": "*"
}
},
"node_modules/@esbuild-plugins/node-modules-polyfill": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz",
@ -3592,15 +3855,6 @@
"@types/responselike": "^1.0.0"
}
},
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/cookie": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz",
@ -3936,12 +4190,6 @@
"integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
"dev": true
},
"node_modules/@types/http-errors": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"dev": true
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
@ -4005,12 +4253,6 @@
"integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==",
"dev": true
},
"node_modules/@types/mime": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.4.tgz",
"integrity": "sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==",
"dev": true
},
"node_modules/@types/mime-types": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz",
@ -4023,15 +4265,6 @@
"dev": true,
"optional": true
},
"node_modules/@types/morgan": {
"version": "1.9.9",
"resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz",
"integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/ms": {
"version": "0.7.34",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
@ -4130,27 +4363,8 @@
"node_modules/@types/semver": {
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ=="
},
"node_modules/@types/serve-static": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz",
"integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==",
"dev": true,
"dependencies": {
"@types/http-errors": "*",
"@types/mime": "*",
"@types/node": "*"
}
},
"node_modules/@types/sharp": {
"version": "0.31.1",
"resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.31.1.tgz",
"integrity": "sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"dev": true
},
"node_modules/@types/shuffle-seed": {
"version": "1.1.3",
@ -6556,6 +6770,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
"dev": true,
"engines": {
"node": ">=6"
}
@ -8427,10 +8642,6 @@
"resolved": "app/ide-desktop/lib/common",
"link": true
},
"node_modules/enso-content": {
"resolved": "app/ide-desktop/lib/content",
"link": true
},
"node_modules/enso-content-config": {
"resolved": "app/ide-desktop/lib/content-config",
"link": true
@ -8452,7 +8663,7 @@
"link": true
},
"node_modules/enso-runner": {
"resolved": "lib/rust/enso-pack/js",
"resolved": "lib/js/runner",
"link": true
},
"node_modules/entities": {
@ -14903,6 +15114,7 @@
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz",
"integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==",
"dev": true,
"dependencies": {
"clsx": "^1.1.1"
},

View File

@ -19,8 +19,8 @@
"workspaces": [
"app/ide-desktop",
"app/ide-desktop/lib/*",
"app/gui2",
"lib/rust/enso-pack/js"
"lib/js/runner",
"app/gui2"
],
"overrides": {
"tslib": "$tslib"