mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 12:12:47 +00:00
* chore(server): guard against synchronous Node IO in daemon modules * chore(server): retire unused sync IO exceptions automatically * chore(server): remove the attachment IO exception after async materialization
153 lines
6.2 KiB
JavaScript
153 lines
6.2 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { test } from "node:test";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const oxlintBin = path.join(repoRoot, "node_modules/.bin/oxlint");
|
|
const pluginPath = path.join(repoRoot, "scripts/oxlint/paseo-plugin.mjs");
|
|
|
|
function lint(source, rule = "paseo/no-raw-pressable", options, filename = "probe.tsx") {
|
|
const config = JSON.stringify({
|
|
jsPlugins: [pluginPath],
|
|
rules: { [rule]: options ? ["error", options] : "error" },
|
|
});
|
|
return lintWithConfig(source, config, filename);
|
|
}
|
|
|
|
function lintWithConfig(source, config, filename) {
|
|
const dir = mkdtempSync(path.join(tmpdir(), "paseo-oxlint-"));
|
|
try {
|
|
writeFileSync(path.join(dir, ".oxlintrc.json"), config);
|
|
mkdirSync(path.dirname(path.join(dir, filename)), { recursive: true });
|
|
writeFileSync(path.join(dir, filename), source);
|
|
const result = spawnSync(oxlintBin, ["-c", ".oxlintrc.json", filename], {
|
|
cwd: dir,
|
|
encoding: "utf8",
|
|
});
|
|
return { failed: result.status !== 0, output: result.stdout + result.stderr };
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
test("a raw Pressable from react-native is reported", () => {
|
|
const result = lint(
|
|
'import { Pressable, View } from "react-native";\nexport { Pressable, View };\n',
|
|
);
|
|
assert.equal(result.failed, true);
|
|
assert.match(result.output, /no-raw-pressable/);
|
|
});
|
|
|
|
test("an aliased gesture-handler touchable is reported", () => {
|
|
const result = lint(
|
|
'import { TouchableOpacity as Tap } from "react-native-gesture-handler";\nexport { Tap };\n',
|
|
);
|
|
assert.equal(result.failed, true);
|
|
assert.match(result.output, /no-raw-pressable/);
|
|
});
|
|
|
|
test("press primitives and pressable types are allowed", () => {
|
|
const result = lint(
|
|
'import { View, type PressableProps } from "react-native";\n' +
|
|
'import { ToolbarButton } from "@/components/ui/pane-content-toolbar";\n' +
|
|
"export type Props = PressableProps;\nexport { View, ToolbarButton };\n",
|
|
);
|
|
assert.equal(result.failed, false, result.output);
|
|
});
|
|
|
|
for (const source of [
|
|
'import { readFileSync as read } from "node:fs"; export { read };',
|
|
'import { execSync } from "child_process"; export { execSync };',
|
|
'import fs from "node:fs"; export const read = fs.readFileSync;',
|
|
'import { default as fs } from "node:fs"; export const read = fs.readFileSync;',
|
|
'import fs = require("node:fs"); export const read = fs.readFileSync;',
|
|
'import * as fs from "fs"; export const read = fs["readFileSync"];',
|
|
'import cp from "node:child_process"; export const run = cp.spawnSync;',
|
|
'const { readFileSync: read } = require("node:fs"); export { read };',
|
|
'const fs = require("fs"); export const read = fs.readFileSync;',
|
|
'export const run = require("node:child_process").execFileSync;',
|
|
'import fs from "node:fs"; const other = fs; export const read = other.statSync;',
|
|
'export { readFileSync as read } from "node:fs";',
|
|
'export const read = (await import("node:fs")).readFileSync;',
|
|
]) {
|
|
test(`sync IO is reported: ${source}`, () => {
|
|
const result = lint(source, "paseo/no-sync-io");
|
|
assert.equal(result.failed, true, result.output);
|
|
assert.match(result.output, /Synchronous Node IO/);
|
|
});
|
|
}
|
|
|
|
test("async IO, types, unrelated methods and shadowed namespaces are allowed", () => {
|
|
const result = lint(
|
|
'import fs from "node:fs"; import { readFile } from "node:fs/promises";\n' +
|
|
'import type { readFileSync } from "node:fs";\n' +
|
|
"export const read = readFile; export const asyncRead = fs.promises.readFile;\n" +
|
|
"export function unrelated(fs: { readFileSync(): void }) { fs.readFileSync(); }\n" +
|
|
"export type Reader = typeof readFileSync;\n",
|
|
"paseo/no-sync-io",
|
|
);
|
|
assert.equal(result.failed, false, result.output);
|
|
});
|
|
|
|
test("sync IO exceptions allow only named APIs in the named server module", () => {
|
|
const options = { allow: { "server/startup.ts": ["readFileSync"] } };
|
|
const source = 'import { readFileSync } from "node:fs"; export { readFileSync };';
|
|
const allowed = lint(
|
|
source,
|
|
"paseo/no-sync-io",
|
|
options,
|
|
"packages/server/src/server/startup.ts",
|
|
);
|
|
assert.equal(allowed.failed, false, allowed.output);
|
|
const otherFile = lint(
|
|
source,
|
|
"paseo/no-sync-io",
|
|
options,
|
|
"packages/server/src/server/session.ts",
|
|
);
|
|
assert.equal(otherFile.failed, true, otherFile.output);
|
|
assert.match(otherFile.output, /Synchronous Node IO/);
|
|
const otherApi = lint(
|
|
'import fs from "node:fs"; export const read = fs.statSync;',
|
|
"paseo/no-sync-io",
|
|
options,
|
|
"packages/server/src/server/startup.ts",
|
|
);
|
|
assert.equal(otherApi.failed, true, otherApi.output);
|
|
assert.match(otherApi.output, /Synchronous Node IO/);
|
|
});
|
|
|
|
test("repository config guards request paths while allowing startup APIs and test fixtures", () => {
|
|
const config = readFileSync(path.join(repoRoot, ".oxlintrc.json"), "utf8").replace(
|
|
'"./scripts/oxlint/paseo-plugin.mjs"',
|
|
JSON.stringify(pluginPath),
|
|
);
|
|
const source = 'import { existsSync } from "node:fs"; export const probe = existsSync;';
|
|
const request = lintWithConfig(source, config, "packages/server/src/server/probe.ts");
|
|
assert.equal(request.failed, true, request.output);
|
|
assert.match(request.output, /Synchronous Node IO/);
|
|
for (const filename of [
|
|
"packages/server/src/server/config.ts",
|
|
"packages/server/src/server/probe.test.ts",
|
|
"packages/app/src/probe.ts",
|
|
]) {
|
|
const result = lintWithConfig(source, config, filename);
|
|
assert.equal(result.failed, false, result.output);
|
|
}
|
|
});
|
|
|
|
test("sync IO exceptions must be removed when a module stops using the API", () => {
|
|
const result = lint(
|
|
'import { readFile } from "node:fs/promises"; export const read = readFile;',
|
|
"paseo/no-sync-io",
|
|
{ allow: { "server/startup.ts": ["readFileSync"] } },
|
|
"packages/server/src/server/startup.ts",
|
|
);
|
|
assert.equal(result.failed, true, result.output);
|
|
assert.match(result.output, /Remove the unused sync IO exception for readFileSync/);
|
|
});
|