- Closes #8179

# Important Notes
- ⚠️ These tests are currently *not run* on any CI workflow.
- There is some unused code for mocking the PM. This has been intentionally kept, as this may be useful in the future.
Note that this may be useful for testing the dashboard, however the dashboard is currently only tested in cloud mode
- that is, without the backend switcher, and with only the remote backend available. As such, currently it uses HTTP API mocks, and no PM mock.
This commit is contained in:
somebody1234 2023-11-28 01:48:37 +10:00 committed by GitHub
parent 8df47a7d65
commit 9b7e3d0f16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
77 changed files with 2065 additions and 846 deletions

20
app/gui2/.snyk Normal file
View File

@ -0,0 +1,20 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.25.0
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
ignore:
javascript/DuplicateIfBody:
- '*':
reason: Intentional.
javascript/VueGetterDoesntReturn:
- '*':
reason: set() should not return... and TypeScript catches these errors.
javascript/MissingArgument:
- '*':
reason: TypeScript catches these errors. This warning is incorrect.
javascript/PromiseNotCaughtGeneral:
- '*':
reason: Not relevant.
javascript/IncompatibleTypesInComparison:
- '*':
reason: TypeScript catches these errors in a more sensible way.
patch: {}

33
app/gui2/e2e/MockApp.vue Normal file
View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import { computed } from 'vue'
import App from '../src/App.vue'
import MockProjectStoreWrapper from '../stories/MockProjectStoreWrapper.vue'
import { getMainFile, setMainFile } from './mockEngine'
const mainFile = computed({
get() {
return getMainFile()
},
set(value) {
setMainFile(value)
},
})
</script>
<template>
<MockProjectStoreWrapper v-model="mainFile"><App :config="{}" /></MockProjectStoreWrapper>
</template>
<style scoped>
:is(.viewport) {
color: var(--color-text);
font-family: 'M PLUS 1', sans-serif;
font-size: 11.5px;
font-weight: 500;
line-height: 20px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
height: 100vh;
}
</style>

11
app/gui2/e2e/actions.ts Normal file
View File

@ -0,0 +1,11 @@
import { type Page } from '@playwright/test'
// =================
// === goToGraph ===
// =================
/** Perform a successful login. */
export async function goToGraph(page: Page) {
await page.goto('/')
// Originally this clicked the play button but for now that is out of scope.
}

View File

@ -0,0 +1,17 @@
import { expect, test } from '@playwright/test'
import * as actions from './actions'
import * as customExpect from './customExpect'
import * as locate from './locate'
test('component browser shows entries, and creates a new node', async ({ page }) => {
await actions.goToGraph(page)
await locate.graphEditor(page).click()
await locate.graphEditor(page).press('Enter')
await customExpect.toExist(locate.componentBrowser(page))
await customExpect.toExist(locate.componentBrowserEntry(page))
const nodeCount = await locate.graphNode(page).count()
await locate.componentBrowserEntry(page).last().click()
await locate.componentBrowserEntry(page).last().click({ force: true })
await expect(locate.componentBrowser(page)).not.toBeVisible()
await expect(locate.graphNode(page)).toHaveCount(nodeCount + 1)
})

View File

@ -0,0 +1,9 @@
import { expect, type Locator } from 'playwright/test'
/** Ensures that at least one of the elements that the Locator points to,
* is an attached and visible DOM node. */
export function toExist(locator: Locator) {
// Counter-intuitive, but correct:
// https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-be-visible
return expect(locator.first()).toBeVisible()
}

View File

@ -0,0 +1,18 @@
import { expect, test } from '@playwright/test'
import * as actions from './actions'
import * as customExpect from './customExpect'
import * as locate from './locate'
test('node can open and load visualization', async ({ page }) => {
await actions.goToGraph(page)
const node = locate.graphNode(page).last()
await node.click({ position: { x: 8, y: 8 } })
await customExpect.toExist(locate.circularMenu(page))
await locate.toggleVisualizationButton(page).click()
await customExpect.toExist(locate.anyVisualization(page))
await locate.showVisualizationSelectorButton(page).click()
await page.getByText('JSON').click()
await customExpect.toExist(locate.jsonVisualization(page))
// The default JSON viz data contains an object.
await expect(locate.jsonVisualization(page)).toContainText('{')
})

View File

@ -0,0 +1,10 @@
import { test } from '@playwright/test'
import * as actions from './actions'
import * as customExpect from './customExpect'
import * as locate from './locate'
test('graph can open and render nodes', async ({ page }) => {
await actions.goToGraph(page)
await customExpect.toExist(locate.graphEditor(page))
await customExpect.toExist(locate.graphNode(page))
})

152
app/gui2/e2e/locate.ts Normal file
View File

@ -0,0 +1,152 @@
import { type Locator, type Page } from '@playwright/test'
import cssEscape from 'css.escape'
// ==============
// === Filter ===
// ==============
class Filter {
constructor(public selector = '') {}
visible<T extends { selector: string }>(this: T): Omit<T, 'visible'> {
return new Filter(this.selector + ':visible') as any
}
first<T extends { selector: string }>(this: T): Omit<T, 'first' | 'last'> {
return new Filter(this.selector + ':first') as any
}
last<T extends { selector: string }>(this: T): Omit<T, 'first' | 'last'> {
return new Filter(this.selector + ':last') as any
}
id<T extends { selector: string }>(this: T, id: string): Omit<T, 'id'> {
return new Filter(this.selector + '#' + cssEscape(id)) as any
}
class(...classes: string[]) {
return new Filter(this.selector + '.' + classes.map(cssEscape).join('.'))
}
toString() {
return this.selector
}
}
// ================
// === Locators ===
// ================
// === Button locators ===
function or(a: (page: Locator | Page) => Locator, b: (page: Locator | Page) => Locator) {
return (page: Locator | Page) => a(page).or(b(page))
}
export function playOrOpenProjectButton(page: Locator | Page) {
return page.getByAltText('Open in editor')
}
// === Auto-evaluation ===
export function enableAutoEvaluationButton(page: Locator | Page) {
return page.getByAltText('Enable auto-evaluation')
}
export function disableAutoEvaluationButton(page: Locator | Page) {
return page.getByAltText('Disable auto-evaluation')
}
export const toggleAutoEvaluationButton = or(
enableAutoEvaluationButton,
disableAutoEvaluationButton,
)
// === Documentation ===
export function showDocumentationButton(page: Locator | Page) {
return page.getByAltText('Show documentation')
}
export function hideDocumentationButton(page: Locator | Page) {
return page.getByAltText('Hide documentation')
}
export const toggleDocumentationButton = or(showDocumentationButton, hideDocumentationButton)
// === Visualization ===
export function showVisualizationButton(page: Locator | Page) {
return page.getByAltText('Show visualization')
}
export function hideVisualizationButton(page: Locator | Page) {
return page.getByAltText('Hide visualization')
}
export const toggleVisualizationButton = or(showVisualizationButton, hideVisualizationButton)
// === Visualization selector ===
export function showVisualizationSelectorButton(page: Locator | Page) {
return page.getByAltText('Show visualization selector')
}
export function hideVisualizationSelectorButton(page: Locator | Page) {
return page.getByAltText('Hide visualization selector')
}
export const toggleVisualizationSelectorButton = or(
showVisualizationSelectorButton,
hideVisualizationSelectorButton,
)
// === Fullscreen ===
export function enterFullscreenButton(page: Locator | Page) {
return page.getByAltText('Enter fullscreen')
}
export function exitFullscreenButton(page: Locator | Page) {
return page.getByAltText('Exit fullscreen')
}
export const toggleFullscreenButton = or(enterFullscreenButton, exitFullscreenButton)
// === Data locators ===
type SanitizeClassName<T extends string> = T extends `${infer A}.${infer B}`
? SanitizeClassName<`${A}${B}`>
: T extends `${infer A} ${infer B}`
? SanitizeClassName<`${A}${B}`>
: T
function componentLocator<T extends string>(className: SanitizeClassName<T>) {
return (page: Locator | Page, filter?: (f: Filter) => { selector: string }) => {
return page.locator(`.${className}${filter?.(new Filter()) ?? ''}`)
}
}
export const graphEditor = componentLocator('GraphEditor')
export const graphNode = componentLocator('GraphNode')
// @ts-expect-error
export const anyVisualization = componentLocator('GraphVisualization > *')
export const circularMenu = componentLocator('CircularMenu')
export const componentBrowser = componentLocator('ComponentBrowser')
export function componentBrowserEntry(
page: Locator | Page,
filter?: (f: Filter) => { selector: string },
) {
return page.locator(`.ComponentBrowser .component${filter?.(new Filter()) ?? ''}`)
}
export const jsonVisualization = componentLocator('JSONVisualization')
export const tableVisualization = componentLocator('TableVisualization')
export const scatterplotVisualization = componentLocator('ScatterplotVisualization')
export const histogramVisualization = componentLocator('HistogramVisualization')
export const heatmapVisualization = componentLocator('HeatmapVisualization')
export const sqlVisualization = componentLocator('SqlVisualization')
export const geoMapVisualization = componentLocator('GeoMapVisualization')
export const imageBase64Visualization = componentLocator('ImageBase64Visualization')
export const warningsVisualization = componentLocator('WarningsVisualization')

58
app/gui2/e2e/main.ts Normal file
View File

@ -0,0 +1,58 @@
import 'enso-dashboard/src/tailwind.css'
import { createPinia } from 'pinia'
import { createApp, ref } from 'vue'
import '../src/assets/base.css'
import { provideGuiConfig } from '../src/providers/guiConfig'
import { provideVisualizationConfig } from '../src/providers/visualizationConfig'
import { MockTransport, MockWebSocket } from '../src/util/net'
import { Vec2 } from '../src/util/vec2'
import MockApp from './MockApp.vue'
import { mockDataHandler, mockLSHandler } from './mockEngine'
MockTransport.addMock('engine', mockLSHandler)
MockWebSocket.addMock('data', mockDataHandler)
const app = createApp(MockApp)
app.use(createPinia())
provideGuiConfig._mock(
ref({
startup: {
project: 'Mock Project',
displayedProjectName: 'Mock Project',
},
engine: { rpcUrl: 'mock://engine', dataUrl: 'mock://data' },
}),
app,
)
// Required for visualization stories.
provideVisualizationConfig._mock(
{
fullscreen: false,
width: 200,
height: 150,
hide() {},
isCircularMenuVisible: false,
nodeSize: new Vec2(200, 150),
currentType: {
module: { kind: 'Builtin' },
name: 'Current Type',
},
types: [
{
module: { kind: 'Builtin' },
name: 'Example',
},
{
module: { kind: 'Builtin' },
name: 'Types',
},
{
module: { kind: 'Builtin' },
name: 'Here',
},
],
updateType() {},
},
app,
)
app.mount('#app')

332
app/gui2/e2e/mockEngine.ts Normal file
View File

@ -0,0 +1,332 @@
import * as random from 'lib0/random'
import {
Builder,
EnsoUUID,
OutboundMessage,
OutboundPayload,
VisualizationContext,
VisualizationUpdate,
} from 'shared/binaryProtocol'
import { mockDataWSHandler as originalMockDataWSHandler } from 'shared/dataServer/mock'
import type {
ContextId,
ExpressionId,
LibraryComponentGroup,
Path,
Uuid,
VisualizationConfiguration,
response,
} from 'shared/languageServerTypes'
import type { SuggestionEntry } from 'shared/languageServerTypes/suggestions'
import { uuidToBits } from 'shared/uuid'
import type { MockTransportData, WebSocketHandler } from 'src/util/net'
import type { QualifiedName } from 'src/util/qualifiedName'
import { mockFsDirectoryHandle, type FileTree } from '../src/util/convert/fsAccess'
import mockDb from '../stories/mockSuggestions.json' assert { type: 'json' }
const mockProjectId = random.uuidv4() as Uuid
const standardBase = 'Standard.Base' as QualifiedName
export function placeholderGroups(): LibraryComponentGroup[] {
return [
{ color: '#4D9A29', name: 'Input', library: standardBase, exports: [] },
{ color: '#B37923', name: 'Web', library: standardBase, exports: [] },
{ color: '#9735B9', name: 'Parse', library: standardBase, exports: [] },
{ color: '#4D9A29', name: 'Select', library: standardBase, exports: [] },
{ color: '#B37923', name: 'Join', library: standardBase, exports: [] },
{ color: '#9735B9', name: 'Transform', library: standardBase, exports: [] },
{ color: '#4D9A29', name: 'Output', library: standardBase, exports: [] },
]
}
let mainFile = `\
from Standard.Base import all
from Standard.Base.Runtime.Ref import Ref
from Standard.Test import Bench
options = Bench.options . set_warmup (Bench.phase_conf 1 2) . set_measure (Bench.phase_conf 3 2)
collect_benches = Bench.build builder->
range_size = 100000000
data = 0.up_to range_size
builder.group "Range" options group_builder->
group_builder.specify "iterate" <|
cell = Ref.new 0
data . each _->
x = cell.get
cell.put x+1
cell.get . should_equal range_size
main =
benches = collect_benches
result = run_main benches
`
export function getMainFile() {
return mainFile
}
export function setMainFile(newMainFile: string) {
return (mainFile = newMainFile)
}
const fileTree = {
src: {
get 'Main.enso'() {
return mainFile
},
},
}
const visualizations = new Map<Uuid, VisualizationConfiguration>()
const visualizationExprIds = new Map<Uuid, ExpressionId>()
const encoder = new TextEncoder()
const encodeJSON = (data: unknown) => encoder.encode(JSON.stringify(data))
const scatterplotJson = encodeJSON({
axis: {
x: { label: 'x-axis label', scale: 'linear' },
y: { label: 'y-axis label', scale: 'logarithmic' },
},
points: { labels: 'visible' },
data: [
{ x: 0.1, y: 0.7, label: 'foo', color: '#FF0000', shape: 'circle', size: 0.2 },
{ x: 0.4, y: 0.2, label: 'baz', color: '#0000FF', shape: 'square', size: 0.3 },
],
})
const mockVizData: Record<string, Uint8Array> = {
// JSON
'Standard.Visualization.Preprocessor.default_preprocessor': scatterplotJson,
'Standard.Visualization.Scatter_Plot.process_to_json_text': scatterplotJson,
'Standard.Visualization.SQL.Visualization.prepare_visualization': encodeJSON({
dialect: 'sql',
code: `SELECT * FROM \`foo\` WHERE \`a\` = ? AND b LIKE ?;`,
interpolations: [
// eslint-disable-next-line camelcase
{ enso_type: 'Data.Numbers.Number', value: '123' },
// eslint-disable-next-line camelcase
{ enso_type: 'Builtins.Main.Text', value: "a'bcd" },
],
}),
'Standard.Visualization.Geo_Map.process_to_json_text': encodeJSON({
latitude: 37.8,
longitude: -122.45,
zoom: 15,
controller: true,
showingLabels: true, // Enables presenting labels when hovering over a point.
layers: [
{
type: 'Scatterplot_Layer',
data: [
{
latitude: 37.8,
longitude: -122.45,
color: [255, 0, 0],
radius: 100,
label: 'an example label',
},
],
},
],
}),
'Standard.Visualization.Histogram.process_to_json_text': encodeJSON({
axis: {
x: { label: 'x-axis label', scale: 'linear' },
y: { label: 'y-axis label', scale: 'logarithmic' },
},
color: 'rgb(1.0,0.0,0.0)',
bins: 10,
data: {
values: [0.1, 0.2, 0.1, 0.15, 0.7],
},
}),
'Standard.Visualization.Table.Visualization.prepare_visualization': encodeJSON({
type: 'Matrix',
// eslint-disable-next-line camelcase
column_count: 5,
// eslint-disable-next-line camelcase
all_rows_count: 10,
json: Array.from({ length: 10 }, (_, i) => Array.from({ length: 5 }, (_, j) => `${i},${j}`)),
}),
'Standard.Visualization.Warnings.process_to_json_text': encodeJSON([
'warning 1',
"warning 2!!&<>;'\x22",
]),
// The following visualizations do not have unique transformation methods, and as such are only kept
// for posterity.
Image: encodeJSON({
mediaType: 'image/svg+xml',
base64: `PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0\
MCI+PGcgY2xpcC1wYXRoPSJ1cmwoI2EpIj48cGF0aCBkPSJNMjAuMDUgMEEyMCAyMCAwIDAgMCAwIDIwLjA1IDIwLjA2IDIwLjA\
2IDAgMSAwIDIwLjA1IDBabTAgMzYuMDVjLTguOTMgMC0xNi4xLTcuMTctMTYuMS0xNi4xIDAtOC45NCA3LjE3LTE2LjEgMTYuMS\
0xNi4xIDguOTQgMCAxNi4xIDcuMTYgMTYuMSAxNi4xYTE2LjE4IDE2LjE4IDAgMCAxLTE2LjEgMTYuMVoiLz48cGF0aCBkPSJNM\
jcuMTIgMTcuNzdhNC42OCA0LjY4IDAgMCAxIDIuMzkgNS45MiAxMC4yMiAxMC4yMiAwIDAgMS05LjU2IDYuODZBMTAuMiAxMC4y\
IDAgMCAxIDkuNzcgMjAuMzZzMS41NSAyLjA4IDQuNTcgMi4wOGMzLjAxIDAgNC4zNi0xLjE0IDUuNi0yLjA4IDEuMjUtLjkzIDI\
uMDktMyA1LjItMyAuNzMgMCAxLjQ2LjIgMS45OC40WiIvPjwvZz48ZGVmcz48Y2xpcFBhdGggaWQ9ImEiPjxwYXRoIGZpbGw9Ii\
NmZmYiIGQ9Ik0wIDBoNDB2NDBIMHoiLz48L2NsaXBQYXRoPjwvZGVmcz48L3N2Zz4=`,
}),
Heatmap: encodeJSON([
['A', 'B', 'C', 'D', 'A'],
['D', 'E', 'D', 'X', 'Z'],
[50, 25, 40, 20, 10],
]),
}
function createMessageId(builder: Builder) {
const messageUuid = random.uuidv4()
const [leastSigBits, mostSigBits] = uuidToBits(messageUuid)
return EnsoUUID.createEnsoUUID(builder, leastSigBits, mostSigBits)
}
function createId(id: Uuid) {
const [low, high] = uuidToBits(id)
return (builder: Builder) => EnsoUUID.createEnsoUUID(builder, low, high)
}
function sendVizData(id: Uuid, config: VisualizationConfiguration) {
const vizData = mockVizData[`${config.visualizationModule}.${config.expression}`]
if (!vizData || !sendData) return
const builder = new Builder()
const exprId = visualizationExprIds.get(id)
const visualizationContextOffset = VisualizationContext.createVisualizationContext(
builder,
createId(id),
createId(config.executionContextId),
exprId ? createId(exprId) : null,
)
const dataOffset = VisualizationUpdate.createDataVector(builder, vizData)
const payload = VisualizationUpdate.createVisualizationUpdate(
builder,
visualizationContextOffset,
dataOffset,
)
const rootTable = OutboundMessage.createOutboundMessage(
builder,
createMessageId,
null, // correlationId
OutboundPayload.VISUALIZATION_UPDATE,
payload,
)
sendData(builder.finish(rootTable).toArrayBuffer())
}
let sendData: ((data: string | Blob | ArrayBufferLike | ArrayBufferView) => void) | undefined
export const mockLSHandler: MockTransportData = async (method, data, transport) => {
switch (method) {
case 'session/initProtocolConnection':
return {
contentRoots: [{ type: 'Project', id: mockProjectId }],
} satisfies response.InitProtocolConnection
case 'executionContext/create': {
const data_ = data as {
contextId: ContextId
}
setTimeout(
() => transport.emit('executionContext/executionComplete', { contextId: data_.contextId }),
100,
)
return {
contextId: data_.contextId,
}
}
case 'executionContext/attachVisualization': {
const data_ = data as {
visualizationId: Uuid
expressionId: ExpressionId
visualizationConfig: VisualizationConfiguration
}
visualizations.set(data_.visualizationId, data_.visualizationConfig)
visualizationExprIds.set(data_.visualizationId, data_.expressionId)
sendVizData(data_.visualizationId, data_.visualizationConfig)
return
}
case 'executionContext/detachVisualization': {
const data_ = data as {
visualizationId: Uuid
expressionId: ExpressionId
contextId: ContextId
}
visualizations.delete(data_.visualizationId)
visualizationExprIds.delete(data_.visualizationId)
return
}
case 'executionContext/modifyVisualization': {
const data_ = data as {
visualizationId: Uuid
visualizationConfig: VisualizationConfiguration
}
visualizations.set(data_.visualizationId, data_.visualizationConfig)
sendVizData(data_.visualizationId, data_.visualizationConfig)
return
}
case 'search/getSuggestionsDatabase':
return {
entries: mockDb.map((suggestion, id) => ({
id,
suggestion: suggestion as SuggestionEntry,
})),
currentVersion: 1,
} satisfies response.GetSuggestionsDatabase
case 'runtime/getComponentGroups':
return { componentGroups: placeholderGroups() } satisfies response.GetComponentGroups
case 'executionContext/push':
case 'executionContext/pop':
case 'executionContext/recompute':
case 'capability/acquire':
return {}
case 'file/list': {
const data_ = data as { path: Path }
if (!data_.path) return Promise.reject(`'path' parameter missing in '${method}'`)
if (data_.path.rootId !== mockProjectId)
return Promise.reject(
`Only the project's 'rootId' is supported, got '${data_.path.rootId}'`,
)
let child: FileTree | string | ArrayBuffer | undefined = fileTree
if (child) {
for (const segment of data_.path.segments) {
child = child?.[segment]
if (!child || typeof child === 'string' || child instanceof ArrayBuffer) break
}
}
if (!child) return Promise.reject(`Folder '/${data_.path.segments.join('/')}' not found.`)
if (typeof child === 'string' || child instanceof ArrayBuffer)
return Promise.reject(`File '/${data_.path.segments.join('/')}' is not a folder.`)
return {
paths: Object.entries(child).map(([name, entry]) => ({
type: typeof entry === 'string' || entry instanceof ArrayBuffer ? 'File' : 'Directory',
name,
path: { rootId: data_.path.rootId, segments: [...data_.path.segments, name] },
})),
} satisfies response.FileList
}
default:
return Promise.reject(`Method '${method}' not mocked`)
}
}
const directory = mockFsDirectoryHandle(fileTree, '(root)')
export const mockDataHandler: WebSocketHandler = originalMockDataWSHandler(
async (segments) => {
if (!segments.length) return
let file
try {
let dir = directory
for (const segment of segments.slice(0, -1)) {
dir = await dir.getDirectoryHandle(segment)
}
const fileHandle = await dir.getFileHandle(segments.at(-1)!)
file = await fileHandle.getFile()
} catch {
return
}
return await file?.arrayBuffer()
},
(send) => (sendData = send),
)

View File

@ -0,0 +1,63 @@
declare const projectIdBrand: unique symbol
/** A name of a project. */
export type ProjectId = string & { [projectIdBrand]: never }
declare const projectNameBrand: unique symbol
/** A name of a project. */
export type ProjectName = string & { [projectNameBrand]: never }
declare const utcDateTimeBrand: unique symbol
/** A name of a project. */
export type UTCDateTime = string & { [utcDateTimeBrand]: never }
/** A value specifying the hostname and port of a socket. */
export interface IpWithSocket {
host: string
port: number
}
export interface OpenProject {
engineVersion: string
languageServerJsonAddress: IpWithSocket
languageServerBinaryAddress: IpWithSocket
projectName: ProjectName
projectNormalizedName: string
projectNamespace: string
}
/** Details for a project. */
export interface ProjectMetadata {
name: ProjectName
namespace: string
id: ProjectId
engineVersion: string | null
created: UTCDateTime
lastOpened: UTCDateTime | null
}
export const projects = new Map<string, ProjectMetadata>()
const openProjects = new Set<string>()
export const methods = {
async 'project/open'(id) {
openProjects.add(id)
const project = projects.get(id)
if (!project) throw new Error(`Cannot find project with ID ${id}.`)
return {
projectName: project.name,
projectNormalizedName: project.name,
projectNamespace: project.namespace,
languageServerJsonAddress: { host: '127.0.0.1', port: 30000 },
languageServerBinaryAddress: { host: '127.0.0.1', port: 30001 },
engineVersion: '',
} satisfies OpenProject
},
async 'project/close'(id) {
openProjects.delete(id)
return {}
},
async 'project/list'(numberOfProjects) {
const projectsList = Array.from(projects.values())
return {
projects: numberOfProjects != null ? projectsList.slice(0, numberOfProjects) : projectsList,
}
},
} satisfies Record<string, (...params: any[]) => Promise<unknown>>

View File

@ -0,0 +1,83 @@
{
"$schema": "https://meta.open-rpc.org/",
"openrpc": "1.2.6",
"info": {
"title": "Enso Project Manager",
"description": "Manages (lists, opens, closes) Enso projects",
"version": "1.0.0"
},
"methods": [
{
"name": "project/open",
"params": [
{ "name": "projectId", "schema": { "type": "string" } },
{
"name": "missingComponentAction",
"schema": { "enum": ["Fail", "Install", "ForceInstallBroken"] }
}
],
"result": {
"name": "openProject",
"schema": {
"type": "object",
"properties": {
"engineVersion": { "type": "string" },
"languageServerJsonAddress": {
"type": "object",
"properties": {
"host": { "type": "string" },
"port": { "type": "number" }
}
},
"languageServerBinaryAddress": {
"type": "object",
"properties": {
"host": { "type": "string" },
"port": { "type": "number" }
}
},
"projectName": { "type": "string" },
"projectNormalizedName": { "type": "string" },
"projectNamespace": { "type": "string" }
}
}
}
},
{
"name": "project/close",
"params": [{ "name": "projectId", "schema": { "type": "string" } }],
"result": {
"name": "closeProject",
"schema": { "type": "null" }
}
},
{
"name": "project/list",
"params": [
{ "name": "numberOfProjects", "schema": { "type": "integer" }, "required": false }
],
"result": {
"name": "listProject",
"schema": {
"type": "object",
"properties": {
"projects": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"namespace": { "type": "string" },
"id": { "type": "string" },
"engineVersion": { "anyOf": [{ "type": "string" }, { "type": "null" }] },
"created": { "type": "string" },
"lastOpened": { "anyOf": [{ "type": "string" }, { "type": "null" }] }
}
}
}
}
}
}
}
]
}

38
app/gui2/e2e/setup.ts Normal file
View File

@ -0,0 +1,38 @@
import { Server } from '@open-rpc/server-js'
import * as random from 'lib0/random.js'
import {
methods as pmMethods,
projects,
type ProjectId,
type ProjectName,
type UTCDateTime,
} from './mockProjectManager'
import pmSpec from './pm-openrpc.json' assert { type: 'json' }
export default function setup() {
const pm = new Server({
transportConfigs: [
{
type: 'WebSocketTransport',
options: {
id: 'websocket',
udp: true,
ipv6: true,
port: 30535,
middleware: [],
},
},
],
openrpcDocument: pmSpec as typeof pmSpec & { openrpc: never },
methodMapping: pmMethods,
})
pm.start()
projects.set('mock project id 0001', {
id: random.uuidv4() as ProjectId,
created: new Date().toISOString() as UTCDateTime,
lastOpened: new Date().toISOString() as UTCDateTime,
engineVersion: '',
name: 'Mock Project Name' as ProjectName,
namespace: 'local',
})
}

View File

@ -1,4 +0,0 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"include": ["./**/*"]
}

View File

@ -1,8 +0,0 @@
import { expect, test } from '@playwright/test'
// See here how to get started:
// https://playwright.dev/docs/intro
test('visits the app root url', async ({ page }) => {
await page.goto('/')
await expect(page.locator('div.greetings > h1')).toHaveText('You did it!')
})

View File

@ -64,16 +64,18 @@
"y-textarea": "^1.0.0",
"y-websocket": "^1.5.0",
"yjs": "^13.6.7",
"zod": "^3.22.2"
"zod": "^3.22.4"
},
"devDependencies": {
"@danmarshall/deckgl-typings": "^4.9.28",
"@eslint/eslintrc": "^2.1.2",
"@eslint/js": "^8.49.0",
"@histoire/plugin-vue": "^0.17.1",
"@playwright/test": "^1.37.0",
"@open-rpc/server-js": "^1.9.4",
"@playwright/test": "^1.40.0",
"@rushstack/eslint-patch": "^1.3.2",
"@tsconfig/node18": "^18.2.0",
"@types/css.escape": "^1.5.2",
"@types/culori": "^2.0.1",
"@types/d3": "^7.4.0",
"@types/hash-sum": "^1.0.0",
@ -92,6 +94,7 @@
"@vue/test-utils": "^2.4.1",
"@vue/tsconfig": "^0.4.0",
"change-case": "^4.1.2",
"css.escape": "^1.5.1",
"d3": "^7.4.0",
"esbuild": "^0.19.3",
"eslint": "^8.49.0",
@ -100,6 +103,7 @@
"hash-wasm": "^4.10.0",
"histoire": "^0.17.2",
"jsdom": "^22.1.0",
"playwright": "^1.39.0",
"postcss-nesting": "^12.0.1",
"prettier": "^3.0.0",
"prettier-plugin-organize-imports": "^3.2.3",

View File

@ -1,112 +1,90 @@
import type { PlaywrightTestConfig } from '@playwright/test'
import { devices } from '@playwright/test'
/** @file Playwright browser testing configuration. */
/** Note that running Playwright in CI poses a number of issues:
* - `backdrop-filter: blur` is disabled, due to issues with Chromium's `--disable-gpu` flag
* (see below).
* - System validation dialogs are not reliable between computers, as they may have different
* default fonts. */
import { defineConfig } from '@playwright/test'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
const DEBUG = process.env.DEBUG_E2E === 'true'
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
export default defineConfig({
globalSetup: './e2e/setup.ts',
testDir: './e2e',
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
...(process.env.CI ? { workers: 1 } : {}),
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:5173',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
/* Only on CI systems run the tests headless */
headless: !!process.env.CI,
expect: {
timeout: 5000,
toHaveScreenshot: { threshold: 0 },
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
use: {
headless: !DEBUG,
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
...(DEBUG
? {}
: {
launchOptions: {
ignoreDefaultArgs: ['--headless'],
args: [
// Much closer to headful Chromium than classic headless.
'--headless=new',
// Required for `backdrop-filter: blur` to work.
'--use-angle=swiftshader',
// FIXME: `--disable-gpu` disables `backdrop-filter: blur`, which is not handled by
// the software (CPU) compositor. This SHOULD be fixed eventually, but this flag
// MUST stay as CI does not have a GPU.
'--disable-gpu',
// Fully disable GPU process.
'--disable-software-rasterizer',
// Disable text subpixel antialiasing.
'--font-render-hinting=none',
'--disable-skia-runtime-opts',
'--disable-system-font-check',
'--disable-font-subpixel-positioning',
'--disable-lcd-text',
],
},
}),
},
// projects: [
// {
// name: 'chromium',
// use: {
// ...devices['Desktop Chrome'],
// },
// },
// {
// name: 'firefox',
// use: {
// ...devices['Desktop Firefox'],
// },
// },
// {
// name: 'webkit',
// use: {
// ...devices['Desktop Safari'],
// },
// },
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
// ],
webServer: {
/**
* Use the dev server by default for faster feedback loop.
* Use the preview server on CI for more realistic testing.
Playwright will re-use the local server if there is already a dev-server running.
*/
command: process.env.CI ? 'vite preview --port 5173' : 'vite dev',
// The command-line flag is required
command: process.env.CI ? 'E2E=true vite preview --port 5173' : 'E2E=true vite dev',
port: 5173,
reuseExistingServer: !process.env.CI,
},
}
export default config
})

File diff suppressed because it is too large Load Diff

View File

@ -25,6 +25,8 @@ import {
WriteBytesCommand,
WriteBytesReply,
WriteFileCommand,
type AnyInboundPayload,
type Offset,
type Table,
} from './binaryProtocol'
import { uuidFromBits, uuidToBits } from './uuid'
@ -104,7 +106,7 @@ export class DataServer extends ObservableV2<DataServerEvents> {
protected send<T = void>(
builder: Builder,
payloadType: InboundPayload,
payloadOffset: number,
payloadOffset: Offset<AnyInboundPayload>,
): Promise<T> {
const messageUuid = random.uuidv4()
const rootTable = InboundMessage.createInboundMessage(

View File

@ -0,0 +1,207 @@
import { createSHA3 } from 'hash-wasm'
import * as random from 'lib0/random'
import {
Builder,
ByteBuffer,
ChecksumBytesCommand,
ChecksumBytesReply,
EnsoDigest,
EnsoUUID,
ErrorPayload,
Error as ErrorResponse,
FileContentsReply,
InboundMessage,
InboundPayload,
InitSessionCommand,
None,
Null,
OutboundMessage,
OutboundPayload,
Path,
ReadBytesCommand,
ReadBytesReply,
ReadFileCommand,
Success,
WriteBytesCommand,
WriteFileCommand,
type AnyOutboundPayload,
type Offset,
type Table,
} from '../binaryProtocol'
import { LanguageServerErrorCode } from '../languageServerTypes'
import { uuidToBits } from '../uuid'
const sha3 = createSHA3(224)
function pathSegments(path: Path) {
return Array.from({ length: path.segmentsLength() }, (_, i) => path.segments(i))
}
function createError(builder: Builder, code: LanguageServerErrorCode, message: string) {
const messageOffset = builder.createString(message)
return {
type: OutboundPayload.ERROR,
offset: ErrorResponse.createError(builder, code, messageOffset, ErrorPayload.NONE, Null),
}
}
function createMessageId(builder: Builder) {
const messageUuid = random.uuidv4()
const [leastSigBits, mostSigBits] = uuidToBits(messageUuid)
return EnsoUUID.createEnsoUUID(builder, leastSigBits, mostSigBits)
}
function createCorrelationId(messageId: EnsoUUID) {
return (builder: Builder) =>
EnsoUUID.createEnsoUUID(builder, messageId.leastSigBits(), messageId.mostSigBits())
}
const PAYLOAD_CONSTRUCTOR = {
[InboundPayload.NONE]: None,
[InboundPayload.INIT_SESSION_CMD]: InitSessionCommand,
[InboundPayload.WRITE_FILE_CMD]: WriteFileCommand,
[InboundPayload.READ_FILE_CMD]: ReadFileCommand,
[InboundPayload.WRITE_BYTES_CMD]: WriteBytesCommand,
[InboundPayload.READ_BYTES_CMD]: ReadBytesCommand,
[InboundPayload.CHECKSUM_BYTES_CMD]: ChecksumBytesCommand,
} satisfies Record<InboundPayload, new () => Table>
export function mockDataWSHandler(
readFile: (segments: string[]) => Promise<ArrayBuffer | null | undefined>,
cb?: (send: (data: string | Blob | ArrayBufferLike | ArrayBufferView) => void) => void,
) {
let sentSend = false
return async (
message: string | Blob | ArrayBufferLike | ArrayBufferView,
send: (data: string | Blob | ArrayBufferLike | ArrayBufferView) => void,
) => {
if (!sentSend) cb?.(send)
sentSend = true
if (!(message instanceof ArrayBuffer)) return
const binaryMessage = InboundMessage.getRootAsInboundMessage(new ByteBuffer(message))
const payloadType = binaryMessage.payloadType()
const payload = binaryMessage.payload(new PAYLOAD_CONSTRUCTOR[payloadType]())
if (!payload) return
const builder = new Builder()
let response: { type: OutboundPayload; offset: Offset<AnyOutboundPayload> } | undefined
switch (payloadType) {
case InboundPayload.NONE: {
response = {
type: OutboundPayload.NONE,
offset: Null,
}
break
}
case InboundPayload.INIT_SESSION_CMD: {
response = {
type: OutboundPayload.SUCCESS,
offset: Success.createSuccess(builder),
}
break
}
case InboundPayload.WRITE_FILE_CMD: {
response = createError(
builder,
LanguageServerErrorCode.AccessDenied,
'Cannot WriteFile to a read-only mock.',
)
break
}
case InboundPayload.READ_FILE_CMD: {
const payload_ = payload as ReadFileCommand
const path = payload_.path()
if (!path) {
response = createError(builder, LanguageServerErrorCode.NotFile, 'Invalid Path')
break
}
const file = await readFile(pathSegments(path))
if (!file) {
response = createError(builder, LanguageServerErrorCode.FileNotFound, 'File not found')
break
}
const contentOffset = builder.createString(file)
response = {
type: OutboundPayload.FILE_CONTENTS_REPLY,
offset: FileContentsReply.createFileContentsReply(builder, contentOffset),
}
break
}
case InboundPayload.WRITE_BYTES_CMD: {
response = createError(
builder,
LanguageServerErrorCode.AccessDenied,
'Cannot WriteBytes to a read-only mock.',
)
break
}
case InboundPayload.READ_BYTES_CMD: {
const payload_ = payload as ReadBytesCommand
const segment = payload_.segment()
const path = segment && segment.path()
if (!path) {
response = createError(
builder,
LanguageServerErrorCode.NotFile,
'Invalid FileSegment or Path',
)
break
}
const file = await readFile(pathSegments(path))
if (!file) {
response = createError(builder, LanguageServerErrorCode.FileNotFound, 'File not found')
break
}
const start = Number(segment.byteOffset())
const slice = file.slice(start, start + Number(segment.length()))
const contentOffset = builder.createString(slice)
const digest = (await sha3).init().update(new Uint8Array(slice)).digest('binary')
const checksumBytesOffset = EnsoDigest.createBytesVector(builder, digest)
const checksumOffset = EnsoDigest.createEnsoDigest(builder, checksumBytesOffset)
response = {
type: OutboundPayload.READ_BYTES_REPLY,
offset: ReadBytesReply.createReadBytesReply(builder, checksumOffset, contentOffset),
}
break
}
case InboundPayload.CHECKSUM_BYTES_CMD: {
const payload_ = payload as ChecksumBytesCommand
const segment = payload_.segment()
const path = segment && segment.path()
if (!path) {
response = createError(
builder,
LanguageServerErrorCode.NotFile,
'Invalid FileSegment or Path',
)
break
}
const file = await readFile(pathSegments(path))
if (!file) {
response = createError(builder, LanguageServerErrorCode.FileNotFound, 'File not found')
break
}
const start = Number(segment.byteOffset())
const slice = file.slice(start, start + Number(segment.length()))
const digest = (await sha3).init().update(new Uint8Array(slice)).digest('binary')
const bytesOffset = EnsoDigest.createBytesVector(builder, digest)
const checksumOffset = EnsoDigest.createEnsoDigest(builder, bytesOffset)
response = {
type: OutboundPayload.CHECKSUM_BYTES_REPLY,
offset: ChecksumBytesReply.createChecksumBytesReply(builder, checksumOffset),
}
break
}
}
if (!response) return
const correlationUuid = binaryMessage.messageId()
if (!correlationUuid) return
const rootTable = OutboundMessage.createOutboundMessage(
builder,
createMessageId,
createCorrelationId(correlationUuid),
response.type,
response.offset,
)
send(builder.finish(rootTable).toArrayBuffer())
}
}

View File

@ -46,6 +46,7 @@ const messageReconnectTimeout = 30000
const setupWS = (wsclient: WebsocketClient, ws?: WebSocket | null | undefined) => {
if (wsclient.shouldConnect && (wsclient.ws === null || ws)) {
// deepcode ignore MissingClose: This is closed by `WebsocketClient` below.
const websocket = ws ?? new WebSocket(wsclient.url)
const binaryType = wsclient.binaryType
let pingTimeout: any = null

View File

@ -18,18 +18,21 @@ const emit = defineEmits<{
<ToggleIcon
icon="no_auto_replay"
class="icon-container button no-auto-evaluate-button"
:alt="`${props.isAutoEvaluationDisabled ? 'Enable' : 'Disable'} auto-evaluation`"
:modelValue="props.isAutoEvaluationDisabled"
@update:modelValue="emit('update:isAutoEvaluationDisabled', $event)"
/>
<ToggleIcon
icon="docs"
class="icon-container button docs-button"
:alt="`${props.isDocsVisible ? 'Hide' : 'Show'} documentation`"
:modelValue="props.isDocsVisible"
@update:modelValue="emit('update:isDocsVisible', $event)"
/>
<ToggleIcon
icon="eye"
class="icon-container button visualization-button"
:alt="`${props.isVisualizationVisible ? 'Hide' : 'Show'} visualization`"
:modelValue="props.isVisualizationVisible"
@update:modelValue="emit('update:isVisualizationVisible', $event)"
/>

View File

@ -369,7 +369,7 @@ const handler = componentBrowserBindings.handler({
@wheel.stop.passive
@scroll="updateScroll"
>
<div class="list-variant" style="">
<div class="list-variant">
<div
v-for="item in visibleComponents"
:key="item.component.suggestionId"

View File

@ -8,7 +8,10 @@ import {
previousNodeDictatedPlacement,
type Environment,
} from '@/components/ComponentBrowser/placement.ts'
import GraphEdges from '@/components/GraphEditor/GraphEdges.vue'
import GraphNodes from '@/components/GraphEditor/GraphNodes.vue'
import { Uploader, uploadedExpression } from '@/components/GraphEditor/upload'
import GraphMouse from '@/components/GraphMouse.vue'
import PlusButton from '@/components/PlusButton.vue'
import TopBar from '@/components/TopBar.vue'
import { provideGraphNavigator } from '@/providers/graphNavigator'
@ -26,9 +29,6 @@ import { Vec2 } from '@/util/vec2'
import * as set from 'lib0/set'
import type { ExprId, NodeMetadata } from 'shared/yjsModel.ts'
import { computed, onMounted, ref, watch } from 'vue'
import GraphEdges from './GraphEditor/GraphEdges.vue'
import GraphNodes from './GraphEditor/GraphNodes.vue'
import GraphMouse from './GraphMouse.vue'
const EXECUTION_MODES = ['design', 'live']
// Difference in position between the component browser and a node for the input of the component browser to
@ -436,7 +436,7 @@ function handleNodeOutputPortDoubleClick(id: ExprId) {
<template>
<div
ref="viewportNode"
class="GraphEditor"
class="GraphEditor viewport"
:class="{ draggingEdge: graphStore.unconnectedEdge != null }"
:style="groupColors"
v-on.="graphNavigator.events"

View File

@ -327,7 +327,7 @@ function pathElements(junctions: JunctionPoints): { start: Vec2; elements: Eleme
const start = junctions.points[0]
if (start == null) return { start: Vec2.Zero, elements: [] }
let prev = start
junctions.points.slice(1).map((j, i) => {
junctions.points.slice(1).forEach((j, i) => {
const d = j.sub(prev)
const radius = Math.min(junctions.maxRadius, Math.abs(d.x), Math.abs(d.y))
const signX = Math.sign(d.x)

View File

@ -271,6 +271,7 @@ const handlePortClick = useDoubleClick(
--output-port-overlap: 0.2px;
--output-port-hover-width: 8px;
}
.outputPort,
.outputPortHoverArea {
x: calc(0px - var(--output-port-width) / 2);
@ -409,6 +410,7 @@ const handlePortClick = useDoubleClick(
.GraphNode.selected .selection:hover:before {
opacity: 0.3;
}
.binding {
user-select: none;
margin-right: 10px;

View File

@ -56,9 +56,8 @@ export class Uploader {
position: Vec2,
stackItem: StackItem,
): Promise<Uploader> {
const projectRootId = await contentRoots.then((roots) =>
roots.find((root) => root.type == 'Project'),
)
const roots = await contentRoots
const projectRootId = roots.find((root) => root.type == 'Project')
if (!projectRootId) throw new Error('Unable to find project root, uploading not possible.')
const instance = new Uploader(
await rpc,

View File

@ -118,7 +118,7 @@ const resizeBottomRight = usePointer((pos, _, type) => {
@pointerdown.stop="config.hide()"
@click="config.hide()"
>
<SvgIcon class="icon" name="eye" />
<SvgIcon class="icon" name="eye" alt="Hide visualization" />
</button>
</div>
<div class="toolbar">
@ -127,7 +127,11 @@ const resizeBottomRight = usePointer((pos, _, type) => {
@pointerdown.stop="(config.fullscreen = !config.fullscreen), blur($event)"
@click.prevent="!isClick($event) && (config.fullscreen = !config.fullscreen)"
>
<SvgIcon class="icon" :name="config.fullscreen ? 'exit_fullscreen' : 'fullscreen'" />
<SvgIcon
class="icon"
:name="config.fullscreen ? 'exit_fullscreen' : 'fullscreen'"
:alt="config.fullscreen ? 'Exit fullscreen' : 'Enter fullscreen'"
/>
</button>
<div class="icon-container">
<button
@ -135,7 +139,13 @@ const resizeBottomRight = usePointer((pos, _, type) => {
@pointerdown.stop="isSelectorVisible = !isSelectorVisible"
@click.prevent="!isClick($event) && (isSelectorVisible = !isSelectorVisible)"
>
<SvgIcon class="icon" :name="config.icon ?? 'columns_increasing'" />
<SvgIcon
class="icon"
:name="config.icon ?? 'columns_increasing'"
:alt="
isSelectorVisible ? 'Hide visualization selector' : 'Show visualization selector'
"
/>
</button>
<VisualizationSelector
v-if="isSelectorVisible"

View File

@ -2,6 +2,10 @@
export const name = 'Geo Map'
export const icon = 'compass'
export const inputType = 'Standard.Table.Data.Table.Table'
export const defaultPreprocessor = [
'Standard.Visualization.Geo_Map',
'process_to_json_text',
] as const
export const scripts = [
// mapbox-gl does not have an ESM release.
'https://api.tiles.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.js',
@ -86,19 +90,14 @@ declare var deck: typeof import('deck.gl')
import SvgIcon from '@/components/SvgIcon.vue'
import { VisualizationContainer } from '@/util/visualizationBuiltins'
import type { Deck } from 'deck.gl'
import { computed, onMounted, onUnmounted, ref, watchPostEffect } from 'vue'
import { computed, onUnmounted, ref, watchPostEffect } from 'vue'
const props = defineProps<{ data: Data }>()
const emit = defineEmits<{
'update:preprocessor': [module: string, method: string, ...args: string[]]
}>()
/** GeoMap Visualization. */
/**
* Mapbox API access token.
* All the limits of API are listed here: https://docs.mapbox.com/api/#rate-limits
*/
/** Mapbox API access token.
* All the limits of API are listed here: https://docs.mapbox.com/api/#rate-limits */
const TOKEN =
'pk.eyJ1IjoiZW5zby1vcmciLCJhIjoiY2tmNnh5MXh2MGlyOTJ5cWdubnFxbXo4ZSJ9.3KdAcCiiXJcSM18nwk09-Q'
const SCATTERPLOT_LAYER = 'Scatterplot_Layer'
@ -142,11 +141,6 @@ watchPostEffect(() => {
}
})
onMounted(() => {
dataPoints.value = []
emit('update:preprocessor', 'Standard.Visualization.Geo_Map', 'process_to_json_text')
})
onUnmounted(() => deckgl.value?.finalize())
/**

View File

@ -55,14 +55,12 @@ interface LsUrls {
function resolveLsUrl(config: GuiConfig): LsUrls {
const engine = config.engine
if (engine == null) throw new Error('Missing engine configuration')
if (engine.rpcUrl != null && engine.dataUrl != null) {
return {
rpcUrl: engine.rpcUrl,
dataUrl: engine.dataUrl,
}
}
throw new Error('Incomplete engine configuration')
}
@ -77,7 +75,6 @@ async function initializeLsRpcConnection(
const requestManager = new RequestManager([transport])
const client = new Client(requestManager)
const connection = new LanguageServer(client)
const initialization = await lsRpcWithRetries(() => connection.initProtocolConnection(clientId), {
onBeforeRetry: (error, _, delay) => {
console.warn(

View File

@ -108,6 +108,7 @@ export async function compile(path: string, projectRoot: Opt<Uuid>, data: DataSe
worker.addEventListener(
'message',
async (
// deepcode ignore InsufficientPostmessageValidation: This event is from a Worker, not another page.
event: MessageEvent<
// === Responses ===
| CompilationResultResponse
@ -157,6 +158,7 @@ export async function compile(path: string, projectRoot: Opt<Uuid>, data: DataSe
switch (url.protocol) {
case 'http:':
case 'https:': {
// deepcode ignore Ssrf: This is a frontend.
const response = await fetch(url)
if (response.ok) {
postMessage<FetchResultWorkerResponse>(worker_, {

View File

@ -1,11 +1,6 @@
function error(message: string): never {
throw new Error(message)
}
let _measureContext: CanvasRenderingContext2D | undefined
function getMeasureContext() {
return (_measureContext ??=
document.createElement('canvas').getContext('2d') ?? error('Could not get canvas 2D context.'))
return (_measureContext ??= document.createElement('canvas').getContext('2d')!)
}
/** Helper function to get text width to make sure that labels on the x axis do not overlap,

View File

@ -1,2 +0,0 @@
/** Removes `readonly` from all keys in a type. UNSAFE. */
export type UnsafeMutable<T> = { -readonly [K in keyof T]: T[K] }

View File

@ -1,4 +1,3 @@
import { Err, Ok, ResultError, rejectionToResult, type Result } from '@/util/result'
import { WebSocketTransport } from '@open-rpc/client-js'
import type {
IJSONRPCNotificationResponse,
@ -10,6 +9,7 @@ import { wait } from 'lib0/promise'
import { LsRpcError } from 'shared/languageServer'
import type { Notifications } from 'shared/languageServerTypes'
import { WebsocketClient } from 'shared/websocket'
import { Err, Ok, ResultError, rejectionToResult, type Result } from './result'
export interface BackoffOptions<E> {
maxRetries?: number
@ -101,8 +101,8 @@ export function createWebsocketClient(
}
}
interface MockTransportData {
(method: string, params: any, transport: MockTransport): Promise<any>
export interface MockTransportData<Methods extends string = string> {
(method: Methods, params: any, transport: MockTransport): Promise<any>
}
export class MockTransport extends Transport {
@ -111,8 +111,8 @@ export class MockTransport extends Transport {
super()
}
static addMock(name: string, data: MockTransportData) {
MockTransport.mocks.set(name, data)
static addMock<Methods extends string>(name: string, data: MockTransportData<Methods>) {
MockTransport.mocks.set(name, data as any)
}
connect(): Promise<any> {
return Promise.resolve()
@ -168,6 +168,7 @@ export class MockWebSocket extends EventTarget implements WebSocket {
super()
this.addEventListener('open', (ev) => this.onopen?.(ev))
this.addEventListener('close', (ev) => this.onclose?.(ev as CloseEvent))
// deepcode ignore InsufficientPostmessageValidation: This is not a `postMessage`.
this.addEventListener('message', (ev) => this.onmessage?.(ev as MessageEvent<any>))
this.addEventListener('error', (ev) => this.onerror?.(ev))
setTimeout(() => this.dispatchEvent(new Event('open')), 0)

View File

@ -3,7 +3,7 @@ import GraphEditor from '@/components/GraphEditor.vue'
</script>
<template>
<GraphEditor></GraphEditor>
<GraphEditor />
</template>
<style scoped></style>

View File

@ -2,35 +2,8 @@
import { useProjectStore } from '@/stores/project'
import { mockFsDirectoryHandle } from '@/util/convert/fsAccess'
import { MockWebSocket, type WebSocketHandler } from '@/util/net'
import { createSHA3 } from 'hash-wasm'
import * as random from 'lib0/random'
import {
Builder,
ByteBuffer,
ChecksumBytesCommand,
ChecksumBytesReply,
EnsoDigest,
EnsoUUID,
ErrorPayload,
Error as ErrorResponse,
FileContentsReply,
InboundMessage,
InboundPayload,
InitSessionCommand,
None,
OutboundMessage,
OutboundPayload,
Path,
ReadBytesCommand,
ReadBytesReply,
ReadFileCommand,
Success,
WriteBytesCommand,
WriteFileCommand,
type Table,
} from 'shared/binaryProtocol'
import { LanguageServerErrorCode, type Path as LSPath } from 'shared/languageServerTypes'
import { uuidToBits } from 'shared/uuid'
import { mockDataWSHandler } from 'shared/dataServer/mock'
import { type Path as LSPath } from 'shared/languageServerTypes'
import { watchEffect } from 'vue'
const projectStore = useProjectStore()
@ -46,54 +19,6 @@ const props = defineProps<{
prefix?: string[] | undefined
}>()
const PAYLOAD_CONSTRUCTOR = {
[InboundPayload.NONE]: None,
[InboundPayload.INIT_SESSION_CMD]: InitSessionCommand,
[InboundPayload.WRITE_FILE_CMD]: WriteFileCommand,
[InboundPayload.READ_FILE_CMD]: ReadFileCommand,
[InboundPayload.WRITE_BYTES_CMD]: WriteBytesCommand,
[InboundPayload.READ_BYTES_CMD]: ReadBytesCommand,
[InboundPayload.CHECKSUM_BYTES_CMD]: ChecksumBytesCommand,
} satisfies Record<InboundPayload, new () => Table>
const sha3 = createSHA3(224)
function pathSegments(path: Path) {
return Array.from({ length: path.segmentsLength() }, (_, i) => path.segments(i))
}
async function readFile(dir: FileSystemDirectoryHandle, segments: string[]) {
if (!segments.length) return
try {
for (const segment of segments.slice(0, -1)) {
dir = await dir.getDirectoryHandle(segment)
}
const file = await dir.getFileHandle(segments.at(-1)!)
return await file.getFile()
} catch {
return
}
}
function createError(builder: Builder, code: LanguageServerErrorCode, message: string) {
const messageOffset = builder.createString(message)
return {
type: OutboundPayload.ERROR,
offset: ErrorResponse.createError(builder, code, messageOffset, ErrorPayload.NONE, 0),
}
}
function createMessageId(builder: Builder) {
const messageUuid = random.uuidv4()
const [leastSigBits, mostSigBits] = uuidToBits(messageUuid)
return EnsoUUID.createEnsoUUID(builder, leastSigBits, mostSigBits)
}
function createCorrelationId(messageId: EnsoUUID) {
return (builder: Builder) =>
EnsoUUID.createEnsoUUID(builder, messageId.leastSigBits(), messageId.mostSigBits())
}
let resolveDataWsHandler: ((handler: WebSocketHandler) => void) | undefined
let dataWsHandler: Promise<WebSocketHandler> = new Promise((resolve) => {
resolveDataWsHandler = resolve
@ -145,134 +70,24 @@ watchEffect(async (onCleanup) => {
ls.emit('file/event', [{ kind: 'Removed', path }]),
)
})
setDataWsHandler(async (message, send) => {
if (!(message instanceof ArrayBuffer)) return
const binaryMessage = InboundMessage.getRootAsInboundMessage(new ByteBuffer(message))
const payloadType = binaryMessage.payloadType()
const payload = binaryMessage.payload(new PAYLOAD_CONSTRUCTOR[payloadType]())
if (!payload) return
const builder = new Builder()
let response: { type: OutboundPayload; offset: number } | undefined
switch (payloadType) {
case InboundPayload.NONE: {
response = {
type: OutboundPayload.NONE,
offset: 0,
setDataWsHandler(
mockDataWSHandler(async (segments) => {
segments = segments.slice(prefixLength)
if (!segments.length) return
let file
try {
let dir = directory
for (const segment of segments.slice(0, -1)) {
dir = await dir.getDirectoryHandle(segment)
}
break
const fileHandle = await dir.getFileHandle(segments.at(-1)!)
file = await fileHandle.getFile()
} catch {
return
}
case InboundPayload.INIT_SESSION_CMD: {
response = {
type: OutboundPayload.SUCCESS,
offset: Success.createSuccess(builder),
}
break
}
case InboundPayload.WRITE_FILE_CMD: {
response = createError(
builder,
LanguageServerErrorCode.AccessDenied,
'Cannot write to a read-only mock.',
)
break
}
case InboundPayload.READ_FILE_CMD: {
const payload_ = payload as ReadFileCommand
const path = payload_.path()
if (!path) {
response = createError(builder, LanguageServerErrorCode.NotFile, 'Invalid Path')
break
}
const file = await readFile(directory, pathSegments(path).slice(prefixLength))
if (!file) {
response = createError(builder, LanguageServerErrorCode.FileNotFound, 'File not found')
break
}
const contentOffset = builder.createString(await file.arrayBuffer())
response = {
type: OutboundPayload.FILE_CONTENTS_REPLY,
offset: FileContentsReply.createFileContentsReply(builder, contentOffset),
}
break
}
case InboundPayload.WRITE_BYTES_CMD: {
response = createError(
builder,
LanguageServerErrorCode.AccessDenied,
'Cannot write to a read-only mock.',
)
break
}
case InboundPayload.READ_BYTES_CMD: {
const payload_ = payload as ReadBytesCommand
const segment = payload_.segment()
const path = segment && segment.path()
if (!path) {
response = createError(
builder,
LanguageServerErrorCode.NotFile,
'Invalid FileSegment or Path',
)
break
}
const file = await readFile(directory, pathSegments(path).slice(prefixLength))
if (!file) {
response = createError(builder, LanguageServerErrorCode.FileNotFound, 'File not found')
break
}
const start = Number(segment.byteOffset())
const slice = await file.slice(start, start + Number(segment.length())).arrayBuffer()
const contentOffset = builder.createString(slice)
const digest = (await sha3).init().update(new Uint8Array(slice)).digest('binary')
const checksumBytesOffset = EnsoDigest.createBytesVector(builder, digest)
const checksumOffset = EnsoDigest.createEnsoDigest(builder, checksumBytesOffset)
response = {
type: OutboundPayload.READ_BYTES_REPLY,
offset: ReadBytesReply.createReadBytesReply(builder, checksumOffset, contentOffset),
}
break
}
case InboundPayload.CHECKSUM_BYTES_CMD: {
const payload_ = payload as ChecksumBytesCommand
const segment = payload_.segment()
const path = segment && segment.path()
if (!path) {
response = createError(
builder,
LanguageServerErrorCode.NotFile,
'Invalid FileSegment or Path',
)
break
}
const file = await readFile(directory, pathSegments(path).slice(prefixLength))
if (!file) {
response = createError(builder, LanguageServerErrorCode.FileNotFound, 'File not found')
break
}
const start = Number(segment.byteOffset())
const slice = await file.slice(start, start + Number(segment.length())).arrayBuffer()
const digest = (await sha3).init().update(new Uint8Array(slice)).digest('binary')
const bytesOffset = EnsoDigest.createBytesVector(builder, digest)
const checksumOffset = EnsoDigest.createEnsoDigest(builder, bytesOffset)
response = {
type: OutboundPayload.CHECKSUM_BYTES_REPLY,
offset: ChecksumBytesReply.createChecksumBytesReply(builder, checksumOffset),
}
break
}
}
if (!response) return
const correlationUuid = binaryMessage.messageId()
if (!correlationUuid) return
const rootTable = OutboundMessage.createOutboundMessage(
builder,
createMessageId,
createCorrelationId(correlationUuid),
response.type,
response.offset,
)
send(builder.finish(rootTable).toArrayBuffer())
})
return await file?.arrayBuffer()
}),
)
})
</script>

View File

@ -38,10 +38,7 @@ function applyEdits(module: NonNullable<typeof projectStore.module>, newText: st
})
}
watchEffect(() => {
if (!projectStore.module) return
applyEdits(projectStore.module, props.modelValue)
})
watchEffect(() => projectStore.module && applyEdits(projectStore.module, props.modelValue))
const text = computed(() => projectStore.module?.doc.contents)

View File

@ -5,7 +5,7 @@ import { MockTransport } from '@/util/net'
import type { QualifiedName } from '@/util/qualifiedName'
import { Vec2 } from '@/util/vec2'
import { defineSetupVue3 } from '@histoire/plugin-vue'
import { uuidv4 } from 'lib0/random'
import * as random from 'lib0/random'
import { createPinia } from 'pinia'
import type { LibraryComponentGroup, Uuid, response } from 'shared/languageServerTypes'
import type { SuggestionEntry } from 'shared/languageServerTypes/suggestions'
@ -13,7 +13,7 @@ import { ref } from 'vue'
import mockDb from './mockSuggestions.json' assert { type: 'json' }
import './story.css'
const mockProjectId = uuidv4() as Uuid
const mockProjectId = random.uuidv4() as Uuid
const standardBase = 'Standard.Base' as QualifiedName
export function placeholderGroups(): LibraryComponentGroup[] {

View File

@ -9,6 +9,7 @@ import { defineConfig, type Plugin } from 'vite'
import topLevelAwait from 'vite-plugin-top-level-await'
import * as tailwindConfig from '../ide-desktop/lib/dashboard/tailwind.config'
import { createGatewayServer } from './ydoc-server'
const localServerPort = 8080
const projectManagerUrl = 'ws://127.0.0.1:30535'
// https://vitejs.dev/config/
@ -27,13 +28,16 @@ export default defineConfig({
},
resolve: {
alias: {
...(process.env.E2E === 'true'
? { '/src/main.ts': fileURLToPath(new URL('./e2e/main.ts', import.meta.url)) }
: {}),
shared: fileURLToPath(new URL('./shared', import.meta.url)),
'rust-ffi': fileURLToPath(new URL('./rust-ffi', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
define: {
REDIRECT_OVERRIDE: JSON.stringify('http://localhost:8080'),
REDIRECT_OVERRIDE: JSON.stringify(`http://localhost:${localServerPort}`),
PROJECT_MANAGER_URL: JSON.stringify(projectManagerUrl),
IS_DEV_MODE: JSON.stringify(process.env.NODE_ENV !== 'production'),
CLOUD_ENV:

View File

@ -28,7 +28,6 @@ import BUILD_INFO from '../../../../build.json' assert { type: 'json' }
// =============
/** The parts of the electron-builder configuration that we want to keep configurable.
*
* @see `args` definition below for fields description. */
export interface Arguments {
// The types come from a third-party API and cannot be changed.

View File

@ -18,7 +18,6 @@ import * as paths from './paths'
* to the PM bundle root;
* - `ENSO_BUILD_IDE_BUNDLED_ENGINE_VERSION` - version of the Engine (backend) that is bundled
* along with this client build.
*
* @see bundlerOptions
*/
export function bundlerOptionsFromEnv(): esbuild.BuildOptions {

View File

@ -57,16 +57,16 @@
*
* To prepare the application to handle deep links:
* - Register a custom URL protocol scheme with the OS (c.f., `electron-builder-config.ts`).
* - Define a listener for Electron {@link OPEN_URL_EVENT}s (c.f., {@link initOpenUrlListener}).
* - Define a listener for Electron `OPEN_URL_EVENT`s (c.f., {@link initOpenUrlListener}).
* - Define a listener for {@link ipc.Channel.openDeepLink} events (c.f., `preload.ts`).
*
* Then when the user clicks on a deep link from an external source to the IDE:
* - The OS redirects the user to the application.
* - The application emits an Electron {@link OPEN_URL_EVENT}.
* - The {@link OPEN_URL_EVENT} listener checks if the {@link URL} is a deep link.
* - If the {@link URL} is a deep link, the {@link OPEN_URL_EVENT} listener prevents Electron from
* - The application emits an Electron `OPEN_URL_EVENT`.
* - The `OPEN_URL_EVENT` listener checks if the {@link URL} is a deep link.
* - If the {@link URL} is a deep link, the `OPEN_URL_EVENT` listener prevents Electron from
* handling the event.
* - The {@link OPEN_URL_EVENT} listener then emits an {@link ipc.Channel.openDeepLink} event.
* - The `OPEN_URL_EVENT` listener then emits an {@link ipc.Channel.openDeepLink} event.
* - The {@link ipc.Channel.openDeepLink} listener registered by the dashboard receives the event.
* Then it parses the {@link URL} from the event's {@link URL} argument. Then it uses the
* {@link URL} to redirect the user to the dashboard, to the page specified in the {@link URL}'s
@ -93,7 +93,6 @@ const logger = contentConfig.logger
/** Configure all the functionality that must be set up in the Electron app to support
* authentication-related flows. Must be called in the Electron app `whenReady` event.
*
* @param window - A function that returns the main Electron window. This argument is a lambda and
* not a variable because the main window is not available when this function is called. This module
* does not use the `window` until after it is initialized, so while the lambda may return `null` in
@ -137,7 +136,6 @@ function initOpenUrlListener(window: () => electron.BrowserWindow) {
/** Handle the 'open-url' event by parsing the received URL, checking if it is a deep link, and
* sending it to the appropriate BrowserWindow via IPC.
*
* @param url - The URL to handle.
* @param window - A function that returns the BrowserWindow to send the parsed URL to. */
export function onOpenUrl(url: URL, window: () => electron.BrowserWindow) {

View File

@ -46,7 +46,6 @@ export const SOURCE_FILE_SUFFIX = fileAssociations.SOURCE_FILE_SUFFIX
*
* For example, this happens when the user double-clicks on a file in the file explorer and the
* application is launched with the file path as an argument.
*
* @param clientArgs - A list of arguments passed to the application, stripped from the initial
* executable name and any electron dev mode arguments.
* @returns The path to the file to open, or `null` if no file was specified. */
@ -143,7 +142,6 @@ export function onFileOpened(event: electron.Event, path: string): string | null
/** Set up the `open-file` event handler that might import a project and invoke the given callback,
* if this IDE instance should load the project. See {@link onFileOpened} for more details.
*
* @param setProjectToOpen - A function that will be called with the ID of the project to open. */
export function setOpenFileEventHandler(setProjectToOpen: (id: string) => void) {
electron.app.on('open-file', (event, path) => {
@ -158,7 +156,6 @@ export function setOpenFileEventHandler(setProjectToOpen: (id: string) => void)
*
* Imports project if necessary. Returns the ID of the project to open. In case of an error,
* the error message is displayed and the error is re-thrown.
*
* @param openedFile - The path to the file to open.
* @returns The ID of the project to open.
* @throws {Error} if the project from the file cannot be opened or imported. */

View File

@ -112,7 +112,6 @@ class App {
* This method should be called before the application is ready, as it only
* modifies the startup options. If the application is already initialized,
* an error will be logged, and the method will have no effect.
*
* @param projectId - The ID of the project to be opened on startup. */
setProjectToOpenOnStartup(projectId: string) {
// Make sure that we are not initialized yet, as this method should be called before the

View File

@ -23,7 +23,6 @@ import * as paths from 'paths'
* The path of the log file is {@link generateUniqueLogFileName automatically generated}.
*
* The log file is created in the {@link paths.LOGS_DIRECTORY logs directory}
*
* @returns The full path of the log file. */
export function addFileLog(): string {
const dirname = paths.LOGS_DIRECTORY
@ -35,7 +34,6 @@ export function addFileLog(): string {
}
/** Generate a unique log file name based on the current timestamp.
*
* @returns The file name log file. */
export function generateUniqueLogFileName(): string {
// Replace ':' with '-' because ':' is not allowed in file names.
@ -54,7 +52,6 @@ export class FileConsumer extends linkedDist.Consumer {
private readonly logFileHandle: number
/** Create a log consumer that writes to a file.
*
* @param logPath - The path of the log file. Must be writeable. */
constructor(logPath: string) {
super()

View File

@ -20,7 +20,7 @@ export const APP_PATH = electron.app.getAppPath()
/** The path of the directory in which the log files of IDE are stored.
*
* This is based on the Electron `logs` directory, see {@link Electron.App.getPath}. */
* This is based on the Electron `logs` directory, see {@link electron.app.getPath}. */
export const LOGS_DIRECTORY = electron.app.getPath('logs')
/** The application assets, all files bundled with it. */

View File

@ -31,7 +31,6 @@ const logger = config.logger
/** Open a project from the given path. Path can be either a source file under the project root,
* or the project bundle. If needed, the project will be imported into the Project Manager-enabled
* location.
*
* @returns Project ID (from Project Manager's metadata) identifying the imported project.
* @throws {Error} if the path does not belong to a valid project. */
export function importProjectFromPath(openedPath: string): string {
@ -62,7 +61,6 @@ export function importProjectFromPath(openedPath: string): string {
}
/** Import the project from a bundle.
*
* @returns Project ID (from Project Manager's metadata) identifying the imported project. */
export function importBundle(bundlePath: string): string {
logger.log(`Importing project '${bundlePath}' from bundle.`)
@ -140,7 +138,6 @@ export async function uploadBundle(bundle: stream.Readable): Promise<string> {
}
/** Import the project so it becomes visible to the Project Manager.
*
* @param rootPath - The path to the project root.
* @returns The project ID (from the Project Manager's metadata) identifying the imported project.
* @throws {Error} if a race condition occurs when generating a unique project directory name. */
@ -191,7 +188,6 @@ interface ProjectMetadata {
* This function checks if the input object has the required properties and correct types
* to match the {@link ProjectMetadata} interface. It can be used at runtime to validate that
* a given object has the expected shape.
*
* @param value - The object to check against the ProjectMetadata interface.
* @returns A boolean value indicating whether the object matches
* the {@link ProjectMetadata} interface. */

View File

@ -44,7 +44,6 @@ export function registerAssociations() {
* For example, this happens on Windows when the browser redirects user using our
* [deep link scheme]{@link common.DEEP_LINK_SCHEME}. On macOS this is not used, as the OS
* handles the URL opening by passing the `open-url` event to the application.
*
* @param clientArgs - A list of arguments passed to the application, stripped from the initial
* executable name and any electron dev mode arguments.
* @returns The URL to open, or `null` if no file was specified. */
@ -70,7 +69,6 @@ export function argsDenoteUrlOpenAttempt(clientArgs: string[]): URL | null {
/** Handle the case where IDE is invoked with a URL to open.
*
* This happens on Windows when the browser redirects user using the deep link scheme.
*
* @param openedUrl - The URL to open. */
export function handleOpenUrl(openedUrl: URL) {
logger.log(`Opening URL '${openedUrl.toString()}'.`)
@ -103,7 +101,6 @@ export function handleOpenUrl(openedUrl: URL) {
* This method registers the callback for both events. Note that on Windows it is necessary to
* use {@link setAsUrlHandler} and {@link unsetAsUrlHandler} to ensure that the callback
* is called.
*
* @param callback - The callback to call when the application is requested to open a URL. */
export function registerUrlCallback(callback: (url: URL) => void) {
// First, register the callback for the `open-url` event. This is used on macOS.
@ -155,7 +152,6 @@ export function registerUrlCallback(callback: (url: URL) => void) {
* The mechanism is built on top of the Electron's
* [instance lock]{@link https://www.electronjs.org/docs/api/app#apprequestsingleinstancelock}
* functionality.
*
* @throws {Error} An error if another instance of the application has already acquired the lock. */
export function setAsUrlHandler() {
logger.log('Expecting URL callback, acquiring the lock.')

View File

@ -21,7 +21,8 @@ const CHECKSUM_TYPE = 'sha256'
function getChecksum(path, type) {
return new Promise(
// This JSDoc annotation is required for correct types that are also type-safe.
/** @param {(value: string) => void} resolve - Fulfill the promise with the given value. */
/** Promise handler resolving to the file's checksum.
* @param {(value: string) => void} resolve - Fulfill the promise with the given value. */
(resolve, reject) => {
const hash = cryptoModule.createHash(type)
const input = fs.createReadStream(path)

View File

@ -39,7 +39,6 @@ export class RemoteLogger {
// === 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.

View File

@ -319,7 +319,7 @@ function parseUserSession(session: cognito.CognitoUserSession): UserSession {
// ==============
/** A wrapper around the Amplify "sign up" endpoint that converts known errors
* to {@link SignUpError}s. */
* to `SignUpError`s. */
async function signUp(
_supportsDeepLinks: boolean,
_username: string,
@ -337,7 +337,7 @@ async function signUp(
// =====================
/** A wrapper around the Amplify "confirm sign up" endpoint that converts known errors
* to {@link ConfirmSignUpError}s. */
* to `ConfirmSignUpError`s. */
async function confirmSignUp(_email: string, _code: string) {
return results.Result.wrapAsync(async () => {
// Ignored.
@ -353,7 +353,7 @@ async function confirmSignUp(_email: string, _code: string) {
// ======================
/** A wrapper around the Amplify "current authenticated user" endpoint that converts known errors
* to {@link AmplifyError}s. */
* to `AmplifyError`s. */
async function currentAuthenticatedUser() {
const result = await results.Result.wrapAsync(
// The methods are not needed.

View File

@ -27,8 +27,8 @@
"devDependencies": {
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@modyfi/vite-plugin-yaml": "^1.0.4",
"@playwright/experimental-ct-react": "^1.38.0",
"@playwright/test": "^1.38.0",
"@playwright/experimental-ct-react": "^1.40.0",
"@playwright/test": "^1.40.0",
"@types/node": "^18.17.5",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",

View File

@ -398,7 +398,6 @@ export const CURRENT_SESSION_NO_CURRENT_USER_ERROR = {
/**
* Convert an {@link AmplifyError} into a {@link CurrentSessionErrorKind} if it is a known error,
* else re-throws the error.
*
* @throws {Error} If the error is not recognized.
*/
export function intoCurrentSessionErrorKind(error: unknown): CurrentSessionErrorKind {
@ -473,7 +472,6 @@ export interface SignUpError extends CognitoError {
/**
* Convert an {@link AmplifyError} into a {@link SignUpError} if it is a known error,
* else re-throws the error.
*
* @throws {Error} If the error is not recognized.
*/
export function intoSignUpErrorOrThrow(error: AmplifyError): SignUpError {

View File

@ -12,7 +12,7 @@ const DEBOUNCE_MS = 1000
// === ControlledInput ===
// =======================
/** Props for an {@link Input}. */
/** Props for a {@link ControlledInput}. */
export interface ControlledInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
value: string
error?: string

View File

@ -41,12 +41,10 @@ function isAuthEvent(value: string): value is AuthEvent {
// =================================
/** Callback called in response to authentication state changes.
*
* @see {@link amplify.Hub.listen}. */
export type ListenerCallback = (event: AuthEvent, data?: unknown) => void
/** Unsubscribe the {@link ListenerCallback} from authentication state changes.
*
* @see {@link amplify.Hub.listen}. */
type UnsubscribeFunction = () => void

View File

@ -115,7 +115,7 @@ export type UserSession = FullUserSession | OfflineUserSession | PartialUserSess
* signing out, etc. All interactions with the authentication API should be done through this
* interface.
*
* See {@link cognito.Cognito} for details on each of the authentication functions. */
* See `Cognito` for details on each of the authentication functions. */
interface AuthContextType {
goOffline: (shouldShowToast?: boolean) => Promise<boolean>
signUp: (email: string, password: string, organizationId: string | null) => Promise<boolean>

View File

@ -19,7 +19,7 @@ interface SessionContextType {
deinitializeSession: () => void
}
/** See {@link AuthContext} for safety details. */
/** See `AuthContext` for safety details. */
const SessionContext = React.createContext<SessionContextType>(
// eslint-disable-next-line no-restricted-syntax
{} as SessionContextType

View File

@ -1,4 +1,4 @@
/** @file Provides an {@link AuthService} which consists of an underyling {@link Cognito} API
/** @file Provides an {@link AuthService} which consists of an underyling `Cognito` API
* wrapper, along with some convenience callbacks to make URL redirects for the authentication flows
* work with Electron. */
import * as amplify from '@aws-amplify/auth'
@ -193,7 +193,7 @@ function saveAccessToken(accessToken: string | null) {
* handle the redirect for us. On the desktop however, we need to handle the redirect ourselves,
* because it's a deep link into the app, and Amplify doesn't handle deep links.
*
* All URLs that don't have a pathname that starts with {@link AUTHENTICATION_PATHNAME_BASE} will be
* All URLs that don't have a pathname that starts with `AUTHENTICATION_PATHNAME_BASE` will be
* ignored by this handler. */
function setDeepLinkHandler(logger: loggerProvider.Logger, navigate: (url: string) => void) {
const onDeepLink = (url: string) => {

View File

@ -4,7 +4,7 @@
* # Providers
*
* The {@link App} component is responsible for defining the global context used by child
* components. For example, it defines a {@link toast.Toaster}, which is used to display temporary
* components. For example, it defines a {@link toastify.ToastContainer}, which is used to display temporary
* notifications to the user. These global components are defined at the top of the {@link App} so
* that they are available to all of the child components.
*

View File

@ -161,7 +161,7 @@ export interface AssetsTableState {
key: backendModule.AssetId,
title?: string
) => void
/** Called when the project is opened via the {@link ProjectActionButton}. */
/** Called when the project is opened via the `ProjectActionButton`. */
doOpenManually: (projectId: backendModule.ProjectId) => void
doOpenIde: (
project: backendModule.ProjectAsset,

View File

@ -18,7 +18,7 @@ import UserPermissions from './userPermissions'
// === Constants ===
// =================
/** The vertical offset of the {@link PermissionTypeSelector} from its parent element, for the
/** The vertical offset of the `PermissionTypeSelector` from its parent element, for the
* input to invite new users. */
const TYPE_SELECTOR_Y_OFFSET_PX = 32

View File

@ -14,7 +14,7 @@ import Modal from './modal'
// === NewLabelModal ===
// =====================
/** Props for a {@link ConfirmDeleteModal}. */
/** Props for a {@link NewLabelModal}. */
export interface NewLabelModalProps {
labelNames: Set<string>
eventTarget: HTMLElement

View File

@ -30,7 +30,7 @@ const ICON_SIZE_PX = 24
const ICON_CLASSES = 'w-6 h-6'
const LOADING_MESSAGE =
'Your environment is being created. It will take some time, please be patient.'
/** The corresponding {@link SpinnerState} for each {@link backendModule.ProjectState},
/** The corresponding {@link spinner.SpinnerState} for each {@link backendModule.ProjectState},
* when using the remote backend. */
const REMOTE_SPINNER_STATE: Record<backendModule.ProjectState, spinner.SpinnerState> = {
[backendModule.ProjectState.closed]: spinner.SpinnerState.initial,
@ -42,7 +42,7 @@ const REMOTE_SPINNER_STATE: Record<backendModule.ProjectState, spinner.SpinnerSt
[backendModule.ProjectState.provisioned]: spinner.SpinnerState.loadingSlow,
[backendModule.ProjectState.opened]: spinner.SpinnerState.done,
}
/** The corresponding {@link SpinnerState} for each {@link backendModule.ProjectState},
/** The corresponding {@link spinner.SpinnerState} for each {@link backendModule.ProjectState},
* when using the local backend. */
const LOCAL_SPINNER_STATE: Record<backendModule.ProjectState, spinner.SpinnerState> = {
[backendModule.ProjectState.closed]: spinner.SpinnerState.initial,

View File

@ -170,7 +170,7 @@ export default function TableRow<T, State = never, RowState = never, Key extends
/** This is SAFE, as the type is defined such that they MUST be
* present if it is specified as a generic parameter.
* See the type definitions of {@link TableRowProps} and
* {@link TableProps}. */
* `TableProps`. */
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
state={state!}
rowState={rowState}

View File

@ -73,7 +73,7 @@ interface AssetRowsDragPayloadItem {
readonly asset: backend.AnyAsset
}
/** Data for a {@link DragEvent} started from an {@link AssetsTable}. */
/** Data for a {@link DragEvent} started from an `AssetsTable`. */
export type AssetRowsDragPayload = readonly AssetRowsDragPayloadItem[]
// ========================

View File

@ -67,7 +67,7 @@ interface AssetEvents {
deleteLabel: AssetDeleteLabelEvent
}
/** A type to ensure that {@link AssetEvents} contains every {@link AssetLEventType}. */
/** A type to ensure that {@link AssetEvents} contains every {@link AssetEventType}. */
// This is meant only as a sanity check, so it is allowed to break lint rules.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type SanityCheck<

View File

@ -47,7 +47,6 @@ export class LocalBackend extends backend.Backend {
return backend.DirectoryId('')
}
/** Return a list of assets in a directory.
*
* @throws An error if the JSON-RPC call fails. */
override async listDirectory(): Promise<backend.AnyAsset[]> {
const result = await this.projectManager.listProjects({})
@ -72,7 +71,6 @@ export class LocalBackend extends backend.Backend {
}
/** Return a list of projects belonging to the current user.
*
* @throws An error if the JSON-RPC call fails. */
override async listProjects(): Promise<backend.ListedProject[]> {
const result = await this.projectManager.listProjects({})
@ -92,7 +90,6 @@ export class LocalBackend extends backend.Backend {
}
/** Create a project.
*
* @throws An error if the JSON-RPC call fails. */
override async createProject(
body: backend.CreateProjectRequestBody
@ -118,7 +115,6 @@ export class LocalBackend extends backend.Backend {
}
/** Close the project identified by the given project ID.
*
* @throws An error if the JSON-RPC call fails. */
override async closeProject(projectId: backend.ProjectId, title: string | null): Promise<void> {
if (LocalBackend.currentlyOpeningProjectId === projectId) {
@ -138,7 +134,6 @@ export class LocalBackend extends backend.Backend {
}
/** Close the project identified by the given project ID.
*
* @throws An error if the JSON-RPC call fails. */
override async getProjectDetails(
projectId: backend.ProjectId,
@ -209,7 +204,6 @@ export class LocalBackend extends backend.Backend {
}
/** Prepare a project for execution.
*
* @throws An error if the JSON-RPC call fails. */
override async openProject(
projectId: backend.ProjectId,
@ -238,7 +232,6 @@ export class LocalBackend extends backend.Backend {
}
/** Change the name of a project.
*
* @throws An error if the JSON-RPC call fails. */
override async projectUpdate(
projectId: backend.ProjectId,
@ -278,7 +271,6 @@ export class LocalBackend extends backend.Backend {
}
/** Delete an arbitrary asset.
*
* @throws An error if the JSON-RPC call fails. */
override async deleteAsset(assetId: backend.AssetId, title: string | null): Promise<void> {
// This is SAFE, as the only asset type on the local backend is projects.
@ -321,7 +313,8 @@ export class LocalBackend extends backend.Backend {
// === Endpoints that intentionally do not work on the Local Backend ===
/** @throws An error stating that the operation is intentionally unavailable on the local
/** Called for any function that does not make sense in the Local Backend.
* @throws An error stating that the operation is intentionally unavailable on the local
* backend. */
invalidOperation(): never {
throw new Error(

View File

@ -127,7 +127,6 @@ export class RemoteBackend extends backendModule.Backend {
protected defaultVersions: Partial<Record<backendModule.VersionType, DefaultVersionInfo>> = {}
/** Create a new instance of the {@link RemoteBackend} API client.
*
* @throws An error if the `Authorization` header is not set on the given `client`. */
constructor(
private readonly client: http.Client,
@ -219,7 +218,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Return organization info for the current user.
*
* @returns `null` if a non-successful status code (not 200-299) was received. */
override async usersMe(): Promise<backendModule.UserOrOrganization | null> {
const response = await this.get<backendModule.UserOrOrganization>(
@ -233,7 +231,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Return a list of assets in a directory.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async listDirectory(
query: backendModule.ListDirectoryRequestParams,
@ -284,7 +281,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Create a directory.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async createDirectory(
body: backendModule.CreateDirectoryRequestBody
@ -301,7 +297,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Change the name of a directory.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async updateDirectory(
directoryId: backendModule.DirectoryId,
@ -324,7 +319,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Change the parent directory of an asset.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async updateAsset(
assetId: backendModule.AssetId,
@ -342,7 +336,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Delete an arbitrary asset.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async deleteAsset(assetId: backendModule.AssetId, title: string | null) {
const response = await this.delete(remoteBackendPaths.deleteAssetPath(assetId))
@ -356,7 +349,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Restore an arbitrary asset from the trash.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async undoDeleteAsset(
assetId: backendModule.AssetId,
@ -375,7 +367,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Return a list of projects belonging to the current user.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async listProjects(): Promise<backendModule.ListedProject[]> {
const response = await this.get<ListProjectsResponseBody>(
@ -399,7 +390,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Create a project.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async createProject(
body: backendModule.CreateProjectRequestBody
@ -416,7 +406,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Close a project.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async closeProject(
projectId: backendModule.ProjectId,
@ -435,7 +424,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Return details for a project.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async getProjectDetails(
projectId: backendModule.ProjectId,
@ -471,7 +459,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Prepare a project for execution.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async openProject(
projectId: backendModule.ProjectId,
@ -492,7 +479,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Update the name or AMI of a project.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async projectUpdate(
projectId: backendModule.ProjectId,
@ -515,7 +501,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Return the resource usage of a project.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async checkResources(
projectId: backendModule.ProjectId,
@ -536,7 +521,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Return a list of files accessible by the current user.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async listFiles(): Promise<backendModule.File[]> {
const response = await this.get<ListFilesResponseBody>(remoteBackendPaths.LIST_FILES_PATH)
@ -548,7 +532,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Upload a file.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async uploadFile(
params: backendModule.UploadFileRequestParams,
@ -591,7 +574,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Create a secret environment variable.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async createSecret(
body: backendModule.CreateSecretRequestBody
@ -608,7 +590,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Return a secret environment variable.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async getSecret(
secretId: backendModule.SecretId,
@ -627,7 +608,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Return the secret environment variables accessible by the user.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async listSecrets(): Promise<backendModule.SecretInfo[]> {
const response = await this.get<ListSecretsResponseBody>(
@ -641,7 +621,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Create a label used for categorizing assets.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async createTag(
body: backendModule.CreateTagRequestBody
@ -658,7 +637,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Return all labels accessible by the user.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async listTags(): Promise<backendModule.Label[]> {
const response = await this.get<ListTagsResponseBody>(remoteBackendPaths.LIST_TAGS_PATH)
@ -670,7 +648,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Set the full list of labels for a specific asset.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async associateTag(
assetId: backendModule.AssetId,
@ -695,7 +672,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Delete a label.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async deleteTag(
tagId: backendModule.TagId,
@ -710,7 +686,6 @@ export class RemoteBackend extends backendModule.Backend {
}
/** Return a list of backend or IDE versions.
*
* @throws An error if a non-successful status code (not 200-299) was received. */
override async listVersions(
params: backendModule.ListVersionsRequestParams

View File

@ -5,7 +5,7 @@ import type * as toastify from 'react-toastify'
// === tryGetMessage ===
// =====================
/** Evaluates the given type only if it the exact same type as {@link Expected}. */
/** Evaluates the given type only if it the exact same type as `Expected`. */
type MustBe<T, Expected> = (<U>() => U extends T ? 1 : 2) extends <U>() => U extends Expected
? 1
: 2

View File

@ -47,15 +47,14 @@ export function useToastAndLog() {
/** A React hook for re-rendering a component once an asynchronous call is over.
*
* This hook will take care of setting an initial value for the component state (so that it can
* render immediately), updating the state once the asynchronous call is over (to re-render the
* component), and cancelling any in-progress asynchronous calls when the component is unmounted (to
* avoid race conditions where "update 1" starts, "update 2" starts and finishes, then "update 1"
* finishes and sets the state).
*
* For further details, see: https://devtrium.com/posts/async-functions-useeffect.
* Also see: https://stackoverflow.com/questions/61751728/asynchronous-calls-with-react-usememo.
*This hook will take care of setting an initial value for the component state (so that it can
*render immediately), updating the state once the asynchronous call is over (to re-render the
*component), and cancelling any in-progress asynchronous calls when the component is unmounted (to
*avoid race conditions where "update 1" starts, "update 2" starts and finishes, then "update 1"
*finishes and sets the state).
*
*For further details, see: https://devtrium.com/posts/async-functions-useeffect.
*Also see: https://stackoverflow.com/questions/61751728/asynchronous-calls-with-react-usememo.
* @param initialValue - The initial value of the state controlled by this hook.
* @param asyncEffect - The asynchronous function used to load the state controlled by this hook.
* @param deps - The list of dependencies that, when updated, trigger the asynchronous effect.

View File

@ -26,7 +26,7 @@ interface NewtypeVariant<TypeName extends string> {
export type Newtype<T, TypeName extends string> = NewtypeVariant<TypeName> & T
/** Extracts the original type out of a {@link Newtype}.
* Its only use is in {@link asNewtype}. */
* Its only use is in {@link newtypeConstructor}. */
type UnNewtype<T extends Newtype<unknown, string>> = T extends infer U & NewtypeVariant<T['_$type']>
? U
: NotNewtype & Omit<T, '_$type'>

View File

@ -21,7 +21,7 @@ export interface Logger {
// === LoggerContext ===
// =====================
/** See {@link AuthContext} for safety details. */
/** See `AuthContext` for safety details. */
// eslint-disable-next-line no-restricted-syntax
const LoggerContext = React.createContext<Logger>({} as Logger)

View File

@ -7,7 +7,7 @@ import * as useRefresh from '../../../../src/authentication/src/useRefresh'
// === Refresh ===
// ===============
/** The type of the state returned by {@link hooks.useRefresh}. */
/** The type of the state returned by {@link useRefresh.useRefresh}. */
export type RefreshState = useRefresh.RefreshState
/** Props for a {@link Refresh}. */

View File

@ -19,7 +19,8 @@ const NAMESPACE = NAME
// This function is required. If narrowing is used instead,
// TypeScript thinks `outputDir` may be `undefined` in functions.
/** @param {string} message - The message with which to throw the `Error`.
/** Throws an error. This is defined so that it is usable as an expression.
* @param {string} message - The message with which to throw the `Error`.
* @returns {never} Always throws an error.
* @throws {Error} Always. */
function error(message) {
@ -44,12 +45,15 @@ export default function esbuildPluginCopyDirectories(options) {
let watchingPath = {}
const outputDir =
build.initialOptions.outdir ?? error('Output directory must be given.')
/** @param {string} root - Path to the directory to watch. */
/** Continuously ensure that the folders are identical on both the content root,
* and the output directory.
* @param {string} root - Path to the directory to watch. */
const continuouslySync = root => {
// It's theoretically possible to use a single `chokidar` instance,
// however the root directory path is needed for calculating the destination path.
const watcher = chokidar.watch(root, { cwd: root })
/** @param {string} path - Path to the file to be copied. */
/** Copies a path from the content root to the output directory.
* @param {string} path - Path to the file to be copied. */
const copy = path => {
void (async () => {
const source = pathModule.resolve(root, path)

View File

@ -47,7 +47,7 @@ interface AuthenticationApi {
/** Open a URL in the system browser. */
openUrlInSystemBrowser: (url: string) => void
/** Set the callback to be called when the system browser redirects back to a URL in the app,
* via a deep link. See {@link setDeepLinkHandler} for details. */
* via a deep link. See `setDeepLinkHandler` for details. */
setDeepLinkHandler: (callback: (url: string) => void) => void
/** Saves the access token to a file. */
saveAccessToken: (accessToken: string | null) => void

View File

@ -15,7 +15,6 @@ export const INDENT_SIZE = 4
// ===================
/** Get the environment variable value.
*
* @param name - The name of the environment variable.
* @returns The value of the environment variable.
* @throws {Error} If the environment variable is not set. */
@ -29,7 +28,6 @@ export function requireEnv(name: string) {
}
/** Read the path from environment variable and resolve it.
*
* @param name - The name of the environment variable.
* @returns The resolved path.
* @throws {Error} If the environment variable is not set. */
@ -38,7 +36,6 @@ export function requireEnvResolvedPath(name: string) {
}
/** Read the path from environment variable and resolve it. Verify that it exists.
*
* @param name - The name of the environment variable.
* @returns The resolved path.
* @throws {Error} If the environment variable is not set or path does not exist. */

679
package-lock.json generated
View File

@ -61,16 +61,18 @@
"y-textarea": "^1.0.0",
"y-websocket": "^1.5.0",
"yjs": "^13.6.7",
"zod": "^3.22.2"
"zod": "^3.22.4"
},
"devDependencies": {
"@danmarshall/deckgl-typings": "^4.9.28",
"@eslint/eslintrc": "^2.1.2",
"@eslint/js": "^8.49.0",
"@histoire/plugin-vue": "^0.17.1",
"@playwright/test": "^1.37.0",
"@open-rpc/server-js": "^1.9.4",
"@playwright/test": "^1.40.0",
"@rushstack/eslint-patch": "^1.3.2",
"@tsconfig/node18": "^18.2.0",
"@types/css.escape": "^1.5.2",
"@types/culori": "^2.0.1",
"@types/d3": "^7.4.0",
"@types/hash-sum": "^1.0.0",
@ -89,6 +91,7 @@
"@vue/test-utils": "^2.4.1",
"@vue/tsconfig": "^0.4.0",
"change-case": "^4.1.2",
"css.escape": "^1.5.1",
"d3": "^7.4.0",
"esbuild": "^0.19.3",
"eslint": "^8.49.0",
@ -97,6 +100,7 @@
"hash-wasm": "^4.10.0",
"histoire": "^0.17.2",
"jsdom": "^22.1.0",
"playwright": "^1.39.0",
"postcss-nesting": "^12.0.1",
"prettier": "^3.0.0",
"prettier-plugin-organize-imports": "^3.2.3",
@ -214,29 +218,6 @@
"name": "enso-content-config",
"version": "1.0.0"
},
"app/ide-desktop/lib/content/node_modules/eslint-plugin-jsdoc": {
"version": "46.8.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.8.1.tgz",
"integrity": "sha512-uTce7IBluPKXIQMWJkIwFsI1gv7sZRmLjctca2K5DIxPi8fSBj9f4iru42XmGwuiMyH2f3nfc60sFmnSGv4Z/A==",
"dev": true,
"dependencies": {
"@es-joy/jsdoccomment": "~0.40.1",
"are-docs-informative": "^0.0.2",
"comment-parser": "1.4.0",
"debug": "^4.3.4",
"escape-string-regexp": "^4.0.0",
"esquery": "^1.5.0",
"is-builtin-module": "^3.2.1",
"semver": "^7.5.4",
"spdx-expression-parse": "^3.0.1"
},
"engines": {
"node": ">=16"
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0"
}
},
"app/ide-desktop/lib/dashboard": {
"name": "enso-dashboard",
"version": "0.1.0",
@ -252,8 +233,8 @@
"devDependencies": {
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@modyfi/vite-plugin-yaml": "^1.0.4",
"@playwright/experimental-ct-react": "^1.38.0",
"@playwright/test": "^1.38.0",
"@playwright/experimental-ct-react": "^1.40.0",
"@playwright/test": "^1.40.0",
"@types/node": "^18.17.5",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
@ -279,29 +260,6 @@
"@esbuild/windows-x64": "^0.17.15"
}
},
"app/ide-desktop/lib/dashboard/node_modules/eslint-plugin-jsdoc": {
"version": "46.8.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.8.1.tgz",
"integrity": "sha512-uTce7IBluPKXIQMWJkIwFsI1gv7sZRmLjctca2K5DIxPi8fSBj9f4iru42XmGwuiMyH2f3nfc60sFmnSGv4Z/A==",
"dev": true,
"dependencies": {
"@es-joy/jsdoccomment": "~0.40.1",
"are-docs-informative": "^0.0.2",
"comment-parser": "1.4.0",
"debug": "^4.3.4",
"escape-string-regexp": "^4.0.0",
"esquery": "^1.5.0",
"is-builtin-module": "^3.2.1",
"semver": "^7.5.4",
"spdx-expression-parse": "^3.0.1"
},
"engines": {
"node": ">=16"
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0"
}
},
"app/ide-desktop/lib/dashboard/src/authentication": {
"name": "enso-authentication",
"version": "1.0.0",
@ -334,70 +292,6 @@
"to-ico": "^1.1.5"
}
},
"app/ide-desktop/node_modules/@types/yargs": {
"version": "17.0.31",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.31.tgz",
"integrity": "sha512-bocYSx4DI8TmdlvxqGpVNXOgCNR1Jj0gNPhhAY+iz1rgKDAaYrAYdFYnhDV1IFuiuVc9HkOwyDcFxaTElF3/wg==",
"dependencies": {
"@types/yargs-parser": "*"
}
},
"app/ide-desktop/node_modules/eslint-plugin-jsdoc": {
"version": "40.3.0",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@es-joy/jsdoccomment": "~0.37.0",
"comment-parser": "1.3.1",
"debug": "^4.3.4",
"escape-string-regexp": "^4.0.0",
"esquery": "^1.5.0",
"semver": "^7.3.8",
"spdx-expression-parse": "^3.0.1"
},
"engines": {
"node": "^14 || ^16 || ^17 || ^18 || ^19"
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0"
}
},
"app/ide-desktop/node_modules/eslint-plugin-jsdoc/node_modules/@es-joy/jsdoccomment": {
"version": "0.37.1",
"dev": true,
"license": "MIT",
"dependencies": {
"comment-parser": "1.3.1",
"esquery": "^1.5.0",
"jsdoc-type-pratt-parser": "~4.0.0"
},
"engines": {
"node": "^14 || ^16 || ^17 || ^18 || ^19 || ^20"
}
},
"app/ide-desktop/node_modules/eslint-plugin-jsdoc/node_modules/comment-parser": {
"version": "1.3.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 12.0.0"
}
},
"app/ide-desktop/node_modules/globals": {
"version": "13.23.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
"integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
"dev": true,
"dependencies": {
"type-fest": "^0.20.2"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"build/prettier": {
"version": "1.0.0",
"extraneous": true,
@ -2992,6 +2886,45 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@json-schema-spec/json-pointer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@json-schema-spec/json-pointer/-/json-pointer-0.1.2.tgz",
"integrity": "sha512-BYY7IavBjwsWWSmVcMz2A9mKiDD9RvacnsItgmy1xV8cmgbtxFfKmKMtkVpD7pYtkx4mIW4800yZBXueVFIWPw==",
"dev": true
},
"node_modules/@json-schema-tools/dereferencer": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@json-schema-tools/dereferencer/-/dereferencer-1.5.1.tgz",
"integrity": "sha512-CUpdGpxNTq1ebMkrgVxS03FHfwkGiw63c+GNzqFAqwqsxR0OsR79aqK8h2ybxTIEhdwiaknSnlUgtUIy7FJ+3A==",
"dev": true,
"dependencies": {
"@json-schema-tools/reference-resolver": "^1.2.1",
"@json-schema-tools/traverse": "^1.7.5",
"fast-safe-stringify": "^2.0.7"
}
},
"node_modules/@json-schema-tools/meta-schema": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@json-schema-tools/meta-schema/-/meta-schema-1.7.0.tgz",
"integrity": "sha512-3pDzVUssW3hVnf8gvSu1sKaVIpLyvmpbxgGfkUoaBiErFKRS2CZOufHD0pUFoa5e6Cd5oa72s402nJbnDz76CA==",
"dev": true
},
"node_modules/@json-schema-tools/reference-resolver": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@json-schema-tools/reference-resolver/-/reference-resolver-1.2.5.tgz",
"integrity": "sha512-xNQgX/ABnwvbIeexL5Czv08lXjHAL80HEUogza7E19eIL/EXD8HM4FvxG1JuTGyi5fA+sSP64C9pabELizcBBw==",
"dev": true,
"dependencies": {
"@json-schema-spec/json-pointer": "^0.1.2",
"isomorphic-fetch": "^3.0.0"
}
},
"node_modules/@json-schema-tools/traverse": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/@json-schema-tools/traverse/-/traverse-1.10.1.tgz",
"integrity": "sha512-vYY5EIxCPzEXEWL/vTjdHy4g92tv1ApUQCjPJsj9gEoXLNNVwJlwwgRZisuvgFBZ3zeLzQygrbehERSpYdmFZA==",
"dev": true
},
"node_modules/@lezer/common": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.1.0.tgz",
@ -3146,6 +3079,46 @@
}
}
},
"node_modules/@open-rpc/meta-schema": {
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/@open-rpc/meta-schema/-/meta-schema-1.14.2.tgz",
"integrity": "sha512-vD4Nbkrb7wYFRcSQf+j228LwOy1C6/KKpy5NADlpMElGrAWPRxhTa2yTi6xG+x88OHzg2+cydQ0GAD6o40KUcg==",
"dev": true
},
"node_modules/@open-rpc/schema-utils-js": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/@open-rpc/schema-utils-js/-/schema-utils-js-1.15.0.tgz",
"integrity": "sha512-YHTt3n3RZo1lRy8oknn2G1y0PWlo7HWtnwLOKfvVxjauKMOmlvBbpPHQZibpzIhgt+yPe4mht1ldhKOwq2tCUw==",
"dev": true,
"dependencies": {
"@json-schema-tools/dereferencer": "1.5.1",
"@json-schema-tools/meta-schema": "^1.6.10",
"@json-schema-tools/reference-resolver": "^1.2.1",
"@open-rpc/meta-schema": "1.14.2",
"ajv": "^6.10.0",
"detect-node": "^2.0.4",
"fast-safe-stringify": "^2.0.7",
"fs-extra": "^9.0.0",
"is-url": "^1.2.4",
"isomorphic-fetch": "^3.0.0"
}
},
"node_modules/@open-rpc/server-js": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@open-rpc/server-js/-/server-js-1.9.4.tgz",
"integrity": "sha512-8+rcewakAO/cpuSNbxJ7nMcmQwD4trCdAeXWssAbpiMj8BIpOARt/jeR7OFePUIwg20LgePQ7qsi14uVgZKr5w==",
"dev": true,
"dependencies": {
"@open-rpc/schema-utils-js": "1.15.0",
"body-parser": "^1.19.0",
"connect": "^3.7.0",
"cors": "^2.8.5",
"json-schema-faker": "^0.5.0-rcv.26",
"lodash": "^4.17.19",
"node-ipc": "9.1.1",
"ws": "^8.0.0"
}
},
"node_modules/@pinia/testing": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@pinia/testing/-/testing-0.1.3.tgz",
@ -3179,13 +3152,30 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/@playwright/experimental-ct-react": {
"version": "1.38.0",
"resolved": "https://registry.npmjs.org/@playwright/experimental-ct-react/-/experimental-ct-react-1.38.0.tgz",
"integrity": "sha512-RRQi99dNWDlkdSIUJpva5T0f+Nq6JSb0fR6MatvJDJkgiARaytk57SgquTmFHglyNu4p/Qj4lRxCCIEy0uZDGA==",
"node_modules/@playwright/experimental-ct-core": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/@playwright/experimental-ct-core/-/experimental-ct-core-1.40.0.tgz",
"integrity": "sha512-fsavfzF5RR4kuXuHpqUAZz1eWhlQLVSSHQAzFFuGklV1RCePtUPfo6Vfru/K4SfcYK6QKKmWnvBxt/3PIamQgA==",
"dev": true,
"dependencies": {
"@playwright/experimental-ct-core": "1.38.0",
"playwright": "1.40.0",
"playwright-core": "1.40.0",
"vite": "^4.4.10"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@playwright/experimental-ct-react": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/@playwright/experimental-ct-react/-/experimental-ct-react-1.40.0.tgz",
"integrity": "sha512-r0JQT18zM8YISO5IsqoBYz5JgCQ3cl9Rfp/Wljuh83JQwQsRE26pJ9Ze3YOoMqwZw5NYPiiBG6z+y8n47w2X/w==",
"dev": true,
"dependencies": {
"@playwright/experimental-ct-core": "1.40.0",
"@vitejs/plugin-react": "^4.0.0"
},
"bin": {
@ -3195,30 +3185,13 @@
"node": ">=16"
}
},
"node_modules/@playwright/experimental-ct-react/node_modules/@playwright/experimental-ct-core": {
"version": "1.38.0",
"resolved": "https://registry.npmjs.org/@playwright/experimental-ct-core/-/experimental-ct-core-1.38.0.tgz",
"integrity": "sha512-jspVRe+D9/gM8aZ6g5TVybSKnYi9OfvUtq67WHo22OfxENXBdDe0hHHlLRQNSdmuKcE5goG8WNVZvOWjolTb/Q==",
"dev": true,
"dependencies": {
"playwright": "1.38.0",
"playwright-core": "1.38.0",
"vite": "^4.3.9"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@playwright/test": {
"version": "1.38.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.0.tgz",
"integrity": "sha512-xis/RXXsLxwThKnlIXouxmIvvT3zvQj1JE39GsNieMUrMpb3/GySHDh2j8itCG22qKVD4MYLBp7xB73cUW/UUw==",
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.0.tgz",
"integrity": "sha512-PdW+kn4eV99iP5gxWNSDQCbhMaDVej+RXL5xr6t04nbKLCBwYtA046t7ofoczHOm8u6c+45hpDKQVZqtqwkeQg==",
"dev": true,
"dependencies": {
"playwright": "1.38.0"
"playwright": "1.40.0"
},
"bin": {
"playwright": "cli.js"
@ -3584,6 +3557,12 @@
"version": "0.3.3",
"license": "MIT"
},
"node_modules/@types/css.escape": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@types/css.escape/-/css.escape-1.5.2.tgz",
"integrity": "sha512-sq0h0y8i83T20MB434AZWgK3byezbBG48jllitFpVXlPOjHc5RNs3bg6rUen/EtMUjM4ZJjD4x+0aik9AXqLdQ==",
"dev": true
},
"node_modules/@types/culori": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/culori/-/culori-2.0.1.tgz",
@ -4153,9 +4132,9 @@
}
},
"node_modules/@types/yargs": {
"version": "17.0.24",
"dev": true,
"license": "MIT",
"version": "17.0.32",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
"integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==",
"dependencies": {
"@types/yargs-parser": "*"
}
@ -5577,6 +5556,84 @@
"dev": true,
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"dev": true,
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/body-parser/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/body-parser/node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/body-parser/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
},
"node_modules/body-parser/node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dev": true,
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/body-parser/node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dev": true,
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/boolbase": {
"version": "1.0.0",
"license": "ISC"
@ -5937,6 +5994,15 @@
"node": ">=10.16.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"dev": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/cac": {
"version": "6.7.14",
"dev": true,
@ -5982,6 +6048,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-me-maybe": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
"dev": true
},
"node_modules/callsites": {
"version": "3.1.0",
"dev": true,
@ -6458,6 +6530,15 @@
"upper-case": "^2.0.2"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/convert-source-map": {
"version": "1.9.0",
"dev": true,
@ -6475,6 +6556,19 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"dev": true,
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/crc": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
@ -6585,6 +6679,12 @@
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
"dev": true
},
"node_modules/cssesc": {
"version": "3.0.0",
"dev": true,
@ -7256,6 +7356,25 @@
"node": ">=0.4.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"dev": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"dev": true,
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/detect-libc": {
"version": "2.0.2",
"dev": true,
@ -7267,8 +7386,7 @@
"node_modules/detect-node": {
"version": "2.1.0",
"dev": true,
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/diacritics": {
"version": "1.3.0",
@ -7550,6 +7668,15 @@
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/easy-stack": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz",
"integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/ecc-jsbn": {
"version": "0.1.2",
"dev": true,
@ -8785,6 +8912,15 @@
"node": ">=0.10.0"
}
},
"node_modules/event-pubsub": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz",
"integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==",
"dev": true,
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@ -8950,6 +9086,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
"dev": true
},
"node_modules/fastq": {
"version": "1.15.0",
"license": "ISC",
@ -9155,6 +9297,12 @@
"node": ">= 6"
}
},
"node_modules/format-util": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/format-util/-/format-util-1.0.5.tgz",
"integrity": "sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==",
"dev": true
},
"node_modules/fs-constants": {
"version": "1.0.0",
"dev": true,
@ -9436,11 +9584,18 @@
}
},
"node_modules/globals": {
"version": "11.12.0",
"version": "13.23.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
"integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
"dev": true,
"license": "MIT",
"dependencies": {
"type-fest": "^0.20.2"
},
"engines": {
"node": ">=4"
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/globalthis": {
@ -9948,6 +10103,31 @@
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dev": true,
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/http-errors/node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"dev": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/http-proxy-agent": {
"version": "5.0.0",
"dev": true,
@ -11064,6 +11244,27 @@
"version": "2.2.1",
"license": "MIT"
},
"node_modules/js-message": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.5.tgz",
"integrity": "sha512-hTqHqrm7jrZ+iN93QsKcNOTSgX3F+2NSgdnF+xvf8FfhC2MPqYRzzgXQ1LlhfyIzPTS6hL6Zea0/gIb6hktkHw==",
"dev": true,
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/js-queue": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.0.tgz",
"integrity": "sha512-SW0rTTG+TBPVD1Kp6HtnOr9kX3//EWA6qMlP2Y/WxbKsSNCBuJbWv3EDB5noKJBEkHYi2mDY+xqMn4Y0QHyjyg==",
"dev": true,
"dependencies": {
"easy-stack": "^1.0.0"
},
"engines": {
"node": ">=1.0.0"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"license": "MIT"
@ -11159,6 +11360,59 @@
"dev": true,
"license": "(AFL-2.1 OR BSD-3-Clause)"
},
"node_modules/json-schema-faker": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/json-schema-faker/-/json-schema-faker-0.5.3.tgz",
"integrity": "sha512-BeIrR0+YSrTbAR9dOMnjbFl1MvHyXnq+Wpdw1FpWZDHWKLzK229hZ5huyPcmzFUfVq1ODwf40WdGVoE266UBUg==",
"dev": true,
"dependencies": {
"json-schema-ref-parser": "^6.1.0",
"jsonpath-plus": "^7.2.0"
},
"bin": {
"jsf": "bin/gen.cjs"
}
},
"node_modules/json-schema-ref-parser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-6.1.0.tgz",
"integrity": "sha512-pXe9H1m6IgIpXmE5JSb8epilNTGsmTb2iPohAXpOdhqGFbQjNeHHsZxU+C8w6T81GZxSPFLeUoqDJmzxx5IGuw==",
"deprecated": "Please switch to @apidevtools/json-schema-ref-parser",
"dev": true,
"dependencies": {
"call-me-maybe": "^1.0.1",
"js-yaml": "^3.12.1",
"ono": "^4.0.11"
}
},
"node_modules/json-schema-ref-parser/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/json-schema-ref-parser/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dev": true,
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/json-schema-ref-parser/node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"devOptional": true,
@ -11201,6 +11455,15 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsonpath-plus": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz",
"integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==",
"dev": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/jsprim": {
"version": "1.4.2",
"dev": true,
@ -11755,6 +12018,15 @@
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
"dev": true
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/memorystream": {
"version": "0.3.1",
"dev": true,
@ -12105,6 +12377,20 @@
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/node-ipc": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.1.1.tgz",
"integrity": "sha512-FAyICv0sIRJxVp3GW5fzgaf9jwwRQxAKDJlmNFUL5hOy+W4X/I5AypyHoq0DXXbo9o/gt79gj++4cMr4jVWE/w==",
"dev": true,
"dependencies": {
"event-pubsub": "4.3.0",
"js-message": "1.0.5",
"js-queue": "2.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/node-releases": {
"version": "2.0.13",
"dev": true,
@ -12491,6 +12777,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ono": {
"version": "4.0.11",
"resolved": "https://registry.npmjs.org/ono/-/ono-4.0.11.tgz",
"integrity": "sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g==",
"dev": true,
"dependencies": {
"format-util": "^1.0.3"
}
},
"node_modules/open": {
"version": "9.1.0",
"dev": true,
@ -13029,12 +13324,12 @@
}
},
"node_modules/playwright": {
"version": "1.38.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.0.tgz",
"integrity": "sha512-fJGw+HO0YY+fU/F1N57DMO+TmXHTrmr905J05zwAQE9xkuwP/QLDk63rVhmyxh03dYnEhnRbsdbH9B0UVVRB3A==",
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.0.tgz",
"integrity": "sha512-gyHAgQjiDf1m34Xpwzaqb76KgfzYrhK7iih+2IzcOCoZWr/8ZqmdBw+t0RU85ZmfJMgtgAiNtBQ/KS2325INXw==",
"dev": true,
"dependencies": {
"playwright-core": "1.38.0"
"playwright-core": "1.40.0"
},
"bin": {
"playwright": "cli.js"
@ -13047,9 +13342,10 @@
}
},
"node_modules/playwright-core": {
"version": "1.38.0",
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.0.tgz",
"integrity": "sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
@ -13556,6 +13852,33 @@
"node": ">=0.12"
}
},
"node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"dev": true,
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/raw-body/node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/rc": {
"version": "1.2.8",
"dev": true,
@ -14484,6 +14807,12 @@
"node": ">= 0.4"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"dev": true
},
"node_modules/sharp": {
"version": "0.31.3",
"dev": true,
@ -15538,6 +15867,15 @@
"node": ">=8.0"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"dev": true,
"engines": {
"node": ">=0.6"
}
},
"node_modules/tosource": {
"version": "2.0.0-alpha.3",
"resolved": "https://registry.npmjs.org/tosource/-/tosource-2.0.0-alpha.3.tgz",
@ -16260,6 +16598,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dev": true,
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/typed-array-buffer": {
"version": "1.0.0",
"dev": true,
@ -16703,6 +17054,15 @@
"spdx-expression-parse": "^3.0.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"dev": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
@ -16718,9 +17078,10 @@
}
},
"node_modules/vite": {
"version": "4.4.9",
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
"integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.18.10",
"postcss": "^8.4.27",
@ -17950,9 +18311,9 @@
}
},
"node_modules/zod": {
"version": "3.22.2",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.2.tgz",
"integrity": "sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==",
"version": "3.22.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
"integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}