Skip to content

Commit d06b01a

Browse files
committed
fix(content-utils): encode combined history state
1 parent b46bebe commit d06b01a

2 files changed

Lines changed: 75 additions & 9 deletions

File tree

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

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { dirname, join as joinPath, resolve } from 'node:path';
66
import { getDebug } from '../internal/debug.js';
77
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
88
import { fileURLToPath, pathToFileURL } from 'node:url';
9+
import { Buffer } from 'node:buffer';
910

1011
const MODULE_ID = '@it-astro:content/git';
1112
const RESOLVED_MODULE_ID = '\x00@it-astro:content/git';
@@ -15,8 +16,8 @@ const RESOLVED_DEV_CONFIG_MODULE_ID = '\x00@it-astro:content/git/dev-config';
1516

1617
const INNER_MODULE_ID = '@it-astro:content/git/internal';
1718
const RESOLVED_INNER_MODULE_ID = '\x00@it-astro:content/git/internal';
18-
const GIT_STATE_START = '/*! @inox-tools/content-utils:git-state:start */';
19-
const GIT_STATE_END = '/*! @inox-tools/content-utils:git-state:end */';
19+
const GIT_STATE_START = '__INOX_CONTENT_GIT_STATE_START__';
20+
const GIT_STATE_END = '__INOX_CONTENT_GIT_STATE_END__';
2021

2122
const debug = getDebug('git-time-plugin');
2223

@@ -73,13 +74,43 @@ export const gitBuildPlugin = (state: IntegrationState): Plugin => {
7374
return liveGit.getFileContentAtCommit(hash, repoPath);
7475
};
7576
let collectedGitInformation: Map<string, BuildGitTrackingInfo> | undefined;
77+
let retainedContentFiles: Set<string> | undefined;
7678

7779
return {
7880
name: '@inox-tools/content-utils/gitTimes',
7981
resolveId(id) {
8082
if (id === MODULE_ID) return RESOLVED_MODULE_ID;
8183
if (id === INNER_MODULE_ID) return RESOLVED_INNER_MODULE_ID;
8284
},
85+
transform(code, id) {
86+
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(/;$/, '');
95+
try {
96+
const contentMap: Map<string, Map<string, unknown>> = devalue.unflatten(
97+
JSON.parse(serializedContent)
98+
);
99+
for (const collection of state.staticOnlyCollections) {
100+
contentMap.delete(collection);
101+
}
102+
retainedContentFiles = new Set();
103+
for (const collection of contentMap.values()) {
104+
for (const entry of collection.values()) {
105+
if (typeof entry === 'object' && entry && 'filePath' in entry && entry.filePath) {
106+
retainedContentFiles.add(entry.filePath as string);
107+
}
108+
}
109+
}
110+
} catch (error) {
111+
debug('Failed to capture content entry paths for combined output:', error);
112+
}
113+
},
83114
writeBundle(info, bundle) {
84115
if (!info.dir) return;
85116
let contentDataEntrypoint: string | undefined;
@@ -105,7 +136,15 @@ export const gitBuildPlugin = (state: IntegrationState): Plugin => {
105136
if (gitStateIsDedicated && contentDataEntrypoint) {
106137
await cleanupState(contentDataEntrypoint, gitStateEntrypoint, loadCommitContent);
107138
} else {
108-
cleanupCombinedState(gitStateEntrypoint, collectedGitInformation!, loadCommitContent);
139+
if (retainedContentFiles === undefined) {
140+
throw new Error('Could not determine retained content files for combined output');
141+
}
142+
cleanupCombinedState(
143+
gitStateEntrypoint,
144+
collectedGitInformation!,
145+
retainedContentFiles,
146+
loadCommitContent
147+
);
109148
}
110149
} finally {
111150
Reflect.deleteProperty(globalThis, buildContentLoaderSymbol);
@@ -127,7 +166,9 @@ export const gitBuildPlugin = (state: IntegrationState): Plugin => {
127166
collectedGitInformation = new Map(trackedFiles);
128167
debug('Git tracked file dates:', trackedFiles);
129168

130-
return `const trackedFiles = ${GIT_STATE_START}${devalue.stringify(collectedGitInformation)}${GIT_STATE_END};
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})));
131172
export { trackedFiles as default };`;
132173
}
133174
},
@@ -144,6 +185,10 @@ type BuildGitTrackingInfo = {
144185
commits: BuildCommitInfo[];
145186
};
146187

188+
function serializeGitState(gitInformation: Map<string, BuildGitTrackingInfo>): string {
189+
return `${GIT_STATE_START}${Buffer.from(devalue.stringify(gitInformation)).toString('base64')}${GIT_STATE_END}`;
190+
}
191+
147192
function materializeCommitContent(
148193
fileInfo: BuildGitTrackingInfo,
149194
loadCommitContent: (hash: string, repoPath: string) => string
@@ -213,20 +258,24 @@ async function cleanupState(
213258
function cleanupCombinedState(
214259
gitState: string,
215260
gitInformation: Map<string, BuildGitTrackingInfo>,
261+
retainedContentFiles: Set<string>,
216262
loadCommitContent: (hash: string, repoPath: string) => string
217263
): void {
218-
for (const fileInfo of gitInformation.values()) {
264+
const retainedGitInformation = new Map(
265+
Array.from(gitInformation.entries()).filter(([path]) => retainedContentFiles.has(path))
266+
);
267+
for (const fileInfo of retainedGitInformation.values()) {
219268
materializeCommitContent(fileInfo, loadCommitContent);
220269
}
221270

222271
const originalContent = readFileSync(gitState, 'utf-8');
223272
const stateStart = originalContent.indexOf(GIT_STATE_START);
224-
const stateEnd = originalContent.indexOf(GIT_STATE_END, stateStart);
273+
const stateEnd = originalContent.lastIndexOf(GIT_STATE_END);
225274
if (stateStart === -1 || stateEnd === -1) {
226275
throw new Error('Could not locate serialized Git history in the combined output chunk');
227276
}
228277

229-
const serializedState = `${GIT_STATE_START}${devalue.stringify(gitInformation)}${GIT_STATE_END}`;
278+
const serializedState = serializeGitState(retainedGitInformation);
230279
writeFileSync(
231280
gitState,
232281
`${originalContent.slice(0, stateStart)}${serializedState}${originalContent.slice(

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ function createState(): IntegrationState {
3030
return {
3131
cleanups: [],
3232
collectCommitHistory: true,
33+
staticOnlyCollections: [],
3334
contentPaths: {
3435
configExists: true,
3536
configPath: '',
@@ -169,6 +170,15 @@ describe('gitBuildPlugin', () => {
169170
);
170171
git.getFileContentAtCommit.mockReturnValue('historical content');
171172

173+
const transform = plugin.transform as NonNullable<Plugin['transform']>;
174+
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' }]])]])
178+
)};`,
179+
'\0astro:data-layer-content'
180+
);
181+
172182
const load = plugin.load as (id: string, options: { ssr: boolean }) => Promise<string>;
173183
const initialState = await load('\x00@it-astro:content/git/internal', { ssr: true });
174184

@@ -191,7 +201,14 @@ describe('gitBuildPlugin', () => {
191201

192202
const finalizedSource = readFileSync(combinedPath, 'utf-8');
193203
expect(finalizedSource).toContain('const preserved = true');
194-
expect(finalizedSource).toContain('historical content');
195-
expect(finalizedSource).not.toContain('content/entry.md');
204+
const encodedState = finalizedSource.match(
205+
/__INOX_CONTENT_GIT_STATE_START__(.*?)__INOX_CONTENT_GIT_STATE_END__/
206+
)?.[1];
207+
expect(encodedState).toBeDefined();
208+
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' },
212+
]);
196213
});
197214
});

0 commit comments

Comments
 (0)