zephyr-astro-integration 0.0.0-canary.7 → 0.0.0-canary.8
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/package.json +2 -2
- package/.eslintrc.json +0 -20
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/jest.config.ts +0 -11
- package/project.json +0 -37
- package/src/__test__/integration.spec.ts +0 -19
- package/src/index.ts +0 -2
- package/src/lib/__test__/astro-integration-zephyr.spec.ts +0 -242
- package/src/lib/astro-integration-zephyr.ts +0 -63
- package/src/lib/internal/__test__/extract-astro-assets-from-build-hook.spec.ts +0 -269
- package/src/lib/internal/__test__/extract-astro-assets-map.spec.ts +0 -316
- package/src/lib/internal/__test__/file-type-detection.spec.ts +0 -108
- package/src/lib/internal/extract-astro-assets-map.ts +0 -275
- package/tsconfig.json +0 -23
- package/tsconfig.lib.json +0 -11
- package/tsconfig.spec.json +0 -14
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { extractAstroAssetsMap } from '../extract-astro-assets-map';
|
|
5
|
-
|
|
6
|
-
const buildAssetsMapMock = jest.fn();
|
|
7
|
-
const logFnMock = jest.fn();
|
|
8
|
-
|
|
9
|
-
// Mock the zephyr-agent buildAssetsMap function
|
|
10
|
-
jest.mock('zephyr-agent', () => ({
|
|
11
|
-
buildAssetsMap: (...args: unknown[]) => buildAssetsMapMock(...args),
|
|
12
|
-
logFn: (...args: unknown[]) => logFnMock(...args),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
describe('extractAstroAssetsMap', () => {
|
|
16
|
-
let tempDir: string;
|
|
17
|
-
|
|
18
|
-
beforeEach(async () => {
|
|
19
|
-
tempDir = join(tmpdir(), `astro-test-${Date.now()}`);
|
|
20
|
-
await mkdir(tempDir, { recursive: true });
|
|
21
|
-
buildAssetsMapMock.mockImplementation((assets) => {
|
|
22
|
-
const result: Record<string, unknown> = {};
|
|
23
|
-
Object.keys(assets).forEach((key, index) => {
|
|
24
|
-
const hash = `hash${index + 1}`;
|
|
25
|
-
result[hash] = {
|
|
26
|
-
hash,
|
|
27
|
-
content: assets[key].content,
|
|
28
|
-
type: assets[key].type,
|
|
29
|
-
filepath: key,
|
|
30
|
-
};
|
|
31
|
-
});
|
|
32
|
-
return result;
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
afterEach(async () => {
|
|
37
|
-
try {
|
|
38
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
39
|
-
} catch {
|
|
40
|
-
// Ignore cleanup errors
|
|
41
|
-
}
|
|
42
|
-
if (logFnMock && typeof logFnMock.mockRestore === 'function') {
|
|
43
|
-
logFnMock.mockRestore();
|
|
44
|
-
}
|
|
45
|
-
jest.clearAllMocks();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe('Basic Asset Extraction', () => {
|
|
49
|
-
it('should extract assets from build directory', async () => {
|
|
50
|
-
// Create test files
|
|
51
|
-
await writeFile(join(tempDir, 'index.html'), '<html><body>Hello</body></html>');
|
|
52
|
-
await writeFile(join(tempDir, 'style.css'), 'body { color: red; }');
|
|
53
|
-
await writeFile(join(tempDir, 'script.js'), 'console.log("hello");');
|
|
54
|
-
|
|
55
|
-
const assetsMap = await extractAstroAssetsMap(tempDir);
|
|
56
|
-
|
|
57
|
-
expect(Object.keys(assetsMap)).toHaveLength(3);
|
|
58
|
-
expect(buildAssetsMapMock).toHaveBeenCalledWith(
|
|
59
|
-
expect.objectContaining({
|
|
60
|
-
'index.html': expect.objectContaining({
|
|
61
|
-
content: expect.any(Buffer),
|
|
62
|
-
type: 'text/html',
|
|
63
|
-
}),
|
|
64
|
-
'style.css': expect.objectContaining({
|
|
65
|
-
content: expect.any(Buffer),
|
|
66
|
-
type: 'text/css',
|
|
67
|
-
}),
|
|
68
|
-
'script.js': expect.objectContaining({
|
|
69
|
-
content: expect.any(Buffer),
|
|
70
|
-
type: 'application/javascript',
|
|
71
|
-
}),
|
|
72
|
-
}),
|
|
73
|
-
expect.any(Function),
|
|
74
|
-
expect.any(Function)
|
|
75
|
-
);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should handle empty directory', async () => {
|
|
79
|
-
const assetsMap = await extractAstroAssetsMap(tempDir);
|
|
80
|
-
expect(Object.keys(assetsMap)).toHaveLength(0);
|
|
81
|
-
expect(buildAssetsMapMock).toHaveBeenCalledWith(
|
|
82
|
-
{},
|
|
83
|
-
expect.any(Function),
|
|
84
|
-
expect.any(Function)
|
|
85
|
-
);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe('File Filtering', () => {
|
|
90
|
-
it('should skip source maps', async () => {
|
|
91
|
-
await writeFile(join(tempDir, 'index.html'), '<html></html>');
|
|
92
|
-
await writeFile(join(tempDir, 'style.css.map'), '{"version":3}');
|
|
93
|
-
await writeFile(join(tempDir, 'script.js.map'), '{"version":3}');
|
|
94
|
-
|
|
95
|
-
const result = await extractAstroAssetsMap(tempDir);
|
|
96
|
-
|
|
97
|
-
expect(result).toBeDefined();
|
|
98
|
-
expect(buildAssetsMapMock).toHaveBeenCalledWith(
|
|
99
|
-
expect.objectContaining({
|
|
100
|
-
'index.html': expect.any(Object),
|
|
101
|
-
}),
|
|
102
|
-
expect.any(Function),
|
|
103
|
-
expect.any(Function)
|
|
104
|
-
);
|
|
105
|
-
expect(buildAssetsMapMock).not.toHaveBeenCalledWith(
|
|
106
|
-
expect.objectContaining({
|
|
107
|
-
'style.css.map': expect.any(Object),
|
|
108
|
-
'script.js.map': expect.any(Object),
|
|
109
|
-
}),
|
|
110
|
-
expect.any(Function),
|
|
111
|
-
expect.any(Function)
|
|
112
|
-
);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('should skip system files', async () => {
|
|
116
|
-
await writeFile(join(tempDir, 'index.html'), '<html></html>');
|
|
117
|
-
await writeFile(join(tempDir, '.DS_Store'), 'system data');
|
|
118
|
-
await writeFile(join(tempDir, 'Thumbs.db'), 'windows thumbnail');
|
|
119
|
-
|
|
120
|
-
const result = await extractAstroAssetsMap(tempDir);
|
|
121
|
-
|
|
122
|
-
expect(result).toBeDefined();
|
|
123
|
-
expect(buildAssetsMapMock).toHaveBeenCalledWith(
|
|
124
|
-
expect.objectContaining({
|
|
125
|
-
'index.html': expect.any(Object),
|
|
126
|
-
}),
|
|
127
|
-
expect.any(Function),
|
|
128
|
-
expect.any(Function)
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('should skip node_modules and git directories', async () => {
|
|
133
|
-
await mkdir(join(tempDir, 'node_modules'), { recursive: true });
|
|
134
|
-
await mkdir(join(tempDir, '.git'), { recursive: true });
|
|
135
|
-
await writeFile(join(tempDir, 'node_modules', 'package.json'), '{}');
|
|
136
|
-
await writeFile(join(tempDir, '.git', 'config'), 'git config');
|
|
137
|
-
await writeFile(join(tempDir, 'index.html'), '<html></html>');
|
|
138
|
-
|
|
139
|
-
const result = await extractAstroAssetsMap(tempDir);
|
|
140
|
-
|
|
141
|
-
expect(result).toBeDefined();
|
|
142
|
-
expect(buildAssetsMapMock).toHaveBeenCalledWith(
|
|
143
|
-
expect.objectContaining({
|
|
144
|
-
'index.html': expect.any(Object),
|
|
145
|
-
}),
|
|
146
|
-
expect.any(Function),
|
|
147
|
-
expect.any(Function)
|
|
148
|
-
);
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
describe('File Type Detection', () => {
|
|
153
|
-
it('should correctly identify file types', async () => {
|
|
154
|
-
const testFiles = [
|
|
155
|
-
['index.html', '<html></html>', 'text/html'],
|
|
156
|
-
['style.css', 'body {}', 'text/css'],
|
|
157
|
-
['script.js', 'console.log()', 'application/javascript'],
|
|
158
|
-
['module.mjs', 'export default {}', 'application/javascript'],
|
|
159
|
-
['data.json', '{}', 'application/json'],
|
|
160
|
-
['image.png', Buffer.from('PNG data'), 'image/png'],
|
|
161
|
-
['image.jpg', Buffer.from('JPG data'), 'image/jpeg'],
|
|
162
|
-
['image.svg', '<svg></svg>', 'image/svg+xml'],
|
|
163
|
-
['favicon.ico', Buffer.from('ICO data'), 'image/x-icon'],
|
|
164
|
-
['font.woff', Buffer.from('WOFF data'), 'font/woff'],
|
|
165
|
-
['font.woff2', Buffer.from('WOFF2 data'), 'font/woff2'],
|
|
166
|
-
['unknown.xyz', 'unknown content', 'application/octet-stream'],
|
|
167
|
-
];
|
|
168
|
-
|
|
169
|
-
for (const [filename, content] of testFiles) {
|
|
170
|
-
await writeFile(join(tempDir, filename.toString()), content);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
await extractAstroAssetsMap(tempDir);
|
|
174
|
-
|
|
175
|
-
const buildAssetsMapCall = (buildAssetsMapMock as jest.Mock).mock.calls[0];
|
|
176
|
-
const assets = buildAssetsMapCall[0];
|
|
177
|
-
|
|
178
|
-
testFiles.forEach(([filename, , expectedType]) => {
|
|
179
|
-
expect(assets[filename.toString()]).toHaveProperty('type', expectedType);
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
describe('Directory Traversal', () => {
|
|
185
|
-
it('should handle nested directories', async () => {
|
|
186
|
-
await mkdir(join(tempDir, 'assets', 'js'), { recursive: true });
|
|
187
|
-
await mkdir(join(tempDir, 'assets', 'css'), { recursive: true });
|
|
188
|
-
await mkdir(join(tempDir, 'images'), { recursive: true });
|
|
189
|
-
|
|
190
|
-
await writeFile(join(tempDir, 'index.html'), '<html></html>');
|
|
191
|
-
await writeFile(join(tempDir, 'assets', 'js', 'main.js'), 'console.log()');
|
|
192
|
-
await writeFile(join(tempDir, 'assets', 'css', 'style.css'), 'body {}');
|
|
193
|
-
await writeFile(join(tempDir, 'images', 'logo.png'), 'PNG data');
|
|
194
|
-
|
|
195
|
-
const result = await extractAstroAssetsMap(tempDir);
|
|
196
|
-
|
|
197
|
-
expect(result).toBeDefined();
|
|
198
|
-
expect(buildAssetsMapMock).toHaveBeenCalledWith(
|
|
199
|
-
expect.objectContaining({
|
|
200
|
-
'index.html': expect.any(Object),
|
|
201
|
-
'assets/js/main.js': expect.any(Object),
|
|
202
|
-
'assets/css/style.css': expect.any(Object),
|
|
203
|
-
'images/logo.png': expect.any(Object),
|
|
204
|
-
}),
|
|
205
|
-
expect.any(Function),
|
|
206
|
-
expect.any(Function)
|
|
207
|
-
);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('should handle deeply nested directories', async () => {
|
|
211
|
-
const deepPath = join(tempDir, 'a', 'b', 'c', 'd', 'e');
|
|
212
|
-
await mkdir(deepPath, { recursive: true });
|
|
213
|
-
await writeFile(join(deepPath, 'deep.txt'), 'deep file');
|
|
214
|
-
|
|
215
|
-
const result = await extractAstroAssetsMap(tempDir);
|
|
216
|
-
|
|
217
|
-
expect(result).toBeDefined();
|
|
218
|
-
expect(buildAssetsMapMock).toHaveBeenCalledWith(
|
|
219
|
-
expect.objectContaining({
|
|
220
|
-
'a/b/c/d/e/deep.txt': expect.objectContaining({
|
|
221
|
-
type: 'text/plain',
|
|
222
|
-
}),
|
|
223
|
-
}),
|
|
224
|
-
expect.any(Function),
|
|
225
|
-
expect.any(Function)
|
|
226
|
-
);
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
describe('Error Handling', () => {
|
|
231
|
-
it('should handle file read errors gracefully', async () => {
|
|
232
|
-
await writeFile(join(tempDir, 'good.txt'), 'readable');
|
|
233
|
-
await writeFile(join(tempDir, 'index.html'), '<html></html>');
|
|
234
|
-
|
|
235
|
-
// Mock readFile to fail for specific file
|
|
236
|
-
const originalReadFile = readFile;
|
|
237
|
-
jest
|
|
238
|
-
.spyOn(require('node:fs/promises'), 'readFile')
|
|
239
|
-
.mockImplementation(async (path: any) => {
|
|
240
|
-
if (path.toString().includes('good.txt')) {
|
|
241
|
-
throw new Error('Permission denied');
|
|
242
|
-
}
|
|
243
|
-
return originalReadFile(path);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
const result = await extractAstroAssetsMap(tempDir);
|
|
247
|
-
|
|
248
|
-
expect(result).toBeDefined();
|
|
249
|
-
|
|
250
|
-
expect(logFnMock).toHaveBeenCalledWith(
|
|
251
|
-
'warn',
|
|
252
|
-
expect.stringMatching(/Failed to read file.*good\.txt/)
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
// Should still process the readable file
|
|
256
|
-
expect(buildAssetsMapMock).toHaveBeenCalledWith(
|
|
257
|
-
expect.objectContaining({
|
|
258
|
-
'index.html': expect.any(Object),
|
|
259
|
-
}),
|
|
260
|
-
expect.any(Function),
|
|
261
|
-
expect.any(Function)
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
jest.restoreAllMocks();
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it('should handle directory read errors gracefully', async () => {
|
|
268
|
-
await writeFile(join(tempDir, 'index.html'), '<html></html>');
|
|
269
|
-
await mkdir(join(tempDir, 'subdir'), { recursive: true });
|
|
270
|
-
|
|
271
|
-
// Mock readdir to fail for subdirectory
|
|
272
|
-
const originalReaddir = require('node:fs/promises').readdir;
|
|
273
|
-
jest
|
|
274
|
-
.spyOn(require('node:fs/promises'), 'readdir')
|
|
275
|
-
.mockImplementation(async (path: any, options) => {
|
|
276
|
-
if (path.toString().includes('subdir')) {
|
|
277
|
-
throw new Error('Access denied');
|
|
278
|
-
}
|
|
279
|
-
return originalReaddir(path, options);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
const result = await extractAstroAssetsMap(tempDir);
|
|
283
|
-
|
|
284
|
-
expect(result).toBeDefined();
|
|
285
|
-
|
|
286
|
-
expect(logFnMock).toHaveBeenCalledWith(
|
|
287
|
-
'warn',
|
|
288
|
-
expect.stringMatching(/Failed to walk directory.*subdir/)
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
jest.restoreAllMocks();
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
describe('Utility Functions', () => {
|
|
296
|
-
it('should test extractBuffer function', async () => {
|
|
297
|
-
await writeFile(join(tempDir, 'test.txt'), 'test content');
|
|
298
|
-
await extractAstroAssetsMap(tempDir);
|
|
299
|
-
|
|
300
|
-
const extractBuffer = (buildAssetsMapMock as jest.Mock).mock.calls[0][1];
|
|
301
|
-
const testAsset = { content: Buffer.from('test'), type: 'text/plain' };
|
|
302
|
-
|
|
303
|
-
expect(extractBuffer(testAsset)).toBe(testAsset.content);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it('should test getAssetType function', async () => {
|
|
307
|
-
await writeFile(join(tempDir, 'test.txt'), 'test content');
|
|
308
|
-
await extractAstroAssetsMap(tempDir);
|
|
309
|
-
|
|
310
|
-
const getAssetType = (buildAssetsMapMock as jest.Mock).mock.calls[0][2];
|
|
311
|
-
const testAsset = { content: Buffer.from('test'), type: 'text/plain' };
|
|
312
|
-
|
|
313
|
-
expect(getAssetType(testAsset)).toBe('text/plain');
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
});
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
// Separate test file for detailed file type detection testing
|
|
2
|
-
// This tests the getFileType logic without requiring file system operations
|
|
3
|
-
|
|
4
|
-
// We need to extract the getFileType function for direct testing
|
|
5
|
-
// Let's test it by examining the behavior through extractAstroAssetsMap
|
|
6
|
-
|
|
7
|
-
import { extractAstroAssetsMap } from '../extract-astro-assets-map';
|
|
8
|
-
import { buildAssetsMap } from 'zephyr-agent';
|
|
9
|
-
import { mkdir, writeFile, rm } from 'node:fs/promises';
|
|
10
|
-
import { join } from 'node:path';
|
|
11
|
-
import { tmpdir } from 'node:os';
|
|
12
|
-
|
|
13
|
-
jest.mock('zephyr-agent', () => ({
|
|
14
|
-
buildAssetsMap: jest.fn(),
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
describe('File Type Detection', () => {
|
|
18
|
-
let tempDir: string;
|
|
19
|
-
|
|
20
|
-
beforeEach(async () => {
|
|
21
|
-
tempDir = join(tmpdir(), `file-type-test-${Date.now()}`);
|
|
22
|
-
await mkdir(tempDir, { recursive: true });
|
|
23
|
-
|
|
24
|
-
(buildAssetsMap as jest.Mock).mockImplementation((assets) => {
|
|
25
|
-
return Object.fromEntries(
|
|
26
|
-
Object.entries(assets).map(([key, value], index) => [
|
|
27
|
-
`hash${index}`,
|
|
28
|
-
{ filepath: key, ...(value as Record<string, unknown>) },
|
|
29
|
-
])
|
|
30
|
-
);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
afterEach(async () => {
|
|
35
|
-
try {
|
|
36
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
37
|
-
} catch {
|
|
38
|
-
// Ignore cleanup errors
|
|
39
|
-
}
|
|
40
|
-
jest.clearAllMocks();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const testCases = [
|
|
44
|
-
// Web files
|
|
45
|
-
['index.html', 'text/html'],
|
|
46
|
-
['INDEX.HTML', 'text/html'], // Case insensitive
|
|
47
|
-
['style.css', 'text/css'],
|
|
48
|
-
['script.js', 'application/javascript'],
|
|
49
|
-
['module.mjs', 'application/javascript'],
|
|
50
|
-
|
|
51
|
-
// Data files
|
|
52
|
-
['data.json', 'application/json'],
|
|
53
|
-
['config.xml', 'text/xml'],
|
|
54
|
-
['readme.txt', 'text/plain'],
|
|
55
|
-
|
|
56
|
-
// Images
|
|
57
|
-
['logo.png', 'image/png'],
|
|
58
|
-
['photo.jpg', 'image/jpeg'],
|
|
59
|
-
['photo.jpeg', 'image/jpeg'],
|
|
60
|
-
['animation.gif', 'image/gif'],
|
|
61
|
-
['icon.svg', 'image/svg+xml'],
|
|
62
|
-
['favicon.ico', 'image/x-icon'],
|
|
63
|
-
|
|
64
|
-
// Fonts
|
|
65
|
-
['font.woff', 'font/woff'],
|
|
66
|
-
['font.woff2', 'font/woff2'],
|
|
67
|
-
['font.ttf', 'font/ttf'],
|
|
68
|
-
['font.eot', 'application/vnd.ms-fontobject'],
|
|
69
|
-
|
|
70
|
-
// Unknown/fallback
|
|
71
|
-
['unknown.xyz', 'application/octet-stream'],
|
|
72
|
-
['no-extension', 'application/octet-stream'],
|
|
73
|
-
['file.', 'application/octet-stream'],
|
|
74
|
-
];
|
|
75
|
-
|
|
76
|
-
test.each(testCases)('should detect %s as %s', async (filename, expectedType) => {
|
|
77
|
-
await writeFile(join(tempDir, filename), 'test content');
|
|
78
|
-
|
|
79
|
-
await extractAstroAssetsMap(tempDir);
|
|
80
|
-
|
|
81
|
-
const buildAssetsMapCall = (buildAssetsMap as jest.Mock).mock.calls[0];
|
|
82
|
-
const assets = buildAssetsMapCall[0];
|
|
83
|
-
|
|
84
|
-
expect(assets[filename]).toHaveProperty('type', expectedType);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('should handle files with multiple extensions correctly', async () => {
|
|
88
|
-
const testFiles = [
|
|
89
|
-
['script.min.js', 'application/javascript'],
|
|
90
|
-
['style.min.css', 'text/css'],
|
|
91
|
-
['backup.tar.gz', 'application/octet-stream'], // Should use last extension
|
|
92
|
-
['data.test.json', 'application/json'],
|
|
93
|
-
];
|
|
94
|
-
|
|
95
|
-
for (const [filename] of testFiles) {
|
|
96
|
-
await writeFile(join(tempDir, filename), 'content');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
await extractAstroAssetsMap(tempDir);
|
|
100
|
-
|
|
101
|
-
const buildAssetsMapCall = (buildAssetsMap as jest.Mock).mock.calls[0];
|
|
102
|
-
const assets = buildAssetsMapCall[0];
|
|
103
|
-
|
|
104
|
-
testFiles.forEach(([filename, expectedType]) => {
|
|
105
|
-
expect(assets[filename]).toHaveProperty('type', expectedType);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
});
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
-
import { join, relative, sep, isAbsolute } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { buildAssetsMap, logFn, type ZeBuildAssetsMap } from 'zephyr-agent';
|
|
5
|
-
|
|
6
|
-
interface AstroAsset {
|
|
7
|
-
content: Buffer | string;
|
|
8
|
-
type: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function extractBuffer(asset: AstroAsset): Buffer | string | undefined {
|
|
12
|
-
return asset.content;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function getAssetType(asset: AstroAsset): string {
|
|
16
|
-
return asset.type;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Normalize path separators to forward slashes for cross-platform consistency */
|
|
20
|
-
function normalizePath(filePath: string): string {
|
|
21
|
-
return filePath.split(sep).join('/');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Check if a path looks like a URL route rather than a filesystem path */
|
|
25
|
-
function looksLikeRoute(path: string): boolean {
|
|
26
|
-
if (!path || !path.startsWith('/')) {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Dynamic routes have brackets
|
|
31
|
-
if (path.includes('[') || path.includes(']')) {
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Check if it's a real absolute filesystem path vs a route
|
|
36
|
-
// Real absolute paths are typically long and contain multiple segments
|
|
37
|
-
const segments = path.split('/').filter(Boolean);
|
|
38
|
-
|
|
39
|
-
// Paths with just 1-2 segments like /about, /blog, /rss.xml are likely routes
|
|
40
|
-
// Real filesystem paths like /Users/name/project/file.js have many segments
|
|
41
|
-
if (segments.length <= 2) {
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
type AstroAssets =
|
|
49
|
-
| Record<string, unknown>
|
|
50
|
-
| Map<string, unknown>
|
|
51
|
-
| Array<unknown>
|
|
52
|
-
| undefined
|
|
53
|
-
| null;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Extract assets map from Astro's build hook assets parameter. This is more efficient
|
|
57
|
-
* than walking the filesystem manually.
|
|
58
|
-
*/
|
|
59
|
-
export async function extractAstroAssetsFromBuildHook(
|
|
60
|
-
assets: AstroAssets,
|
|
61
|
-
outputPath: string
|
|
62
|
-
): Promise<ZeBuildAssetsMap> {
|
|
63
|
-
const astroAssets: Record<string, AstroAsset> = {};
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
// Handle different possible structures of the assets parameter
|
|
67
|
-
if (!assets) {
|
|
68
|
-
// Fallback to filesystem walking if assets is not available
|
|
69
|
-
return await extractAstroAssetsMap(outputPath);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Assets might be an object, Map, or array depending on Astro version
|
|
73
|
-
const assetEntries = extractAssetEntries(assets);
|
|
74
|
-
|
|
75
|
-
for (const [filePath, assetInfo] of assetEntries) {
|
|
76
|
-
try {
|
|
77
|
-
let fullPath: string | null = null;
|
|
78
|
-
|
|
79
|
-
// Handle URL objects or string paths
|
|
80
|
-
if (assetInfo && typeof assetInfo === 'object' && 'href' in assetInfo) {
|
|
81
|
-
// It's a URL object
|
|
82
|
-
fullPath = fileURLToPath(assetInfo as URL);
|
|
83
|
-
} else if (typeof assetInfo === 'string' && assetInfo) {
|
|
84
|
-
// It's a string path - skip if it looks like a route
|
|
85
|
-
if (looksLikeRoute(assetInfo)) {
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Only treat as absolute if it's really an absolute filesystem path
|
|
90
|
-
// (not just a route starting with /)
|
|
91
|
-
fullPath =
|
|
92
|
-
isAbsolute(assetInfo) && !looksLikeRoute(assetInfo)
|
|
93
|
-
? assetInfo // Absolute file system path
|
|
94
|
-
: join(outputPath, assetInfo); // Relative path or route
|
|
95
|
-
} else if (typeof filePath === 'string' && filePath) {
|
|
96
|
-
// Use the key as the file path only if it looks like a file path
|
|
97
|
-
if (looksLikeRoute(filePath)) {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Only treat as absolute if it's really an absolute filesystem path
|
|
102
|
-
fullPath =
|
|
103
|
-
isAbsolute(filePath) && !looksLikeRoute(filePath)
|
|
104
|
-
? filePath // Absolute file system path
|
|
105
|
-
: join(outputPath, filePath); // Relative path or route
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Skip if we couldn't determine a valid file path
|
|
109
|
-
if (!fullPath) {
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Skip files we don't want to upload
|
|
114
|
-
const relativePath = normalizePath(relative(outputPath, fullPath));
|
|
115
|
-
if (shouldSkipFile(relativePath)) {
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Read the file content
|
|
120
|
-
const content = await readFile(fullPath);
|
|
121
|
-
const fileType = getFileType(relativePath);
|
|
122
|
-
|
|
123
|
-
astroAssets[relativePath] = {
|
|
124
|
-
content,
|
|
125
|
-
type: fileType,
|
|
126
|
-
};
|
|
127
|
-
} catch (readError) {
|
|
128
|
-
logFn('warn', `Failed to read asset file ${filePath}: ${readError}`);
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// If we didn't find any assets from the hook, fallback to filesystem walking
|
|
134
|
-
if (Object.keys(astroAssets).length === 0) {
|
|
135
|
-
return await extractAstroAssetsMap(outputPath);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return buildAssetsMap(astroAssets, extractBuffer, getAssetType);
|
|
139
|
-
} catch (error) {
|
|
140
|
-
logFn(
|
|
141
|
-
'warn',
|
|
142
|
-
'Error processing assets from Astro build hook:' + JSON.stringify(error, null, 2)
|
|
143
|
-
);
|
|
144
|
-
// Fallback to filesystem walking on any error
|
|
145
|
-
return await extractAstroAssetsMap(outputPath);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Extract asset entries from the Astro assets parameter. Handles different possible data
|
|
151
|
-
* structures.
|
|
152
|
-
*/
|
|
153
|
-
function extractAssetEntries(assets: AstroAssets): [string, unknown][] {
|
|
154
|
-
const entries: [string, unknown][] = [];
|
|
155
|
-
|
|
156
|
-
if (Array.isArray(assets)) {
|
|
157
|
-
// Handle array of assets
|
|
158
|
-
assets.forEach((asset) => {
|
|
159
|
-
if (typeof asset === 'string') {
|
|
160
|
-
entries.push([asset, asset]);
|
|
161
|
-
} else if (asset && typeof asset === 'object') {
|
|
162
|
-
// Could be an object with path/url properties
|
|
163
|
-
const assetObj = asset as Record<string, unknown>;
|
|
164
|
-
const path =
|
|
165
|
-
assetObj['path'] || assetObj['url'] || assetObj['href'] || assetObj['pathname'];
|
|
166
|
-
if (path && typeof path === 'string') {
|
|
167
|
-
entries.push([path, asset]);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
} else if (assets instanceof Map) {
|
|
172
|
-
// Handle Map objects
|
|
173
|
-
for (const [key, value] of assets.entries()) {
|
|
174
|
-
entries.push([key, value]);
|
|
175
|
-
}
|
|
176
|
-
} else if (assets && typeof assets === 'object') {
|
|
177
|
-
// Handle plain objects
|
|
178
|
-
for (const [key, value] of Object.entries(assets)) {
|
|
179
|
-
if (Array.isArray(value)) {
|
|
180
|
-
// If value is an array, it might contain multiple assets for this route
|
|
181
|
-
value.forEach((item) => {
|
|
182
|
-
entries.push([key, item]);
|
|
183
|
-
});
|
|
184
|
-
} else {
|
|
185
|
-
entries.push([key, value]);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return entries;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export async function extractAstroAssetsMap(buildDir: string): Promise<ZeBuildAssetsMap> {
|
|
194
|
-
const assets: Record<string, AstroAsset> = {};
|
|
195
|
-
|
|
196
|
-
// Recursively walk through the build directory
|
|
197
|
-
async function walkDir(dirPath: string): Promise<void> {
|
|
198
|
-
try {
|
|
199
|
-
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
200
|
-
|
|
201
|
-
for (const entry of entries) {
|
|
202
|
-
const fullPath = join(dirPath, entry.name);
|
|
203
|
-
|
|
204
|
-
if (entry.isDirectory()) {
|
|
205
|
-
await walkDir(fullPath);
|
|
206
|
-
} else if (entry.isFile()) {
|
|
207
|
-
// Get relative path from build directory
|
|
208
|
-
const relativePath = normalizePath(relative(buildDir, fullPath));
|
|
209
|
-
|
|
210
|
-
// Skip certain files that shouldn't be uploaded
|
|
211
|
-
if (shouldSkipFile(relativePath)) {
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
const content = await readFile(fullPath);
|
|
217
|
-
const fileType = getFileType(relativePath);
|
|
218
|
-
|
|
219
|
-
assets[relativePath] = {
|
|
220
|
-
content,
|
|
221
|
-
type: fileType,
|
|
222
|
-
};
|
|
223
|
-
} catch (readError) {
|
|
224
|
-
logFn('warn', `Failed to read file ${fullPath}: ${readError}`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
} catch (error) {
|
|
229
|
-
logFn('warn', `Failed to walk directory ${dirPath}: ${error}`);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
await walkDir(buildDir);
|
|
234
|
-
|
|
235
|
-
return buildAssetsMap(assets, extractBuffer, getAssetType);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function shouldSkipFile(filePath: string): boolean {
|
|
239
|
-
// Skip common files that shouldn't be uploaded
|
|
240
|
-
const skipPatterns = [
|
|
241
|
-
/\.map$/, // Source maps
|
|
242
|
-
/node_modules/, // Node modules
|
|
243
|
-
/\.git/, // Git files
|
|
244
|
-
/\.DS_Store$/, // macOS files
|
|
245
|
-
/thumbs\.db$/i, // Windows files
|
|
246
|
-
];
|
|
247
|
-
|
|
248
|
-
return skipPatterns.some((pattern) => pattern.test(filePath));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function getFileType(filePath: string): string {
|
|
252
|
-
const extension = filePath.split('.').pop()?.toLowerCase() || '';
|
|
253
|
-
|
|
254
|
-
const typeMap: Record<string, string> = {
|
|
255
|
-
html: 'text/html',
|
|
256
|
-
css: 'text/css',
|
|
257
|
-
js: 'application/javascript',
|
|
258
|
-
mjs: 'application/javascript',
|
|
259
|
-
json: 'application/json',
|
|
260
|
-
png: 'image/png',
|
|
261
|
-
jpg: 'image/jpeg',
|
|
262
|
-
jpeg: 'image/jpeg',
|
|
263
|
-
gif: 'image/gif',
|
|
264
|
-
svg: 'image/svg+xml',
|
|
265
|
-
ico: 'image/x-icon',
|
|
266
|
-
woff: 'font/woff',
|
|
267
|
-
woff2: 'font/woff2',
|
|
268
|
-
ttf: 'font/ttf',
|
|
269
|
-
eot: 'application/vnd.ms-fontobject',
|
|
270
|
-
xml: 'text/xml',
|
|
271
|
-
txt: 'text/plain',
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
return typeMap[extension] || 'application/octet-stream';
|
|
275
|
-
}
|