Skip to content

Commit 6b53164

Browse files
committed
fix(content-utils): preserve Unicode build history
1 parent d06b01a commit 6b53164

2 files changed

Lines changed: 76 additions & 34 deletions

File tree

packages/content-utils/src/integration/gitPlugin.ts

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ export const gitBuildPlugin = (state: IntegrationState): Plugin => {
7373
liveGit.setProjectRoot(projectRoot);
7474
return liveGit.getFileContentAtCommit(hash, repoPath);
7575
};
76+
const gitStateStart = GIT_STATE_START;
77+
const gitStateEnd = GIT_STATE_END;
78+
let initialSerializedGitState: string | undefined;
7679
let collectedGitInformation: Map<string, BuildGitTrackingInfo> | undefined;
7780
let retainedContentFiles: Set<string> | undefined;
7881

@@ -84,14 +87,20 @@ export const gitBuildPlugin = (state: IntegrationState): Plugin => {
8487
},
8588
transform(code, id) {
8689
if (id !== '\0astro:data-layer-content') return;
87-
const exportPrefix = 'export default ';
88-
const exportStart = code.lastIndexOf(exportPrefix);
89-
if (exportStart === -1) return;
90-
91-
const serializedContent = code
92-
.slice(exportStart + exportPrefix.length)
93-
.trim()
94-
.replace(/;$/, '');
90+
const ast = this.parse(code);
91+
const defaultExport = ast.body.find((node) => node.type === 'ExportDefaultDeclaration');
92+
if (
93+
defaultExport?.type !== 'ExportDefaultDeclaration' ||
94+
typeof defaultExport.declaration.start !== 'number' ||
95+
typeof defaultExport.declaration.end !== 'number'
96+
) {
97+
return;
98+
}
99+
100+
const serializedContent = code.slice(
101+
defaultExport.declaration.start,
102+
defaultExport.declaration.end
103+
);
95104
try {
96105
const contentMap: Map<string, Map<string, unknown>> = devalue.unflatten(
97106
JSON.parse(serializedContent)
@@ -136,13 +145,16 @@ export const gitBuildPlugin = (state: IntegrationState): Plugin => {
136145
if (gitStateIsDedicated && contentDataEntrypoint) {
137146
await cleanupState(contentDataEntrypoint, gitStateEntrypoint, loadCommitContent);
138147
} else {
139-
if (retainedContentFiles === undefined) {
148+
if (retainedContentFiles === undefined || initialSerializedGitState === undefined) {
140149
throw new Error('Could not determine retained content files for combined output');
141150
}
142151
cleanupCombinedState(
143152
gitStateEntrypoint,
144153
collectedGitInformation!,
145154
retainedContentFiles,
155+
initialSerializedGitState,
156+
gitStateStart,
157+
gitStateEnd,
146158
loadCommitContent
147159
);
148160
}
@@ -166,9 +178,14 @@ export const gitBuildPlugin = (state: IntegrationState): Plugin => {
166178
collectedGitInformation = new Map(trackedFiles);
167179
debug('Git tracked file dates:', trackedFiles);
168180

169-
const serializedState = serializeGitState(collectedGitInformation);
170-
return `const trackedFilesPayload = ${JSON.stringify(serializedState)};
171-
const trackedFiles = JSON.parse(atob(trackedFilesPayload.slice(${GIT_STATE_START.length}, -${GIT_STATE_END.length})));
181+
initialSerializedGitState = serializeGitState(
182+
collectedGitInformation,
183+
gitStateStart,
184+
gitStateEnd
185+
);
186+
return `const trackedFilesPayload = ${JSON.stringify(initialSerializedGitState)};
187+
const trackedFilesBytes = Uint8Array.from(atob(trackedFilesPayload.slice(${gitStateStart.length}, -${gitStateEnd.length})), (byte) => byte.charCodeAt(0));
188+
const trackedFiles = JSON.parse(new TextDecoder().decode(trackedFilesBytes));
172189
export { trackedFiles as default };`;
173190
}
174191
},
@@ -185,8 +202,12 @@ type BuildGitTrackingInfo = {
185202
commits: BuildCommitInfo[];
186203
};
187204

188-
function serializeGitState(gitInformation: Map<string, BuildGitTrackingInfo>): string {
189-
return `${GIT_STATE_START}${Buffer.from(devalue.stringify(gitInformation)).toString('base64')}${GIT_STATE_END}`;
205+
function serializeGitState(
206+
gitInformation: Map<string, BuildGitTrackingInfo>,
207+
stateStart: string,
208+
stateEnd: string
209+
): string {
210+
return `${stateStart}${Buffer.from(devalue.stringify(gitInformation)).toString('base64')}${stateEnd}`;
190211
}
191212

192213
function materializeCommitContent(
@@ -259,6 +280,9 @@ function cleanupCombinedState(
259280
gitState: string,
260281
gitInformation: Map<string, BuildGitTrackingInfo>,
261282
retainedContentFiles: Set<string>,
283+
initialSerializedState: string,
284+
stateStartToken: string,
285+
stateEndToken: string,
262286
loadCommitContent: (hash: string, repoPath: string) => string
263287
): void {
264288
const retainedGitInformation = new Map(
@@ -269,17 +293,22 @@ function cleanupCombinedState(
269293
}
270294

271295
const originalContent = readFileSync(gitState, 'utf-8');
272-
const stateStart = originalContent.indexOf(GIT_STATE_START);
273-
const stateEnd = originalContent.lastIndexOf(GIT_STATE_END);
274-
if (stateStart === -1 || stateEnd === -1) {
275-
throw new Error('Could not locate serialized Git history in the combined output chunk');
296+
const stateStart = originalContent.indexOf(initialSerializedState);
297+
if (
298+
stateStart === -1 ||
299+
originalContent.indexOf(initialSerializedState, stateStart + initialSerializedState.length) !==
300+
-1
301+
) {
302+
throw new Error(
303+
'Could not uniquely locate serialized Git history in the combined output chunk'
304+
);
276305
}
277306

278-
const serializedState = serializeGitState(retainedGitInformation);
307+
const serializedState = serializeGitState(retainedGitInformation, stateStartToken, stateEndToken);
279308
writeFileSync(
280309
gitState,
281310
`${originalContent.slice(0, stateStart)}${serializedState}${originalContent.slice(
282-
stateEnd + GIT_STATE_END.length
311+
stateStart + initialSerializedState.length
283312
)}`,
284313
'utf-8'
285314
);

packages/content-utils/tests/git-plugin.test.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os';
33
import { join } from 'node:path';
44
import { pathToFileURL } from 'node:url';
55
import * as devalue from 'devalue';
6-
import type { Plugin } from 'vite';
6+
import { parseAst, type Plugin } from 'vite';
77
import { afterEach, describe, expect, it, vi } from 'vitest';
88
import type { IntegrationState } from '../src/integration/state.js';
99

@@ -157,24 +157,37 @@ describe('gitBuildPlugin', () => {
157157
git.collectGitInfoForContentFiles.mockResolvedValue(
158158
new Map([
159159
[
160-
'entry.md',
160+
'café.md',
161161
{
162162
authors: [],
163163
coAuthors: [],
164-
commits: [{ hash: 'entry', repoPath: 'content/entry.md' }],
164+
commits: [{ hash: 'entry', repoPath: 'content/café.md' }],
165165
earliest: 1,
166166
latest: 1,
167167
},
168168
],
169169
])
170170
);
171-
git.getFileContentAtCommit.mockReturnValue('historical content');
171+
git.getFileContentAtCommit.mockReturnValue('café history');
172172

173173
const transform = plugin.transform as NonNullable<Plugin['transform']>;
174174
await transform.call(
175-
{} as Parameters<typeof transform.call>[0],
176-
`export default ${devalue.stringify(
177-
new Map([['blog', new Map([['entry', { filePath: 'entry.md' }]])]])
175+
{ parse: parseAst } as Parameters<typeof transform.call>[0],
176+
`export default${devalue.stringify(
177+
new Map([
178+
[
179+
'blog',
180+
new Map([
181+
[
182+
'entry',
183+
{
184+
body: 'export default function example() {}',
185+
filePath: 'café.md',
186+
},
187+
],
188+
]),
189+
],
190+
])
178191
)};`,
179192
'\0astro:data-layer-content'
180193
);
@@ -201,14 +214,14 @@ describe('gitBuildPlugin', () => {
201214

202215
const finalizedSource = readFileSync(combinedPath, 'utf-8');
203216
expect(finalizedSource).toContain('const preserved = true');
204-
const encodedState = finalizedSource.match(
205-
/__INOX_CONTENT_GIT_STATE_START__(.*?)__INOX_CONTENT_GIT_STATE_END__/
206-
)?.[1];
207-
expect(encodedState).toBeDefined();
217+
// Import the generated output to exercise its runtime UTF-8 decoder.
218+
const { default: flattenedState } = await import(
219+
`${pathToFileURL(combinedPath).href}?finalized`
220+
);
208221
const finalizedState: Map<string, { commits: Array<Record<string, unknown>> }> =
209-
devalue.unflatten(JSON.parse(Buffer.from(encodedState!, 'base64').toString('utf-8')));
210-
expect(finalizedState.get('entry.md')?.commits).toEqual([
211-
{ content: 'historical content', hash: 'entry' },
222+
devalue.unflatten(flattenedState);
223+
expect(finalizedState.get('café.md')?.commits).toEqual([
224+
{ content: 'café history', hash: 'entry' },
212225
]);
213226
});
214227
});

0 commit comments

Comments
 (0)