@fgv/ts-res-ui-components 5.0.0-14 → 5.0.0-16

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.
@@ -17,17 +17,21 @@ export declare class BrowserZipLoader implements IZipLoader {
17
17
  */
18
18
  loadFromUrl(url: string, options?: ZipLoadOptions, onProgress?: ZipProgressCallback): Promise<Result<ZipLoadResult>>;
19
19
  /**
20
- * Build file tree from JSZip instance
20
+ * Build file tree from ZipFileTreeAccessors (using ts-extras)
21
21
  */
22
- private buildFileTree;
22
+ private buildFileTreeFromAccessors;
23
23
  /**
24
- * Load manifest from ZIP
24
+ * Recursively process file tree items
25
25
  */
26
- private loadManifest;
26
+ private processFileTreeItems;
27
27
  /**
28
- * Load configuration from ZIP
28
+ * Load manifest from ZIP using ZipFileTreeAccessors
29
29
  */
30
- private loadConfiguration;
30
+ private loadManifestFromAccessors;
31
+ /**
32
+ * Load configuration from ZIP using ZipFileTreeAccessors
33
+ */
34
+ private loadConfigurationFromAccessors;
31
35
  /**
32
36
  * Find common root directory from file paths
33
37
  */
@@ -1,29 +1,7 @@
1
1
  import { succeed, fail } from '@fgv/ts-utils';
2
+ import { ZipFileTree as ExtrasZipFileTree } from '@fgv/ts-extras';
2
3
  import { parseManifest, parseConfiguration, zipTreeToFiles, zipTreeToDirectory, isZipFile } from './zipUtils';
3
4
  import { processImportedFiles, processImportedDirectory } from '../tsResIntegration';
4
- // Dynamic import for JSZip to support both Node.js and browser environments
5
- let JSZip = null;
6
- /**
7
- * Get JSZip instance (assumes JSZip is available)
8
- */
9
- function getJSZip() {
10
- if (JSZip)
11
- return JSZip;
12
- // Check if JSZip is globally available
13
- if (typeof window !== 'undefined' && window.JSZip) {
14
- JSZip = window.JSZip;
15
- return JSZip;
16
- }
17
- // Try to get JSZip from require/import (will work in bundled environments)
18
- try {
19
- // eslint-disable-next-line @typescript-eslint/no-var-requires
20
- JSZip = require('jszip');
21
- return JSZip;
22
- }
23
- catch (error) {
24
- throw new Error('JSZip is not available. Please install jszip as a dependency: npm install jszip');
25
- }
26
- }
27
5
  /**
28
6
  * Browser-based ZIP loader implementation
29
7
  */
@@ -47,27 +25,27 @@ export class BrowserZipLoader {
47
25
  */
48
26
  async loadFromBuffer(buffer, options = {}, onProgress) {
49
27
  onProgress?.('parsing-zip', 0, 'Parsing ZIP archive');
50
- const JSZipClass = getJSZip();
51
- const zip = new JSZipClass();
52
- const loadedZip = await zip.loadAsync(buffer).catch((error) => {
53
- throw new Error(`Failed to parse ZIP: ${error instanceof Error ? error.message : String(error)}`);
54
- });
28
+ const zipAccessorsResult = ExtrasZipFileTree.ZipFileTreeAccessors.fromBuffer(buffer);
29
+ if (zipAccessorsResult.isFailure()) {
30
+ return fail(`Failed to parse ZIP: ${zipAccessorsResult.message}`);
31
+ }
32
+ const zipAccessors = zipAccessorsResult.value;
55
33
  onProgress?.('parsing-zip', 100, 'ZIP archive parsed');
56
- // Build file tree
57
- const fileTree = await this.buildFileTree(loadedZip, onProgress);
34
+ // Build file tree using the new adapter
35
+ const fileTree = await this.buildFileTreeFromAccessors(zipAccessors, onProgress);
58
36
  // Load manifest
59
37
  onProgress?.('loading-manifest', 0, 'Loading manifest');
60
- const manifest = await this.loadManifest(loadedZip);
38
+ const manifest = await this.loadManifestFromAccessors(zipAccessors);
61
39
  onProgress?.('loading-manifest', 100, 'Manifest loaded');
62
40
  // Load configuration
63
41
  onProgress?.('loading-config', 0, 'Loading configuration');
64
- const config = await this.loadConfiguration(loadedZip, options);
42
+ const config = await this.loadConfigurationFromAccessors(zipAccessors, options);
65
43
  onProgress?.('loading-config', 100, 'Configuration loaded');
66
44
  // Extract files and directory structure
67
45
  onProgress?.('extracting-files', 0, 'Extracting files');
68
- const files = zipTreeToFiles(fileTree);
46
+ const filesList = zipTreeToFiles(fileTree);
69
47
  const directory = zipTreeToDirectory(fileTree);
70
- onProgress?.('extracting-files', 100, `Extracted ${files.length} files`);
48
+ onProgress?.('extracting-files', 100, `Extracted ${filesList.length} files`);
71
49
  // Process resources if requested
72
50
  let processedResources = null;
73
51
  if (options.autoProcessResources) {
@@ -82,8 +60,8 @@ export class BrowserZipLoader {
82
60
  throw new Error(`Failed to process resources from directory: ${processResult.message}`);
83
61
  }
84
62
  }
85
- else if (files.length > 0) {
86
- const processResult = await processImportedFiles(files, configToUse || undefined);
63
+ else if (filesList.length > 0) {
64
+ const processResult = await processImportedFiles(filesList, configToUse || undefined);
87
65
  if (processResult.isSuccess()) {
88
66
  processedResources = processResult.value;
89
67
  }
@@ -97,7 +75,7 @@ export class BrowserZipLoader {
97
75
  return succeed({
98
76
  manifest,
99
77
  config: options.overrideConfig || config,
100
- files,
78
+ files: filesList,
101
79
  directory,
102
80
  processedResources
103
81
  });
@@ -120,86 +98,85 @@ export class BrowserZipLoader {
120
98
  return this.loadFromBuffer(buffer, options, onProgress);
121
99
  }
122
100
  /**
123
- * Build file tree from JSZip instance
101
+ * Build file tree from ZipFileTreeAccessors (using ts-extras)
124
102
  */
125
- async buildFileTree(zip, onProgress) {
103
+ async buildFileTreeFromAccessors(zipAccessors, onProgress) {
126
104
  const files = new Map();
127
105
  const directories = new Set();
128
- const zipFiles = Object.keys(zip.files);
129
- let processed = 0;
130
- // Pre-load all file contents for performance
131
- for (const filename of zipFiles) {
132
- const zipEntry = zip.files[filename];
133
- if (zipEntry.dir) {
134
- directories.add(filename);
135
- files.set(filename, {
136
- name: filename
137
- .split('/')
138
- .filter((p) => p)
139
- .pop() || filename,
140
- path: filename,
106
+ // Get all children from root
107
+ const rootChildrenResult = zipAccessors.getChildren('/');
108
+ if (rootChildrenResult.isFailure()) {
109
+ throw new Error(`Failed to read ZIP contents: ${rootChildrenResult.message}`);
110
+ }
111
+ // Process all items recursively
112
+ await this.processFileTreeItems(zipAccessors, '/', rootChildrenResult.value, files, directories, onProgress);
113
+ return {
114
+ files,
115
+ directories,
116
+ root: this.findCommonRoot(Array.from(files.keys()))
117
+ };
118
+ }
119
+ /**
120
+ * Recursively process file tree items
121
+ */
122
+ async processFileTreeItems(zipAccessors, currentPath, items, files, directories, onProgress, processed = { count: 0 }) {
123
+ for (const item of items) {
124
+ const itemPath = item.absolutePath.startsWith('/') ? item.absolutePath.substring(1) : item.absolutePath;
125
+ if (item.type === 'directory') {
126
+ directories.add(itemPath);
127
+ files.set(itemPath, {
128
+ name: item.name,
129
+ path: itemPath,
141
130
  size: 0,
142
131
  isDirectory: true,
143
- lastModified: zipEntry.date
132
+ lastModified: undefined
144
133
  });
134
+ // Recursively process children
135
+ const childrenResult = zipAccessors.getChildren(item.absolutePath);
136
+ if (childrenResult.isSuccess()) {
137
+ await this.processFileTreeItems(zipAccessors, item.absolutePath, childrenResult.value, files, directories, onProgress, processed);
138
+ }
145
139
  }
146
- else {
147
- // Load file content
148
- const content = await zipEntry.async('string');
149
- files.set(filename, {
150
- name: filename.split('/').pop() || filename,
151
- path: filename,
140
+ else if (item.type === 'file') {
141
+ // Get file content
142
+ const contentResult = item.getRawContents();
143
+ const content = contentResult.isSuccess() ? contentResult.value : '';
144
+ files.set(itemPath, {
145
+ name: item.name,
146
+ path: itemPath,
152
147
  size: content.length,
153
148
  isDirectory: false,
154
- lastModified: zipEntry.date,
149
+ lastModified: undefined,
155
150
  content
156
151
  });
157
152
  }
158
- processed++;
159
- const progress = Math.round((processed / zipFiles.length) * 100);
160
- onProgress?.('extracting-files', progress, `Processing ${filename}`);
153
+ processed.count++;
154
+ onProgress?.('extracting-files', processed.count, `Processing ${itemPath}`);
161
155
  }
162
- return {
163
- files,
164
- directories,
165
- root: this.findCommonRoot(Array.from(files.keys()))
166
- };
167
156
  }
168
157
  /**
169
- * Load manifest from ZIP
158
+ * Load manifest from ZIP using ZipFileTreeAccessors
170
159
  */
171
- async loadManifest(zip) {
172
- const manifestFile = zip.files['manifest.json'];
173
- if (!manifestFile) {
160
+ async loadManifestFromAccessors(zipAccessors) {
161
+ const manifestResult = zipAccessors.getFileContents('manifest.json');
162
+ if (manifestResult.isFailure()) {
174
163
  return null;
175
164
  }
176
- const manifestData = await manifestFile.async('string').catch((error) => {
177
- console.warn('Failed to read manifest file:', error);
178
- return null;
179
- });
180
- if (!manifestData)
181
- return null;
182
- const parseResult = parseManifest(manifestData);
165
+ const parseResult = parseManifest(manifestResult.value);
183
166
  return parseResult.orDefault() ?? null;
184
167
  }
185
168
  /**
186
- * Load configuration from ZIP
169
+ * Load configuration from ZIP using ZipFileTreeAccessors
187
170
  */
188
- async loadConfiguration(zip, options) {
171
+ async loadConfigurationFromAccessors(zipAccessors, options) {
189
172
  if (options.overrideConfig) {
190
173
  return options.overrideConfig;
191
174
  }
192
- const configFile = zip.files['config.json'];
193
- if (!configFile) {
175
+ const configResult = zipAccessors.getFileContents('config.json');
176
+ if (configResult.isFailure()) {
194
177
  return null;
195
178
  }
196
- const configData = await configFile.async('string').catch((error) => {
197
- console.warn('Failed to read config file:', error);
198
- return null;
199
- });
200
- if (!configData)
201
- return null;
202
- const parseResult = parseConfiguration(configData);
179
+ const parseResult = parseConfiguration(configResult.value);
203
180
  return parseResult.orDefault() ?? null;
204
181
  }
205
182
  /**
@@ -5,31 +5,9 @@ exports.createBrowserZipLoader = createBrowserZipLoader;
5
5
  exports.loadZipFile = loadZipFile;
6
6
  exports.loadZipFromUrl = loadZipFromUrl;
7
7
  const ts_utils_1 = require("@fgv/ts-utils");
8
+ const ts_extras_1 = require("@fgv/ts-extras");
8
9
  const zipUtils_1 = require("./zipUtils");
9
10
  const tsResIntegration_1 = require("../tsResIntegration");
10
- // Dynamic import for JSZip to support both Node.js and browser environments
11
- let JSZip = null;
12
- /**
13
- * Get JSZip instance (assumes JSZip is available)
14
- */
15
- function getJSZip() {
16
- if (JSZip)
17
- return JSZip;
18
- // Check if JSZip is globally available
19
- if (typeof window !== 'undefined' && window.JSZip) {
20
- JSZip = window.JSZip;
21
- return JSZip;
22
- }
23
- // Try to get JSZip from require/import (will work in bundled environments)
24
- try {
25
- // eslint-disable-next-line @typescript-eslint/no-var-requires
26
- JSZip = require('jszip');
27
- return JSZip;
28
- }
29
- catch (error) {
30
- throw new Error('JSZip is not available. Please install jszip as a dependency: npm install jszip');
31
- }
32
- }
33
11
  /**
34
12
  * Browser-based ZIP loader implementation
35
13
  */
@@ -53,27 +31,27 @@ class BrowserZipLoader {
53
31
  */
54
32
  async loadFromBuffer(buffer, options = {}, onProgress) {
55
33
  onProgress?.('parsing-zip', 0, 'Parsing ZIP archive');
56
- const JSZipClass = getJSZip();
57
- const zip = new JSZipClass();
58
- const loadedZip = await zip.loadAsync(buffer).catch((error) => {
59
- throw new Error(`Failed to parse ZIP: ${error instanceof Error ? error.message : String(error)}`);
60
- });
34
+ const zipAccessorsResult = ts_extras_1.ZipFileTree.ZipFileTreeAccessors.fromBuffer(buffer);
35
+ if (zipAccessorsResult.isFailure()) {
36
+ return (0, ts_utils_1.fail)(`Failed to parse ZIP: ${zipAccessorsResult.message}`);
37
+ }
38
+ const zipAccessors = zipAccessorsResult.value;
61
39
  onProgress?.('parsing-zip', 100, 'ZIP archive parsed');
62
- // Build file tree
63
- const fileTree = await this.buildFileTree(loadedZip, onProgress);
40
+ // Build file tree using the new adapter
41
+ const fileTree = await this.buildFileTreeFromAccessors(zipAccessors, onProgress);
64
42
  // Load manifest
65
43
  onProgress?.('loading-manifest', 0, 'Loading manifest');
66
- const manifest = await this.loadManifest(loadedZip);
44
+ const manifest = await this.loadManifestFromAccessors(zipAccessors);
67
45
  onProgress?.('loading-manifest', 100, 'Manifest loaded');
68
46
  // Load configuration
69
47
  onProgress?.('loading-config', 0, 'Loading configuration');
70
- const config = await this.loadConfiguration(loadedZip, options);
48
+ const config = await this.loadConfigurationFromAccessors(zipAccessors, options);
71
49
  onProgress?.('loading-config', 100, 'Configuration loaded');
72
50
  // Extract files and directory structure
73
51
  onProgress?.('extracting-files', 0, 'Extracting files');
74
- const files = (0, zipUtils_1.zipTreeToFiles)(fileTree);
52
+ const filesList = (0, zipUtils_1.zipTreeToFiles)(fileTree);
75
53
  const directory = (0, zipUtils_1.zipTreeToDirectory)(fileTree);
76
- onProgress?.('extracting-files', 100, `Extracted ${files.length} files`);
54
+ onProgress?.('extracting-files', 100, `Extracted ${filesList.length} files`);
77
55
  // Process resources if requested
78
56
  let processedResources = null;
79
57
  if (options.autoProcessResources) {
@@ -88,8 +66,8 @@ class BrowserZipLoader {
88
66
  throw new Error(`Failed to process resources from directory: ${processResult.message}`);
89
67
  }
90
68
  }
91
- else if (files.length > 0) {
92
- const processResult = await (0, tsResIntegration_1.processImportedFiles)(files, configToUse || undefined);
69
+ else if (filesList.length > 0) {
70
+ const processResult = await (0, tsResIntegration_1.processImportedFiles)(filesList, configToUse || undefined);
93
71
  if (processResult.isSuccess()) {
94
72
  processedResources = processResult.value;
95
73
  }
@@ -103,7 +81,7 @@ class BrowserZipLoader {
103
81
  return (0, ts_utils_1.succeed)({
104
82
  manifest,
105
83
  config: options.overrideConfig || config,
106
- files,
84
+ files: filesList,
107
85
  directory,
108
86
  processedResources
109
87
  });
@@ -126,86 +104,85 @@ class BrowserZipLoader {
126
104
  return this.loadFromBuffer(buffer, options, onProgress);
127
105
  }
128
106
  /**
129
- * Build file tree from JSZip instance
107
+ * Build file tree from ZipFileTreeAccessors (using ts-extras)
130
108
  */
131
- async buildFileTree(zip, onProgress) {
109
+ async buildFileTreeFromAccessors(zipAccessors, onProgress) {
132
110
  const files = new Map();
133
111
  const directories = new Set();
134
- const zipFiles = Object.keys(zip.files);
135
- let processed = 0;
136
- // Pre-load all file contents for performance
137
- for (const filename of zipFiles) {
138
- const zipEntry = zip.files[filename];
139
- if (zipEntry.dir) {
140
- directories.add(filename);
141
- files.set(filename, {
142
- name: filename
143
- .split('/')
144
- .filter((p) => p)
145
- .pop() || filename,
146
- path: filename,
112
+ // Get all children from root
113
+ const rootChildrenResult = zipAccessors.getChildren('/');
114
+ if (rootChildrenResult.isFailure()) {
115
+ throw new Error(`Failed to read ZIP contents: ${rootChildrenResult.message}`);
116
+ }
117
+ // Process all items recursively
118
+ await this.processFileTreeItems(zipAccessors, '/', rootChildrenResult.value, files, directories, onProgress);
119
+ return {
120
+ files,
121
+ directories,
122
+ root: this.findCommonRoot(Array.from(files.keys()))
123
+ };
124
+ }
125
+ /**
126
+ * Recursively process file tree items
127
+ */
128
+ async processFileTreeItems(zipAccessors, currentPath, items, files, directories, onProgress, processed = { count: 0 }) {
129
+ for (const item of items) {
130
+ const itemPath = item.absolutePath.startsWith('/') ? item.absolutePath.substring(1) : item.absolutePath;
131
+ if (item.type === 'directory') {
132
+ directories.add(itemPath);
133
+ files.set(itemPath, {
134
+ name: item.name,
135
+ path: itemPath,
147
136
  size: 0,
148
137
  isDirectory: true,
149
- lastModified: zipEntry.date
138
+ lastModified: undefined
150
139
  });
140
+ // Recursively process children
141
+ const childrenResult = zipAccessors.getChildren(item.absolutePath);
142
+ if (childrenResult.isSuccess()) {
143
+ await this.processFileTreeItems(zipAccessors, item.absolutePath, childrenResult.value, files, directories, onProgress, processed);
144
+ }
151
145
  }
152
- else {
153
- // Load file content
154
- const content = await zipEntry.async('string');
155
- files.set(filename, {
156
- name: filename.split('/').pop() || filename,
157
- path: filename,
146
+ else if (item.type === 'file') {
147
+ // Get file content
148
+ const contentResult = item.getRawContents();
149
+ const content = contentResult.isSuccess() ? contentResult.value : '';
150
+ files.set(itemPath, {
151
+ name: item.name,
152
+ path: itemPath,
158
153
  size: content.length,
159
154
  isDirectory: false,
160
- lastModified: zipEntry.date,
155
+ lastModified: undefined,
161
156
  content
162
157
  });
163
158
  }
164
- processed++;
165
- const progress = Math.round((processed / zipFiles.length) * 100);
166
- onProgress?.('extracting-files', progress, `Processing ${filename}`);
159
+ processed.count++;
160
+ onProgress?.('extracting-files', processed.count, `Processing ${itemPath}`);
167
161
  }
168
- return {
169
- files,
170
- directories,
171
- root: this.findCommonRoot(Array.from(files.keys()))
172
- };
173
162
  }
174
163
  /**
175
- * Load manifest from ZIP
164
+ * Load manifest from ZIP using ZipFileTreeAccessors
176
165
  */
177
- async loadManifest(zip) {
178
- const manifestFile = zip.files['manifest.json'];
179
- if (!manifestFile) {
166
+ async loadManifestFromAccessors(zipAccessors) {
167
+ const manifestResult = zipAccessors.getFileContents('manifest.json');
168
+ if (manifestResult.isFailure()) {
180
169
  return null;
181
170
  }
182
- const manifestData = await manifestFile.async('string').catch((error) => {
183
- console.warn('Failed to read manifest file:', error);
184
- return null;
185
- });
186
- if (!manifestData)
187
- return null;
188
- const parseResult = (0, zipUtils_1.parseManifest)(manifestData);
171
+ const parseResult = (0, zipUtils_1.parseManifest)(manifestResult.value);
189
172
  return parseResult.orDefault() ?? null;
190
173
  }
191
174
  /**
192
- * Load configuration from ZIP
175
+ * Load configuration from ZIP using ZipFileTreeAccessors
193
176
  */
194
- async loadConfiguration(zip, options) {
177
+ async loadConfigurationFromAccessors(zipAccessors, options) {
195
178
  if (options.overrideConfig) {
196
179
  return options.overrideConfig;
197
180
  }
198
- const configFile = zip.files['config.json'];
199
- if (!configFile) {
181
+ const configResult = zipAccessors.getFileContents('config.json');
182
+ if (configResult.isFailure()) {
200
183
  return null;
201
184
  }
202
- const configData = await configFile.async('string').catch((error) => {
203
- console.warn('Failed to read config file:', error);
204
- return null;
205
- });
206
- if (!configData)
207
- return null;
208
- const parseResult = (0, zipUtils_1.parseConfiguration)(configData);
185
+ const parseResult = (0, zipUtils_1.parseConfiguration)(configResult.value);
209
186
  return parseResult.orDefault() ?? null;
210
187
  }
211
188
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fgv/ts-res-ui-components",
3
- "version": "5.0.0-14",
3
+ "version": "5.0.0-16",
4
4
  "description": "Reusable React components for ts-res resource visualization and management",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -21,13 +21,13 @@
21
21
  "homepage": "https://github.com/ErikFortune/fgv/tree/main/libraries/ts-res-ui-components#readme",
22
22
  "dependencies": {
23
23
  "@heroicons/react": "~2.2.0",
24
- "jszip": "~3.10.1",
25
24
  "tslib": "^2.8.1",
26
25
  "json-edit-react": "~1.28.2",
27
- "@fgv/ts-res": "5.0.0-14",
28
- "@fgv/ts-utils": "5.0.0-14",
29
- "@fgv/ts-json": "5.0.0-14",
30
- "@fgv/ts-json-base": "5.0.0-14"
26
+ "@fgv/ts-utils": "5.0.0-16",
27
+ "@fgv/ts-res": "5.0.0-16",
28
+ "@fgv/ts-json": "5.0.0-16",
29
+ "@fgv/ts-extras": "5.0.0-16",
30
+ "@fgv/ts-json-base": "5.0.0-16"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": ">=18.0.0",
@@ -56,7 +56,7 @@
56
56
  "@rushstack/heft-web-rig": "0.29.4",
57
57
  "@rushstack/eslint-config": "~4.4.0",
58
58
  "@rushstack/heft-jest-plugin": "~0.16.11",
59
- "@fgv/ts-utils-jest": "5.0.0-14"
59
+ "@fgv/ts-utils-jest": "5.0.0-16"
60
60
  },
61
61
  "scripts": {
62
62
  "build": "heft build --clean",
@@ -1,4 +1,5 @@
1
1
  import { Result, succeed, fail } from '@fgv/ts-utils';
2
+ import { ZipFileTree as ExtrasZipFileTree } from '@fgv/ts-extras';
2
3
  import {
3
4
  IZipLoader,
4
5
  ZipLoadOptions,
@@ -12,31 +13,6 @@ import { parseManifest, parseConfiguration, zipTreeToFiles, zipTreeToDirectory,
12
13
  import { processImportedFiles, processImportedDirectory } from '../tsResIntegration';
13
14
  import { ProcessedResources } from '../../types';
14
15
 
15
- // Dynamic import for JSZip to support both Node.js and browser environments
16
- let JSZip: any = null;
17
-
18
- /**
19
- * Get JSZip instance (assumes JSZip is available)
20
- */
21
- function getJSZip(): any {
22
- if (JSZip) return JSZip;
23
-
24
- // Check if JSZip is globally available
25
- if (typeof window !== 'undefined' && (window as any).JSZip) {
26
- JSZip = (window as any).JSZip;
27
- return JSZip;
28
- }
29
-
30
- // Try to get JSZip from require/import (will work in bundled environments)
31
- try {
32
- // eslint-disable-next-line @typescript-eslint/no-var-requires
33
- JSZip = require('jszip');
34
- return JSZip;
35
- } catch (error) {
36
- throw new Error('JSZip is not available. Please install jszip as a dependency: npm install jszip');
37
- }
38
- }
39
-
40
16
  /**
41
17
  * Browser-based ZIP loader implementation
42
18
  */
@@ -73,32 +49,32 @@ export class BrowserZipLoader implements IZipLoader {
73
49
  ): Promise<Result<ZipLoadResult>> {
74
50
  onProgress?.('parsing-zip', 0, 'Parsing ZIP archive');
75
51
 
76
- const JSZipClass = getJSZip();
77
- const zip = new JSZipClass();
78
- const loadedZip = await zip.loadAsync(buffer).catch((error: any) => {
79
- throw new Error(`Failed to parse ZIP: ${error instanceof Error ? error.message : String(error)}`);
80
- });
52
+ const zipAccessorsResult = ExtrasZipFileTree.ZipFileTreeAccessors.fromBuffer(buffer);
53
+ if (zipAccessorsResult.isFailure()) {
54
+ return fail(`Failed to parse ZIP: ${zipAccessorsResult.message}`);
55
+ }
81
56
 
57
+ const zipAccessors = zipAccessorsResult.value;
82
58
  onProgress?.('parsing-zip', 100, 'ZIP archive parsed');
83
59
 
84
- // Build file tree
85
- const fileTree = await this.buildFileTree(loadedZip, onProgress);
60
+ // Build file tree using the new adapter
61
+ const fileTree = await this.buildFileTreeFromAccessors(zipAccessors, onProgress);
86
62
 
87
63
  // Load manifest
88
64
  onProgress?.('loading-manifest', 0, 'Loading manifest');
89
- const manifest = await this.loadManifest(loadedZip);
65
+ const manifest = await this.loadManifestFromAccessors(zipAccessors);
90
66
  onProgress?.('loading-manifest', 100, 'Manifest loaded');
91
67
 
92
68
  // Load configuration
93
69
  onProgress?.('loading-config', 0, 'Loading configuration');
94
- const config = await this.loadConfiguration(loadedZip, options);
70
+ const config = await this.loadConfigurationFromAccessors(zipAccessors, options);
95
71
  onProgress?.('loading-config', 100, 'Configuration loaded');
96
72
 
97
73
  // Extract files and directory structure
98
74
  onProgress?.('extracting-files', 0, 'Extracting files');
99
- const files = zipTreeToFiles(fileTree);
75
+ const filesList = zipTreeToFiles(fileTree);
100
76
  const directory = zipTreeToDirectory(fileTree);
101
- onProgress?.('extracting-files', 100, `Extracted ${files.length} files`);
77
+ onProgress?.('extracting-files', 100, `Extracted ${filesList.length} files`);
102
78
 
103
79
  // Process resources if requested
104
80
  let processedResources: ProcessedResources | null = null;
@@ -114,8 +90,8 @@ export class BrowserZipLoader implements IZipLoader {
114
90
  } else {
115
91
  throw new Error(`Failed to process resources from directory: ${processResult.message}`);
116
92
  }
117
- } else if (files.length > 0) {
118
- const processResult = await processImportedFiles(files, configToUse || undefined);
93
+ } else if (filesList.length > 0) {
94
+ const processResult = await processImportedFiles(filesList, configToUse || undefined);
119
95
  if (processResult.isSuccess()) {
120
96
  processedResources = processResult.value;
121
97
  } else {
@@ -131,7 +107,7 @@ export class BrowserZipLoader implements IZipLoader {
131
107
  return succeed({
132
108
  manifest,
133
109
  config: options.overrideConfig || config,
134
- files,
110
+ files: filesList,
135
111
  directory,
136
112
  processedResources
137
113
  });
@@ -168,99 +144,128 @@ export class BrowserZipLoader implements IZipLoader {
168
144
  }
169
145
 
170
146
  /**
171
- * Build file tree from JSZip instance
147
+ * Build file tree from ZipFileTreeAccessors (using ts-extras)
172
148
  */
173
- private async buildFileTree(zip: any, onProgress?: ZipProgressCallback): Promise<ZipFileTree> {
149
+ private async buildFileTreeFromAccessors(
150
+ zipAccessors: ExtrasZipFileTree.ZipFileTreeAccessors,
151
+ onProgress?: ZipProgressCallback
152
+ ): Promise<ZipFileTree> {
174
153
  const files = new Map<string, ZipFileItem>();
175
154
  const directories = new Set<string>();
176
155
 
177
- const zipFiles = Object.keys(zip.files);
178
- let processed = 0;
179
-
180
- // Pre-load all file contents for performance
181
- for (const filename of zipFiles) {
182
- const zipEntry = zip.files[filename];
183
-
184
- if (zipEntry.dir) {
185
- directories.add(filename);
186
- files.set(filename, {
187
- name:
188
- filename
189
- .split('/')
190
- .filter((p) => p)
191
- .pop() || filename,
192
- path: filename,
156
+ // Get all children from root
157
+ const rootChildrenResult = zipAccessors.getChildren('/');
158
+ if (rootChildrenResult.isFailure()) {
159
+ throw new Error(`Failed to read ZIP contents: ${rootChildrenResult.message}`);
160
+ }
161
+
162
+ // Process all items recursively
163
+ await this.processFileTreeItems(
164
+ zipAccessors,
165
+ '/',
166
+ rootChildrenResult.value,
167
+ files,
168
+ directories,
169
+ onProgress
170
+ );
171
+
172
+ return {
173
+ files,
174
+ directories,
175
+ root: this.findCommonRoot(Array.from(files.keys()))
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Recursively process file tree items
181
+ */
182
+ private async processFileTreeItems(
183
+ zipAccessors: ExtrasZipFileTree.ZipFileTreeAccessors,
184
+ currentPath: string,
185
+ items: readonly any[],
186
+ files: Map<string, ZipFileItem>,
187
+ directories: Set<string>,
188
+ onProgress?: ZipProgressCallback,
189
+ processed: { count: number } = { count: 0 }
190
+ ): Promise<void> {
191
+ for (const item of items) {
192
+ const itemPath = item.absolutePath.startsWith('/') ? item.absolutePath.substring(1) : item.absolutePath;
193
+
194
+ if (item.type === 'directory') {
195
+ directories.add(itemPath);
196
+ files.set(itemPath, {
197
+ name: item.name,
198
+ path: itemPath,
193
199
  size: 0,
194
200
  isDirectory: true,
195
- lastModified: zipEntry.date
201
+ lastModified: undefined
196
202
  });
197
- } else {
198
- // Load file content
199
- const content = await zipEntry.async('string');
200
203
 
201
- files.set(filename, {
202
- name: filename.split('/').pop() || filename,
203
- path: filename,
204
+ // Recursively process children
205
+ const childrenResult = zipAccessors.getChildren(item.absolutePath);
206
+ if (childrenResult.isSuccess()) {
207
+ await this.processFileTreeItems(
208
+ zipAccessors,
209
+ item.absolutePath,
210
+ childrenResult.value,
211
+ files,
212
+ directories,
213
+ onProgress,
214
+ processed
215
+ );
216
+ }
217
+ } else if (item.type === 'file') {
218
+ // Get file content
219
+ const contentResult = item.getRawContents();
220
+ const content = contentResult.isSuccess() ? contentResult.value : '';
221
+
222
+ files.set(itemPath, {
223
+ name: item.name,
224
+ path: itemPath,
204
225
  size: content.length,
205
226
  isDirectory: false,
206
- lastModified: zipEntry.date,
227
+ lastModified: undefined,
207
228
  content
208
229
  });
209
230
  }
210
231
 
211
- processed++;
212
- const progress = Math.round((processed / zipFiles.length) * 100);
213
- onProgress?.('extracting-files', progress, `Processing ${filename}`);
232
+ processed.count++;
233
+ onProgress?.('extracting-files', processed.count, `Processing ${itemPath}`);
214
234
  }
215
-
216
- return {
217
- files,
218
- directories,
219
- root: this.findCommonRoot(Array.from(files.keys()))
220
- };
221
235
  }
222
236
 
223
237
  /**
224
- * Load manifest from ZIP
238
+ * Load manifest from ZIP using ZipFileTreeAccessors
225
239
  */
226
- private async loadManifest(zip: any): Promise<any> {
227
- const manifestFile = zip.files['manifest.json'];
228
- if (!manifestFile) {
240
+ private async loadManifestFromAccessors(
241
+ zipAccessors: ExtrasZipFileTree.ZipFileTreeAccessors
242
+ ): Promise<any> {
243
+ const manifestResult = zipAccessors.getFileContents('manifest.json');
244
+ if (manifestResult.isFailure()) {
229
245
  return null;
230
246
  }
231
247
 
232
- const manifestData = await manifestFile.async('string').catch((error: any) => {
233
- console.warn('Failed to read manifest file:', error);
234
- return null;
235
- });
236
-
237
- if (!manifestData) return null;
238
-
239
- const parseResult = parseManifest(manifestData);
248
+ const parseResult = parseManifest(manifestResult.value);
240
249
  return parseResult.orDefault() ?? null;
241
250
  }
242
251
 
243
252
  /**
244
- * Load configuration from ZIP
253
+ * Load configuration from ZIP using ZipFileTreeAccessors
245
254
  */
246
- private async loadConfiguration(zip: any, options: ZipLoadOptions): Promise<any> {
255
+ private async loadConfigurationFromAccessors(
256
+ zipAccessors: ExtrasZipFileTree.ZipFileTreeAccessors,
257
+ options: ZipLoadOptions
258
+ ): Promise<any> {
247
259
  if (options.overrideConfig) {
248
260
  return options.overrideConfig;
249
261
  }
250
262
 
251
- const configFile = zip.files['config.json'];
252
- if (!configFile) {
263
+ const configResult = zipAccessors.getFileContents('config.json');
264
+ if (configResult.isFailure()) {
253
265
  return null;
254
266
  }
255
267
 
256
- const configData = await configFile.async('string').catch((error: any) => {
257
- console.warn('Failed to read config file:', error);
258
- return null;
259
- });
260
-
261
- if (!configData) return null;
262
-
263
- const parseResult = parseConfiguration(configData);
268
+ const parseResult = parseConfiguration(configResult.value);
264
269
  return parseResult.orDefault() ?? null;
265
270
  }
266
271