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 <jonkoops@gmail.com>
This commit is contained in:
Jon Koops
2026-03-26 16:23:46 +01:00
parent a9dd4fd28d
commit a7a14b78c1
6 changed files with 34 additions and 31 deletions
+5
View File
@@ -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.
+5
View File
@@ -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.
+1 -1
View File
@@ -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,
@@ -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);
@@ -16,30 +16,27 @@
import { TemplateDirectoryAccess, TemplateFileAccess } from './types';
type WritableFileHandle = FileSystemFileHandle & {
createWritable(): Promise<{
write(data: string | Blob | BufferSource): Promise<void>;
close(): Promise<void>;
}>;
};
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<IterableDirectoryHandle>)
| (() => Promise<FileSystemDirectoryHandle>)
| 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<TemplateFileAccess> {
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);
}
@@ -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<IterableDirectoryHandle | undefined> {
static async getDirectory(): Promise<FileSystemDirectoryHandle | undefined> {
const directory = await get(WebFileSystemStore.key);
return directory.handle;
}