Skip to content

Commit

Permalink
More
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesdaniels committed Dec 14, 2023
1 parent 358f20f commit bfcde4e
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 249 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 9 additions & 8 deletions packages/@apphosting/adapter-angular/src/bin/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
import { spawn } from "child_process";
import { loadConfig } from "../utils.js";

const build = (cwd=process.cwd()) => new Promise<void>((resolve, reject) => {
const build = (cwd = process.cwd()) =>
new Promise<void>((resolve, reject) => {
// TODO warn if the build script contains anything other than `ng build`
const process = spawn("npm", ["run", "build"], { cwd, shell: true, stdio: "pipe" });
process.stdout.on('data', (it: Buffer) => console.log(it.toString().trim()));
process.stderr.on('data', (it: Buffer) => console.error(it.toString().trim()));
process.on("exit", code => {
if (code === 0) return resolve();
reject();
})
});
process.stdout.on("data", (it: Buffer) => console.log(it.toString().trim()));
process.stderr.on("data", (it: Buffer) => console.error(it.toString().trim()));
process.on("exit", (code) => {
if (code === 0) return resolve();
reject();
});
});

const config = await loadConfig(process.cwd());

Expand Down
84 changes: 45 additions & 39 deletions packages/@apphosting/adapter-angular/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,52 @@ import { fileURLToPath } from "url";
export const { readJson } = fsExtra;

export async function loadConfig(cwd: string) {
// dynamically load NextJS so this can be used in an NPX context
const { NodeJsAsyncHost }: typeof import("@angular-devkit/core/node") = await import(`${cwd}/node_modules/@angular-devkit/core/node/index.js`);
const { workspaces }: typeof import("@angular-devkit/core") = await import(`${cwd}/node_modules/@angular-devkit/core/src/index.js`);
const { WorkspaceNodeModulesArchitectHost }: typeof import("@angular-devkit/architect/node") = await import(`${cwd}/node_modules/@angular-devkit/architect/node/index.js`);

const host = workspaces.createWorkspaceHost(new NodeJsAsyncHost());
const { workspace } = await workspaces.readWorkspace(cwd, host);
const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, cwd);

const apps: string[] = [];
workspace.projects.forEach((value, key) => {
if (value.extensions.projectType === "application") apps.push(key);
});
if (apps.length !== 1) throw new Error("asdf");

const project = apps[0];
if (!project) throw new Error("Unable to determine the application to deploy");

const workspaceProject = workspace.projects.get(project);
if (!workspaceProject) throw new Error(`No project ${project} found.`);

const target = "build";
if (!workspaceProject.targets.has(target)) throw new Error("yada");

const { builder, defaultConfiguration: configuration = "production" } = workspaceProject.targets.get(target)!;
if (builder !== "@angular-devkit/build-angular:application") throw new Error("foo");

const buildTarget = {
project,
target,
configuration,
};

const options = await architectHost.getOptionsForTarget(buildTarget);
if (!options) throw new Error('yada');
return options;
// dynamically load NextJS so this can be used in an NPX context
const { NodeJsAsyncHost }: typeof import("@angular-devkit/core/node") = await import(
`${cwd}/node_modules/@angular-devkit/core/node/index.js`
);
const { workspaces }: typeof import("@angular-devkit/core") = await import(
`${cwd}/node_modules/@angular-devkit/core/src/index.js`
);
const { WorkspaceNodeModulesArchitectHost }: typeof import("@angular-devkit/architect/node") =
await import(`${cwd}/node_modules/@angular-devkit/architect/node/index.js`);

const host = workspaces.createWorkspaceHost(new NodeJsAsyncHost());
const { workspace } = await workspaces.readWorkspace(cwd, host);
const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, cwd);

const apps: string[] = [];
workspace.projects.forEach((value, key) => {
if (value.extensions.projectType === "application") apps.push(key);
});
const project = apps[0];
if (apps.length > 1 || !project) throw new Error("Unable to determine the application to deploy");

const workspaceProject = workspace.projects.get(project);
if (!workspaceProject) throw new Error(`No project ${project} found.`);

const target = "build";
if (!workspaceProject.targets.has(target)) throw new Error("Could not find build target.");

const { builder, defaultConfiguration: configuration = "production" } =
workspaceProject.targets.get(target)!;
if (builder !== "@angular-devkit/build-angular:application") {
throw new Error("Only the Angular application builder is supported.");
}

const buildTarget = {
project,
target,
configuration,
};

const options = await architectHost.getOptionsForTarget(buildTarget);
if (!options) throw new Error("Not able to find options for build target.");
return options;
}

export const isMain = (meta: ImportMeta) => {
if (!meta) return false;
if (!process.argv[1]) return false;
return process.argv[1] === fileURLToPath(meta.url);
if (!meta) return false;
if (!process.argv[1]) return false;
return process.argv[1] === fileURLToPath(meta.url);
};
3 changes: 2 additions & 1 deletion packages/@apphosting/build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"colorette": "^2.0.20",
"commander": "^11.1.0",
"npm-pick-manifest": "^9.0.0",
"ts-node": "^10.9.1"
"ts-node": "^10.9.1",
"@apphosting/discover": "*"
},
"devDependencies": {
"@types/commander": "*",
Expand Down
31 changes: 6 additions & 25 deletions packages/@apphosting/build/src/bin/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,16 @@ import { parse as semverParse } from "semver";
import { yellow, bgRed, bold } from "colorette";
// @ts-expect-error TODO add interface
import pickManifest from "npm-pick-manifest";

import type { SpawnOptionsWithoutStdio } from "child_process";

const spawnPromise = (command: string, args: readonly string[], options?: SpawnOptionsWithoutStdio) => new Promise<Buffer>((resolve, reject) => {
const process = spawn(command, args, options);
const buffers: Buffer[] = [];
process.stdout.on('data', (it: Buffer) => buffers.push(it));
process.stderr.on('data', (it: Buffer) => console.error(it.toString().trim()));
process.on("exit", code => {
if (code === 0) return resolve(Buffer.concat(buffers));
reject();
});
});
import { discover } from "@apphosting/discover";

program
.option('--framework <string>')
.argument('<directory>', "path to the project's root directory")
.action(async (cwd, options: { framework?: string, permitPrerelease?: boolean }) => {
.option("--framework <string>")
.argument("<directory>", "path to the project's root directory")
.action(async (cwd, options: { framework?: string; permitPrerelease?: boolean }) => {
const { framework: expectedFramework } = options;

// TODO look at sharing code with the discovery module, rather than npx
const discoveryReturnValue = await spawnPromise(
"npx",
["-y", "-p", "@apphosting/discover", "discover", cwd],
{ shell: true },
);
// TODO type
const discoveryResults = JSON.parse(discoveryReturnValue.toString());
const nonBundledFrameworks = discoveryResults.discovered.filter((it: any) => !it.bundledWith);
const discoveryResults = await discover(cwd);
const nonBundledFrameworks = discoveryResults.filter((it) => !it.bundledWith);
if (nonBundledFrameworks.length === 0) throw new Error("Did not discover any frameworks.");
if (nonBundledFrameworks.length > 1) throw new Error("Found conflicting frameworks.");
if (expectedFramework && nonBundledFrameworks[0].framework !== expectedFramework) {
Expand Down
185 changes: 11 additions & 174 deletions packages/@apphosting/discover/src/bin/discover.ts
Original file line number Diff line number Diff line change
@@ -1,181 +1,18 @@
#! /usr/bin/env node
import { program } from "commander";
import fsExtra from "fs-extra";
import YarnLockfile from '@yarnpkg/lockfile';
import { parse as parseYaml } from "yaml";
import { performance } from "node:perf_hooks";
import * as toml from "toml";

export const PLATFORMS = [
// [id, files[], defaultPackageManger, packageManagers[], frameworks[]]
['nodejs', ['package.json'], 'npm', [
// [id, lockfiles[]]
['npm', ['npm-shrinkwrap.json', 'package-lock.json']],
['yarn', ['yarn.lock']],
['pnpm', ['pnpm-lock.yaml']],
], [
// [id, deps[], files[], bundles[]]
["nextjs", ["next"], [], ["react"]],
["angular", ["@angular/core"], ["angular.json"], ["vite"]],
["astro", ["astro"], ["astro.config.js", "astro.config.mjs", "astro.config.cjs", "astro.config.ts"], ["lit", "react", "preact", "svelte", "vue", "vite"]],
["nuxt", ["nuxt"], ["nuxt.config.js"], ["vue"]],
["lit", ["lit", "lit-element"], [], []],
["vue", ["vue"], [], []],
["vite", ["vite"], [], ["vue", "react", "preact", "lit", "svelte"]],
["preact", ["preact"], [], []],
["react", ["react", "react-dom"], [], []],
["svelte", ["svelte"], [], []],
["sveltekit", ["@sveltejs/kit"], [], ["svelte", "vite"]]
]],
['python', [], 'pip', [
// [id, lockfiles[]]
['pip', []],
['pipenv', ["Pipfile.lock"]],
['poetry', ['poetry.lock']]
], [
// [id, deps[], files[], bundles[]]
["flask", ["flask"], [], []],
["django", ["django"], [], []],
]]
] as const;

type DiscoveredFramework = {
framework: typeof PLATFORMS[number][4][number][0],
version: string,
packageManager: typeof PLATFORMS[number][3][number][0],
platform: typeof PLATFORMS[number][0],
bundledWith?: Array<typeof PLATFORMS[number][4][number][0]>
};
import { discover } from "../index.js";

program
.option('--github-token <string>')
.option('--github-repo <string>')
.argument('<directory>', "path to the project's root directory")
.action(async (path, { githubRepo, githubToken }: { githubRepo?: string, githubToken?: string }) => {
if (githubRepo && !githubToken) throw new Error('needs token');

const { join } = await (githubRepo ? import('node:path') : import('node:path/posix'));

const { readFile, pathExists, readJson } = githubRepo ? {
readFile: async function(path: string) {
const response = await fetch(`https://api.github.com/repos/${githubRepo}/contents/${path}`, {
headers: { authorization: `Bearer ${githubToken}`, accept: "application/vnd.github.raw" },
});
if (!response.ok) throw new Error('fail.');
return Buffer.from(await response.text());
},
pathExists: async function(path: string) {
const response = await fetch(`https://api.github.com/repos/${githubRepo}/contents/${path}`, {
method: "HEAD",
headers: { authorization: `Bearer ${githubToken}`, accept: "application/vnd.github.raw" },
});
return response.ok;
},
readJson: async function(path: string) {
const response = await fetch(`https://api.github.com/repos/${githubRepo}/contents/${path}`, {
headers: { authorization: `Bearer ${githubToken}`, accept: "application/vnd.github.raw" },
});
if (!response.ok) throw new Error('fail.');
return await response.json();
},
} : fsExtra;

const discoveredFrameworks: Array<DiscoveredFramework> = [];

await Promise.all(PLATFORMS.map(async ([platform, files, defaultPackageManager, packageManagers, frameworkDefinitions]) => {
const filesExist = await Promise.all(files.map(it => pathExists(join(path, it))));
if (files.length && !filesExist.some(it => it)) return;
const discoverFrameworks = (fallback=false) => {
return async ([packageManager, possibleLockfiles]: typeof packageManagers[number]) => {
const possibleLockfilesExist = await Promise.all(possibleLockfiles.map(it => pathExists(join(path, it))));
const [lockfile] = possibleLockfilesExist.map((exists, index) => exists ? possibleLockfiles[index] : undefined).filter(it => !!it);
if (!lockfile && !fallback) return false;

let packages = new Map<string,string>();
if (platform === "nodejs") {
// TODO support npm-shrinkwrap.json
// TODO handle workspaces
if (lockfile === "package-lock.json" || lockfile === "npm-shrinkwrap.json") {
const packageJSON = await readJson(join(path, lockfile));
packages = new Map(Object.keys(packageJSON.packages).map(pkg => {
const name = pkg.replace(/^node_modules\//, "");
const version: string = packageJSON.packages[pkg].version;
return [name, version];
}));
} else if (lockfile === "yarn.lock") {
const file = await readFile(join(path, lockfile));
const yarnLock = YarnLockfile.parse(file.toString());
if (yarnLock.type !== "success") throw new Error(`unable to read ${lockfile}`);
packages = new Map(Object.keys(yarnLock.object).map(pkg => {
const parts = pkg.split("@");
const version = parts.pop()!;
return [parts.join("@"), version];
}));
} else if (lockfile === "pnpm-lock.yaml") {
const file = await readFile(join(path, lockfile));
const pnpmLock = parseYaml(file.toString());
packages = new Map(Object.keys(pnpmLock.packages).map(pkg => {
const parts = pkg.replace(/^\//, "").split("(")[0].split("@");
const version = parts.pop()!;
return [parts.join("@"), version];
}));
}
} else if (platform === "python") {
if (packageManager === "pip") {
const requirementsFile = "requirements.txt";
const requirementsFileExists = await pathExists(join(path, requirementsFile));
if (!requirementsFileExists) return false;
const file = await readFile(join(path, requirementsFile));
packages = new Map(file.toString().split("\n").map(it => {
return [it.trim().replace("-", "_").toLowerCase(), "*"];
}));
} else if (lockfile === "Pipfile.lock") {
const pipfileLock = await readJson(join(path, lockfile));
// TODO include develop too?
packages = new Map(Object.keys(pipfileLock.default).map(name => {
// TODO convert to Node semver?
const version = pipfileLock.default[name].version.split('==')[1];
return [name, version];
}));
} else if (lockfile === "poetry.lock") {
const poetryLock = await readFile(join(path, lockfile));
packages = new Map(toml.parse(poetryLock.toString()).package?.map((it: any) => [it.name, it.version]));
}
}

for (const [framework, requiredPackages, requiredFiles=[] ] of frameworkDefinitions) {
const requiredPackagePresent = requiredPackages.some(it => packages.has(it));
if (!requiredPackagePresent) continue;
const requiredFileExist = requiredFiles.length === 0 || (await Promise.all(requiredFiles.map(it => pathExists(join(path, it))))).some(it => it);
if (!requiredFileExist) continue;
const [packageName] = requiredPackages;
if (packageName) discoveredFrameworks.push({framework, version: packages.get(packageName)!, packageManager, platform });
};

return !!lockfile;
}
};
const packageManagerResults = await Promise.all(packageManagers.map(discoverFrameworks(false)));
if (!packageManagerResults.some(it => it)) {
const fallback = packageManagers.find(([id]) => id === defaultPackageManager);
if (fallback) await discoverFrameworks(true)(fallback);
}
}));

for (const { framework, platform } of discoveredFrameworks) {
const [,,,,defitions] = PLATFORMS.find(([id]) => id === platform)!;
const [,,,bundles] = defitions.find(([id]) => id === framework)!;
for (const bundle of bundles) {
const discovery = discoveredFrameworks.find(({framework}) => framework === bundle);
if (discovery) {
discovery.bundledWith ||= [];
discovery.bundledWith.push(framework);
}
}
}

process.stdout.write(Buffer.from(JSON.stringify({ discovered: discoveredFrameworks }, undefined, 2)));
process.stderr.write(`\nDone in ${performance.now()}ms`);
});
.option("--github-token <string>")
.option("--github-repo <string>")
.argument("<directory>", "path to the project's root directory")
.action(
async (path, { githubRepo, githubToken }: { githubRepo?: string; githubToken?: string }) => {
const discoveredFrameworks = await discover(path, githubRepo, githubToken);
process.stdout.write(JSON.stringify({ discovered: discoveredFrameworks }, undefined, 2));
process.stderr.write(`\nDone in ${performance.now()}ms`);
},
);

program.parse();
Loading

0 comments on commit bfcde4e

Please sign in to comment.