Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,31 @@ Select which scripts to omit from the output manifest `scripts` field. For
example if the build script interferes with your deployment target, but you want
to preserve all of the other scripts, set it to `["build"]`.

By default, all scripts are omitted, and the [pickFromScripts](#pickfromscripts)
configuration overrules this configuration.
By default, all scripts are omitted. For the target manifest the
[pickFromScripts](#pickfromscripts) configuration overrules this one, but
`omitFromScripts` still applies to internal dependencies when both are set.

::: info Scripts in internal dependencies
The `pickFromScripts` and `omitFromScripts` options only apply to the target
package manifest. Internal dependency manifests preserve their scripts by
default (e.g. for `postinstall` hooks like Prisma client generation), with one
exception: the `prepare` script is always stripped from internal dependencies
because it runs during `pnpm install` and typically depends on devDependency
binaries that are not available in the isolated output.
Internal dependency manifests preserve their scripts by default (e.g. for
`postinstall` hooks like Prisma client generation), with one exception. The
`prepare` script is always stripped from internal dependencies because it runs
during `pnpm install` and typically depends on devDependency binaries that are
not available in the isolated output.

`omitFromScripts` applies to internal dependency manifests as well, on top of
that `prepare` strip. This is the way to remove a script the default cannot
anticipate. A `preinstall` with `npx only-allow pnpm` in every workspace
package, for example, fails the install when you deploy the output with npm, so
`omitFromScripts: ["preinstall"]` removes it everywhere.

`pickFromScripts` is target-only. It does not turn into an allowlist for
internal dependencies, which would silently drop their `postinstall` hooks.

Both options rewrite the manifests in the output. They do not affect the packing
step that collects each internal package, and `npm pack` and `pnpm pack` run
`prepare`, `prepack` and `postpack` from the package's original manifest. Listing
one of those three names does not stop it running during isolation, so a
`prepack` that fails still fails the isolate.
:::

### omitPackageManager
Expand Down
142 changes: 142 additions & 0 deletions src/isolate.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { isolate } from "./isolate";

describe("isolate integration", () => {
const temporaryDirectories: string[] = [];
/** Each case shells out to `npm pack`, which outlasts vitest's 5s default */
const ISOLATE_TEST_TIMEOUT = 30_000;
const leftPadPatchContents = "diff --git a/index.js b/index.js\n";
const leftPadPatchHash =
"2692094a267de7e28825147fd6cb2ebde098a4e68c25dfa3976ac806f4a1a784";
Expand Down Expand Up @@ -71,6 +73,146 @@ describe("isolate integration", () => {
).resolves.toMatchObject({ name: "functions-node", version: "1.0.0" });
});

/**
* An npm workspace whose target and internal package both carry lifecycle
* scripts, so a test can assert which ones survive into the output.
*/
async function createScriptsWorkspace() {
const workspaceRoot = await fs.mkdtemp(
path.join(os.tmpdir(), "isolate-omit-internal-scripts-test-"),
);
temporaryDirectories.push(workspaceRoot);
const targetPackageDir = path.join(workspaceRoot, "packages", "functions");
const sharedPackageDir = path.join(workspaceRoot, "packages", "shared");
await fs.ensureDir(targetPackageDir);
await fs.ensureDir(sharedPackageDir);
await fs.writeJson(path.join(workspaceRoot, "package.json"), {
name: "workspace",
version: "1.0.0",
private: true,
workspaces: ["packages/*"],
});
await fs.writeJson(path.join(workspaceRoot, "package-lock.json"), {
name: "workspace",
version: "1.0.0",
lockfileVersion: 3,
requires: true,
packages: {
"": {
name: "workspace",
version: "1.0.0",
workspaces: ["packages/*"],
},
"node_modules/shared": {
resolved: "packages/shared",
link: true,
},
"packages/functions": {
name: "functions",
version: "1.0.0",
dependencies: { shared: "1.0.0" },
},
"packages/shared": {
name: "shared",
version: "1.0.0",
},
},
});
await fs.writeJson(path.join(targetPackageDir, "package.json"), {
name: "functions",
version: "1.0.0",
main: "./index.js",
files: ["index.js"],
scripts: {
preinstall: "echo target-preinstall",
build: "echo target-build",
test: "echo target-test",
},
dependencies: { shared: "1.0.0" },
});
await fs.writeFile(path.join(targetPackageDir, "index.js"), "export {};\n");
await fs.writeJson(path.join(sharedPackageDir, "package.json"), {
name: "shared",
version: "1.0.0",
main: "./index.js",
files: ["index.js"],
scripts: {
preinstall: "echo shared-preinstall",
prepare: "echo shared-prepare",
postinstall: "echo shared-postinstall",
build: "echo shared-build",
},
});
await fs.writeFile(path.join(sharedPackageDir, "index.js"), "export {};\n");

return targetPackageDir;
}

it(
"omits configured scripts from internal package manifests",
async () => {
const targetPackageDir = await createScriptsWorkspace();

const isolateDir = await isolate({
targetPackagePath: targetPackageDir,
buildDirName: ".",
workspaceRoot: "../..",
omitFromScripts: ["preinstall"],
});

const sharedManifest = await fs.readJson(
path.join(isolateDir, "packages", "shared", "package.json"),
);
const targetManifest = await fs.readJson(
path.join(isolateDir, "package.json"),
);

expect(sharedManifest.scripts).toEqual({
postinstall: "echo shared-postinstall",
build: "echo shared-build",
});
expect(targetManifest.scripts).toEqual({
build: "echo target-build",
test: "echo target-test",
});
},
ISOLATE_TEST_TIMEOUT,
);

it(
"applies omitFromScripts to internal packages when pickFromScripts is also set",
async () => {
const targetPackageDir = await createScriptsWorkspace();

const isolateDir = await isolate({
targetPackagePath: targetPackageDir,
buildDirName: ".",
workspaceRoot: "../..",
pickFromScripts: ["build"],
omitFromScripts: ["preinstall"],
});

const targetManifest = await fs.readJson(
path.join(isolateDir, "package.json"),
);
const sharedManifest = await fs.readJson(
path.join(isolateDir, "packages", "shared", "package.json"),
);

/**
* `pickFromScripts` is target-only, so it drops the target's `test`
* script that omission alone keeps, while `omitFromScripts` still has to
* reach the internal package.
*/
expect(targetManifest.scripts).toEqual({ build: "echo target-build" });
expect(sharedManifest.scripts).toEqual({
postinstall: "echo shared-postinstall",
build: "echo shared-build",
});
},
ISOLATE_TEST_TIMEOUT,
);

it.each([10, 11])(
"writes pnpm %i patch settings to the lockfile and workspace configuration",
async (majorVersion) => {
Expand Down
1 change: 1 addition & 0 deletions src/isolate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ export function createIsolator(initialConfig?: IsolateConfig) {
packagesRegistry,
isolateDir,
forceNpm: config.forceNpm,
omitFromScripts: config.omitFromScripts,
workspaceRootDir,
});

Expand Down
67 changes: 67 additions & 0 deletions src/lib/manifest/helpers/adapt-internal-package-manifests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,73 @@ describe("adaptInternalPackageManifests", () => {
});
});

it("should omit the configured scripts alongside prepare", async () => {
const manifest: PackageManifest = {
name: "@repo/common",
version: "1.0.0",
scripts: {
preinstall: "npx only-allow pnpm",
prepare: "tsdown",
postinstall: "prisma generate",
build: "tsdown",
},
};

const packagesRegistry = createRegistry({
"@repo/common": {
rootRelativeDir: "packages/common",
manifest,
},
});

await adaptInternalPackageManifests({
internalPackageNames: ["@repo/common"],
packagesRegistry,
isolateDir: "/output",
forceNpm: true,
omitFromScripts: ["preinstall", "build"],
workspaceRootDir: "/workspace",
});

const writtenManifest = writeManifest.mock.calls[0]![1];

expect(writtenManifest.scripts).toEqual({
postinstall: "prisma generate",
});
});

it("should leave scripts untouched when omitFromScripts lists an absent name", async () => {
const manifest: PackageManifest = {
name: "@repo/common",
version: "1.0.0",
scripts: {
postinstall: "prisma generate",
},
};

const packagesRegistry = createRegistry({
"@repo/common": {
rootRelativeDir: "packages/common",
manifest,
},
});

await adaptInternalPackageManifests({
internalPackageNames: ["@repo/common"],
packagesRegistry,
isolateDir: "/output",
forceNpm: false,
omitFromScripts: ["preinstall"],
workspaceRootDir: "/workspace",
});

const writtenManifest = writeManifest.mock.calls[0]![1];

expect(writtenManifest.scripts).toEqual({
postinstall: "prisma generate",
});
});

it("should strip devDependencies from internal dependency manifests", async () => {
const manifest: PackageManifest = {
name: "@repo/shared",
Expand Down
11 changes: 10 additions & 1 deletion src/lib/manifest/helpers/adapt-internal-package-manifests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ export async function adaptInternalPackageManifests({
packagesRegistry,
isolateDir,
forceNpm,
omitFromScripts,
workspaceRootDir,
}: {
internalPackageNames: string[];
packagesRegistry: PackagesRegistry;
isolateDir: string;
forceNpm: boolean;
omitFromScripts?: string[];
workspaceRootDir: string;
}) {
const packageManager = usePackageManager();
Expand All @@ -49,9 +51,16 @@ export async function adaptInternalPackageManifests({
* which are not available in the isolated output. Other lifecycle
* scripts like `postinstall` are preserved because they handle runtime
* setup (e.g. Prisma client generation).
*
* This rewrites the output manifest only. The packing step ran earlier
* against the original manifest, so `prepare`, `prepack` and `postpack`
* have already executed by the time we get here and cannot be omitted.
*/
if (strippedManifest.scripts) {
strippedManifest.scripts = omit(strippedManifest.scripts, ["prepare"]);
strippedManifest.scripts = omit(strippedManifest.scripts, [
"prepare",
...(omitFromScripts ?? []),
]);
}

const preparedManifest = isPnpmWorkspaceOutput
Expand Down
Loading