From a7a14b78c1442b20fd95e0d7dd418762b9dc30e3 Mon Sep 17 00:00:00 2001 From: Jon Koops Date: Thu, 26 Mar 2026 16:23:46 +0100 Subject: [PATCH] chore: add `DOM.AsyncIterable` lib and use standard filesystem types Add `DOM.AsyncIterable` to the shared TypeScript configuration in `@backstage/cli`, making standard async iteration methods available on DOM APIs like `FileSystemDirectoryHandle`. This aligns behavior with TypeScript 6.0, where this lib is included in `DOM` by default. With the async iterable types now available, replace the custom `IterableDirectoryHandle` and `WritableFileHandle` types in the scaffolder plugin with the standard `FileSystemDirectoryHandle` and `FileSystemFileHandle` DOM types. Add type guard functions for `FileSystemHandle` since it is not a discriminated union. Signed-off-by: Jon Koops --- .changeset/ts6-cli-dom-asynciterable.md | 5 +++ .changeset/ts6-plugin-scaffolder.md | 5 +++ packages/cli/config/tsconfig.json | 2 +- .../useTemplateDirectory.test.ts | 11 ++---- .../src/lib/filesystem/WebFileSystemAccess.ts | 39 +++++++++---------- .../src/lib/filesystem/WebFileSystemStore.ts | 3 +- 6 files changed, 34 insertions(+), 31 deletions(-) create mode 100644 .changeset/ts6-cli-dom-asynciterable.md create mode 100644 .changeset/ts6-plugin-scaffolder.md diff --git a/.changeset/ts6-cli-dom-asynciterable.md b/.changeset/ts6-cli-dom-asynciterable.md new file mode 100644 index 0000000000..e27a81d60f --- /dev/null +++ b/.changeset/ts6-cli-dom-asynciterable.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added `DOM.AsyncIterable` to the default `lib` in the shared TypeScript configuration, enabling standard async iteration support for DOM APIs such as `FileSystemDirectoryHandle`. This aligns behavior with [TypeScript 6.0](https://devblogs.microsoft.com/typescript/announcing-typescript-6-0/#the-dom-lib-now-contains-domiterable-and-domasynciterable), where this lib is included in `DOM` by default. diff --git a/.changeset/ts6-plugin-scaffolder.md b/.changeset/ts6-plugin-scaffolder.md new file mode 100644 index 0000000000..07aa9b642c --- /dev/null +++ b/.changeset/ts6-plugin-scaffolder.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Removed custom `IterableDirectoryHandle` and `WritableFileHandle` types in favor of the standard DOM `FileSystemDirectoryHandle` and `FileSystemFileHandle` types, which are now available through the `DOM.AsyncIterable` lib added to the shared TypeScript configuration. diff --git a/packages/cli/config/tsconfig.json b/packages/cli/config/tsconfig.json index 40a572da4d..5d6a4bbc27 100644 --- a/packages/cli/config/tsconfig.json +++ b/packages/cli/config/tsconfig.json @@ -12,7 +12,7 @@ "incremental": true, "isolatedModules": true, "jsx": "react", - "lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2023"], + "lib": ["DOM", "DOM.Iterable", "DOM.AsyncIterable", "ScriptHost", "ES2023"], "module": "ES2020", "moduleResolution": "bundler", "noEmit": false, diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/useTemplateDirectory.test.ts b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/useTemplateDirectory.test.ts index bdc1534f27..f4e7d4d1a4 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/useTemplateDirectory.test.ts +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/useTemplateDirectory.test.ts @@ -21,10 +21,7 @@ import { TemplateDirectoryAccess, WebFileSystemStore, } from '../../../lib/filesystem'; -import { - IterableDirectoryHandle, - WebFileSystemAccess, -} from '../../../lib/filesystem/WebFileSystemAccess'; +import { WebFileSystemAccess } from '../../../lib/filesystem/WebFileSystemAccess'; jest.mock('../../../lib/filesystem/createExampleTemplate'); @@ -49,7 +46,7 @@ describe('useTemplateDirectory', () => { }); it('should return an access when there is existing directory in the file system store', async () => { - const handle = {} as IterableDirectoryHandle; + const handle = {} as FileSystemDirectoryHandle; jest.spyOn(WebFileSystemStore, 'getDirectory').mockResolvedValue(handle); @@ -65,7 +62,7 @@ describe('useTemplateDirectory', () => { const handle = {}; jest .spyOn(WebFileSystemStore, 'getDirectory') - .mockResolvedValue(handle as IterableDirectoryHandle); + .mockResolvedValue(handle as FileSystemDirectoryHandle); const setDirectory = jest .spyOn(WebFileSystemStore, 'setDirectory') .mockResolvedValue(undefined); @@ -92,7 +89,7 @@ describe('useTemplateDirectory', () => { (createExampleTemplate as jest.Mock).mockResolvedValue(handle); jest .spyOn(WebFileSystemStore, 'getDirectory') - .mockResolvedValue(handle as IterableDirectoryHandle); + .mockResolvedValue(handle as FileSystemDirectoryHandle); const setDirectory = jest .spyOn(WebFileSystemStore, 'setDirectory') .mockResolvedValue(undefined); diff --git a/plugins/scaffolder/src/lib/filesystem/WebFileSystemAccess.ts b/plugins/scaffolder/src/lib/filesystem/WebFileSystemAccess.ts index ddee37117b..3b2fff5ee7 100644 --- a/plugins/scaffolder/src/lib/filesystem/WebFileSystemAccess.ts +++ b/plugins/scaffolder/src/lib/filesystem/WebFileSystemAccess.ts @@ -16,30 +16,27 @@ import { TemplateDirectoryAccess, TemplateFileAccess } from './types'; -type WritableFileHandle = FileSystemFileHandle & { - createWritable(): Promise<{ - write(data: string | Blob | BufferSource): Promise; - close(): Promise; - }>; -}; +function isFileHandle( + handle: FileSystemHandle, +): handle is FileSystemFileHandle { + return handle.kind === 'file'; +} -// A nicer type than the one from the TS lib -export interface IterableDirectoryHandle extends FileSystemDirectoryHandle { - values(): AsyncIterable< - | ({ kind: 'file' } & WritableFileHandle) - | ({ kind: 'directory' } & IterableDirectoryHandle) - >; +function isDirectoryHandle( + handle: FileSystemHandle, +): handle is FileSystemDirectoryHandle { + return handle.kind === 'directory'; } const showDirectoryPicker = (window as any).showDirectoryPicker as - | (() => Promise) + | (() => Promise) | undefined; class WebFileAccess implements TemplateFileAccess { readonly path: string; - private readonly handle: WritableFileHandle; + private readonly handle: FileSystemFileHandle; - constructor(path: string, handle: WritableFileHandle) { + constructor(path: string, handle: FileSystemFileHandle) { this.path = path; this.handle = handle; } @@ -57,9 +54,9 @@ class WebFileAccess implements TemplateFileAccess { /** @internal */ export class WebDirectoryAccess implements TemplateDirectoryAccess { - private readonly handle: IterableDirectoryHandle; + private readonly handle: FileSystemDirectoryHandle; - constructor(handle: IterableDirectoryHandle) { + constructor(handle: FileSystemDirectoryHandle) { this.handle = handle; } @@ -72,13 +69,13 @@ export class WebDirectoryAccess implements TemplateDirectoryAccess { } private async *listDirectoryContents( - dirHandle: IterableDirectoryHandle, + dirHandle: FileSystemDirectoryHandle, basePath: string[] = [], ): AsyncIterable { for await (const handle of dirHandle.values()) { - if (handle.kind === 'file') { + if (isFileHandle(handle)) { yield new WebFileAccess([...basePath, handle.name].join('/'), handle); - } else if (handle.kind === 'directory') { + } else if (isDirectoryHandle(handle)) { // Skip git storage directory if (handle.name === '.git') { continue; @@ -116,7 +113,7 @@ export class WebFileSystemAccess { return Boolean(showDirectoryPicker); } - static fromHandle(handle: IterableDirectoryHandle) { + static fromHandle(handle: FileSystemDirectoryHandle) { return new WebDirectoryAccess(handle); } diff --git a/plugins/scaffolder/src/lib/filesystem/WebFileSystemStore.ts b/plugins/scaffolder/src/lib/filesystem/WebFileSystemStore.ts index 1ea90dcf76..8b741d36d3 100644 --- a/plugins/scaffolder/src/lib/filesystem/WebFileSystemStore.ts +++ b/plugins/scaffolder/src/lib/filesystem/WebFileSystemStore.ts @@ -16,12 +16,11 @@ import { get, set } from 'idb-keyval'; import { TemplateDirectoryAccess } from './types'; -import { IterableDirectoryHandle } from './WebFileSystemAccess'; export class WebFileSystemStore { private static readonly key = 'scalfolder-template-editor-directory'; - static async getDirectory(): Promise { + static async getDirectory(): Promise { const directory = await get(WebFileSystemStore.key); return directory.handle; }