The daemon imports the plugin SDK at runtime, but the workspace was named @paseo/plugin and marked private, so it was never published. 0.5.0-beta.1 shipped @getpaseo/server depending on a package that does not exist and `npm install @getpaseo/cli@beta` failed with a 404 for every user. Two things hid it. sync-workspace-versions only rewrites deps matching @getpaseo/*, so the "*" range on @paseo/plugin survived untouched into the published tarball. And `npm pack --dry-run`, which is all release:check and CI run, lists files without ever resolving the dependency graph. Renaming to @getpaseo/plugin rather than publishing the old name: @paseo is not our scope on npm, so that name could never have been published at all. The rename also brings the package under the version-sync filter, which is what pins it correctly from here on. Plugins scaffolded against the old name still import @paseo/plugin. Both spellings resolve through plugin-sdk-specifiers.ts, tagged COMPAT with a removal date, so existing plugins keep loading. This does not repair 0.5.0-beta.1 — a published tarball's dependencies cannot be changed. The package publishes with the next release.
58 lines
1.9 KiB
TypeScript
58 lines
1.9 KiB
TypeScript
import { existsSync, readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const srcDir = path.dirname(fileURLToPath(import.meta.url));
|
|
const forbiddenSpecifiers = new Set([
|
|
"react",
|
|
"react/jsx-runtime",
|
|
"react-native",
|
|
"use-sync-external-store",
|
|
"use-sync-external-store/shim/with-selector",
|
|
]);
|
|
|
|
function localFile(fromFile: string, specifier: string): string {
|
|
const withoutJs = specifier.replace(/\.js$/, "");
|
|
for (const candidate of [`${withoutJs}.ts`, `${withoutJs}.tsx`, withoutJs]) {
|
|
const resolved = path.resolve(path.dirname(fromFile), candidate);
|
|
if (existsSync(resolved)) return resolved;
|
|
}
|
|
throw new Error(`Could not resolve ${specifier} from ${fromFile}`);
|
|
}
|
|
|
|
function valueImportSpecifiers(source: string): string[] {
|
|
const specifiers: string[] = [];
|
|
const pattern = /(?:^|\n)(?:export|import)\s+(?!type\b)(?:[\s\S]*?)from\s+["']([^"']+)["']/g;
|
|
for (const match of source.matchAll(pattern)) {
|
|
if (match[1]) specifiers.push(match[1]);
|
|
}
|
|
return specifiers;
|
|
}
|
|
|
|
function collectBareSpecifiers(entry: string): string[] {
|
|
const seen = new Set<string>();
|
|
const bare: string[] = [];
|
|
const queue = [entry];
|
|
while (queue.length > 0) {
|
|
const file = queue.pop();
|
|
if (!file || seen.has(file)) continue;
|
|
seen.add(file);
|
|
for (const specifier of valueImportSpecifiers(readFileSync(file, "utf8"))) {
|
|
if (specifier.startsWith(".")) {
|
|
queue.push(localFile(file, specifier));
|
|
continue;
|
|
}
|
|
bare.push(specifier);
|
|
}
|
|
}
|
|
return bare;
|
|
}
|
|
|
|
describe("@getpaseo/plugin/server", () => {
|
|
it("does not load react or the client hook graph", () => {
|
|
const specifiers = collectBareSpecifiers(path.join(srcDir, "server.ts"));
|
|
expect(specifiers.filter((specifier) => forbiddenSpecifiers.has(specifier))).toEqual([]);
|
|
});
|
|
});
|