diff --git a/packages/@apphosting/adapter-angular/package.json b/packages/@apphosting/adapter-angular/package.json index 2cdd1212..9374225b 100644 --- a/packages/@apphosting/adapter-angular/package.json +++ b/packages/@apphosting/adapter-angular/package.json @@ -23,7 +23,7 @@ "scripts": { "build": "rm -rf dist && tsc && chmod +x ./dist/bin/*", "test": "npm run test:unit && npm run test:functional", - "test:unit": "ts-mocha -p tsconfig.json src/**/*.spec.ts", + "test:unit": "ts-mocha -p tsconfig.json 'src/**/*.spec.ts' 'src/*.spec.ts'", "test:functional": "node --loader ts-node/esm ./e2e/run-local.ts", "localregistry:start": "npx verdaccio --config ../publish-dev/verdaccio-config.yaml", "localregistry:publish": "(npm view --registry=http://localhost:4873 @apphosting/adapter-angular && npm unpublish --@apphosting:registry=http://localhost:4873 --force); npm publish --@apphosting:registry=http://localhost:4873" diff --git a/packages/@apphosting/adapter-angular/src/bin/build.spec.ts b/packages/@apphosting/adapter-angular/src/bin/build.spec.ts index 84e8a3a5..3156a59b 100644 --- a/packages/@apphosting/adapter-angular/src/bin/build.spec.ts +++ b/packages/@apphosting/adapter-angular/src/bin/build.spec.ts @@ -15,6 +15,7 @@ describe("build commands", () => { outputBundleOptions = { browserDirectory: resolve(tmpDir, "dist", "test", "browser"), bundleYamlPath: resolve(tmpDir, ".apphosting", "bundle.yaml"), + outputDirectoryBasePath: resolve(tmpDir, ".apphosting"), serverFilePath: resolve(tmpDir, "dist", "test", "server", "server.mjs"), needsServerGenerated: false, }; @@ -82,6 +83,7 @@ metadata: const expectedOutputBundleOptions = { browserDirectory: "/browser", bundleYamlPath: resolve(".apphosting", "bundle.yaml"), + outputDirectoryBasePath: resolve(".apphosting"), needsServerGenerated: false, serverFilePath: path.join("/server", "server.mjs"), }; diff --git a/packages/@apphosting/adapter-angular/src/bin/build.ts b/packages/@apphosting/adapter-angular/src/bin/build.ts index 1a7b756e..0776a464 100644 --- a/packages/@apphosting/adapter-angular/src/bin/build.ts +++ b/packages/@apphosting/adapter-angular/src/bin/build.ts @@ -4,7 +4,7 @@ import { checkBuildConditions, validateOutputDirectory, parseOutputBundleOptions, - outputBundleExists, + metaFrameworkOutputBundleExists, } from "../utils.js"; import { getBuildOptions, runBuild } from "@apphosting/common"; @@ -21,7 +21,11 @@ if (!output) { } const angularVersion = process.env.FRAMEWORK_VERSION || "unspecified"; -if (!outputBundleExists()) { +// Frameworks like nitro, analog, nuxt generate the output bundle during their own build process +// when `npm run build` is called which we don't want to overwrite immediately after. +// We only want to overwrite if the existing output is from a previous framework adapter +// build on a plain angular app. +if (!metaFrameworkOutputBundleExists()) { const outputBundleOptions = parseOutputBundleOptions(output); const root = process.cwd(); await generateBuildOutput(root, outputBundleOptions, angularVersion); diff --git a/packages/@apphosting/adapter-angular/src/interface.ts b/packages/@apphosting/adapter-angular/src/interface.ts index 355a6797..22f167b9 100644 --- a/packages/@apphosting/adapter-angular/src/interface.ts +++ b/packages/@apphosting/adapter-angular/src/interface.ts @@ -4,6 +4,7 @@ import { URL } from "node:url"; // options to help generate output directory export interface OutputBundleOptions { bundleYamlPath: string; + outputDirectoryBasePath: string; serverFilePath: string; browserDirectory: string; needsServerGenerated: boolean; diff --git a/packages/@apphosting/adapter-angular/src/utils.spec.ts b/packages/@apphosting/adapter-angular/src/utils.spec.ts new file mode 100644 index 00000000..d3a88192 --- /dev/null +++ b/packages/@apphosting/adapter-angular/src/utils.spec.ts @@ -0,0 +1,69 @@ +const importUtils = import("@apphosting/adapter-angular/dist/utils.js"); +import assert from "assert"; +import fs from "fs"; +import * as path from "path"; +import { stringify as yamlStringify } from "yaml"; +import os from "os"; +import type { OutputBundleConfig } from "@apphosting/common"; + +function generateTmpDir(): string { + return fs.mkdtempSync(path.join(os.tmpdir(), "test-files")); +} + +describe("metaFrameworkOutputBundleExists", () => { + let bundlePath: string; + const originalCwd = process.cwd.bind(process); + + beforeEach(() => { + const tmpDir = generateTmpDir(); + process.cwd = () => tmpDir; + fs.mkdirSync(path.resolve(tmpDir, ".apphosting")); + bundlePath = path.resolve(tmpDir, ".apphosting", "bundle.yaml"); + }); + + afterEach(() => { + process.cwd = originalCwd; + }); + + it("unrecognized bundle", async () => { + const { metaFrameworkOutputBundleExists } = await importUtils; + const content = "chicken: bok bok"; + fs.writeFileSync(bundlePath, yamlStringify(content)); + assert(!metaFrameworkOutputBundleExists()); + }); + + it("no bundle exists", async () => { + const { metaFrameworkOutputBundleExists } = await importUtils; + assert(!metaFrameworkOutputBundleExists()); + }); + + it("meta-framework bundle exists", async () => { + const { metaFrameworkOutputBundleExists } = await importUtils; + const outputBundle: OutputBundleConfig = { + version: "v1", + runConfig: { + runCommand: `does not matter`, + }, + metadata: { + framework: "nitro", + }, + }; + fs.writeFileSync(bundlePath, yamlStringify(outputBundle)); + assert(metaFrameworkOutputBundleExists()); + }); + + it("angular bundle exists", async () => { + const { metaFrameworkOutputBundleExists } = await importUtils; + const outputBundle: OutputBundleConfig = { + version: "v1", + runConfig: { + runCommand: `does not matter`, + }, + metadata: { + framework: "angular", + }, + }; + fs.writeFileSync(bundlePath, yamlStringify(outputBundle)); + assert(!metaFrameworkOutputBundleExists()); + }); +}); diff --git a/packages/@apphosting/adapter-angular/src/utils.ts b/packages/@apphosting/adapter-angular/src/utils.ts index e5be7de1..6ce3bbd1 100644 --- a/packages/@apphosting/adapter-angular/src/utils.ts +++ b/packages/@apphosting/adapter-angular/src/utils.ts @@ -7,6 +7,7 @@ import { resolve, normalize, relative, dirname, join } from "path"; import { stringify as yamlStringify } from "yaml"; import { OutputBundleOptions, OutputPaths, buildManifestSchema } from "./interface.js"; import { createRequire } from "node:module"; +import { parse as parseYaml } from "yaml"; import stripAnsi from "strip-ansi"; import { BuildOptions, @@ -14,10 +15,12 @@ import { EnvVarConfig, Metadata, Availability, + updateOrCreateGitignore, } from "@apphosting/common"; // fs-extra is CJS, readJson can't be imported using shorthand -export const { writeFile, move, readJson, mkdir, copyFile, readFileSync, existsSync } = fsExtra; +export const { writeFile, move, readJson, mkdir, copyFile, readFileSync, existsSync, ensureDir } = + fsExtra; const require = createRequire(import.meta.url); const __filename = fileURLToPath(import.meta.url); @@ -135,6 +138,7 @@ export function populateOutputBundleOptions(outputPaths: OutputPaths): OutputBun } return { bundleYamlPath: resolve(outputBundleDir, "bundle.yaml"), + outputDirectoryBasePath: outputBundleDir, serverFilePath: resolve(baseDirectory, serverRelativePath, "server.mjs"), browserDirectory: resolve(baseDirectory, browserRelativePath), needsServerGenerated, @@ -210,6 +214,10 @@ export async function generateBuildOutput( await generateServer(outputBundleOptions); } await generateBundleYaml(outputBundleOptions, cwd, angularVersion); + // generateBundleYaml creates the output directory (if it does not already exist). + // We need to make sure it is gitignored. + const normalizedBundleDir = normalize(relative(cwd, outputBundleOptions.outputDirectoryBasePath)); + updateOrCreateGitignore(cwd, [`/${normalizedBundleDir}/`]); } // add environment variable to bundle.yaml if needed for specific versions @@ -233,7 +241,7 @@ async function generateBundleYaml( cwd: string, angularVersion: string, ): Promise { - await mkdir(dirname(opts.bundleYamlPath)); + await ensureDir(dirname(opts.bundleYamlPath)); const outputBundle: OutputBundleConfig = { version: "v1", runConfig: { @@ -270,10 +278,18 @@ export const isMain = (meta: ImportMeta) => { return process.argv[1] === fileURLToPath(meta.url); }; -export const outputBundleExists = () => { +export const metaFrameworkOutputBundleExists = () => { const outputBundleDir = resolve(".apphosting"); - if (existsSync(outputBundleDir)) { - return true; + const bundleYamlPath = join(outputBundleDir, "bundle.yaml"); + if (existsSync(bundleYamlPath)) { + try { + const bundle = parseYaml(readFileSync(bundleYamlPath, "utf8")); + if (bundle?.metadata?.framework && bundle.metadata.framework !== "angular") { + return true; + } + } catch (e) { + logger.debug("Failed to parse bundle.yaml, assuming it can be overwritten", e); + } } return false; };