diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32d12a04..312a4a58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,12 @@ jobs: name: npm-blocks-dist path: packages/blocks/dist + - name: Upload npm @viamrobotics/prime-editor artifacts + uses: actions/upload-artifact@v3 + with: + name: npm-editor-dist + path: packages/editor/dist + - name: Upload GitHub Pages artifacts uses: actions/upload-pages-artifact@v1 with: @@ -80,6 +86,12 @@ jobs: name: npm-blocks-dist path: packages/blocks/dist + - name: Download @viamrobotics/prime-editor npm artifacts + uses: actions/download-artifact@v3 + with: + name: npm-editor-dist + path: packages/editor/dist + - name: Publish 🚀 run: pnpm publish -r --ignore-scripts env: diff --git a/README.md b/README.md index 81dc6782..657c3097 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ A collection of UI elements. [`@viamrobotics/prime-core`][core] - Core components. Exported as Svelte components. +[`@viamrobotics/prime-editor`][editor] - Text-editing components over libraries like [codemirror](https://codemirror.net/). Exported as Svelte components. + [`@viamrobotics/prime-blocks`][blocks] - Large blocks of UI that often have dependencies like [Threlte][threlte]. Exported as Svelte components. [legacy]: https://github.com/viamrobotics/prime/tree/main/packages/legacy diff --git a/packages/core/.npmrc b/packages/core/.npmrc deleted file mode 100644 index 0c05da45..00000000 --- a/packages/core/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -engine-strict=true -resolution-mode=highest diff --git a/packages/editor/.eslintignore b/packages/editor/.eslintignore new file mode 100644 index 00000000..b26ae6de --- /dev/null +++ b/packages/editor/.eslintignore @@ -0,0 +1,2 @@ +.svelte-kit +dist diff --git a/packages/editor/.eslintrc.cjs b/packages/editor/.eslintrc.cjs new file mode 100644 index 00000000..d6e81609 --- /dev/null +++ b/packages/editor/.eslintrc.cjs @@ -0,0 +1,41 @@ +'use strict'; + +/** @type {import('node:path')} */ +const path = require('node:path'); + +module.exports = { + root: true, + extends: ['@viamrobotics/eslint-config/svelte'], + parserOptions: { + project: ['./tsconfig.json'], + tsconfigRootDir: __dirname, + }, + settings: { + tailwindcss: { + config: path.join(__dirname, 'tailwind.config.ts'), + }, + }, + env: { + browser: true, + node: true, + }, + rules: { + // TODO(mc, 2024-01-03): move to base config? + 'multiline-comment-style': 'off', + }, + overrides: [ + { + files: 'src/routes/**/*', + rules: { + 'no-console': 'off', + 'sonarjs/no-duplicate-string': 'off', + }, + }, + { + files: ['.eslintrc.cjs', '.prettierrc.cjs'], + rules: { + '@typescript-eslint/no-unsafe-assignment': 'off', + }, + }, + ], +}; diff --git a/packages/editor/.prettierignore b/packages/editor/.prettierignore new file mode 100644 index 00000000..b26ae6de --- /dev/null +++ b/packages/editor/.prettierignore @@ -0,0 +1,2 @@ +.svelte-kit +dist diff --git a/packages/editor/.prettierrc.cjs b/packages/editor/.prettierrc.cjs new file mode 100644 index 00000000..853e0bf3 --- /dev/null +++ b/packages/editor/.prettierrc.cjs @@ -0,0 +1,12 @@ +'use strict'; + +/** @type {import('node:path')} */ +const path = require('node:path'); + +/** @type {import('@viamrobotics/prettier-config/svelte')} */ +const baseConfig = require('@viamrobotics/prettier-config/svelte'); + +module.exports = { + ...baseConfig, + tailwindConfig: path.join(__dirname, 'tailwind.config.ts'), +}; diff --git a/packages/editor/README.md b/packages/editor/README.md new file mode 100644 index 00000000..dc7a4da5 --- /dev/null +++ b/packages/editor/README.md @@ -0,0 +1,39 @@ +# `@viamrobotics/prime-editor` + +## Getting started + +`@viamrobotics/prime-editor` is a collection of Svelte components that wrap code-editing tools. + +This primarily based around [Codemirror](https://codemirror.net/) + +## Installation + +Install use the README instructions at [@viamrobotics/prime-core](https://github.com/viamrobotics/prime/tree/main/packages/core) to install prime-core. Then install prime-editor using your package manager of choice: + +``` +pnpm add --save-dev @viamrobotics/prime-editor +``` + +These components require that ssr is disabled. +Ensure that you have a `+layout.ts` above all pages that use these components: + +```js +export const prerender = false; +export const ssr = false; +``` + +## Usage + +Once installed, you can use the components in your app: + +```html + + + console.log(e)} +/> +``` diff --git a/packages/editor/package.json b/packages/editor/package.json new file mode 100644 index 00000000..a0deb6b0 --- /dev/null +++ b/packages/editor/package.json @@ -0,0 +1,93 @@ +{ + "name": "@viamrobotics/prime-editor", + "version": "0.0.1", + "publishConfig": { + "access": "public" + }, + "scripts": { + "prepare": "svelte-kit sync", + "dev": "vite dev", + "build": "vite build && pnpm run package", + "preview": "vite preview", + "package": "svelte-kit sync && svelte-package && publint", + "check": "concurrently -g pnpm:check-*", + "check-svelte": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check-lint": "pnpm run _prettier --check && pnpm run _eslint", + "format": "pnpm run _prettier --write", + "test": "svelte-kit sync && vitest run", + "test:watch": "vitest", + "_prettier": "prettier \"**/*.{js,cjs,ts,svelte,css,json,yml,yaml,md,mdx}\"", + "_eslint": "eslint \".*.cjs\" \"**/*.{js,cjs,ts,svelte}\"" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "svelte": "./dist/index.js" + } + }, + "files": [ + "dist", + "!__tests__" + ], + "peerDependencies": { + "svelte": ">=4.0.0 <5", + "@codemirror/lang-json": ">=6 <7", + "@codemirror/merge": ">=6 <7", + "@codemirror/state": ">=6 <7", + "@codemirror/view": ">=6 <7", + "classnames": ">=2 <3", + "codemirror": ">=6 <7", + "lodash-es": ">=4 <5", + "@viamrobotics/prime-core": ">=0.0.141" + }, + "devDependencies": { + "@codemirror/lang-json": "^6.0.1", + "@codemirror/merge": "^6.6.1", + "@codemirror/state": "^6.3.1", + "@codemirror/view": "^6.22.0", + "classnames": "^2.3.2", + "codemirror": "^6.0.1", + "lodash-es": "^4.17.21", + "@sveltejs/adapter-auto": "^3.0.0", + "@sveltejs/kit": "^2.0.3", + "@sveltejs/package": "^2.2.3", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@testing-library/dom": "^9.3.3", + "@testing-library/jest-dom": "^6.1.5", + "@testing-library/svelte": "^4.1.0", + "@testing-library/user-event": "^14.5.1", + "@types/lodash-es": "^4.17.12", + "@types/prismjs": "^1.26.3", + "@typescript-eslint/eslint-plugin": "^6.15.0", + "@typescript-eslint/parser": "^6.15.0", + "@viamrobotics/eslint-config": "^0.3.0", + "@viamrobotics/prettier-config": "^0.3.4", + "@viamrobotics/prime-core": "workspace:^", + "@viamrobotics/typescript-config": "^0.1.0", + "autoprefixer": "^10.4.16", + "concurrently": "^8.2.2", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-sonarjs": "^0.23.0", + "eslint-plugin-svelte": "^2.35.1", + "eslint-plugin-tailwindcss": "^3.13.0", + "eslint-plugin-unicorn": "^49.0.0", + "jsdom": "^23.0.1", + "postcss": "^8.4.32", + "prettier": "^3.1.1", + "prettier-plugin-svelte": "^3.1.2", + "prettier-plugin-tailwindcss": "^0.5.9", + "publint": "^0.2.6", + "svelte": "^4.2.8", + "svelte-check": "^3.6.2", + "tailwindcss": "^3.3.7", + "tslib": "^2.6.2", + "type-fest": "^4.8.3", + "typescript": "^5.3.3", + "vite": "^5.0.10", + "vitest": "^1.1.0" + }, + "svelte": "./dist/index.js", + "types": "./dist/index.d.ts", + "type": "module" +} diff --git a/packages/editor/postcss.config.js b/packages/editor/postcss.config.js new file mode 100644 index 00000000..2aa7205d --- /dev/null +++ b/packages/editor/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/packages/editor/src/app.d.ts b/packages/editor/src/app.d.ts new file mode 100644 index 00000000..71ce9748 --- /dev/null +++ b/packages/editor/src/app.d.ts @@ -0,0 +1,16 @@ +/* + * See https://kit.svelte.dev/docs/types#app + * for information about these interfaces + */ +declare global { + namespace App { + /* + * interface Error {} + * interface Locals {} + * interface PageData {} + * interface Platform {} + */ + } +} + +export {}; diff --git a/packages/editor/src/app.html b/packages/editor/src/app.html new file mode 100644 index 00000000..99a8e24d --- /dev/null +++ b/packages/editor/src/app.html @@ -0,0 +1,18 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/packages/editor/src/lib/__tests__/json-diff.spec.ts b/packages/editor/src/lib/__tests__/json-diff.spec.ts new file mode 100644 index 00000000..5442af66 --- /dev/null +++ b/packages/editor/src/lib/__tests__/json-diff.spec.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import type { ComponentProps } from 'svelte'; +import { render, screen } from '@testing-library/svelte'; + +import Subject from '../json-diff.svelte'; + +const renderSubject = (props?: Partial>) => { + return render(Subject, { + labelPrefix: 'test', + beforeValue: '', + afterValue: '', + ...props, + }); +}; + +describe('json diff', () => { + it('should render both read-only editors with sorted JSON', async () => { + const beforeJson = { aa: 1, bb: 2 }; + const afterJson = { aa: 1, cc: 4, bb: 3 }; + renderSubject({ + beforeValue: JSON.stringify(beforeJson, null, 2), + afterValue: JSON.stringify(afterJson, null, 2), + }); + const beforeEditor = await screen.findByLabelText('test-before'); + const afterEditor = await screen.findByLabelText('test-after'); + expect(beforeEditor).toHaveTextContent(/\{ "aa": 1, "bb": 2\}/iu); + expect(afterEditor).toHaveTextContent(/\{ "aa": 1, "cc": 4, "bb": 3\}/iu); + }); + + it('should destroy the MergeView when the component is unmounted', () => { + const { unmount } = renderSubject(); + // Check that the editor elements are in the DOM before destruction + expect(screen.queryAllByLabelText(/test-(?:before|after)/iu)).toHaveLength( + 2 + ); + unmount(); + // Check that the editor elements are no longer in the DOM + expect(screen.queryAllByLabelText(/test-(?:before|after)/iu)).toHaveLength( + 0 + ); + }); +}); diff --git a/packages/editor/src/lib/__tests__/json-editor.spec.ts b/packages/editor/src/lib/__tests__/json-editor.spec.ts new file mode 100644 index 00000000..9865fba7 --- /dev/null +++ b/packages/editor/src/lib/__tests__/json-editor.spec.ts @@ -0,0 +1,68 @@ +import { describe, expect, it, vi } from 'vitest'; +import type { ComponentProps } from 'svelte'; +import { render, screen } from '@testing-library/svelte'; +import userEvent from '@testing-library/user-event'; + +import Subject from '../json-editor.svelte'; + +const renderSubject = (props?: Partial>) => { + return render(Subject, { + label: 'test-editor', + initialValue: '', + debouncePeriodMS: 0, + ...props, + }); +}; + +describe('json editor', () => { + it('should render an editor with initial value', async () => { + const initialJson = { foo: 'bar', baz: 42 }; + renderSubject({ + initialValue: JSON.stringify(initialJson, null, 2), + }); + const editor = await screen.findByLabelText('test-editor'); + expect(editor).toHaveTextContent(/\{\s*"foo": "bar",\s*"baz": 42\s*\}/u); + }); + + it('should call onChange when content changes', async () => { + const onChange = vi.fn(); + renderSubject({ onChange }); + const editor = await screen.findByLabelText('test-editor'); + + await userEvent.type(editor, '{{"key": "value"}'); + + expect(onChange).toHaveBeenCalledWith('{"key": "value"}'); + }); + + it('should set readonly mode correctly', async () => { + renderSubject({ readonly: true }); + const editor = await screen.findByLabelText('test-editor'); + expect(editor).toHaveAttribute('aria-readonly', 'true'); + }); + + it('should show error state when isInvalid is true', async () => { + renderSubject({ isInvalid: true, errorMessageID: 'error-message' }); + const editor = await screen.findByLabelText('test-editor'); + expect(editor).toHaveAttribute('aria-invalid', 'true'); + expect(editor).toHaveAttribute('aria-errormessage', 'error-message'); + }); + + it('should update when initialValue prop changes', async () => { + const { rerender } = renderSubject({ + initialValue: '{"initial": "value"}', + }); + let editor = await screen.findByLabelText('test-editor'); + expect(editor).toHaveTextContent(/"initial": "value"/u); + + rerender({ initialValue: '{"updated": "value"}', label: 'test-editor' }); + editor = await screen.findByLabelText('test-editor'); + expect(editor).toHaveTextContent(/"updated": "value"/u); + }); + + it('should destroy the editor when the component is unmounted', () => { + const { unmount } = renderSubject(); + expect(screen.queryByLabelText('test-editor')).toBeInTheDocument(); + unmount(); + expect(screen.queryByLabelText('test-editor')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/editor/src/lib/codemirror.ts b/packages/editor/src/lib/codemirror.ts new file mode 100644 index 00000000..42bf4f1c --- /dev/null +++ b/packages/editor/src/lib/codemirror.ts @@ -0,0 +1,173 @@ +/** + * Codemirror extension wrappers. + */ +import { json } from '@codemirror/lang-json'; +import { unifiedMergeView, updateOriginalDoc } from '@codemirror/merge'; +import { + ChangeSet, + EditorState, + type Extension, + StateEffect, + StateField, + type Transaction, +} from '@codemirror/state'; +import { EditorView } from '@codemirror/view'; +import { basicSetup } from 'codemirror'; +import { noop } from 'lodash-es'; + +/** Options and initial values for a JSON editor's extensions. */ +export interface ExtensionOptions { + label: string; + errorMessageID?: string | undefined; + isInvalid?: boolean | undefined; + readonly?: boolean; + onFocus?: (event: FocusEvent) => void; + onBlur?: (event: FocusEvent) => void; + diffOriginal?: string | undefined; +} + +/** Create an editor StateField and updater function. */ +const createSource = (): [ + source: StateField, + update: (view: EditorView | undefined, nextValue: T | undefined) => void, +] => { + const updateEffect = StateEffect.define(); + + const source = StateField.define({ + create: () => undefined, + update: (prevValue, transaction) => { + let nextValue = prevValue; + + for (const effect of transaction.effects) { + if (effect.is(updateEffect)) { + nextValue = effect.value; + } + } + + return nextValue; + }, + }); + + const update = (view: EditorView | undefined, nextValue: T | undefined) => { + view?.dispatch({ effects: [updateEffect.of(nextValue)] }); + }; + + return [source, update]; +}; + +const [readonly, setReadOnly] = createSource(); +const [label, setLabel] = createSource(); +const [errorMessageID, setErrorMessageID] = createSource(); +const [isInvalid, setIsInvalid] = createSource(); + +const setDiffOriginal = ( + view: EditorView | undefined, + nextOriginal: string | undefined +) => { + if (nextOriginal !== undefined) { + view?.dispatch({ + effects: updateOriginalDoc.of({ + doc: view.state.toText(nextOriginal), + changes: ChangeSet.empty(0), + }), + }); + } +}; + +/** + * Create a CodeMirror extension list for a JSON editor. + * + * Adds various state fields and effects so that props changes + * can be hooked to the editor without needing to completely + * reconfigure or recreate the extensions list. + * + * @param options Initial values for editor state + * @returns The extension list and hooks to reactively update the state + * + */ + +export const createJsonExtensions = ( + options: ExtensionOptions +): readonly Extension[] => { + const extensions = [ + readonly.init(() => options.readonly), + label.init(() => options.label), + errorMessageID.init(() => options.errorMessageID), + isInvalid.init(() => options.isInvalid), + basicSetup, + json(), + EditorView.contentAttributes.compute( + [label, errorMessageID, isInvalid], + (state) => { + const computedAttributes: Record = {}; + const labelValue = state.field(label); + if (labelValue !== undefined) { + computedAttributes['aria-label'] = labelValue; + } + if (state.field(isInvalid) === true) { + computedAttributes['aria-invalid'] = 'true'; + } + const errorMessageIDValue = state.field(errorMessageID); + if (errorMessageIDValue !== undefined) { + computedAttributes['aria-errormessage'] = errorMessageIDValue; + } + return computedAttributes; + } + ), + EditorView.editable.from(readonly, (readonlyValue) => !readonlyValue), + EditorState.readOnly.from(readonly), + EditorView.domEventObservers({ + blur: options.onBlur ?? noop, + focus: options.onFocus ?? noop, + }), + ]; + + if (options.diffOriginal !== undefined) { + extensions.push( + unifiedMergeView({ + original: options.diffOriginal, + mergeControls: false, + }) + ); + } + + return extensions; +}; + +/** + * Dispatch change events when the contents of the editor change. + * + * Create a new dispatch function whenever the editor is re-created + */ +export const createDispatchTransactions = ( + onChange: (nextValue: string) => unknown +) => { + let currentValue: string | undefined; + + return (transactions: readonly Transaction[], view: EditorView) => { + view.update(transactions); + + let nextValue = view.state.doc.toString(); + + if (nextValue === '') { + nextValue = '{}'; + } + + if (currentValue === undefined) { + currentValue = nextValue; + } + + if (nextValue !== currentValue) { + currentValue = nextValue; + onChange(currentValue); + } + }; +}; + +export { + setDiffOriginal, + setErrorMessageID, + setIsInvalid, + setLabel, + setReadOnly, +}; diff --git a/packages/editor/src/lib/editor-wrapper.svelte b/packages/editor/src/lib/editor-wrapper.svelte new file mode 100644 index 00000000..a8e765f5 --- /dev/null +++ b/packages/editor/src/lib/editor-wrapper.svelte @@ -0,0 +1,39 @@ + + + +
+ + diff --git a/packages/editor/src/lib/index.ts b/packages/editor/src/lib/index.ts new file mode 100644 index 00000000..1e63d590 --- /dev/null +++ b/packages/editor/src/lib/index.ts @@ -0,0 +1,2 @@ +export { default as JsonEditor } from './json-editor.svelte'; +export { default as JsonDiff } from './json-diff.svelte'; diff --git a/packages/editor/src/lib/json-diff.svelte b/packages/editor/src/lib/json-diff.svelte new file mode 100644 index 00000000..196170e9 --- /dev/null +++ b/packages/editor/src/lib/json-diff.svelte @@ -0,0 +1,47 @@ + + + diff --git a/packages/editor/src/lib/json-editor.svelte b/packages/editor/src/lib/json-editor.svelte new file mode 100644 index 00000000..19302bb1 --- /dev/null +++ b/packages/editor/src/lib/json-editor.svelte @@ -0,0 +1,125 @@ + + + diff --git a/packages/editor/src/routes/+layout.svelte b/packages/editor/src/routes/+layout.svelte new file mode 100644 index 00000000..ae611705 --- /dev/null +++ b/packages/editor/src/routes/+layout.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/editor/src/routes/+layout.ts b/packages/editor/src/routes/+layout.ts new file mode 100644 index 00000000..ae88a275 --- /dev/null +++ b/packages/editor/src/routes/+layout.ts @@ -0,0 +1,2 @@ +export const prerender = false; +export const ssr = false; diff --git a/packages/editor/src/routes/+page.svelte b/packages/editor/src/routes/+page.svelte new file mode 100644 index 00000000..271a830a --- /dev/null +++ b/packages/editor/src/routes/+page.svelte @@ -0,0 +1,45 @@ + + +
+

@viamrobotics/prime-editor Playground

+

JsonDiff

+ +
+

JsonEditor (0 sec debounce)

+

Prints change events to the console

+
+ console.log(e)} + /> +
+

JsonEditor (5 sec debounce)

+

Prints change events to the console

+
+ console.log(e)} + /> +

JsonEditor (diff original)

+ +
diff --git a/packages/editor/src/vitest.setup.ts b/packages/editor/src/vitest.setup.ts new file mode 100644 index 00000000..19acfcf4 --- /dev/null +++ b/packages/editor/src/vitest.setup.ts @@ -0,0 +1,13 @@ +import '@testing-library/jest-dom/vitest'; +import '@testing-library/svelte/vitest'; +import { vi } from 'vitest'; + +// See: https://github.com/jsdom/jsdom/issues/3002 +// TypeError: range(...).getClientRects is not a function +// (required for codemirror) +global.Range.prototype.getBoundingClientRect = vi.fn(); +global.Range.prototype.getClientRects = () => ({ + item: vi.fn(), + length: 0, + [Symbol.iterator]: vi.fn(), +}); diff --git a/packages/editor/static/favicon.png b/packages/editor/static/favicon.png new file mode 100644 index 00000000..825b9e65 Binary files /dev/null and b/packages/editor/static/favicon.png differ diff --git a/packages/editor/svelte.config.js b/packages/editor/svelte.config.js new file mode 100644 index 00000000..3dfcf4b5 --- /dev/null +++ b/packages/editor/svelte.config.js @@ -0,0 +1,34 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + + kit: { + adapter: adapter(), + + /* + * Merge our own includes with the generated includes from SvelteKit + * so that our JS config can be properly type-checked / linted + */ + typescript: { + config: (tsconfig) => ({ + ...tsconfig, + include: [ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + ...tsconfig.include, + '../.eslintrc.cjs', + '../.prettierrc.cjs', + '../postcss.config.js', + '../svelte.config.js', + '../tailwind.config.ts', + '../plugins.ts', + '../theme.ts', + ], + }), + }, + }, +}; + +export default config; diff --git a/packages/editor/tailwind.config.ts b/packages/editor/tailwind.config.ts new file mode 100644 index 00000000..8498cd50 --- /dev/null +++ b/packages/editor/tailwind.config.ts @@ -0,0 +1,12 @@ +import { theme } from '@viamrobotics/prime-core/theme'; +import { plugins } from '@viamrobotics/prime-core/plugins'; + +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + './src/**/*.{html,js,svelte,ts}', + './node_modules/@viamrobotics/prime-core/**/*.{ts,svelte}', + ], + theme, + plugins, +}; diff --git a/packages/editor/tsconfig.json b/packages/editor/tsconfig.json new file mode 100644 index 00000000..c2f28637 --- /dev/null +++ b/packages/editor/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": [ + "@viamrobotics/typescript-config/base.json", + "./.svelte-kit/tsconfig.json" + ], + "compilerOptions": { + "types": ["svelte", "vite/client", "vitest", "@testing-library/jest-dom"], + "noEmit": true + } +} diff --git a/packages/editor/vite.config.ts b/packages/editor/vite.config.ts new file mode 100644 index 00000000..0dadc483 --- /dev/null +++ b/packages/editor/vite.config.ts @@ -0,0 +1,16 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig(({ mode }) => ({ + plugins: [sveltekit()], + resolve: { + conditions: mode === 'test' ? ['browser'] : [], + }, + test: { + include: ['src/**/__tests__/*.spec.ts'], + setupFiles: ['src/vitest.setup.ts'], + environment: 'jsdom', + mockReset: true, + unstubGlobals: true, + }, +})); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8505ac8..a05203a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -301,6 +301,144 @@ importers: specifier: workspace:* version: link:../core + packages/editor: + devDependencies: + '@codemirror/lang-json': + specifier: ^6.0.1 + version: 6.0.1 + '@codemirror/merge': + specifier: ^6.6.1 + version: 6.6.7 + '@codemirror/state': + specifier: ^6.3.1 + version: 6.4.1 + '@codemirror/view': + specifier: ^6.22.0 + version: 6.32.0 + '@sveltejs/adapter-auto': + specifier: ^3.0.0 + version: 3.0.0(@sveltejs/kit@2.0.3) + '@sveltejs/kit': + specifier: ^2.0.3 + version: 2.0.3(@sveltejs/vite-plugin-svelte@3.0.1)(svelte@4.2.17)(vite@5.2.11) + '@sveltejs/package': + specifier: ^2.2.3 + version: 2.2.3(svelte@4.2.17)(typescript@5.4.5) + '@sveltejs/vite-plugin-svelte': + specifier: ^3.0.0 + version: 3.0.1(svelte@4.2.17)(vite@5.2.11) + '@testing-library/dom': + specifier: ^9.3.3 + version: 9.3.3 + '@testing-library/jest-dom': + specifier: ^6.1.5 + version: 6.1.5(vitest@1.1.0) + '@testing-library/svelte': + specifier: ^4.1.0 + version: 4.1.0(svelte@4.2.17) + '@testing-library/user-event': + specifier: ^14.5.1 + version: 14.5.1(@testing-library/dom@9.3.3) + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 + '@types/prismjs': + specifier: ^1.26.3 + version: 1.26.3 + '@typescript-eslint/eslint-plugin': + specifier: ^6.15.0 + version: 6.15.0(@typescript-eslint/parser@6.15.0)(eslint@8.56.0)(typescript@5.4.5) + '@typescript-eslint/parser': + specifier: ^6.15.0 + version: 6.15.0(eslint@8.56.0)(typescript@5.4.5) + '@viamrobotics/eslint-config': + specifier: ^0.3.0 + version: 0.3.0(@typescript-eslint/eslint-plugin@6.15.0)(@typescript-eslint/parser@6.15.0)(eslint-config-prettier@9.1.0)(eslint-plugin-sonarjs@0.23.0)(eslint-plugin-svelte@2.35.1)(eslint-plugin-tailwindcss@3.13.0)(eslint-plugin-unicorn@49.0.0)(eslint@8.56.0) + '@viamrobotics/prettier-config': + specifier: ^0.3.4 + version: 0.3.4(prettier-plugin-svelte@3.1.2)(prettier-plugin-tailwindcss@0.5.9)(prettier@3.1.1) + '@viamrobotics/prime-core': + specifier: workspace:^ + version: link:../core + '@viamrobotics/typescript-config': + specifier: ^0.1.0 + version: 0.1.0(typescript@5.4.5) + autoprefixer: + specifier: ^10.4.16 + version: 10.4.16(postcss@8.4.38) + classnames: + specifier: ^2.3.2 + version: 2.3.2 + codemirror: + specifier: ^6.0.1 + version: 6.0.1(@lezer/common@1.2.1) + concurrently: + specifier: ^8.2.2 + version: 8.2.2 + eslint: + specifier: ^8.56.0 + version: 8.56.0 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.56.0) + eslint-plugin-sonarjs: + specifier: ^0.23.0 + version: 0.23.0(eslint@8.56.0) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.35.1(eslint@8.56.0)(svelte@4.2.17) + eslint-plugin-tailwindcss: + specifier: ^3.13.0 + version: 3.13.0(tailwindcss@3.4.3) + eslint-plugin-unicorn: + specifier: ^49.0.0 + version: 49.0.0(eslint@8.56.0) + jsdom: + specifier: ^23.0.1 + version: 23.0.1 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + postcss: + specifier: ^8.4.32 + version: 8.4.38 + prettier: + specifier: ^3.1.1 + version: 3.1.1 + prettier-plugin-svelte: + specifier: ^3.1.2 + version: 3.1.2(prettier@3.1.1)(svelte@4.2.17) + prettier-plugin-tailwindcss: + specifier: ^0.5.9 + version: 0.5.9(prettier-plugin-svelte@3.1.2)(prettier@3.1.1) + publint: + specifier: ^0.2.6 + version: 0.2.6 + svelte: + specifier: ^4.2.8 + version: 4.2.17 + svelte-check: + specifier: ^3.6.2 + version: 3.6.2(@babel/core@7.24.6)(less@4.2.0)(postcss@8.4.38)(svelte@4.2.17) + tailwindcss: + specifier: ^3.3.7 + version: 3.4.3 + tslib: + specifier: ^2.6.2 + version: 2.6.2 + type-fest: + specifier: ^4.8.3 + version: 4.8.3 + typescript: + specifier: ^5.3.3 + version: 5.4.5 + vite: + specifier: ^5.0.10 + version: 5.2.11 + vitest: + specifier: ^1.1.0 + version: 1.1.0(jsdom@23.0.1) + packages/legacy: devDependencies: '@floating-ui/dom': @@ -2008,6 +2146,85 @@ packages: '@babel/helper-validator-identifier': 7.24.6 to-fast-properties: 2.0.0 + /@codemirror/autocomplete@6.18.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.32.0)(@lezer/common@1.2.1): + resolution: {integrity: sha512-5DbOvBbY4qW5l57cjDsmmpDh3/TeK1vXfTHa+BUMrRzdWdcxKZ4U4V7vQaTtOpApNU4kLS4FQ6cINtLg245LXA==} + peerDependencies: + '@codemirror/language': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + '@lezer/common': ^1.0.0 + dependencies: + '@codemirror/language': 6.10.2 + '@codemirror/state': 6.4.1 + '@codemirror/view': 6.32.0 + '@lezer/common': 1.2.1 + dev: true + + /@codemirror/commands@6.6.0: + resolution: {integrity: sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==} + dependencies: + '@codemirror/language': 6.10.2 + '@codemirror/state': 6.4.1 + '@codemirror/view': 6.32.0 + '@lezer/common': 1.2.1 + dev: true + + /@codemirror/lang-json@6.0.1: + resolution: {integrity: sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==} + dependencies: + '@codemirror/language': 6.10.2 + '@lezer/json': 1.0.2 + dev: true + + /@codemirror/language@6.10.2: + resolution: {integrity: sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==} + dependencies: + '@codemirror/state': 6.4.1 + '@codemirror/view': 6.32.0 + '@lezer/common': 1.2.1 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + style-mod: 4.1.2 + dev: true + + /@codemirror/lint@6.8.1: + resolution: {integrity: sha512-IZ0Y7S4/bpaunwggW2jYqwLuHj0QtESf5xcROewY6+lDNwZ/NzvR4t+vpYgg9m7V8UXLPYqG+lu3DF470E5Oxg==} + dependencies: + '@codemirror/state': 6.4.1 + '@codemirror/view': 6.32.0 + crelt: 1.0.6 + dev: true + + /@codemirror/merge@6.6.7: + resolution: {integrity: sha512-fgZHAuLuxIQi1U/oeszzJHAGlQfkGC3Rmd9/Lxs4yO9GUC798h9640aiPWTuAyY3+H2XmlzQcg5wfG9mObKqRQ==} + dependencies: + '@codemirror/language': 6.10.2 + '@codemirror/state': 6.4.1 + '@codemirror/view': 6.32.0 + '@lezer/highlight': 1.2.1 + style-mod: 4.1.2 + dev: true + + /@codemirror/search@6.5.6: + resolution: {integrity: sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==} + dependencies: + '@codemirror/state': 6.4.1 + '@codemirror/view': 6.32.0 + crelt: 1.0.6 + dev: true + + /@codemirror/state@6.4.1: + resolution: {integrity: sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==} + dev: true + + /@codemirror/view@6.32.0: + resolution: {integrity: sha512-AgVNvED2QTsZp5e3syoHLsrWtwJFYWdx1Vr/m3f4h1ATQz0ax60CfXF3Htdmk69k2MlYZw8gXesnQdHtzyVmAw==} + dependencies: + '@codemirror/state': 6.4.1 + style-mod: 4.1.2 + w3c-keyname: 2.2.8 + dev: true + /@colors/colors@1.5.0: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -3108,6 +3325,30 @@ packages: resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} dev: true + /@lezer/common@1.2.1: + resolution: {integrity: sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==} + dev: true + + /@lezer/highlight@1.2.1: + resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==} + dependencies: + '@lezer/common': 1.2.1 + dev: true + + /@lezer/json@1.0.2: + resolution: {integrity: sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==} + dependencies: + '@lezer/common': 1.2.1 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + dev: true + + /@lezer/lr@1.4.2: + resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==} + dependencies: + '@lezer/common': 1.2.1 + dev: true + /@mapbox/geojson-rewind@0.5.2: resolution: {integrity: sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==} hasBin: true @@ -7002,6 +7243,20 @@ packages: estree-walker: 3.0.3 periscopic: 3.1.0 + /codemirror@6.0.1(@lezer/common@1.2.1): + resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} + dependencies: + '@codemirror/autocomplete': 6.18.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.32.0)(@lezer/common@1.2.1) + '@codemirror/commands': 6.6.0 + '@codemirror/language': 6.10.2 + '@codemirror/lint': 6.8.1 + '@codemirror/search': 6.5.6 + '@codemirror/state': 6.4.1 + '@codemirror/view': 6.32.0 + transitivePeerDependencies: + - '@lezer/common' + dev: true + /collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} dev: false @@ -7187,6 +7442,10 @@ packages: path-type: 4.0.0 dev: true + /crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + dev: true + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -10222,7 +10481,6 @@ packages: /lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} - dev: false /lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} @@ -13647,6 +13905,10 @@ packages: webpack: 5.89.0(esbuild@0.18.20) dev: true + /style-mod@4.1.2: + resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} + dev: true + /style-to-object@0.4.4: resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==} dependencies: @@ -15092,6 +15354,10 @@ packages: '@vue/shared': 3.3.4 dev: true + /w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + dev: true + /w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'}