Skip to content

Commit

Permalink
check and prompt update
Browse files Browse the repository at this point in the history
  • Loading branch information
emily-shen committed Feb 20, 2025
1 parent 9744261 commit 2ef9aa4
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 99 deletions.
52 changes: 33 additions & 19 deletions packages/wrangler/e2e/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ describe("types", () => {
.split("\n");

expect(file[0]).toMatchInlineSnapshot(
`"// Generated by Wrangler by running \`wrangler types ./types.d.ts\`"`
`"// Generated by Wrangler by running \`wrangler types ./types.d.ts\` (hash: e82ba4d7b995dd9ca6fb0332d81f889b)"`
);
expect(file[1]).match(
/\/\/ Runtime types generated with workerd@1\.\d+\.\d \d\d\d\d-\d\d-\d\d ([a-z_]+,?)*/
Expand All @@ -96,29 +96,43 @@ describe("types", () => {
"FAKE RUNTIME",
].join("\n")
);
console.log(
[
file[0],
file[1],
"FAKE ENV",
"// Begin runtime types",
"FAKE RUNTIME",
].join("\n")
);

await helper.run(`wrangler types`);

const file2 = (await readFile(typesPath)).toString();

expect(file2).toMatchInlineSnapshot(`
"// Generated by Wrangler by running \`wrangler types\`
// Runtime types generated with [email protected] 2023-01-01 nodejs_compat,no_global_navigator
// eslint-disable-next-line @typescript-eslint/no-empty-interface,@typescript-eslint/no-empty-object-type
interface Env {
}
// regenerates env types
expect(file2).toContain("interface Env {");
// uses cached runtime types
expect(file2).toContain("// Begin runtime types");
expect(file2).toContain("FAKE RUNTIME");
});

// Begin runtime types
FAKE RUNTIME"
`);
it("should prompt you to update types if they've been changed", async () => {
const helper = new WranglerE2ETestHelper();
await helper.seed(seed);
await helper.run(`wrangler types`);
seed["wrangler.toml"] = dedent`
name = "test-worker"
main = "src/index.ts"
compatibility_date = "2023-01-01"
compatibility_flags = ["nodejs_compat", "no_global_navigator"]
[vars]
BEEP = "BOOP"
`;
await helper.seed(seed);
const worker = helper.runLongLived("wrangler dev");
await worker.readUntil(/ It looks like your types might be out of date./);
seed["wrangler.toml"] = dedent`
name = "test-worker"
main = "src/index.ts"
compatibility_date = "2023-01-01"
compatibility_flags = ["nodejs_compat"]
[vars]
BEEP = "BOOP"
ASDf = "ADSfadsf"
`;
await helper.seed(seed);
await worker.readUntil(/ It looks like your types might be out of date./);
});
});
66 changes: 33 additions & 33 deletions packages/wrangler/src/__tests__/type-generation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,18 +451,18 @@ describe("generate types", () => {
expect(fs.existsSync("./worker-configuration.d.ts")).toBe(true);
expect(fs.readFileSync("./worker-configuration.d.ts", "utf-8"))
.toMatchInlineSnapshot(`
"// Generated by Wrangler by running \`wrangler\`
// Runtime types generated with workerd@
interface Env {
SOMETHING: \\"asdasdfasdf\\";
ANOTHER: \\"thing\\";
\\"some-other-var\\": \\"some-other-value\\";
OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"};
}
"// Generated by Wrangler by running \`wrangler\` (hash: 1baaf305bc7d12c97f589487a5ecfafb)
// Runtime types generated with workerd@
interface Env {
SOMETHING: \\"asdasdfasdf\\";
ANOTHER: \\"thing\\";
\\"some-other-var\\": \\"some-other-value\\";
OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"};
}
// Begin runtime types
<runtime types go here>"
`);
// Begin runtime types
<runtime types go here>"
`);
});

describe("when nothing was found", () => {
Expand Down Expand Up @@ -1028,18 +1028,18 @@ describe("generate types", () => {

expect(fs.readFileSync("./cloudflare-env.d.ts", "utf-8"))
.toMatchInlineSnapshot(`
"// Generated by Wrangler by running \`wrangler\`
// Runtime types generated with workerd@
interface Env {
SOMETHING: \\"asdasdfasdf\\";
ANOTHER: \\"thing\\";
\\"some-other-var\\": \\"some-other-value\\";
OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"};
}
"// Generated by Wrangler by running \`wrangler\` (hash: 1baaf305bc7d12c97f589487a5ecfafb)
// Runtime types generated with workerd@
interface Env {
SOMETHING: \\"asdasdfasdf\\";
ANOTHER: \\"thing\\";
\\"some-other-var\\": \\"some-other-value\\";
OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"};
}
// Begin runtime types
<runtime types go here>"
`);
// Begin runtime types
<runtime types go here>"
`);
});

it("should error if the user points to a non-d.ts file", async () => {
Expand Down Expand Up @@ -1082,18 +1082,18 @@ describe("generate types", () => {

expect(fs.readFileSync("./my-cloudflare-env-interface.d.ts", "utf-8"))
.toMatchInlineSnapshot(`
"// Generated by Wrangler by running \`wrangler\`
// Runtime types generated with workerd@
interface MyCloudflareEnvInterface {
SOMETHING: \\"asdasdfasdf\\";
ANOTHER: \\"thing\\";
\\"some-other-var\\": \\"some-other-value\\";
OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"};
}
"// Generated by Wrangler by running \`wrangler\` (hash: 6b0c21549436b64a143346d5df73ba4d)
// Runtime types generated with workerd@
interface MyCloudflareEnvInterface {
SOMETHING: \\"asdasdfasdf\\";
ANOTHER: \\"thing\\";
\\"some-other-var\\": \\"some-other-value\\";
OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"};
}
// Begin runtime types
<runtime types go here>"
`);
// Begin runtime types
<runtime types go here>"
`);
});
});

Expand Down
4 changes: 4 additions & 0 deletions packages/wrangler/src/api/startDevWorker/ConfigController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { getClassNamesWhichUseSQLite } from "../../dev/class-names-sqlite";
import { getLocalPersistencePath } from "../../dev/get-local-persistence-path";
import { UserError } from "../../errors";
import { logger } from "../../logger";
import { checkTypesDiff } from "../../type-generation/helpers";
import { requireApiToken, requireAuth } from "../../user";
import {
DEFAULT_INSPECTOR_PORT,
Expand Down Expand Up @@ -369,6 +370,9 @@ async function resolveConfig(
logger.warn("SQLite in Durable Objects is only supported in local mode.");
}

// prompt user to update their types if we detect that it is out of date
await checkTypesDiff(config, entry);

return resolved;
}
export class ConfigController extends Controller<ConfigControllerEventMap> {
Expand Down
66 changes: 66 additions & 0 deletions packages/wrangler/src/type-generation/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { readFileSync } from "fs";
import { version } from "workerd";
import { logger } from "../logger";
import { generateEnvTypes } from ".";
import type { Config } from "../config";
import type { Entry } from "../deployment-bundle/entry";

export const checkTypesDiff = async (config: Config, entry: Entry) => {
if (!entry.file.endsWith(".ts")) {
return;
}
let maybeExistingTypesFile: string[];
try {
// Note: this checks the default location only
maybeExistingTypesFile = readFileSync(
"./worker-configuration.d.ts",
"utf-8"
).split("\n");
} catch {
return;
}
const existingEnvHeader = maybeExistingTypesFile.find((line) =>
line.startsWith("// Generated by Wrangler by running")
);
const maybeExistingHash =
existingEnvHeader?.match(/hash: (?<hash>.*)\)/)?.groups?.hash;
const previousStrictVars = existingEnvHeader?.match(
/--strict-vars(=|\s)(?<result>true|false)/
)?.groups?.result;
const previousEnvInterface = existingEnvHeader?.match(
/--env-interface(=|\s)(?<result>[a-zA-Z][a-zA-Z0-9_]*)/
)?.groups?.result;

let newEnvHeader: string | undefined;
try {
const { envHeader } = await generateEnvTypes(
config,
{ strictVars: previousStrictVars === "false" ? false : true },
previousEnvInterface ?? "Env",
"worker-configuration.d.ts",
entry,
// don't log anything
false
);
newEnvHeader = envHeader;
} catch (e) {
logger.error(e);
}

const newHash = newEnvHeader?.match(/hash: (?<hash>.*)\)/)?.groups?.hash;

const existingRuntimeHeader = maybeExistingTypesFile.find((line) =>
line.startsWith("// Runtime types generated with")
);
const newRuntimeHeader = `// Runtime types generated with workerd@${version} ${config.compatibility_date} ${config.compatibility_flags.sort().join(",")}`;

const envOutOfDate = existingEnvHeader && maybeExistingHash !== newHash;
const runtimeOutOfDate =
existingRuntimeHeader && existingRuntimeHeader !== newRuntimeHeader;

if (envOutOfDate || runtimeOutOfDate) {
logger.log(
"❓ It looks like your types might be out of date. Have you updated your config file since last running `wrangler types`?"
);
}
};
104 changes: 58 additions & 46 deletions packages/wrangler/src/type-generation/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as fs from "node:fs";
import { basename, dirname, extname, join, relative, resolve } from "node:path";
import { hash as blake3hash } from "blake3-wasm";
import chalk from "chalk";
import { findUpSync } from "find-up";
import { getNodeCompat } from "miniflare";
Expand Down Expand Up @@ -139,47 +140,10 @@ export const typesCommand = createCommand({
const types = [];
if (args.includeEnv) {
logger.log(`Generating project types...\n`);
const secrets = getVarsForDev(
// We do not want `getVarsForDev()` to merge in the standard vars into the dev vars
// because we want to be able to work with secrets differently to vars.
// So we pass in a fake vars object here.
{ ...config, vars: {} },
args.env,
true
) as Record<string, string>;

const configBindingsWithSecrets = {
kv_namespaces: config.kv_namespaces ?? [],
vars: collectAllVars(args),
wasm_modules: config.wasm_modules,
text_blobs: {
...config.text_blobs,
},
data_blobs: config.data_blobs,
durable_objects: config.durable_objects,
r2_buckets: config.r2_buckets,
d1_databases: config.d1_databases,
services: config.services,
analytics_engine_datasets: config.analytics_engine_datasets,
dispatch_namespaces: config.dispatch_namespaces,
logfwdr: config.logfwdr,
unsafe: config.unsafe,
rules: config.rules,
queues: config.queues,
send_email: config.send_email,
vectorize: config.vectorize,
hyperdrive: config.hyperdrive,
mtls_certificates: config.mtls_certificates,
browser: config.browser,
ai: config.ai,
version_metadata: config.version_metadata,
secrets,
assets: config.assets,
workflows: config.workflows,
};

const { envHeader, envTypes } = await generateEnvTypes(
configBindingsWithSecrets,
config,
args,
envInterface,
outputPath,
entrypoint
Expand Down Expand Up @@ -286,12 +250,53 @@ type ConfigToDTS = Partial<Omit<Config, "vars">> & { vars: VarTypes } & {
secrets: Secrets;
};

async function generateEnvTypes(
configToDTS: ConfigToDTS,
export async function generateEnvTypes(
config: Config,
args: Partial<(typeof typesCommand)["args"]>,
envInterface: string,
outputPath: string,
entrypoint?: Entry
entrypoint?: Entry,
log = true
): Promise<{ envHeader?: string; envTypes?: string }> {
const secrets = getVarsForDev(
// We do not want `getVarsForDev()` to merge in the standard vars into the dev vars
// because we want to be able to work with secrets differently to vars.
// So we pass in a fake vars object here.
{ ...config, vars: {} },
args.env,
true
) as Record<string, string>;

const configToDTS: ConfigToDTS = {
kv_namespaces: config.kv_namespaces ?? [],
vars: collectAllVars(args),
wasm_modules: config.wasm_modules,
text_blobs: {
...config.text_blobs,
},
data_blobs: config.data_blobs,
durable_objects: config.durable_objects,
r2_buckets: config.r2_buckets,
d1_databases: config.d1_databases,
services: config.services,
analytics_engine_datasets: config.analytics_engine_datasets,
dispatch_namespaces: config.dispatch_namespaces,
logfwdr: config.logfwdr,
unsafe: config.unsafe,
rules: config.rules,
queues: config.queues,
send_email: config.send_email,
vectorize: config.vectorize,
hyperdrive: config.hyperdrive,
mtls_certificates: config.mtls_certificates,
browser: config.browser,
ai: config.ai,
version_metadata: config.version_metadata,
secrets,
assets: config.assets,
workflows: config.workflows,
};

const entrypointFormat = entrypoint?.format ?? "modules";
const fullOutputPath = resolve(outputPath);

Expand Down Expand Up @@ -530,13 +535,20 @@ async function generateEnvTypes(
envTypeStructure.map(([key, value]) => `${key}: ${value};`),
modulesTypeStructure
);
const envHeader = `// Generated by Wrangler by running \`${wranglerCommandUsed}\``;
// todo replace envInterface with generic
const hash = blake3hash(consoleOutput).toString("hex").slice(0, 32);

logger.log(chalk.dim(consoleOutput));
const envHeader = `// Generated by Wrangler by running \`${wranglerCommandUsed}\` (hash: ${hash})`;

if (log) {
logger.log(chalk.dim(consoleOutput));
}

return { envHeader, envTypes: fileContent };
} else {
logger.log(chalk.dim("No project types to add.\n"));
if (log) {
logger.log(chalk.dim("No project types to add.\n"));
}
return {
envHeader: undefined,
envTypes: undefined,
Expand Down Expand Up @@ -627,7 +639,7 @@ type VarTypes = Record<string, string[]>;
* @returns an object which keys are the variable names and values are arrays containing all the computed types for such variables
*/
function collectAllVars(
args: (typeof typesCommand)["args"]
args: Partial<(typeof typesCommand)["args"]>
): Record<string, string[]> {
const varsInfo: Record<string, Set<string>> = {};

Expand Down
Loading

0 comments on commit 2ef9aa4

Please sign in to comment.