diff --git a/.gitignore b/.gitignore
index 68a8e969f..0560e4baa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,7 @@ Thumbs.db
.nx/cache
.nx/workspace-data
+
+vite.config.*.timestamp*
+vitest.config.*.timestamp*
+test-output
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 000000000..de1c9f40a
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,5 @@
+{
+ "recommendations": [
+ "ms-playwright.playwright"
+ ]
+}
diff --git a/apps/client-e2e/eslint.config.mjs b/apps/client-e2e/eslint.config.mjs
new file mode 100644
index 000000000..1603594d7
--- /dev/null
+++ b/apps/client-e2e/eslint.config.mjs
@@ -0,0 +1,15 @@
+import playwright from "eslint-plugin-playwright";
+import baseConfig from "../../eslint.config.mjs";
+
+export default [
+ playwright.configs["flat/recommended"],
+ ...baseConfig,
+ {
+ files: [
+ "**/*.ts",
+ "**/*.js"
+ ],
+ // Override or add rules here
+ rules: {}
+ }
+];
diff --git a/apps/client-e2e/package.json b/apps/client-e2e/package.json
new file mode 100644
index 000000000..1ada09ee3
--- /dev/null
+++ b/apps/client-e2e/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@triliumnext/client-e2e",
+ "version": "0.0.1",
+ "private": true,
+ "nx": {
+ "implicitDependencies": [
+ "@triliumnext/client"
+ ]
+ }
+}
diff --git a/apps/client-e2e/playwright.config.ts b/apps/client-e2e/playwright.config.ts
new file mode 100644
index 000000000..cbedd6740
--- /dev/null
+++ b/apps/client-e2e/playwright.config.ts
@@ -0,0 +1,68 @@
+import { defineConfig, devices } from '@playwright/test';
+import { nxE2EPreset } from '@nx/playwright/preset';
+import { workspaceRoot } from '@nx/devkit';
+
+// For CI, you may want to set BASE_URL to the deployed application.
+const baseURL = process.env['BASE_URL'] || 'http://localhost:4200';
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// require('dotenv').config();
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ ...nxE2EPreset(__filename, { testDir: './src' }),
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ baseURL,
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ command: 'npx nx run @triliumnext/client:serve-static',
+ url: 'http://localhost:4200',
+ reuseExistingServer: !process.env.CI,
+ cwd: workspaceRoot
+ },
+ projects: [
+ {
+ name: "chromium",
+ use: { ...devices["Desktop Chrome"] },
+ },
+
+ {
+ name: "firefox",
+ use: { ...devices["Desktop Firefox"] },
+ },
+
+ {
+ name: "webkit",
+ use: { ...devices["Desktop Safari"] },
+ },
+
+ // Uncomment for mobile browsers support
+ /* {
+ name: 'Mobile Chrome',
+ use: { ...devices['Pixel 5'] },
+ },
+ {
+ name: 'Mobile Safari',
+ use: { ...devices['iPhone 12'] },
+ }, */
+
+ // Uncomment for branded browsers
+ /* {
+ name: 'Microsoft Edge',
+ use: { ...devices['Desktop Edge'], channel: 'msedge' },
+ },
+ {
+ name: 'Google Chrome',
+ use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+ } */
+ ],
+});
diff --git a/apps/client-e2e/src/example.spec.ts b/apps/client-e2e/src/example.spec.ts
new file mode 100644
index 000000000..fa8f1f335
--- /dev/null
+++ b/apps/client-e2e/src/example.spec.ts
@@ -0,0 +1,8 @@
+import { test, expect } from '@playwright/test';
+
+test('has title', async ({ page }) => {
+ await page.goto('/');
+
+ // Expect h1 to contain a substring.
+ expect(await page.locator('h1').innerText()).toContain('Welcome');
+});
diff --git a/apps/client-e2e/tsconfig.json b/apps/client-e2e/tsconfig.json
new file mode 100644
index 000000000..1df867f3a
--- /dev/null
+++ b/apps/client-e2e/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "outDir": "out-tsc/playwright",
+ "sourceMap": false
+ },
+ "include": [
+ "**/*.ts",
+ "**/*.js",
+ "playwright.config.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.spec.js",
+ "src/**/*.test.ts",
+ "src/**/*.test.js",
+ "src/**/*.d.ts"
+ ],
+ "exclude": [
+ "out-tsc",
+ "test-output",
+ "eslint.config.js",
+ "eslint.config.mjs",
+ "eslint.config.cjs"
+ ]
+}
diff --git a/apps/client/.swcrc b/apps/client/.swcrc
new file mode 100644
index 000000000..a2d5b04f4
--- /dev/null
+++ b/apps/client/.swcrc
@@ -0,0 +1,8 @@
+{
+ "jsc": {
+ "parser": {
+ "syntax": "typescript"
+ },
+ "target": "es2016"
+ }
+}
diff --git a/apps/client/eslint.config.mjs b/apps/client/eslint.config.mjs
new file mode 100644
index 000000000..724052a2e
--- /dev/null
+++ b/apps/client/eslint.config.mjs
@@ -0,0 +1,5 @@
+import baseConfig from "../../eslint.config.mjs";
+
+export default [
+ ...baseConfig
+];
diff --git a/apps/client/package.json b/apps/client/package.json
new file mode 100644
index 000000000..5bbc39f08
--- /dev/null
+++ b/apps/client/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "@triliumnext/client",
+ "version": "0.0.1",
+ "private": true
+}
diff --git a/apps/client/src/app/app.element.css b/apps/client/src/app/app.element.css
new file mode 100644
index 000000000..27d098404
--- /dev/null
+++ b/apps/client/src/app/app.element.css
@@ -0,0 +1,424 @@
+/*
+ * Remove template code below
+ */
+ html {
+ -webkit-text-size-adjust: 100%;
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
+ 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
+ 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+ 'Noto Color Emoji';
+ line-height: 1.5;
+ tab-size: 4;
+ scroll-behavior: smooth;
+ }
+ body {
+ font-family: inherit;
+ line-height: inherit;
+ margin: 0;
+ }
+ h1,
+ h2,
+ p,
+ pre {
+ margin: 0;
+ }
+ *,
+ ::before,
+ ::after {
+ box-sizing: border-box;
+ border-width: 0;
+ border-style: solid;
+ border-color: currentColor;
+ }
+ h1,
+ h2 {
+ font-size: inherit;
+ font-weight: inherit;
+ }
+ a {
+ color: inherit;
+ text-decoration: inherit;
+ }
+ pre {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
+ 'Liberation Mono', 'Courier New', monospace;
+ }
+ svg {
+ display: block;
+ vertical-align: middle;
+ }
+
+ svg {
+ shape-rendering: auto;
+ text-rendering: optimizeLegibility;
+ }
+ pre {
+ background-color: rgba(55, 65, 81, 1);
+ border-radius: 0.25rem;
+ color: rgba(229, 231, 235, 1);
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
+ 'Liberation Mono', 'Courier New', monospace;
+ overflow: scroll;
+ padding: 0.5rem 0.75rem;
+ }
+
+ .shadow {
+ box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
+ 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+ }
+ .rounded {
+ border-radius: 1.5rem;
+ }
+
+ .wrapper {
+ width: 100%;
+ }
+ .container {
+ margin-left: auto;
+ margin-right: auto;
+ max-width: 768px;
+ padding-bottom: 3rem;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ color: rgba(55, 65, 81, 1);
+ width: 100%;
+ }
+ #welcome {
+ margin-top: 2.5rem;
+ }
+ #welcome h1 {
+ font-size: 3rem;
+ font-weight: 500;
+ letter-spacing: -0.025em;
+ line-height: 1;
+ }
+ #welcome span {
+ display: block;
+ font-size: 1.875rem;
+ font-weight: 300;
+ line-height: 2.25rem;
+ margin-bottom: 0.5rem;
+ }
+ #hero {
+ align-items: center;
+ background-color: hsla(214, 62%, 21%, 1);
+ border: none;
+ box-sizing: border-box;
+ color: rgba(55, 65, 81, 1);
+ display: grid;
+ grid-template-columns: 1fr;
+ margin-top: 3.5rem;
+ }
+ #hero .text-container {
+ color: rgba(255, 255, 255, 1);
+ padding: 3rem 2rem;
+ }
+ #hero .text-container h2 {
+ font-size: 1.5rem;
+ line-height: 2rem;
+ position: relative;
+ }
+ #hero .text-container h2 svg {
+ color: hsla(162, 47%, 50%, 1);
+ height: 2rem;
+ left: -0.25rem;
+ position: absolute;
+ top: 0;
+ width: 2rem;
+ }
+ #hero .text-container h2 span {
+ margin-left: 2.5rem;
+ }
+ #hero .text-container a {
+ background-color: rgba(255, 255, 255, 1);
+ border-radius: 0.75rem;
+ color: rgba(55, 65, 81, 1);
+ display: inline-block;
+ margin-top: 1.5rem;
+ padding: 1rem 2rem;
+ text-decoration: inherit;
+ }
+ #hero .logo-container {
+ display: none;
+ justify-content: center;
+ padding-left: 2rem;
+ padding-right: 2rem;
+ }
+ #hero .logo-container svg {
+ color: rgba(255, 255, 255, 1);
+ width: 66.666667%;
+ }
+
+ #middle-content {
+ align-items: flex-start;
+ display: grid;
+ gap: 4rem;
+ grid-template-columns: 1fr;
+ margin-top: 3.5rem;
+ }
+
+ #learning-materials {
+ padding: 2.5rem 2rem;
+ }
+ #learning-materials h2 {
+ font-weight: 500;
+ font-size: 1.25rem;
+ letter-spacing: -0.025em;
+ line-height: 1.75rem;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ }
+ .list-item-link {
+ align-items: center;
+ border-radius: 0.75rem;
+ display: flex;
+ margin-top: 1rem;
+ padding: 1rem;
+ transition-property: background-color, border-color, color, fill, stroke,
+ opacity, box-shadow, transform, filter, backdrop-filter,
+ -webkit-backdrop-filter;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+ width: 100%;
+ }
+ .list-item-link svg:first-child {
+ margin-right: 1rem;
+ height: 1.5rem;
+ transition-property: background-color, border-color, color, fill, stroke,
+ opacity, box-shadow, transform, filter, backdrop-filter,
+ -webkit-backdrop-filter;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+ width: 1.5rem;
+ }
+ .list-item-link > span {
+ flex-grow: 1;
+ font-weight: 400;
+ transition-property: background-color, border-color, color, fill, stroke,
+ opacity, box-shadow, transform, filter, backdrop-filter,
+ -webkit-backdrop-filter;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+ }
+ .list-item-link > span > span {
+ color: rgba(107, 114, 128, 1);
+ display: block;
+ flex-grow: 1;
+ font-size: 0.75rem;
+ font-weight: 300;
+ line-height: 1rem;
+ transition-property: background-color, border-color, color, fill, stroke,
+ opacity, box-shadow, transform, filter, backdrop-filter,
+ -webkit-backdrop-filter;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+ }
+ .list-item-link svg:last-child {
+ height: 1rem;
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+ width: 1rem;
+ }
+ .list-item-link:hover {
+ color: rgba(255, 255, 255, 1);
+ background-color: hsla(162, 47%, 50%, 1);
+ }
+ .list-item-link:hover > span {
+ }
+ .list-item-link:hover > span > span {
+ color: rgba(243, 244, 246, 1);
+ }
+ .list-item-link:hover svg:last-child {
+ transform: translateX(0.25rem);
+ }
+
+ #other-links {
+ }
+ .button-pill {
+ padding: 1.5rem 2rem;
+ transition-duration: 300ms;
+ transition-property: background-color, border-color, color, fill, stroke,
+ opacity, box-shadow, transform, filter, backdrop-filter,
+ -webkit-backdrop-filter;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ align-items: center;
+ display: flex;
+ }
+ .button-pill svg {
+ transition-property: background-color, border-color, color, fill, stroke,
+ opacity, box-shadow, transform, filter, backdrop-filter,
+ -webkit-backdrop-filter;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+ flex-shrink: 0;
+ width: 3rem;
+ }
+ .button-pill > span {
+ letter-spacing: -0.025em;
+ font-weight: 400;
+ font-size: 1.125rem;
+ line-height: 1.75rem;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ }
+ .button-pill span span {
+ display: block;
+ font-size: 0.875rem;
+ font-weight: 300;
+ line-height: 1.25rem;
+ }
+ .button-pill:hover svg,
+ .button-pill:hover {
+ color: rgba(255, 255, 255, 1) !important;
+ }
+ #nx-console:hover {
+ background-color: rgba(0, 122, 204, 1);
+ }
+ #nx-console svg {
+ color: rgba(0, 122, 204, 1);
+ }
+ #nx-console-jetbrains {
+ margin-top: 2rem;
+ }
+ #nx-console-jetbrains:hover {
+ background-color: rgba(255, 49, 140, 1);
+ }
+ #nx-console-jetbrains svg {
+ color: rgba(255, 49, 140, 1);
+ }
+ #nx-repo:hover {
+ background-color: rgba(24, 23, 23, 1);
+ }
+ #nx-repo svg {
+ color: rgba(24, 23, 23, 1);
+ }
+
+ #nx-cloud {
+ margin-bottom: 2rem;
+ margin-top: 2rem;
+ padding: 2.5rem 2rem;
+ }
+ #nx-cloud > div {
+ align-items: center;
+ display: flex;
+ }
+ #nx-cloud > div svg {
+ border-radius: 0.375rem;
+ flex-shrink: 0;
+ width: 3rem;
+ }
+ #nx-cloud > div h2 {
+ font-size: 1.125rem;
+ font-weight: 400;
+ letter-spacing: -0.025em;
+ line-height: 1.75rem;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ }
+ #nx-cloud > div h2 span {
+ display: block;
+ font-size: 0.875rem;
+ font-weight: 300;
+ line-height: 1.25rem;
+ }
+ #nx-cloud p {
+ font-size: 1rem;
+ line-height: 1.5rem;
+ margin-top: 1rem;
+ }
+ #nx-cloud pre {
+ margin-top: 1rem;
+ }
+ #nx-cloud a {
+ color: rgba(107, 114, 128, 1);
+ display: block;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ margin-top: 1.5rem;
+ text-align: right;
+ }
+ #nx-cloud a:hover {
+ text-decoration: underline;
+ }
+
+ #commands {
+ padding: 2.5rem 2rem;
+
+ margin-top: 3.5rem;
+ }
+ #commands h2 {
+ font-size: 1.25rem;
+ font-weight: 400;
+ letter-spacing: -0.025em;
+ line-height: 1.75rem;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ }
+ #commands p {
+ font-size: 1rem;
+ font-weight: 300;
+ line-height: 1.5rem;
+ margin-top: 1rem;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ }
+ details {
+ align-items: center;
+ display: flex;
+ margin-top: 1rem;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ width: 100%;
+ }
+ details pre > span {
+ color: rgba(181, 181, 181, 1);
+ }
+ summary {
+ border-radius: 0.5rem;
+ display: flex;
+ font-weight: 400;
+ padding: 0.5rem;
+ cursor: pointer;
+ transition-property: background-color, border-color, color, fill, stroke,
+ opacity, box-shadow, transform, filter, backdrop-filter,
+ -webkit-backdrop-filter;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+ }
+ summary:hover {
+ background-color: rgba(243, 244, 246, 1);
+ }
+ summary svg {
+ height: 1.5rem;
+ margin-right: 1rem;
+ width: 1.5rem;
+ }
+
+ #love {
+ color: rgba(107, 114, 128, 1);
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ margin-top: 3.5rem;
+ opacity: 0.6;
+ text-align: center;
+ }
+ #love svg {
+ color: rgba(252, 165, 165, 1);
+ width: 1.25rem;
+ height: 1.25rem;
+ display: inline;
+ margin-top: -0.25rem;
+ }
+
+ @media screen and (min-width: 768px) {
+ #hero {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+ #hero .logo-container {
+ display: flex;
+ }
+ #middle-content {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+ }
diff --git a/apps/client/src/app/app.element.spec.ts b/apps/client/src/app/app.element.spec.ts
new file mode 100644
index 000000000..2c12da184
--- /dev/null
+++ b/apps/client/src/app/app.element.spec.ts
@@ -0,0 +1,21 @@
+import { AppElement } from './app.element';
+
+describe('AppElement', () => {
+ let app: AppElement;
+
+ beforeEach(() => {
+ app = new AppElement();
+ });
+
+ it('should create successfully', () => {
+ expect(app).toBeTruthy();
+ });
+
+ it('should have a greeting', () => {
+ app.connectedCallback();
+
+ expect(app.querySelector('h1').innerHTML).toContain(
+ 'Welcome @triliumnext/client'
+ );
+ });
+});
diff --git a/apps/client/src/app/app.element.ts b/apps/client/src/app/app.element.ts
new file mode 100644
index 000000000..fab80aa49
--- /dev/null
+++ b/apps/client/src/app/app.element.ts
@@ -0,0 +1,409 @@
+import './app.element.css';
+
+export class AppElement extends HTMLElement {
+ public static observedAttributes = [
+
+ ];
+
+ connectedCallback() {
+ const title = '@triliumnext/client';
+ this.innerHTML = `
+
+
+
+
+
+ Hello there,
+ Welcome ${title} 👋
+
+
+
+
+
+
+
+
+
+
+
+
Next steps
+
Here are some things you can do with Nx:
+
+
+
+
+
+ Add UI library
+
+ # Generate UI lib
+nx g @nx/angular:lib ui
+
+# Add a component
+nx g @nx/angular:component ui/src/lib/button
+
+
+
+
+
+
+ View interactive project graph
+
+ nx graph
+
+
+
+
+
+
+ Run affected commands
+
+ # see what's been affected by changes
+nx affected:graph
+
+# run tests for current changes
+nx affected:test
+
+# run e2e tests for current changes
+nx affected:e2e
+
+
+
+
+ Carefully crafted with
+
+
+
+
+
+
+ `;
+ }
+}
+customElements.define('triliumnext-root', AppElement);
diff --git a/apps/client/src/assets/.gitkeep b/apps/client/src/assets/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/client/src/favicon.ico b/apps/client/src/favicon.ico
new file mode 100644
index 000000000..317ebcb23
Binary files /dev/null and b/apps/client/src/favicon.ico differ
diff --git a/apps/client/src/index.html b/apps/client/src/index.html
new file mode 100644
index 000000000..e206d4837
--- /dev/null
+++ b/apps/client/src/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Client
+
+
+
+
+
+
+
+
+
diff --git a/apps/client/src/main.ts b/apps/client/src/main.ts
new file mode 100644
index 000000000..fdb879ded
--- /dev/null
+++ b/apps/client/src/main.ts
@@ -0,0 +1 @@
+import './app/app.element';
diff --git a/apps/client/src/styles.css b/apps/client/src/styles.css
new file mode 100644
index 000000000..90d4ee007
--- /dev/null
+++ b/apps/client/src/styles.css
@@ -0,0 +1 @@
+/* You can add global styles to this file, and also import other style files */
diff --git a/apps/client/tsconfig.app.json b/apps/client/tsconfig.app.json
new file mode 100644
index 000000000..ec59d3293
--- /dev/null
+++ b/apps/client/tsconfig.app.json
@@ -0,0 +1,36 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "types": [
+ "node"
+ ],
+ "rootDir": "src",
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo"
+ },
+ "exclude": [
+ "out-tsc",
+ "dist",
+ "jest.config.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.test.ts",
+ "vite.config.ts",
+ "vite.config.mts",
+ "vitest.config.ts",
+ "vitest.config.mts",
+ "src/**/*.test.tsx",
+ "src/**/*.spec.tsx",
+ "src/**/*.test.js",
+ "src/**/*.spec.js",
+ "src/**/*.test.jsx",
+ "src/**/*.spec.jsx",
+ "eslint.config.js",
+ "eslint.config.cjs",
+ "eslint.config.mjs"
+ ],
+ "include": [
+ "src/**/*.ts"
+ ]
+}
diff --git a/apps/client/tsconfig.json b/apps/client/tsconfig.json
new file mode 100644
index 000000000..63dbe35fb
--- /dev/null
+++ b/apps/client/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ]
+}
diff --git a/apps/client/tsconfig.spec.json b/apps/client/tsconfig.spec.json
new file mode 100644
index 000000000..164775969
--- /dev/null
+++ b/apps/client/tsconfig.spec.json
@@ -0,0 +1,35 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/vitest",
+ "types": [
+ "vitest/globals",
+ "vitest/importMeta",
+ "vite/client",
+ "node",
+ "vitest"
+ ],
+ "module": "esnext",
+ "moduleResolution": "bundler"
+ },
+ "include": [
+ "vite.config.ts",
+ "vite.config.mts",
+ "vitest.config.ts",
+ "vitest.config.mts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.test.tsx",
+ "src/**/*.spec.tsx",
+ "src/**/*.test.js",
+ "src/**/*.spec.js",
+ "src/**/*.test.jsx",
+ "src/**/*.spec.jsx",
+ "src/**/*.d.ts"
+ ],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ]
+}
diff --git a/apps/client/vite.config.ts b/apps/client/vite.config.ts
new file mode 100644
index 000000000..26be2b87e
--- /dev/null
+++ b/apps/client/vite.config.ts
@@ -0,0 +1,23 @@
+
+import { defineConfig } from 'vite';
+
+export default defineConfig(() => ({
+ root: __dirname,
+ cacheDir: '../../node_modules/.vite/apps/client',
+ plugins: [],
+ // Uncomment this if you are using workers.
+ // worker: {
+ // plugins: [ nxViteTsPaths() ],
+ // },
+ test: {
+ 'watch': false,
+ 'globals': true,
+ 'environment': "jsdom",
+ 'include': ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
+ 'reporters': ["default"],
+ 'coverage': {
+ 'reportsDirectory': './test-output/vitest/coverage',
+ 'provider': 'v8' as const,
+}
+ },
+}));
diff --git a/apps/client/webpack.config.js b/apps/client/webpack.config.js
new file mode 100644
index 000000000..2bcef6b62
--- /dev/null
+++ b/apps/client/webpack.config.js
@@ -0,0 +1,26 @@
+
+const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
+const { join } = require('path');
+
+module.exports = {
+ output: {
+ path: join(__dirname, 'dist'),
+ },
+ devServer: {
+ port: 4200
+ },
+ plugins: [
+ new NxAppWebpackPlugin({
+ tsConfig: './tsconfig.app.json',
+ compiler: 'swc',
+ main: './src/main.ts',
+ index: './src/index.html',
+ baseHref: '/',
+ assets: ["./src/favicon.ico","./src/assets"],
+ styles: ["./src/styles.css"],
+ outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
+ optimization: process.env['NODE_ENV'] === 'production',
+ })
+ ],
+};
+
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 1058fd63b..805361011 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,55 +1,54 @@
-import nx from "@nx/eslint-plugin";
-
-export default [
- ...nx.configs["flat/base"],
- ...nx.configs["flat/typescript"],
- ...nx.configs["flat/javascript"],
- {
- "ignores": [
+import nx from "@nx/eslint-plugin";
+
+export default [
+ ...nx.configs["flat/base"],
+ ...nx.configs["flat/typescript"],
+ ...nx.configs["flat/javascript"],
+ {
"ignores": [
"**/dist",
"**/vite.config.*.timestamp*",
"**/vitest.config.*.timestamp*"
- ]
- },
- {
- files: [
- "**/*.ts",
- "**/*.tsx",
- "**/*.js",
- "**/*.jsx"
- ],
- rules: {
- "@nx/enforce-module-boundaries": [
- "error",
- {
- enforceBuildableLibDependency: true,
- allow: [
- "^.*/eslint(\\.base)?\\.config\\.[cm]?js$"
- ],
- depConstraints: [
- {
- sourceTag: "*",
- onlyDependOnLibsWithTags: [
- "*"
- ]
- }
- ]
- }
- ]
- }
- },
- {
- files: [
- "**/*.ts",
- "**/*.tsx",
- "**/*.cts",
- "**/*.mts",
- "**/*.js",
- "**/*.jsx",
- "**/*.cjs",
- "**/*.mjs"
- ],
- // Override or add rules here
- rules: {}
- }
-];
+ ]
+ },
+ {
+ files: [
+ "**/*.ts",
+ "**/*.tsx",
+ "**/*.js",
+ "**/*.jsx"
+ ],
+ rules: {
+ "@nx/enforce-module-boundaries": [
+ "error",
+ {
+ enforceBuildableLibDependency: true,
+ allow: [
+ "^.*/eslint(\\.base)?\\.config\\.[cm]?js$"
+ ],
+ depConstraints: [
+ {
+ sourceTag: "*",
+ onlyDependOnLibsWithTags: [
+ "*"
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ files: [
+ "**/*.ts",
+ "**/*.tsx",
+ "**/*.cts",
+ "**/*.mts",
+ "**/*.js",
+ "**/*.jsx",
+ "**/*.cjs",
+ "**/*.mjs"
+ ],
+ // Override or add rules here
+ rules: {}
+ }
+];
diff --git a/nx.json b/nx.json
index 2fdfd0fc1..29f05c4d0 100644
--- a/nx.json
+++ b/nx.json
@@ -60,6 +60,12 @@
"buildDepsTargetName": "build-deps",
"watchDepsTargetName": "watch-deps"
}
+ },
+ {
+ "plugin": "@nx/playwright/plugin",
+ "options": {
+ "targetName": "e2e"
+ }
}
],
"targetDefaults": {
@@ -87,6 +93,19 @@
"production",
"^production"
]
+ },
+ "e2e-ci--**/*": {
+ "dependsOn": [
+ "^build"
+ ]
+ }
+ },
+ "generators": {
+ "@nx/web:application": {
+ "style": "css",
+ "linter": "eslint",
+ "unitTestRunner": "vitest",
+ "e2eTestRunner": "playwright"
}
}
}
diff --git a/package-lock.json b/package-lock.json
index c27f4b3b0..e794ec487 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,15 +18,18 @@
},
"devDependencies": {
"@eslint/js": "^9.8.0",
+ "@nx/devkit": "20.8.0",
"@nx/esbuild": "20.8.0",
"@nx/eslint": "20.8.0",
"@nx/eslint-plugin": "20.8.0",
"@nx/express": "20.8.0",
"@nx/js": "20.8.0",
"@nx/node": "20.8.0",
+ "@nx/playwright": "20.8.0",
"@nx/vite": "20.8.0",
"@nx/web": "20.8.0",
"@nx/webpack": "20.8.0",
+ "@playwright/test": "^1.36.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
"@svgr/webpack": "^8.0.1",
"@swc-node/register": "~1.9.1",
@@ -40,10 +43,13 @@
"esbuild": "^0.19.2",
"eslint": "^9.8.0",
"eslint-config-prettier": "^10.0.0",
+ "eslint-plugin-playwright": "^1.6.2",
"jiti": "2.4.2",
+ "jsdom": "~22.1.0",
"jsonc-eslint-parser": "^2.1.0",
"nx": "20.8.0",
"react-refresh": "^0.10.0",
+ "swc-loader": "0.1.15",
"tslib": "^2.3.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.19.0",
@@ -52,6 +58,14 @@
"webpack-cli": "^5.1.4"
}
},
+ "apps/client": {
+ "name": "@triliumnext/client",
+ "version": "0.0.1"
+ },
+ "apps/client-e2e": {
+ "name": "@triliumnext/client-e2e",
+ "version": "0.0.1"
+ },
"apps/server": {
"name": "@triliumnext/server",
"version": "0.0.1",
@@ -170,6 +184,18 @@
"version": "0.0.1",
"extraneous": true
},
+ "apps/server/node_modules/entities": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
+ "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"apps/server/node_modules/eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
@@ -206,6 +232,18 @@
"node": ">=14.14"
}
},
+ "apps/server/node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"apps/server/node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -218,6 +256,45 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "apps/server/node_modules/jsdom": {
+ "version": "26.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.2.1",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.5.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.16",
+ "parse5": "^7.2.1",
+ "rrweb-cssom": "^0.8.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.1.1",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.1.1",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
"apps/server/node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
@@ -239,6 +316,18 @@
"node": ">= 0.6"
}
},
+ "apps/server/node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"apps/server/node_modules/strip-bom": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-5.0.0.tgz",
@@ -318,6 +407,18 @@
"node": ">= 0.6"
}
},
+ "apps/server/node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@adobe/css-tools": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz",
@@ -5684,6 +5785,29 @@
"node": ">= 10"
}
},
+ "node_modules/@nx/playwright": {
+ "version": "20.8.0",
+ "resolved": "https://registry.npmjs.org/@nx/playwright/-/playwright-20.8.0.tgz",
+ "integrity": "sha512-HQd6GCk4j2qNwpRY6OwaiazxXc9R/4KC0mNHN9cE7IWK2mUix+RavFYBZ8+dh7Q+O1mU0euyM7rjbT7xs1sPrw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nx/devkit": "20.8.0",
+ "@nx/eslint": "20.8.0",
+ "@nx/js": "20.8.0",
+ "@phenomnomnominal/tsquery": "~5.0.1",
+ "minimatch": "9.0.3",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "@playwright/test": "^1.36.0"
+ },
+ "peerDependenciesMeta": {
+ "@playwright/test": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@nx/vite": {
"version": "20.8.0",
"resolved": "https://registry.npmjs.org/@nx/vite/-/vite-20.8.0.tgz",
@@ -6135,6 +6259,22 @@
"node": ">=14"
}
},
+ "node_modules/@playwright/test": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz",
+ "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.52.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.16.tgz",
@@ -8098,6 +8238,24 @@
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
"license": "MIT"
},
+ "node_modules/@tootallnate/once": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@triliumnext/client": {
+ "resolved": "apps/client",
+ "link": true
+ },
+ "node_modules/@triliumnext/client-e2e": {
+ "resolved": "apps/client-e2e",
+ "link": true
+ },
"node_modules/@triliumnext/commons": {
"resolved": "packages/commons",
"link": true
@@ -9710,6 +9868,14 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/abab": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
+ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
+ "deprecated": "Use your platform's native atob() and btoa() methods instead",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@@ -13441,6 +13607,20 @@
],
"license": "BSD-2-Clause"
},
+ "node_modules/domexception": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
+ "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
@@ -14051,6 +14231,60 @@
"eslint": ">=7.0.0"
}
},
+ "node_modules/eslint-plugin-playwright": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-1.8.3.tgz",
+ "integrity": "sha512-h87JPFHkz8a6oPhn8GRGGhSQoAJjx0AkOv1jME6NoMk2FpEsfvfJJNaQDxLSqSALkCr0IJXPGTnp6SIRVu5Nqg==",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "examples"
+ ],
+ "dependencies": {
+ "globals": "^13.23.0"
+ },
+ "engines": {
+ "node": ">=16.6.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=8.40.0",
+ "eslint-plugin-jest": ">=25"
+ },
+ "peerDependenciesMeta": {
+ "eslint-plugin-jest": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-playwright/node_modules/globals": {
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint-plugin-playwright/node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/eslint-scope": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
@@ -18017,37 +18251,41 @@
}
},
"node_modules/jsdom": {
- "version": "26.1.0",
- "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
- "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+ "version": "22.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz",
+ "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "cssstyle": "^4.2.1",
- "data-urls": "^5.0.0",
- "decimal.js": "^10.5.0",
- "html-encoding-sniffer": "^4.0.0",
- "http-proxy-agent": "^7.0.2",
- "https-proxy-agent": "^7.0.6",
+ "abab": "^2.0.6",
+ "cssstyle": "^3.0.0",
+ "data-urls": "^4.0.0",
+ "decimal.js": "^10.4.3",
+ "domexception": "^4.0.0",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^3.0.0",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.1",
"is-potential-custom-element-name": "^1.0.1",
- "nwsapi": "^2.2.16",
- "parse5": "^7.2.1",
- "rrweb-cssom": "^0.8.0",
+ "nwsapi": "^2.2.4",
+ "parse5": "^7.1.2",
+ "rrweb-cssom": "^0.6.0",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
- "tough-cookie": "^5.1.1",
- "w3c-xmlserializer": "^5.0.0",
+ "tough-cookie": "^4.1.2",
+ "w3c-xmlserializer": "^4.0.0",
"webidl-conversions": "^7.0.0",
- "whatwg-encoding": "^3.1.1",
- "whatwg-mimetype": "^4.0.0",
- "whatwg-url": "^14.1.1",
- "ws": "^8.18.0",
- "xml-name-validator": "^5.0.0"
+ "whatwg-encoding": "^2.0.0",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^12.0.1",
+ "ws": "^8.13.0",
+ "xml-name-validator": "^4.0.0"
},
"engines": {
- "node": ">=18"
+ "node": ">=16"
},
"peerDependencies": {
- "canvas": "^3.0.0"
+ "canvas": "^2.5.0"
},
"peerDependenciesMeta": {
"canvas": {
@@ -18055,22 +18293,81 @@
}
}
},
- "node_modules/jsdom/node_modules/html-encoding-sniffer": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
- "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "node_modules/jsdom/node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "whatwg-encoding": "^3.1.1"
+ "debug": "4"
},
"engines": {
- "node": ">=18"
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/jsdom/node_modules/cssstyle": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz",
+ "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "rrweb-cssom": "^0.6.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/jsdom/node_modules/data-urls": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz",
+ "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "abab": "^2.0.6",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^12.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/jsdom/node_modules/http-proxy-agent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/jsdom/node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
}
},
"node_modules/jsdom/node_modules/parse5": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
"integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"entities": "^4.5.0"
@@ -18079,16 +18376,97 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
- "node_modules/jsdom/node_modules/whatwg-encoding": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
- "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
- "license": "MIT",
+ "node_modules/jsdom/node_modules/rrweb-cssom": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
+ "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsdom/node_modules/tough-cookie": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
+ "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
+ "dev": true,
+ "license": "BSD-3-Clause",
"dependencies": {
- "iconv-lite": "0.6.3"
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.2.0",
+ "url-parse": "^1.5.3"
},
"engines": {
- "node": ">=18"
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsdom/node_modules/tr46": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz",
+ "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/jsdom/node_modules/universalify": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
+ "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/jsdom/node_modules/w3c-xmlserializer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
+ "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/jsdom/node_modules/whatwg-mimetype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jsdom/node_modules/whatwg-url": {
+ "version": "12.0.1",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz",
+ "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^4.1.1",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/jsdom/node_modules/xml-name-validator": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+ "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12"
}
},
"node_modules/jsesc": {
@@ -20962,6 +21340,53 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/playwright": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
+ "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.52.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
+ "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/plumb": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/plumb/-/plumb-0.1.0.tgz",
@@ -21813,6 +22238,19 @@
"license": "MIT",
"optional": true
},
+ "node_modules/psl": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
+ "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/lupomontero"
+ }
+ },
"node_modules/pump": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
@@ -21871,6 +22309,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/querystringify": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -24623,6 +25068,20 @@
"express": ">=4.0.0 || >=5.0.0-beta"
}
},
+ "node_modules/swc-loader": {
+ "version": "0.1.15",
+ "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.1.15.tgz",
+ "integrity": "sha512-cn1WPIeQJvXM4bbo3OwdEIapsQ4uUGOfyFj0h2+2+brT0k76DCGnZXDE2KmcqTd2JSQ+b61z2NPMib7eEwMYYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "loader-utils": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@swc/core": "^1.2.52",
+ "webpack": ">=2"
+ }
+ },
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -25270,7 +25729,7 @@
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -25501,6 +25960,17 @@
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
"license": "MIT"
},
+ "node_modules/url-parse": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
+ "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "querystringify": "^2.1.1",
+ "requires-port": "^1.0.0"
+ }
+ },
"node_modules/use-callback-ref": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
@@ -26373,6 +26843,7 @@
"version": "5.98.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz",
"integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint-scope": "^3.7.7",
@@ -26761,6 +27232,7 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
@@ -26774,6 +27246,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
diff --git a/package.json b/package.json
index 6a0e9949f..10a9efde5 100644
--- a/package.json
+++ b/package.json
@@ -9,15 +9,18 @@
"private": true,
"devDependencies": {
"@eslint/js": "^9.8.0",
+ "@nx/devkit": "20.8.0",
"@nx/esbuild": "20.8.0",
"@nx/eslint": "20.8.0",
"@nx/eslint-plugin": "20.8.0",
"@nx/express": "20.8.0",
"@nx/js": "20.8.0",
"@nx/node": "20.8.0",
+ "@nx/playwright": "20.8.0",
"@nx/vite": "20.8.0",
"@nx/web": "20.8.0",
"@nx/webpack": "20.8.0",
+ "@playwright/test": "^1.36.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
"@svgr/webpack": "^8.0.1",
"@swc-node/register": "~1.9.1",
@@ -31,10 +34,13 @@
"esbuild": "^0.19.2",
"eslint": "^9.8.0",
"eslint-config-prettier": "^10.0.0",
+ "eslint-plugin-playwright": "^1.6.2",
"jiti": "2.4.2",
+ "jsdom": "~22.1.0",
"jsonc-eslint-parser": "^2.1.0",
"nx": "20.8.0",
"react-refresh": "^0.10.0",
+ "swc-loader": "0.1.15",
"tslib": "^2.3.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.19.0",
diff --git a/tsconfig.json b/tsconfig.json
index 5fdc35592..bee892ca8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -14,6 +14,12 @@
},
{
"path": "./packages/turndown-plugin-gfm"
+ },
+ {
+ "path": "./apps/client-e2e"
+ },
+ {
+ "path": "./apps/client"
}
]
}