From 17f8d3315fbdf08de981b0c08a93660e1e5d13bd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 22:37:49 +0000 Subject: [PATCH 1/6] Update vitest monorepo to v3.0.7 --- bun.lock | 8 ++++---- package.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bun.lock b/bun.lock index 3398afe..5772ab0 100644 --- a/bun.lock +++ b/bun.lock @@ -40,8 +40,8 @@ "@testing-library/user-event": "^14.6.1", "@types/sinonjs__fake-timers": "^8.1.5", "@types/wicg-file-system-access": "^2023.10.5", - "@vitest/coverage-istanbul": "3.0.6", - "@vitest/coverage-v8": "3.0.6", + "@vitest/coverage-istanbul": "3.0.7", + "@vitest/coverage-v8": "3.0.7", "bun-types": "^1.2.2", "jsdom": "^26.0.0", "solid-devtools": "^0.33.0", @@ -437,9 +437,9 @@ "@vinxi/server-components": ["@vinxi/server-components@0.5.0", "", { "dependencies": { "@vinxi/plugin-directives": "0.5.0", "acorn": "^8.10.0", "acorn-loose": "^8.3.0", "acorn-typescript": "^1.4.3", "astring": "^1.8.6", "magicast": "^0.2.10", "recast": "^0.23.4" }, "peerDependencies": { "vinxi": "^0.5.0" } }, "sha512-2p6ZYzoqF7ZAriU0rC9KJWSX/n5qHhUBs7x04SLYzmy9lFxQNw3YHsmsA4b3aHDU+Mxw26wyFwvIbrL6eU3Gyw=="], - "@vitest/coverage-istanbul": ["@vitest/coverage-istanbul@3.0.6", "", { "dependencies": { "@istanbuljs/schema": "^0.1.3", "debug": "^4.4.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-instrument": "^6.0.3", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magicast": "^0.3.5", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "vitest": "3.0.6" } }, "sha512-e+8HkmVlPpqOZXIWGE8opxex3trTMCeCMHax7yG0JbWOtGRVKBjuNS/GGA/eta89LuXUrCIcQrRfJHLUrWl7Wg=="], + "@vitest/coverage-istanbul": ["@vitest/coverage-istanbul@3.0.7", "", { "dependencies": { "@istanbuljs/schema": "^0.1.3", "debug": "^4.4.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-instrument": "^6.0.3", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magicast": "^0.3.5", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "vitest": "3.0.7" } }, "sha512-hkd7rlfnqQJFlg6IPv9aFNaxJNkWLasdfaMJR3MBsBkxddSYy5ax9sW6Vv1/3tmmyT9m/b0lHDNknybKJ33cXw=="], - "@vitest/coverage-v8": ["@vitest/coverage-v8@3.0.6", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", "debug": "^4.4.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", "std-env": "^3.8.0", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "@vitest/browser": "3.0.6", "vitest": "3.0.6" }, "optionalPeers": ["@vitest/browser"] }, "sha512-JRTlR8Bw+4BcmVTICa7tJsxqphAktakiLsAmibVLAWbu1lauFddY/tXeM6sAyl1cgkPuXtpnUgaCPhTdz1Qapg=="], + "@vitest/coverage-v8": ["@vitest/coverage-v8@3.0.7", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", "debug": "^4.4.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", "std-env": "^3.8.0", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "@vitest/browser": "3.0.7", "vitest": "3.0.7" }, "optionalPeers": ["@vitest/browser"] }, "sha512-Av8WgBJLTrfLOer0uy3CxjlVuWK4CzcLBndW1Nm2vI+3hZ2ozHututkfc7Blu1u6waeQ7J8gzPK/AsBRnWA5mQ=="], "@vitest/expect": ["@vitest/expect@3.0.6", "", { "dependencies": { "@vitest/spy": "3.0.6", "@vitest/utils": "3.0.6", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-zBduHf/ja7/QRX4HdP1DSq5XrPgdN+jzLOwaTq/0qZjYfgETNFCKf9nOAp2j3hmom3oTbczuUzrzg9Hafh7hNg=="], diff --git a/package.json b/package.json index b349e55..b104a17 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,8 @@ "@testing-library/user-event": "^14.6.1", "@types/sinonjs__fake-timers": "^8.1.5", "@types/wicg-file-system-access": "^2023.10.5", - "@vitest/coverage-istanbul": "3.0.6", - "@vitest/coverage-v8": "3.0.6", + "@vitest/coverage-istanbul": "3.0.7", + "@vitest/coverage-v8": "3.0.7", "bun-types": "^1.2.2", "jsdom": "^26.0.0", "solid-devtools": "^0.33.0", From 7cdefe3f6ef1ee24e2066028cdfd24247f98982c Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Wed, 5 Mar 2025 15:48:21 +0100 Subject: [PATCH 2/6] fix error with circular reactivity --- src/components/textarea/textarea.tsx | 27 +++++++++++++++++++++------ src/features/file/grid.tsx | 21 ++++++++++----------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/components/textarea/textarea.tsx b/src/components/textarea/textarea.tsx index 80433d2..c04480d 100644 --- a/src/components/textarea/textarea.tsx +++ b/src/components/textarea/textarea.tsx @@ -1,4 +1,4 @@ -import { Component, createEffect, createMemo, createSignal, For, onMount, untrack } from 'solid-js'; +import { Component, createEffect, createMemo, createSignal, For, on, onMount, untrack } from 'solid-js'; import { debounce } from '@solid-primitives/scheduled'; import { createSelection, getTextNodes } from '@solid-primitives/selection'; import { createSource } from '~/features/source'; @@ -18,12 +18,21 @@ interface TextareaProps { export function Textarea(props: TextareaProps) { const [selection, setSelection] = createSelection(); const [editorRef, setEditorRef] = createSignal(); + let mounted = false; const source = createSource(props.value); - createEffect(() => { - props.oninput?.(source.in); - }); + createEffect(on(() => [props.oninput, source.in] as const, ([oninput, text]) => { + if (!mounted) { + return; + } + + oninput?.(text); + })); + + onMount((() => { + mounted = true; + })); createEffect(() => { source.in = props.value; @@ -109,13 +118,19 @@ const Suggestions: Component = () => { const ref = untrack(() => suggestionRef()!); if (m === undefined) { - ref.hidePopover(); + if (ref.matches(':popover-open')) { + ref.hidePopover(); + } return; } m.style.setProperty('anchor-name', '--suggestions'); - ref.showPopover(); + + if (ref.matches(':not(:popover-open)')) { + ref.showPopover(); + } + ref.focus() return m; diff --git a/src/features/file/grid.tsx b/src/features/file/grid.tsx index 59696f7..0deb756 100644 --- a/src/features/file/grid.tsx +++ b/src/features/file/grid.tsx @@ -29,7 +29,7 @@ const groupBy = (rows: DataSetRowNode[]) => { : ({ kind: 'group', key, groupedBy: 'key', nodes: group(nodes.map(n => ({ ...n, _key: n._key.slice(key.length + 1) }))) }) ); - return group(rows.map(r => ({ ...r, _key: r.value.key }))) as any; + return group(rows.filter(r => r.value.key).map(r => ({ ...r, _key: r.value.key }))) as any; } export function Grid(props: { class?: string, rows: Entry[], locales: string[], api?: (api: GridApi) => any, children?: (key: string) => JSX.Element }) { @@ -65,19 +65,18 @@ export function Grid(props: { class?: string, rows: Entry[], locales: string[], const [api, setApi] = createSignal>(); // Normalize dataset in order to make sure all the files have the correct structure - createEffect(() => { - // For tracking - props.rows; - // const value = untrack(() => rows.value); + // createEffect(() => { + // // For tracking + // props.rows; - rows.mutateEach(({ key, ...locales }) => ({ key, ...Object.fromEntries(Object.entries(locales).map(([locale, value]) => [locale, value ?? ''])) })) - }); + // rows.mutateEach(({ key, ...locales }) => ({ key, ...Object.fromEntries(Object.entries(locales).map(([locale, value]) => [locale, value ?? ''])) })) + // }); - createEffect(() => { - const l = addedLocales(); + // createEffect(() => { + // const l = addedLocales(); - rows.mutateEach(({ key, ...rest }) => ({ key, ...rest, ...Object.fromEntries(l.map(locale => [locale, rest[locale] ?? ''])) })); - }); + // rows.mutateEach(({ key, ...rest }) => ({ key, ...rest, ...Object.fromEntries(l.map(locale => [locale, rest[locale] ?? ''])) })); + // }); createEffect(() => { props.api?.({ From 5e7f7729990f86a746b97da51a5e5413dbc432dc Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Wed, 5 Mar 2025 15:48:59 +0100 Subject: [PATCH 3/6] implement file system observer --- src/features/file/helpers.ts | 177 +++++++++++++++++- src/routes/(editor)/edit.tsx | 4 +- src/routes/(editor)/experimental.tsx | 1 + .../experimental/file-system-observer.tsx | 32 ++++ 4 files changed, 208 insertions(+), 6 deletions(-) create mode 100644 src/routes/(editor)/experimental/file-system-observer.tsx diff --git a/src/features/file/helpers.ts b/src/features/file/helpers.ts index 544bbd2..a4b68bd 100644 --- a/src/features/file/helpers.ts +++ b/src/features/file/helpers.ts @@ -1,6 +1,11 @@ import { Accessor, createEffect, from, createSignal } from "solid-js"; import { json } from "./parser"; import { filter } from "~/utilities"; +import { isServer } from "solid-js/web"; +import { installIntoGlobal } from 'iterator-helpers-polyfill'; +import { debounce } from "@solid-primitives/scheduled"; + +installIntoGlobal(); interface Files extends Record { } interface Contents extends Map> { } @@ -13,8 +18,16 @@ export const read = (file: File): Promise | undefined> => { } }; -export const readFiles = (directory: Accessor): Accessor => { - return createPolled(directory, async (directory, prev) => { +export const readFiles = (directory: Accessor): Accessor => { + return (!isServer && 'FileSystemObserver' in window) ? readFiles__observer(directory) : readFiles__polled(directory) +}; + +const readFiles__polled = (directory: Accessor): Accessor => { + return createPolled(directory, async (directory, prev) => { + if (!directory) { + return prev; + } + const next: Files = Object.fromEntries(await Array.fromAsync( filter(directory.values(), (handle): handle is FileSystemFileHandle => handle.kind === 'file' && handle.name.endsWith('.json')), async handle => [await handle.getUniqueId(), { file: await handle.getFile(), handle }] @@ -39,9 +52,130 @@ export const readFiles = (directory: Accessor): Acces }, { interval: 1000, initialValue: {} }); }; +const readFiles__observer = (directory: Accessor): Accessor => { + const [files, setFiles] = createSignal({}); + + const observer = new FileSystemObserver(debounce(async records => { + for (const record of records) { + switch (record.type) { + case 'modified': { + if (record.changedHandle.kind === 'file') { + const handle = record.changedHandle as FileSystemFileHandle; + const id = await handle.getUniqueId(); + const file = await handle.getFile(); + + setFiles(prev => ({ ...prev, [id]: { file, handle } })); + } + + break; + } + + default: { + console.log(record); + + break; + } + } + } + }, 10)); + + createEffect((last = undefined) => { + if (last) { + observer.unobserve(last); + } + + const dir = directory(); + + if (!dir) { + return; + } + + observer.observe(dir); + + (async () => { + setFiles(Object.fromEntries( + await dir.values() + .filter((handle): handle is FileSystemFileHandle => handle.kind === 'file' && handle.name.endsWith('.json')) + .map(async handle => [await handle.getUniqueId(), { file: await handle.getFile(), handle }] as const) + .toArray() + )); + })(); + + return dir; + }); + + return files; +}; + +const HANDLE = Symbol('handle'); const LAST_MODIFIED = Symbol('lastModified'); -export const contentsOf = (directory: Accessor): Accessor => { - return createPolled(directory, async (directory, prev) => { +export const contentsOf = (directory: Accessor): Accessor => { + return (!isServer && 'FileSystemObserver' in window) ? contentsOf__observer(directory) : contentsOf__polled(directory) +}; + +const contentsOf__observer = (directory: Accessor): Accessor => { + const [contents, setContents] = createSignal(new Map); + + const observer = new FileSystemObserver(debounce(async records => { + for (const record of records) { + switch (record.type) { + case 'modified': { + if (record.changedHandle.kind === 'file') { + const handle = record.changedHandle as FileSystemFileHandle; + const id = await handle.getUniqueId(); + const file = await handle.getFile(); + const entries = (await read(file))!; + entries[LAST_MODIFIED] = file.lastModified; + + setContents(prev => new Map([...prev, [id, entries]])); + } + + break; + } + + default: { + console.log(record); + + break; + } + } + } + }, 10)); + + createEffect((last = undefined) => { + if (last) { + observer.unobserve(last); + } + + const dir = directory(); + + if (!dir) { + return; + } + + observer.observe(dir); + + (async () => { + setContents(new Map(await walk(dir).map(async ({ id, file }) => { + const entries = (await read(file))!; + entries[LAST_MODIFIED] = file.lastModified; + + return [id, entries] as const; + }).toArray())); + })(); + + return dir; + }); + + return contents; +}; + +const contentsOf__polled = (directory: Accessor): Accessor => { + return createPolled(directory, async (directory, prev) => { + if (!directory) { + return prev; + } + const files = await Array.fromAsync(walk(directory)); const next = async () => new Map(await Promise.all(files.map(async ({ id, file }) => { @@ -116,6 +250,41 @@ async function* walk(directory: FileSystemDirectoryHandle, path: string[] = []): declare global { interface Map { + [HANDLE]: FileSystemFileHandle; [LAST_MODIFIED]: number; } + + type FileSystemObserverCallback = ( + records: FileSystemChangeRecord[], + observer: FileSystemObserver + ) => void; + + interface FileSystemObserverObserveOptions { + recursive?: boolean; + } + + type FileSystemChangeType = 'appeared' | 'disappeared' | 'modified' | 'moved' | 'unknown' | 'errored'; + + interface FileSystemChangeRecord { + readonly changedHandle: FileSystemHandle; + readonly relativePathComponents: ReadonlyArray; + readonly type: FileSystemChangeType; + readonly relativePathMovedFrom?: ReadonlyArray; + } + + interface FileSystemObserver { + observe( + handle: FileSystemHandle, + options?: FileSystemObserverObserveOptions + ): Promise; + unobserve(handle: FileSystemHandle): void; + disconnect(): void; + } + + interface FileSystemObserverConstructor { + new(callback: FileSystemObserverCallback): FileSystemObserver; + readonly prototype: FileSystemObserver; + } + + var FileSystemObserver: FileSystemObserverConstructor; } \ No newline at end of file diff --git a/src/routes/(editor)/edit.tsx b/src/routes/(editor)/edit.tsx index 95604ae..0eed71e 100644 --- a/src/routes/(editor)/edit.tsx +++ b/src/routes/(editor)/edit.tsx @@ -13,9 +13,8 @@ import { useI18n } from "~/features/i18n"; import { makePersisted } from "@solid-primitives/storage"; import { writeClipboard } from "@solid-primitives/clipboard"; import { destructure } from "@solid-primitives/destructure"; -import css from "./edit.module.css"; import { contentsOf } from "~/features/file/helpers"; -import { createHtmlParser, createMarkdownParser, createSource } from "~/features/source"; +import css from "./edit.module.css"; const isInstalledPWA = !isServer && window.matchMedia('(display-mode: standalone)').matches; @@ -346,6 +345,7 @@ const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter(); const files = readFiles(() => props.directory); + // const __contents = contentsOf(() => props.directory); const [contents] = createResource(files, (files) => Promise.all(Object.entries(files).map(async ([id, { file, handle }]) => ({ id, handle, lang: file.name.split('.').at(0)!, entries: (await read(file))! }))), { initialValue: [] }); const [entries, rows] = destructure(() => { diff --git a/src/routes/(editor)/experimental.tsx b/src/routes/(editor)/experimental.tsx index 5ddac59..6282f50 100644 --- a/src/routes/(editor)/experimental.tsx +++ b/src/routes/(editor)/experimental.tsx @@ -18,6 +18,7 @@ export default function Experimental(props: ParentProps) { + }> diff --git a/src/routes/(editor)/experimental/file-system-observer.tsx b/src/routes/(editor)/experimental/file-system-observer.tsx new file mode 100644 index 0000000..44f4eb9 --- /dev/null +++ b/src/routes/(editor)/experimental/file-system-observer.tsx @@ -0,0 +1,32 @@ +import { createEffect, createSignal, on } from "solid-js"; +import { readFiles } from "~/features/file"; +import { contentsOf } from "~/features/file/helpers"; + +export default function FileObserver(props: {}) { + const [dir, setDir] = createSignal(); + + const files = readFiles(dir); + const contents = contentsOf(dir); + + const open = async () => { + const handle = await window.showDirectoryPicker(); + + setDir(handle) + }; + + createEffect(() => { + console.log('dir', dir()); + }); + + createEffect(() => { + console.log('files', files()); + }); + + createEffect(() => { + console.log('contents', contents()); + }); + + return
+ +
; +} \ No newline at end of file From 0f575192912d14b061b27dfc0dde4277a37e2e30 Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Wed, 5 Mar 2025 16:04:01 +0100 Subject: [PATCH 4/6] move the session secret to GH secret --- .github/workflows/app.yml | 1 + Dockerfile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/app.yml b/.github/workflows/app.yml index 9e94c86..8fc766a 100644 --- a/.github/workflows/app.yml +++ b/.github/workflows/app.yml @@ -49,6 +49,7 @@ jobs: - name: Build container images run: | + echo 'SESSION_SECRET=${{ secrets.SESSION_PASSWORD }}' > .env docker build . --file Dockerfile --tag ${{ secrets.ACR_LOGIN_SERVER }}/$IMAGE_NAME:${{needs.versionize.outputs.semver}} docker build . --file Dockerfile --tag ${{ secrets.ACR_LOGIN_SERVER }}/$IMAGE_NAME:latest diff --git a/Dockerfile b/Dockerfile index dbc4245..1395753 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN cd /temp/prod && bun install --frozen-lockfile --production FROM base AS prerelease COPY --from=install /temp/dev/node_modules node_modules COPY . . -RUN echo "SESSION_SECRET=$(head -c 64 /dev/random | base64)" > .env +# RUN echo "SESSION_SECRET=$(head -c 64 /dev/random | base64)" > .env ENV NODE_ENV=production ENV SERVER_PRESET=bun From ee8006f834fd21f87d5cd96f04d2033148fcf29a Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Wed, 5 Mar 2025 16:34:17 +0100 Subject: [PATCH 5/6] idk anymore. yolo --- src/features/theme/context.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/features/theme/context.tsx b/src/features/theme/context.tsx index 5f75c50..bc2e860 100644 --- a/src/features/theme/context.tsx +++ b/src/features/theme/context.tsx @@ -18,6 +18,8 @@ export interface State { const getSession = async () => { 'use server'; + console.log(process.env.SESSION_SECRET); + return useSession({ password: process.env.SESSION_SECRET!, }); From 0786d53474ad2ed76e6086e7a4e7da6ae5c674eb Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Wed, 5 Mar 2025 16:38:01 +0100 Subject: [PATCH 6/6] revert --- src/features/theme/context.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/features/theme/context.tsx b/src/features/theme/context.tsx index bc2e860..5f75c50 100644 --- a/src/features/theme/context.tsx +++ b/src/features/theme/context.tsx @@ -18,8 +18,6 @@ export interface State { const getSession = async () => { 'use server'; - console.log(process.env.SESSION_SECRET); - return useSession({ password: process.env.SESSION_SECRET!, });