Skip to content

Commit

Permalink
fix: prevent corruption of detectComponents() cache
Browse files Browse the repository at this point in the history
  • Loading branch information
fwouts committed Dec 4, 2022
1 parent 1e50a69 commit c3fb8bc
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 59 deletions.
1 change: 1 addition & 0 deletions core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"acorn": "^8.8.1",
"assert-never": "^1.2.1",
"axios": "^1.2.0",
"exclusive-promises": "^1.0.3",
"express": "^4.18.2",
"fs-extra": "^11.1.0",
"get-port": "^5",
Expand Down
125 changes: 67 additions & 58 deletions core/src/detect-components.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { RPCs } from "@previewjs/api";
import type { TypeAnalyzer } from "@previewjs/type-analyzer";
import { exclusivePromiseRunner } from "exclusive-promises";
import fs from "fs-extra";
import path from "path";
import type { FrameworkPlugin, Workspace } from ".";
Expand All @@ -8,7 +9,11 @@ import { findFiles } from "./find-files";

type ProjectComponents = RPCs.DetectComponentsResponse["components"];

export async function detectComponents(
// Prevent concurrent running of detectComponents()
// to avoid corrupting the cache and optimise for cache hits.
const oneAtATime = exclusivePromiseRunner();

export function detectComponents(
workspace: Workspace,
frameworkPlugin: FrameworkPlugin,
typeAnalyzer: TypeAnalyzer,
Expand All @@ -17,65 +22,69 @@ export async function detectComponents(
forceRefresh?: boolean;
} = {}
): Promise<RPCs.DetectComponentsResponse> {
const cacheFilePath = path.join(
getCacheDir(workspace.rootDirPath),
"components.json"
);
const absoluteFilePaths = options.filePaths
? options.filePaths.map((filePath) =>
path.join(workspace.rootDirPath, filePath)
return oneAtATime(async () => {
const cacheFilePath = path.join(
getCacheDir(workspace.rootDirPath),
"components.json"
);
const absoluteFilePaths = options.filePaths
? options.filePaths.map((filePath) =>
path.join(workspace.rootDirPath, filePath)
)
: await findFiles(
workspace.rootDirPath,
"**/*.@(js|jsx|ts|tsx|svelte|vue)"
);
const filePathsSet = new Set(
absoluteFilePaths.map((absoluteFilePath) =>
path
.relative(workspace.rootDirPath, absoluteFilePath)
.replace(/\\/g, "/")
)
: await findFiles(
workspace.rootDirPath,
"**/*.@(js|jsx|ts|tsx|svelte|vue)"
);
const filePathsSet = new Set(
absoluteFilePaths.map((absoluteFilePath) =>
path.relative(workspace.rootDirPath, absoluteFilePath).replace(/\\/g, "/")
)
);
const existingCacheLastModified =
!options.forceRefresh && fs.existsSync(cacheFilePath)
? fs.statSync(cacheFilePath).mtimeMs
: 0;
const existingCache: ProjectComponents = existingCacheLastModified
? JSON.parse(fs.readFileSync(cacheFilePath, "utf8"))
: {};
const changedAbsoluteFilePaths = absoluteFilePaths.filter(
(absoluteFilePath) => {
const entry = workspace.reader.readSync(absoluteFilePath);
return (
entry?.kind === "file" &&
entry.lastModifiedMillis() > existingCacheLastModified
);
);
const existingCacheLastModified =
!options.forceRefresh && fs.existsSync(cacheFilePath)
? fs.statSync(cacheFilePath).mtimeMs
: 0;
const existingCache: ProjectComponents = existingCacheLastModified
? JSON.parse(fs.readFileSync(cacheFilePath, "utf8"))
: {};
const changedAbsoluteFilePaths = absoluteFilePaths.filter(
(absoluteFilePath) => {
const entry = workspace.reader.readSync(absoluteFilePath);
return (
entry?.kind === "file" &&
entry.lastModifiedMillis() > existingCacheLastModified
);
}
);
const recycledComponents = Object.fromEntries(
Object.entries(existingCache).filter(([filePath]) =>
filePathsSet.has(filePath)
)
);
const refreshedComponents = await detectComponentsCore(
workspace,
frameworkPlugin,
typeAnalyzer,
changedAbsoluteFilePaths
);
const allComponents = {
...recycledComponents,
...refreshedComponents,
};
const components = Object.keys(allComponents)
.sort()
.reduce<ProjectComponents>((ordered, filePath) => {
ordered[filePath] = allComponents[filePath]!;
return ordered;
}, {});
if (!options.filePaths) {
await fs.mkdirp(path.dirname(cacheFilePath));
await fs.writeFile(cacheFilePath, JSON.stringify(components));
}
);
const recycledComponents = Object.fromEntries(
Object.entries(existingCache).filter(([filePath]) =>
filePathsSet.has(filePath)
)
);
const refreshedComponents = await detectComponentsCore(
workspace,
frameworkPlugin,
typeAnalyzer,
changedAbsoluteFilePaths
);
const allComponents = {
...recycledComponents,
...refreshedComponents,
};
const components = Object.keys(allComponents)
.sort()
.reduce<ProjectComponents>((ordered, filePath) => {
ordered[filePath] = allComponents[filePath]!;
return ordered;
}, {});
if (!options.filePaths) {
await fs.mkdirp(path.dirname(cacheFilePath));
await fs.writeFile(cacheFilePath, JSON.stringify(components));
}
return { components };
return { components };
});
}

async function detectComponentsCore(
Expand Down
3 changes: 2 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c3fb8bc

Please sign in to comment.