Skip to content

Commit

Permalink
fix(plugin-system): Resolved issue with instanciating the `PluginLoad…
Browse files Browse the repository at this point in the history
…er` from the provided module
  • Loading branch information
sullivanpj committed Jan 29, 2024
1 parent 18c1841 commit 778bb6d
Show file tree
Hide file tree
Showing 14 changed files with 957 additions and 428 deletions.
5 changes: 4 additions & 1 deletion docs/api-reports/packages/plugin-system/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,10 @@ export { PluginManager as PluginManager_alias_2 }
// @public (undocumented)
interface PluginManagerOptions {
autoInstall: boolean;
defaultLoader: string;
defaultLoader: string | {
provider: string;
loader: new (_rootPath?: string, _tsconfig?: string, _autoInstall?: boolean) => IPluginLoader<any, any>;
};
discoveryMode: PluginDiscoveryMode;
rootPath: string;
tsconfig?: string;
Expand Down
15 changes: 12 additions & 3 deletions docs/api-reports/packages/plugin-system/documents-model.api.json
Original file line number Diff line number Diff line change
Expand Up @@ -2741,15 +2741,24 @@
{
"kind": "PropertySignature",
"canonicalReference": "@storm-stack/plugin-system!PluginManagerOptions#defaultLoader:member",
"docComment": "/**\n * The path to the plugin's module loader.\n */\n",
"docComment": "/**\n * The path to the plugin's module loader or an object containing the provider string and loader instance.\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "defaultLoader: "
},
{
"kind": "Content",
"text": "string"
"text": "string | {\n provider: string;\n loader: new (_rootPath?: string, _tsconfig?: string, _autoInstall?: boolean) => "
},
{
"kind": "Reference",
"text": "IPluginLoader",
"canonicalReference": "@storm-stack/plugin-system!IPluginLoader:interface"
},
{
"kind": "Content",
"text": "<any, any>;\n }"
},
{
"kind": "Content",
Expand All @@ -2762,7 +2771,7 @@
"name": "defaultLoader",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
"endIndex": 4
}
},
{
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"chalk": "5.3.0",
"commander": "11.1.0",
"console-table-printer": "2.11.2",
"figlet": "^1.7.0"
"figlet": "^1.7.0",
"prompts": "^2.4.2",
"terminal-link": "^3.0.0"
},
"devDependencies": {
"@types/figlet": "^1.5.8",
Expand Down
175 changes: 175 additions & 0 deletions packages/cli/src/program/error-report.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import archiver from "archiver";
import * as checkpoint from "checkpoint-client";
import globby from "globby";
import stripAnsi from "strip-ansi";
import tmp from "tmp";
import { P, match } from "ts-pattern";

import {
type CreateErrorReportInput,
createErrorReport,
makeErrorReportCompleted,
uploadZip
} from "./errorReporting";
import type { MigrateTypes } from "./migrateTypes";
import type { RustPanic } from "./panic";
import { ErrorArea } from "./panic";
import { mapScalarValues, maskSchema } from "./utils/maskSchema";

// cleanup the temporary files even when an uncaught exception occurs
tmp.setGracefulCleanup();

type SendPanic = {
error: RustPanic;
cliVersion: string;
enginesVersion: string;

// retrieve the database version for the given schema or url, without throwing any error
getDatabaseVersionSafe: (
args: MigrateTypes.GetDatabaseVersionParams
) => Promise<string | undefined>;
};
export async function sendPanic({
error,
cliVersion,
enginesVersion,
getDatabaseVersionSafe
}: SendPanic): Promise<number> {
const schema: string | undefined = match(error)
.with({ schemaPath: P.not(P.nullish) }, (err) => {
return fs.readFileSync(err.schemaPath, "utf-8");
})
.with({ schema: P.not(P.nullish) }, (err) => err.schema)
.otherwise(() => undefined);

const maskedSchema: string | undefined = schema ? maskSchema(schema) : undefined;

let dbVersion: string | undefined;
if (error.area === ErrorArea.LIFT_CLI) {
// For a SQLite datasource like `url = "file:dev.db"` only schema will be defined
const getDatabaseVersionParams: MigrateTypes.GetDatabaseVersionParams | undefined = match({
schema,
introspectionUrl: error.introspectionUrl
})
.with({ schema: P.not(undefined) }, ({ schema }) => {
return {
datasource: {
tag: "SchemaString",
schema
}
} as const;
})
.with({ introspectionUrl: P.not(undefined) }, ({ introspectionUrl }) => {
return {
datasource: {
tag: "ConnectionString",
url: introspectionUrl
}
} as const;
})
.otherwise(() => undefined);

dbVersion = await getDatabaseVersionSafe(getDatabaseVersionParams);
}

const migrateRequest = error.request
? JSON.stringify(
mapScalarValues(error.request, (value) => {
if (typeof value === "string") {
return maskSchema(value);
}
return value;
})
)
: undefined;

const params: CreateErrorReportInput = {
area: error.area,
kind: "PANIC",
cliVersion,
binaryVersion: enginesVersion,
command: process.argv.slice(2).join(" "),
jsStackTrace: stripAnsi(error.stack || error.message),
rustStackTrace: error.rustStack,
operatingSystem: `${os.arch()} ${os.platform()} ${os.release()}`,
platform: process.platform,
liftRequest: migrateRequest,
schemaFile: maskedSchema,
fingerprint: await checkpoint.getSignature(),
sqlDump: undefined,
dbVersion: dbVersion
};

// Get an AWS S3 signed URL from the server, so we can upload a zip file
const signedUrl = await createErrorReport(params);

// Create & upload the zip file
// only log if something fails
try {
if (error.schemaPath) {
const zip = await makeErrorZip(error);
await uploadZip(zip, signedUrl);
}
} catch (zipUploadError) {
console.error(`Error uploading zip file: ${zipUploadError.message}`);
}

// Mark the error report as completed
const id = await makeErrorReportCompleted(signedUrl);

return id;
}

async function makeErrorZip(error: RustPanic): Promise<Buffer> {
if (!error.schemaPath) {
throw new Error(`Can't make zip without schema path`);
}
const schemaDir = path.dirname(error.schemaPath);
const tmpFileObj = tmp.fileSync();
const outputFile = fs.createWriteStream(tmpFileObj.name);
const zip = archiver("zip", { zlib: { level: 9 } });

zip.pipe(outputFile);

// add schema file
// Note: the following reads `error.schemaPath` for the second time, we could just re-use
// `maskedSchema` from the `sendPanic` function's scope.
const schemaFile = maskSchema(fs.readFileSync(error.schemaPath, "utf-8"));
zip.append(schemaFile, { name: path.basename(error.schemaPath) });

if (fs.existsSync(schemaDir)) {
const filePaths = await globby("migrations/**/*", {
// globby doesn't have it in its types but it's part of mrmlnc/fast-glob
// @ts-ignore
cwd: schemaDir
});

for (const filePath of filePaths) {
let file = fs.readFileSync(path.resolve(schemaDir, filePath), "utf-8");
if (
filePath.endsWith("schema.prisma") ||
filePath.endsWith(path.basename(error.schemaPath))
) {
// Remove credentials from schema datasource url
file = maskSchema(file);
}
zip.append(file, { name: path.basename(filePath) });
}
}

zip.finalize();

return new Promise((resolve, reject) => {
outputFile.on("close", () => {
const buffer = fs.readFileSync(tmpFileObj.name);
resolve(buffer);
});

zip.on("error", (err) => {
reject(err);
});
});
}
22 changes: 11 additions & 11 deletions packages/cli/src/program/shutdown.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { StormLog } from "@storm-stack/logging";
import { MaybePromise } from "@storm-stack/utilities";
import type { StormLog } from "@storm-stack/logging";
import type { MaybePromise } from "@storm-stack/utilities";

const errorTypes = ["unhandledRejection", "uncaughtException"];
const signalTraps = ["SIGTERM", "SIGINT", "SIGUSR2"];
Expand All @@ -19,31 +19,31 @@ export function registerShutdown(config: {
await config.onShutdown();
}

errorTypes.map(type => {
process.on(type, async e => {
errorTypes.map((type) => {
process.on(type, async (e) => {
try {
config.logger.info(`process.on ${type}`);
config.logger.error(e);
await shutdown();
config.logger.info(`shutdown process done, exiting with code 0`);
config.logger.info("Shutdown process complete, exiting with code 0");
process.exit(0);
} catch (e) {
config.logger.warn(`shutdown process failed, exiting with code 1`);
config.logger.warn("Shutdown process failed, exiting with code 1");
config.logger.error(e);
process.exit(1);
}
});
});

signalTraps.map(type => {
signalTraps.map((type) => {
process.once(type, async () => {
try {
config.logger.info(`process.on ${type}`);
await shutdown();
config.logger.info(`shutdown process done, exiting with code 0`);
config.logger.info("Shutdown process complete, exiting with code 0");
process.exit(0);
} catch (e) {
config.logger.warn(`shutdown process failed, exiting with code 1`);
config.logger.warn("Shutdown process failed, exiting with code 1");
config.logger.error(e);
process.exit(1);
}
Expand All @@ -54,10 +54,10 @@ export function registerShutdown(config: {
try {
config.logger.info(`Manual shutdown ${reason ? `(${reason})` : ""}`);
await shutdown();
config.logger.info(`shutdown process done, exiting with code 0`);
config.logger.info("Shutdown process complete, exiting with code 0");
process.exit(0);
} catch (e) {
config.logger.warn(`shutdown process failed, exiting with code 1`);
config.logger.warn("Shutdown process failed, exiting with code 1");
config.logger.error(e);
process.exit(1);
}
Expand Down
14 changes: 14 additions & 0 deletions packages/cli/src/utilities/cli-link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import chalk from "chalk";
import terminalLink from "terminal-link";

/**
* Create a link to a URL in the terminal.
*
* @param url - The URL to link to.
* @returns A terminal link
*/
export function link(url: string): string {
return terminalLink(url, url, {
fallback: (url) => chalk.underline(url)
});
}
3 changes: 3 additions & 0 deletions packages/cli/src/utilities/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export * from "./create-cli-options";
export * from "./execute";
export * from "./cli-link";
export * from "./is-ci";
export * from "./is-interactive";
77 changes: 77 additions & 0 deletions packages/cli/src/utilities/is-ci.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Returns true if the current environment is a CI environment.
*
* @returns True if the current environment is a CI environment.
*/
export const isCI = (): boolean => {
const env = process.env;

// From https://github.com/watson/ci-info/blob/44e98cebcdf4403f162195fbcf90b1f69fc6e047/index.js#L54-L61
// Evaluating at runtime makes it possible to change the values in our tests
// This list is probably not exhaustive though `process.env.CI` should be enough
// but since we were using this utility in the past, we want to keep the same behavior
return !!(
env.STORM_CI || // Custom CI
env.CI || // Travis CI, CircleCI, Cirrus CI, GitLab CI, Appveyor, CodeShip, dsari
env.CONTINUOUS_INTEGRATION || // Travis CI, Cirrus CI
env.BUILD_NUMBER || // Jenkins, TeamCity
env.RUN_ID || // TaskCluster, dsari
// From `env` from v4.0.0 https://github.com/watson/ci-info/blob/3e1488e98680f1f776785fe8708a157b7f00e568/vendors.json
env.AGOLA_GIT_REF ||
env.AC_APPCIRCLE ||
env.APPVEYOR ||
env.CODEBUILD ||
env.TF_BUILD ||
env.bamboo_planKey ||
env.BITBUCKET_COMMIT ||
env.BITRISE_IO ||
env.BUDDY_WORKSPACE_ID ||
env.BUILDKITE ||
env.CIRCLECI ||
env.CIRRUS_CI ||
env.CF_BUILD_ID ||
env.CM_BUILD_ID ||
env.CI_NAME ||
env.DRONE ||
env.DSARI ||
env.EARTHLY_CI ||
env.EAS_BUILD ||
env.GERRIT_PROJECT ||
env.GITEA_ACTIONS ||
env.GITHUB_ACTIONS ||
env.GITLAB_CI ||
env.GOCD ||
env.BUILDER_OUTPUT ||
env.HARNESS_BUILD_ID ||
env.JENKINS_URL ||
env.BUILD_ID ||
env.LAYERCI ||
env.MAGNUM ||
env.NETLIFY ||
env.NEVERCODE ||
env.PROW_JOB_ID ||
env.RELEASE_BUILD_ID ||
env.RENDER ||
env.SAILCI ||
env.HUDSON ||
env.JENKINS_URL ||
env.BUILD_ID ||
env.SCREWDRIVER ||
env.SEMAPHORE ||
env.SOURCEHUT ||
env.STRIDER ||
env.TASK_ID ||
env.RUN_ID ||
env.TEAMCITY_VERSION ||
env.TRAVIS ||
env.VELA ||
env.NOW_BUILDER ||
// See https://github.com/prisma/prisma/issues/22380 for why we commented it out
// Users deploying on Vercel might have this env var set in the local dev env
// env.VERCEL ||
env.APPCENTER_BUILD_ID ||
env.CI_XCODE_PROJECT ||
env.XCS ||
false
);
};

0 comments on commit 778bb6d

Please sign in to comment.