@fgv/ts-res-browser 5.0.0-2 → 5.0.0-21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +132 -21
- package/dist/bundle.js +1 -1
- package/dist/bundle.js.LICENSE.txt +11 -1
- package/package.json +42 -36
- package/.rush/temp/587277bfc395406ad444e989b6c309b9698f534c.tar.log +0 -15
- package/.rush/temp/chunked-rush-logs/ts-res-browser.build.chunks.jsonl +0 -35
- package/.rush/temp/operation/build/all.log +0 -35
- package/.rush/temp/operation/build/log-chunks.jsonl +0 -35
- package/.rush/temp/operation/build/state.json +0 -3
- package/.rush/temp/shrinkwrap-deps.json +0 -1225
- package/TS_RES_BROWSER_PLAN.md +0 -689
- package/babel.config.js +0 -17
- package/config/jest.config.json +0 -13
- package/config/rig.json +0 -16
- package/postcss.config.js +0 -3
- package/public/index.html +0 -15
- package/rush-logs/ts-res-browser.build.cache.log +0 -3
- package/rush-logs/ts-res-browser.build.log +0 -35
- package/src/App.tsx +0 -274
- package/src/cli.ts +0 -44
- package/src/components/common/FileImporter.tsx +0 -143
- package/src/components/common/NavigationWarningModal.tsx +0 -62
- package/src/components/layout/AppLayout.tsx +0 -46
- package/src/components/layout/Header.tsx +0 -22
- package/src/components/layout/MessagesWindow.tsx +0 -108
- package/src/components/layout/Sidebar.tsx +0 -99
- package/src/components/tools/CompiledBrowser.tsx +0 -949
- package/src/components/tools/ConfigurationTool.tsx +0 -1405
- package/src/components/tools/FilterTool.tsx +0 -1114
- package/src/components/tools/ImportTool.tsx +0 -249
- package/src/components/tools/ResolutionViewer.tsx +0 -1672
- package/src/components/tools/SourceBrowser.tsx +0 -496
- package/src/components/tools/ZipLoader.tsx +0 -302
- package/src/hooks/useAppState.ts +0 -111
- package/src/hooks/useFileImport.ts +0 -86
- package/src/hooks/useNavigationWarning.ts +0 -67
- package/src/hooks/useResourceManager.ts +0 -295
- package/src/hooks/useUrlParams.ts +0 -56
- package/src/index.css +0 -38
- package/src/main.tsx +0 -16
- package/src/types/app.ts +0 -70
- package/src/types/fileSystemAccess.d.ts +0 -94
- package/src/utils/browserFileTreeAccessors.ts +0 -245
- package/src/utils/fileImport.ts +0 -336
- package/src/utils/fileTreeConverter.ts +0 -160
- package/src/utils/filterResources.ts +0 -677
- package/src/utils/tsResIntegration.ts +0 -467
- package/src/utils/urlParams.ts +0 -238
- package/src/utils/zip/browserZipFileTreeAccessors.ts +0 -385
- package/tailwind.config.js +0 -21
- package/test-data/README.md +0 -184
- package/test-data/config-variations/README.md +0 -64
- package/test-data/config-variations/composition-terminology.json +0 -112
- package/test-data/config-variations/enterprise-complex.json +0 -129
- package/test-data/config-variations/gaming-app.json +0 -87
- package/test-data/config-variations/high-priority-language.json +0 -65
- package/test-data/config-variations/minimal-basic.json +0 -37
- package/test-data/config-variations/qualifier-default-values.json +0 -71
- package/test-data/config-variations/territory-first.json +0 -68
- package/test-data/config-variations/territory-hierarchy-test.json +0 -31
- package/test-data/config-variations/territory-hierarchy-unconstrained.json +0 -32
- package/test-data/default-matching-test/greeting.res.json +0 -83
- package/test-data/default-matching-test/only-defaults.res.json +0 -48
- package/test-data/sample-project/package.json +0 -27
- package/test-data/sample-project/src/resources/app-config.json +0 -27
- package/test-data/sample-project/src/resources/app.json +0 -177
- package/test-data/sample-project/src/resources/config/app.env=development.json +0 -21
- package/test-data/sample-project/src/resources/config/app.json +0 -21
- package/test-data/sample-project/src/resources/config/app.platform=mobile.json +0 -23
- package/test-data/sample-project/src/resources/feature-flags.json +0 -23
- package/test-data/sample-project/src/resources/home=CA/common.json +0 -16
- package/test-data/sample-project/src/resources/images/density=hdpi.json +0 -29
- package/test-data/sample-project/src/resources/images/home.json +0 -29
- package/test-data/sample-project/src/resources/images/logout.json +0 -14
- package/test-data/sample-project/src/resources/images/settings.json +0 -14
- package/test-data/sample-project/src/resources/images/ui-icons.json +0 -29
- package/test-data/sample-project/src/resources/images/user.density=hdpi.json +0 -14
- package/test-data/sample-project/src/resources/images/user.json +0 -14
- package/test-data/sample-project/src/resources/lang=en-US/common.json +0 -15
- package/test-data/sample-project/src/resources/language=en-CA/ui-terms.json +0 -19
- package/test-data/sample-project/src/resources/language=en-GB/ui-terms.json +0 -30
- package/test-data/sample-project/src/resources/language=fr-CA/ui-terms.json +0 -25
- package/test-data/sample-project/src/resources/language=nl-BE/ui-terms.json +0 -17
- package/test-data/sample-project/src/resources/region=europe/feature-flags.json +0 -17
- package/test-data/sample-project/src/resources/region=north-america/feature-flags.json +0 -18
- package/test-data/sample-project/src/resources/strings/dashboard.home=CA,language=fr.json +0 -20
- package/test-data/sample-project/src/resources/strings/dashboard.json +0 -18
- package/test-data/sample-project/src/resources/strings/dashboard.role=admin.json +0 -23
- package/test-data/sample-project/src/resources/strings/dashboard.role=user.json +0 -21
- package/test-data/sample-project/src/resources/strings/language=en-US.json +0 -16
- package/test-data/sample-project/src/resources/strings.json +0 -78
- package/test-data/sample-project/src/resources/territory=BE/app-config.json +0 -19
- package/test-data/sample-project/src/resources/territory=CA/app-config.json +0 -21
- package/test-data/sample-project/src/resources/territory=GB/app-config.json +0 -25
- package/test-data/sample-project/src/resources/territory=NL/app-config.json +0 -25
- package/test-data/sample-project/src/resources/ui-terms.json +0 -96
- package/test-data/sample-project/src/resources-config.json +0 -111
- package/tsconfig.json +0 -27
- package/webpack.config.js +0 -60
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
import { Result, succeed, fail, FileTree } from '@fgv/ts-utils';
|
|
2
|
-
import { ImportedDirectory, ImportedFile } from './fileImport';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Custom FileTreeAccessors implementation for browser environment
|
|
6
|
-
* that preserves directory structure for ts-res qualifier extraction
|
|
7
|
-
*/
|
|
8
|
-
export class BrowserFileTreeAccessors implements FileTree.IFileTreeAccessors {
|
|
9
|
-
private readonly rootDirectory: ImportedDirectory;
|
|
10
|
-
private readonly pathSeparator = '/';
|
|
11
|
-
|
|
12
|
-
constructor(rootDirectory: ImportedDirectory) {
|
|
13
|
-
this.rootDirectory = rootDirectory;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
public static create(rootDirectory: ImportedDirectory): Result<BrowserFileTreeAccessors> {
|
|
17
|
-
try {
|
|
18
|
-
return succeed(new BrowserFileTreeAccessors(rootDirectory));
|
|
19
|
-
} catch (error) {
|
|
20
|
-
return fail(
|
|
21
|
-
`Failed to create BrowserFileTreeAccessors: ${error instanceof Error ? error.message : String(error)}`
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Path manipulation methods (browser-compatible, no Node.js dependencies)
|
|
27
|
-
public resolveAbsolutePath(...paths: string[]): string {
|
|
28
|
-
// Start with root path or first absolute path
|
|
29
|
-
let result = '';
|
|
30
|
-
|
|
31
|
-
for (const path of paths) {
|
|
32
|
-
if (!path) continue;
|
|
33
|
-
|
|
34
|
-
if (path.startsWith('/')) {
|
|
35
|
-
// Absolute path - replace current result
|
|
36
|
-
result = path;
|
|
37
|
-
} else {
|
|
38
|
-
// Relative path - append to current result
|
|
39
|
-
result = this.joinPaths(result, path);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Ensure it starts with / (absolute)
|
|
44
|
-
if (!result.startsWith('/')) {
|
|
45
|
-
result = '/' + result;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return this.normalizePath(result);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
public getExtension(path: string): string {
|
|
52
|
-
const baseName = this.getBaseName(path);
|
|
53
|
-
const lastDot = baseName.lastIndexOf('.');
|
|
54
|
-
return lastDot === -1 ? '' : baseName.substring(lastDot);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
public getBaseName(path: string, suffix?: string): string {
|
|
58
|
-
if (!path) return '';
|
|
59
|
-
|
|
60
|
-
// Remove trailing slashes
|
|
61
|
-
const normalizedPath = path.replace(/\/+$/, '');
|
|
62
|
-
|
|
63
|
-
// Get the last segment
|
|
64
|
-
const lastSlash = normalizedPath.lastIndexOf('/');
|
|
65
|
-
let baseName = lastSlash === -1 ? normalizedPath : normalizedPath.substring(lastSlash + 1);
|
|
66
|
-
|
|
67
|
-
// Remove suffix if provided
|
|
68
|
-
if (suffix && baseName.endsWith(suffix)) {
|
|
69
|
-
baseName = baseName.substring(0, baseName.length - suffix.length);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return baseName;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
public joinPaths(...paths: string[]): string {
|
|
76
|
-
if (paths.length === 0) return '';
|
|
77
|
-
|
|
78
|
-
const parts: string[] = [];
|
|
79
|
-
|
|
80
|
-
for (const path of paths) {
|
|
81
|
-
if (!path) continue;
|
|
82
|
-
|
|
83
|
-
// Split by separator and filter empty parts
|
|
84
|
-
const pathParts = path.split(this.pathSeparator).filter((part) => part.length > 0);
|
|
85
|
-
parts.push(...pathParts);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return '/' + parts.join(this.pathSeparator);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// File system access methods
|
|
92
|
-
public getItem(path: string): Result<FileTree.FileTreeItem> {
|
|
93
|
-
try {
|
|
94
|
-
const absolutePath = this.ensureAbsolute(path);
|
|
95
|
-
const result = this.findItemAtPath(absolutePath);
|
|
96
|
-
|
|
97
|
-
if (!result) {
|
|
98
|
-
return fail(`Item not found at path: ${absolutePath}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (result.type === 'file') {
|
|
102
|
-
return FileTree.FileItem.create(absolutePath, this);
|
|
103
|
-
} else {
|
|
104
|
-
return FileTree.DirectoryItem.create(absolutePath, this);
|
|
105
|
-
}
|
|
106
|
-
} catch (error) {
|
|
107
|
-
return fail(`Failed to get item: ${error instanceof Error ? error.message : String(error)}`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
public getFileContents(path: string): Result<string> {
|
|
112
|
-
try {
|
|
113
|
-
const absolutePath = this.ensureAbsolute(path);
|
|
114
|
-
const result = this.findItemAtPath(absolutePath);
|
|
115
|
-
|
|
116
|
-
if (!result) {
|
|
117
|
-
return fail(`File not found at path: ${absolutePath}`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (result.type !== 'file') {
|
|
121
|
-
return fail(`Path is not a file: ${absolutePath}`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const file = result.item as ImportedFile;
|
|
125
|
-
return succeed(file.content);
|
|
126
|
-
} catch (error) {
|
|
127
|
-
return fail(`Failed to get file contents: ${error instanceof Error ? error.message : String(error)}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
public getChildren(path: string): Result<ReadonlyArray<FileTree.FileTreeItem>> {
|
|
132
|
-
try {
|
|
133
|
-
const absolutePath = this.ensureAbsolute(path);
|
|
134
|
-
const result = this.findItemAtPath(absolutePath);
|
|
135
|
-
|
|
136
|
-
if (!result) {
|
|
137
|
-
return fail(`Directory not found at path: ${absolutePath}`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (result.type !== 'directory') {
|
|
141
|
-
return fail(`Path is not a directory: ${absolutePath}`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const directory = result.item as ImportedDirectory;
|
|
145
|
-
const children: FileTree.FileTreeItem[] = [];
|
|
146
|
-
|
|
147
|
-
// Add files
|
|
148
|
-
for (const file of directory.files) {
|
|
149
|
-
const childPath = this.joinPaths(absolutePath, file.name);
|
|
150
|
-
const fileItemResult = FileTree.FileItem.create(childPath, this);
|
|
151
|
-
|
|
152
|
-
if (fileItemResult.isSuccess()) {
|
|
153
|
-
children.push(fileItemResult.value);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Add subdirectories
|
|
158
|
-
for (const subDirectory of directory.directories) {
|
|
159
|
-
const childPath = this.joinPaths(absolutePath, subDirectory.name);
|
|
160
|
-
const dirItemResult = FileTree.DirectoryItem.create(childPath, this);
|
|
161
|
-
|
|
162
|
-
if (dirItemResult.isSuccess()) {
|
|
163
|
-
children.push(dirItemResult.value);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return succeed(children);
|
|
168
|
-
} catch (error) {
|
|
169
|
-
return fail(`Failed to get children: ${error instanceof Error ? error.message : String(error)}`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Helper methods
|
|
174
|
-
private ensureAbsolute(path: string): string {
|
|
175
|
-
return path.startsWith('/') ? path : '/' + path;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
private normalizePath(path: string): string {
|
|
179
|
-
if (!path) return '/';
|
|
180
|
-
|
|
181
|
-
// Split into parts, filter empty ones, and handle . and ..
|
|
182
|
-
const parts = path.split('/').filter((part) => part.length > 0 && part !== '.');
|
|
183
|
-
const normalized: string[] = [];
|
|
184
|
-
|
|
185
|
-
for (const part of parts) {
|
|
186
|
-
if (part === '..') {
|
|
187
|
-
if (normalized.length > 0) {
|
|
188
|
-
normalized.pop();
|
|
189
|
-
}
|
|
190
|
-
} else {
|
|
191
|
-
normalized.push(part);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return '/' + normalized.join('/');
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
private findItemAtPath(
|
|
199
|
-
absolutePath: string
|
|
200
|
-
): { item: ImportedDirectory | ImportedFile; type: 'file' | 'directory' } | null {
|
|
201
|
-
const normalizedPath = this.normalizePath(absolutePath);
|
|
202
|
-
|
|
203
|
-
// Handle root path
|
|
204
|
-
if (normalizedPath === '/' || normalizedPath === '') {
|
|
205
|
-
return { item: this.rootDirectory, type: 'directory' };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Split path into segments (excluding root slash)
|
|
209
|
-
const segments = normalizedPath
|
|
210
|
-
.substring(1)
|
|
211
|
-
.split('/')
|
|
212
|
-
.filter((seg) => seg.length > 0);
|
|
213
|
-
|
|
214
|
-
let currentDir = this.rootDirectory;
|
|
215
|
-
|
|
216
|
-
// Navigate through directory segments
|
|
217
|
-
for (let i = 0; i < segments.length - 1; i++) {
|
|
218
|
-
const segment = segments[i];
|
|
219
|
-
const subDir = currentDir.directories.find((dir) => dir.name === segment);
|
|
220
|
-
|
|
221
|
-
if (!subDir) {
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
currentDir = subDir;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Get the final item (file or directory)
|
|
229
|
-
const finalSegment = segments[segments.length - 1];
|
|
230
|
-
|
|
231
|
-
// Check if it's a file
|
|
232
|
-
const file = currentDir.files.find((f) => f.name === finalSegment);
|
|
233
|
-
if (file) {
|
|
234
|
-
return { item: file, type: 'file' };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Check if it's a directory
|
|
238
|
-
const directory = currentDir.directories.find((d) => d.name === finalSegment);
|
|
239
|
-
if (directory) {
|
|
240
|
-
return { item: directory, type: 'directory' };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return null;
|
|
244
|
-
}
|
|
245
|
-
}
|
package/src/utils/fileImport.ts
DELETED
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
import { Result, succeed, fail } from '@fgv/ts-utils';
|
|
2
|
-
|
|
3
|
-
export interface ImportedFile {
|
|
4
|
-
name: string;
|
|
5
|
-
path: string;
|
|
6
|
-
content: string;
|
|
7
|
-
handle?: FileSystemFileHandle;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface ImportedDirectory {
|
|
11
|
-
name: string;
|
|
12
|
-
path: string;
|
|
13
|
-
files: ImportedFile[];
|
|
14
|
-
directories: ImportedDirectory[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface FileImportOptions {
|
|
18
|
-
acceptedTypes?: string[];
|
|
19
|
-
multiple?: boolean;
|
|
20
|
-
includeDirectories?: boolean;
|
|
21
|
-
startIn?: string | FileSystemHandle;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Check if File System Access API is supported
|
|
26
|
-
*/
|
|
27
|
-
export function isFileSystemAccessSupported(): boolean {
|
|
28
|
-
return 'showDirectoryPicker' in window && 'showOpenFilePicker' in window;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Modern File System Access API implementation
|
|
33
|
-
*/
|
|
34
|
-
export class ModernFileImporter {
|
|
35
|
-
/**
|
|
36
|
-
* Pick a directory using File System Access API
|
|
37
|
-
*/
|
|
38
|
-
async pickDirectory(options: FileImportOptions = {}): Promise<Result<ImportedDirectory>> {
|
|
39
|
-
try {
|
|
40
|
-
if (!isFileSystemAccessSupported()) {
|
|
41
|
-
return fail('File System Access API not supported');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const directoryHandle = await window.showDirectoryPicker({
|
|
45
|
-
mode: 'read',
|
|
46
|
-
startIn: options.startIn || 'documents'
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const importedDirectory = await this.processDirectoryHandle(directoryHandle);
|
|
50
|
-
return succeed(importedDirectory);
|
|
51
|
-
} catch (error) {
|
|
52
|
-
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
53
|
-
return fail('Directory selection was cancelled');
|
|
54
|
-
}
|
|
55
|
-
return fail(`Failed to pick directory: ${error instanceof Error ? error.message : String(error)}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Pick files using File System Access API
|
|
61
|
-
*/
|
|
62
|
-
async pickFiles(options: FileImportOptions = {}): Promise<Result<ImportedFile[]>> {
|
|
63
|
-
try {
|
|
64
|
-
if (!isFileSystemAccessSupported()) {
|
|
65
|
-
return fail('File System Access API not supported');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const fileHandles = await window.showOpenFilePicker({
|
|
69
|
-
multiple: options.multiple ?? true,
|
|
70
|
-
types: options.acceptedTypes
|
|
71
|
-
? [
|
|
72
|
-
{
|
|
73
|
-
description: 'Resource files',
|
|
74
|
-
accept: {
|
|
75
|
-
'application/json': options.acceptedTypes
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
]
|
|
79
|
-
: undefined,
|
|
80
|
-
excludeAcceptAllOption: false,
|
|
81
|
-
startIn: options.startIn || 'documents'
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
const importedFiles: ImportedFile[] = [];
|
|
85
|
-
for (const fileHandle of fileHandles) {
|
|
86
|
-
const result = await this.processFileHandle(fileHandle);
|
|
87
|
-
if (result.isSuccess()) {
|
|
88
|
-
importedFiles.push(result.value);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return succeed(importedFiles);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
95
|
-
return fail('File selection was cancelled');
|
|
96
|
-
}
|
|
97
|
-
return fail(`Failed to pick files: ${error instanceof Error ? error.message : String(error)}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
private async processDirectoryHandle(
|
|
102
|
-
directoryHandle: FileSystemDirectoryHandle,
|
|
103
|
-
basePath = ''
|
|
104
|
-
): Promise<ImportedDirectory> {
|
|
105
|
-
const files: ImportedFile[] = [];
|
|
106
|
-
const directories: ImportedDirectory[] = [];
|
|
107
|
-
const currentPath = basePath ? `${basePath}/${directoryHandle.name}` : directoryHandle.name;
|
|
108
|
-
|
|
109
|
-
for await (const [name, handle] of directoryHandle.entries()) {
|
|
110
|
-
if (handle.kind === 'file') {
|
|
111
|
-
const result = await this.processFileHandle(handle as FileSystemFileHandle, currentPath);
|
|
112
|
-
if (result.isSuccess()) {
|
|
113
|
-
files.push(result.value);
|
|
114
|
-
}
|
|
115
|
-
} else if (handle.kind === 'directory') {
|
|
116
|
-
const subDirectory = await this.processDirectoryHandle(
|
|
117
|
-
handle as FileSystemDirectoryHandle,
|
|
118
|
-
currentPath
|
|
119
|
-
);
|
|
120
|
-
directories.push(subDirectory);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
name: directoryHandle.name,
|
|
126
|
-
path: currentPath,
|
|
127
|
-
files,
|
|
128
|
-
directories
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
private async processFileHandle(
|
|
133
|
-
fileHandle: FileSystemFileHandle,
|
|
134
|
-
basePath = ''
|
|
135
|
-
): Promise<Result<ImportedFile>> {
|
|
136
|
-
try {
|
|
137
|
-
const file = await fileHandle.getFile();
|
|
138
|
-
const content = await file.text();
|
|
139
|
-
const filePath = basePath ? `${basePath}/${file.name}` : file.name;
|
|
140
|
-
|
|
141
|
-
return succeed({
|
|
142
|
-
name: file.name,
|
|
143
|
-
path: filePath,
|
|
144
|
-
content,
|
|
145
|
-
handle: fileHandle
|
|
146
|
-
});
|
|
147
|
-
} catch (error) {
|
|
148
|
-
return fail(
|
|
149
|
-
`Failed to read file ${fileHandle.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Fallback implementation using traditional file input
|
|
157
|
-
*/
|
|
158
|
-
export class FallbackFileImporter {
|
|
159
|
-
/**
|
|
160
|
-
* Pick a directory using webkitdirectory attribute
|
|
161
|
-
*/
|
|
162
|
-
async pickDirectory(): Promise<Result<ImportedDirectory>> {
|
|
163
|
-
return new Promise((resolve) => {
|
|
164
|
-
const input = document.createElement('input');
|
|
165
|
-
input.type = 'file';
|
|
166
|
-
input.multiple = true;
|
|
167
|
-
input.webkitdirectory = true;
|
|
168
|
-
|
|
169
|
-
input.addEventListener('change', async (event) => {
|
|
170
|
-
const files = (event.target as HTMLInputElement).files;
|
|
171
|
-
if (!files || files.length === 0) {
|
|
172
|
-
resolve(fail('No directory selected'));
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
try {
|
|
177
|
-
const importedDirectory = await this.processFileList(files);
|
|
178
|
-
resolve(succeed(importedDirectory));
|
|
179
|
-
} catch (error) {
|
|
180
|
-
resolve(
|
|
181
|
-
fail(`Failed to process directory: ${error instanceof Error ? error.message : String(error)}`)
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
input.addEventListener('cancel', () => {
|
|
187
|
-
resolve(fail('Directory selection was cancelled'));
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
input.click();
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Pick files using traditional file input
|
|
196
|
-
*/
|
|
197
|
-
async pickFiles(options: FileImportOptions = {}): Promise<Result<ImportedFile[]>> {
|
|
198
|
-
return new Promise((resolve) => {
|
|
199
|
-
const input = document.createElement('input');
|
|
200
|
-
input.type = 'file';
|
|
201
|
-
input.multiple = options.multiple ?? true;
|
|
202
|
-
|
|
203
|
-
if (options.acceptedTypes) {
|
|
204
|
-
input.accept = options.acceptedTypes.join(',');
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
input.addEventListener('change', async (event) => {
|
|
208
|
-
const files = (event.target as HTMLInputElement).files;
|
|
209
|
-
if (!files || files.length === 0) {
|
|
210
|
-
resolve(fail('No files selected'));
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
const importedFiles: ImportedFile[] = [];
|
|
216
|
-
for (const file of Array.from(files)) {
|
|
217
|
-
const content = await file.text();
|
|
218
|
-
importedFiles.push({
|
|
219
|
-
name: file.name,
|
|
220
|
-
path: file.name,
|
|
221
|
-
content
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
resolve(succeed(importedFiles));
|
|
225
|
-
} catch (error) {
|
|
226
|
-
resolve(fail(`Failed to read files: ${error instanceof Error ? error.message : String(error)}`));
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
input.addEventListener('cancel', () => {
|
|
231
|
-
resolve(fail('File selection was cancelled'));
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
input.click();
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private async processFileList(files: FileList): Promise<ImportedDirectory> {
|
|
239
|
-
const fileMap = new Map<string, ImportedFile[]>();
|
|
240
|
-
const dirMap = new Map<string, ImportedDirectory>();
|
|
241
|
-
|
|
242
|
-
// Process all files
|
|
243
|
-
for (const file of Array.from(files)) {
|
|
244
|
-
const content = await file.text();
|
|
245
|
-
const pathParts = file.webkitRelativePath.split('/');
|
|
246
|
-
const fileName = pathParts[pathParts.length - 1];
|
|
247
|
-
const dirPath = pathParts.slice(0, -1).join('/');
|
|
248
|
-
|
|
249
|
-
const importedFile: ImportedFile = {
|
|
250
|
-
name: fileName,
|
|
251
|
-
path: file.webkitRelativePath,
|
|
252
|
-
content
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
if (!fileMap.has(dirPath)) {
|
|
256
|
-
fileMap.set(dirPath, []);
|
|
257
|
-
}
|
|
258
|
-
fileMap.get(dirPath)!.push(importedFile);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Build directory structure
|
|
262
|
-
const rootDirName = files[0].webkitRelativePath.split('/')[0];
|
|
263
|
-
return this.buildDirectoryTree(rootDirName, '', fileMap, dirMap);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
private buildDirectoryTree(
|
|
267
|
-
name: string,
|
|
268
|
-
path: string,
|
|
269
|
-
fileMap: Map<string, ImportedFile[]>,
|
|
270
|
-
dirMap: Map<string, ImportedDirectory>
|
|
271
|
-
): ImportedDirectory {
|
|
272
|
-
const currentPath = path ? `${path}/${name}` : name;
|
|
273
|
-
const files = fileMap.get(currentPath) || [];
|
|
274
|
-
const directories: ImportedDirectory[] = [];
|
|
275
|
-
|
|
276
|
-
// Find subdirectories
|
|
277
|
-
for (const [filePath] of fileMap) {
|
|
278
|
-
if (filePath.startsWith(currentPath + '/')) {
|
|
279
|
-
const relativePath = filePath.substring(currentPath.length + 1);
|
|
280
|
-
const nextDirName = relativePath.split('/')[0];
|
|
281
|
-
|
|
282
|
-
if (relativePath.includes('/') && !directories.some((d) => d.name === nextDirName)) {
|
|
283
|
-
const subDir = this.buildDirectoryTree(nextDirName, currentPath, fileMap, dirMap);
|
|
284
|
-
directories.push(subDir);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return {
|
|
290
|
-
name,
|
|
291
|
-
path: currentPath,
|
|
292
|
-
files,
|
|
293
|
-
directories
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Universal file importer that automatically selects the best available method
|
|
300
|
-
*/
|
|
301
|
-
export class FileImporter {
|
|
302
|
-
private modernImporter = new ModernFileImporter();
|
|
303
|
-
private fallbackImporter = new FallbackFileImporter();
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Pick a directory using the best available method
|
|
307
|
-
*/
|
|
308
|
-
async pickDirectory(options: FileImportOptions = {}): Promise<Result<ImportedDirectory>> {
|
|
309
|
-
if (isFileSystemAccessSupported()) {
|
|
310
|
-
return this.modernImporter.pickDirectory(options);
|
|
311
|
-
} else {
|
|
312
|
-
return this.fallbackImporter.pickDirectory();
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Pick files using the best available method
|
|
318
|
-
*/
|
|
319
|
-
async pickFiles(options: FileImportOptions = {}): Promise<Result<ImportedFile[]>> {
|
|
320
|
-
if (isFileSystemAccessSupported()) {
|
|
321
|
-
return this.modernImporter.pickFiles(options);
|
|
322
|
-
} else {
|
|
323
|
-
return this.fallbackImporter.pickFiles(options);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Get the current import method being used
|
|
329
|
-
*/
|
|
330
|
-
getCurrentMethod(): 'modern' | 'fallback' {
|
|
331
|
-
return isFileSystemAccessSupported() ? 'modern' : 'fallback';
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Export singleton instance
|
|
336
|
-
export const fileImporter = new FileImporter();
|