Skip to content
148 changes: 129 additions & 19 deletions src/cli/commands/build/build.spec.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
import "../../../../tests/_mocks/fs.js";
import { vol } from "memfs";
import { build } from "./build.js";
import "../../../../tests/_mocks/posix.js";
import { childProcess, directLaunches, mockInternalProcesses } from "../../../../tests/_mocks/internal-process.js";
import { vol, fs } from "memfs";
import path from "node:path";
import process from "node:process";
import { DEFAULT_CONFIG } from "../../../config.js";
import { convertToNativePaths } from "../../../test.helpers.js";

vi.mock("../../../core/utils/logger", () => {
return {
logger: {
error: vi.fn(),
log: vi.fn(),
warn: vi.fn(),
silly: vi.fn(),
},
};
});
vi.mock("../../../core/utils/command.js");

describe("swa build", () => {
beforeEach(() => {
let build: typeof import("./build.js").build;

beforeEach(async () => {
vi.resetModules();
for (const module of [
"../../../core/utils/workflow-config.js",
"../../../core/utils/options.js",
"../../../core/utils/file.js",
"../../../core/frameworks/detect.js",
])
vi.doUnmock(module);
vi.doMock("../../../core/utils/logger.js", () => ({
logger: { error: vi.fn(), log: vi.fn(), warn: vi.fn(), silly: vi.fn() },
}));
vi.doMock("../../../core/utils/command.js");
({ build } = await import("./build.js"));
vol.reset();
vol.fromJSON({ "/trusted/bin/npm": "inert native fixture; never executed" });
fs.chmodSync("/trusted/bin/npm", 0o755);
vi.stubEnv("PATH", "/trusted/bin");
});

afterEach(() => {
vi.unstubAllEnvs();
});

it("should run app build command", async () => {
Expand All @@ -36,8 +49,16 @@ describe("swa build", () => {

await build({ ...DEFAULT_CONFIG, appBuildCommand: "npm run something" });

expect(command.runCommand).toHaveBeenCalledWith("npm install", ".");
expect(command.execInternalCommand).toHaveBeenCalledWith({
executable: "/trusted/bin/npm",
args: ["install"],
cwd: process.cwd(),
});
expect(command.runCommand).toHaveBeenCalledTimes(1);
expect(command.runCommand).toHaveBeenCalledWith("npm run something", ".");
expect(vi.mocked(command.execInternalCommand).mock.invocationCallOrder[0]).toBeLessThan(
vi.mocked(command.runCommand).mock.invocationCallOrder[0],
);
});

it("should run command in package.json path", async () => {
Expand All @@ -51,8 +72,16 @@ describe("swa build", () => {
appBuildCommand: "npm run something",
});

expect(command.runCommand).toHaveBeenCalledWith("npm install", "app");
expect(command.execInternalCommand).toHaveBeenCalledWith({
executable: "/trusted/bin/npm",
args: ["install"],
cwd: path.resolve("app"),
});
expect(command.runCommand).toHaveBeenCalledTimes(1);
expect(command.runCommand).toHaveBeenCalledWith("npm run something", ".");
expect(vi.mocked(command.execInternalCommand).mock.invocationCallOrder[0]).toBeLessThan(
vi.mocked(command.runCommand).mock.invocationCallOrder[0],
);
});

it("should run api build command", async () => {
Expand All @@ -70,9 +99,16 @@ describe("swa build", () => {

await build({ ...DEFAULT_CONFIG, apiLocation: "api", apiBuildCommand: "npm run something" });

expect(command.runCommand).toBeCalledTimes(2);
expect(command.runCommand).toHaveBeenCalledWith("npm install", "api");
expect(command.execInternalCommand).toHaveBeenCalledWith({
executable: "/trusted/bin/npm",
args: ["install"],
cwd: path.resolve("api"),
});
expect(command.runCommand).toHaveBeenCalledTimes(1);
expect(command.runCommand).toHaveBeenCalledWith("npm run something", "api");
expect(vi.mocked(command.execInternalCommand).mock.invocationCallOrder[0]).toBeLessThan(
vi.mocked(command.runCommand).mock.invocationCallOrder[0],
);
});

it("should run command in package.json path", async () => {
Expand All @@ -86,6 +122,12 @@ describe("swa build", () => {
apiBuildCommand: "npm run something",
});

expect(command.execInternalCommand).toHaveBeenCalledWith({
executable: "/trusted/bin/npm",
args: ["install"],
cwd: path.resolve("api"),
});
expect(command.runCommand).toHaveBeenCalledTimes(1);
expect(command.runCommand).toHaveBeenCalledWith("npm run something", "api");
});

Expand All @@ -109,3 +151,71 @@ describe("swa build", () => {
// expect(execSyncMock).toHaveBeenCalledWith("npm run build --if-present", { cwd: convertToNativePaths("src/node-ts") });
});
});

describe("automatic dependency installs are internal, user build strings are deliberate", () => {
let build: typeof import("./build.js").build;

beforeEach(async () => {
vi.resetModules();
vi.doUnmock("../../../core/utils/command.js");
vi.doMock("../../../core/utils/workflow-config.js", () => ({ readWorkflowFile: () => undefined }));
vi.doMock("../../../core/utils/options.js", () => ({ isUserOrConfigOption: () => false }));
vi.doMock("../../../core/utils/file.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../../../core/utils/file.js")>()),
// Preserve the original finder isolation, independently of command execution.
findUpPackageJsonDir: async () => "/workspace/app",
}));
vi.doMock("../../../core/utils/logger.js", () => ({
logger: { log: vi.fn(), warn: vi.fn(), error: vi.fn(), silly: vi.fn() },
}));
vi.doMock("../../../core/frameworks/detect.js", () => ({ detectProjectFolders: vi.fn(), generateConfiguration: vi.fn() }));
({ build } = await import("./build.js"));
vol.reset();
mockInternalProcesses();
vi.spyOn(process, "cwd").mockReturnValue("/workspace");
vi.stubEnv("PATH", "/trusted/bin");
vol.fromJSON({
"/bin/sh": "inert system-shell fixture; never executed",
"/workspace/app/package.json": "{}",
"/trusted/bin/npm": "inert fixture",
"/trusted/bin/yarn": "inert fixture",
"/trusted/bin/pnpm": "inert fixture",
"/workspace/npm": "inert fixture",
"/workspace/yarn": "inert fixture",
"/workspace/pnpm": "inert fixture",
"/workspace/app/npm": "inert off-PATH fixture",
"/workspace/app/yarn": "inert off-PATH fixture",
"/workspace/app/pnpm": "inert off-PATH fixture",
"/workspace/app/.tools/npm": "inert configured fixture",
"/workspace/app/.tools/yarn": "inert configured fixture",
"/workspace/app/.tools/pnpm": "inert configured fixture",
});
for (const file of Object.keys(vol.toJSON())) fs.chmodSync(file, 0o755);
});

afterEach(() => {
vi.unstubAllEnvs();
});

it.each([
["npm", "package-lock.json"],
["yarn", "yarn.lock"],
["pnpm", "pnpm-lock.yaml"],
])("runs automatic %s install through the caller-selected identity without off-PATH CWD preemption", async (manager, lock) => {
vol.fromJSON({ [`/workspace/app/${lock}`]: "" });
for (const directory of ["/trusted/bin", "/workspace/app/.tools"]) {
vi.stubEnv("PATH", directory === "/trusted/bin" ? directory : `${directory}:/trusted/bin`);
await build({ ...DEFAULT_CONFIG, appLocation: "/workspace/app", outputLocation: ".", appBuildCommand: "npm run build && echo done" });
expect(childProcess.execSync.mock.calls.map(([command]) => command)).not.toContain(`${manager} install`);
const launches = directLaunches().map(({ file, args, options }) => ({ file, args, cwd: options?.cwd }));
expect(launches).toContainEqual({ file: `${directory}/${manager}`, args: ["install"], cwd: "/workspace/app" });
expect(launches.some(({ file }) => file === `/workspace/app/${manager}` || file === `/workspace/${manager}`)).toBe(false);
}
});

it("preserves an explicit user build command as a shell string", async () => {
const command = "npm run build && echo done";
await build({ ...DEFAULT_CONFIG, appLocation: "/workspace/app", outputLocation: ".", appBuildCommand: command });
expect(childProcess.execSync).toHaveBeenCalledWith(command, expect.objectContaining({ cwd: "/workspace/app", shell: "/bin/sh" }));
});
});
6 changes: 4 additions & 2 deletions src/cli/commands/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { findUpPackageJsonDir, pathExists } from "../../../core/utils/file.js";
import { isUserOrConfigOption } from "../../../core/utils/options.js";
import { logger } from "../../../core/utils/logger.js";
import { readWorkflowFile } from "../../../core/utils/workflow-config.js";
import { runCommand } from "../../../core/utils/command.js";
import { execInternalCommand, runCommand } from "../../../core/utils/command.js";
import { swaCliConfigFilename } from "../../../core/utils/cli-config.js";
import { resolveTrustedPackageManager } from "../../../core/utils/executable.js";

export async function build(options: SWACLIConfig) {
const workflowConfig = readWorkflowFile();
Expand Down Expand Up @@ -135,7 +136,8 @@ async function installNpmDependencies(packageJsonPath: string): Promise<void> {
try {
const packageManager = await detectPackageManager(packageJsonPath);
logger.log(`Installing dependencies with "${packageManager} install"...`);
runCommand(`${packageManager} install`, packageJsonPath);
const spec = resolveTrustedPackageManager(packageManager, { cwd: path.resolve(packageJsonPath) });
execInternalCommand({ ...spec, args: [...spec.args, "install"], cwd: path.resolve(packageJsonPath) });
} catch (error) {
logger.error(`Cannot install dependencies:`);
logger.error(error as Error, true);
Expand Down
9 changes: 5 additions & 4 deletions src/cli/commands/db/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export async function init(options: SWACLIConfig) {
if (databaseType === undefined || !isValidDatabaseType(databaseType)) {
logger.error(
`--database-type is a required field. Please provide the type of the database you want to connect (mssql, postgresql, cosmosdb_nosql, mysql, cosmosdb_postgresql).`,
true
true,
);
return;
}
Expand All @@ -44,13 +44,14 @@ export async function init(options: SWACLIConfig) {

logger.log(`Creating ${DATA_API_BUILDER_DEFAULT_CONFIG_FILE_NAME} configuration file`, "swa");

const dataApiBinary = await getDataApiBuilderBinaryPath();
const dataApiBinary = await getDataApiBuilderBinaryPath({ cwd: path.resolve(directory) });
if (!dataApiBinary) {
logger.error(
`Could not find or install ${DATA_API_BUILDER_BINARY_NAME} binary.
If you already have data-api-builder installed, try running "dab init" directly to generate the config file. Exiting!!`,
true
true,
);
return;
}

let args: string[] = [
Expand Down Expand Up @@ -86,7 +87,7 @@ export async function init(options: SWACLIConfig) {
if (!cosmosdb_nosqlDatabase) {
logger.error(
`--cosmosdb_nosql-database is required when database-type is cosmosdb_nosql, ${DATA_API_BUILDER_DEFAULT_CONFIG_FILE_NAME} will not be created`,
true
true,
);
}
// create file staticwebapp.database.schema.json directly if database type cosmosdb_nosql since needed argument
Expand Down
Loading
Loading