Skip to content

Commit f39968a

Browse files
pashabitzConvex, Inc.
authored and
Convex, Inc.
committed
[CX-5945] Warn about https file (#24871)
If there is a `https.ts` or `https.js` file at the top level directory of user convex code and the file imports `httpRouter`: 1. Warn user that this file will not be used to set up HTTP actions 2. Log to Sentry GitOrigin-RevId: 8bb9b523d4510894923daa205aa284d7c2c9dbb5
1 parent feec55c commit f39968a

File tree

10 files changed

+164
-12
lines changed

10 files changed

+164
-12
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
"prepack": "node scripts/prepack.mjs",
146146
"postpack": "node scripts/postpack.mjs",
147147
"test": "NODE_OPTIONS=\"--experimental-vm-modules --no-warnings=ExperimentalWarning\" jest --silent",
148+
"test-not-silent": "NODE_OPTIONS=\"--experimental-vm-modules --no-warnings=ExperimentalWarning\" jest",
148149
"test-esm": "node ./scripts/test-esm.mjs && ./scripts/checkdeps.mjs",
149150
"pack-internal": "echo TODO maybe set an environment variable"
150151
},
@@ -192,6 +193,7 @@
192193
"devDependencies": {
193194
"@auth0/auth0-react": "2.0.1",
194195
"@babel/parser": "^7.21.3",
196+
"@babel/types": "7.24.0",
195197
"@clerk/clerk-react": "4.18.0",
196198
"@commander-js/extra-typings": "^11.1.0",
197199
"@jest/globals": "^28.1.0",

src/bundler/index.test.ts

+89-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { test, expect } from "@jest/globals";
1+
import { jest, expect, test } from "@jest/globals";
22
import { oneoffContext } from "./context.js";
33

44
// Although these tests are run as ESM by ts-lint, this file is built as both
@@ -8,6 +8,8 @@ const dirname = "src/bundler";
88

99
import {
1010
bundle,
11+
doesImportConvexHttpRouter,
12+
entryPoints,
1113
entryPointsByEnvironment,
1214
useNodeDirectiveRegex,
1315
mustBeIsolate,
@@ -23,21 +25,26 @@ const sorted = <T>(arr: T[], key: (el: T) => any): T[] => {
2325
return newArr.sort(cmp);
2426
};
2527

28+
afterEach(() => {
29+
jest.resetAllMocks();
30+
});
31+
2632
test("bundle function is present", () => {
2733
expect(typeof bundle).toEqual("function");
2834
});
2935

3036
test("bundle finds JavaScript functions", async () => {
37+
const fixtureDir = dirname + "/test_fixtures/js/project01";
3138
const entryPoints = await entryPointsByEnvironment(
3239
oneoffContext,
33-
dirname + "/test_fixtures/js",
40+
fixtureDir,
3441
false,
3542
);
3643
const bundles = sorted(
3744
(
3845
await bundle(
3946
oneoffContext,
40-
dirname + "/test_fixtures/js",
47+
fixtureDir,
4148
entryPoints.isolate,
4249
false,
4350
"browser",
@@ -50,6 +57,85 @@ test("bundle finds JavaScript functions", async () => {
5057
expect(bundles[1].path).toEqual("foo.js");
5158
});
5259

60+
test("returns true when simple import httpRouter found", async () => {
61+
const result = await doesImportConvexHttpRouter(`
62+
import { httpRouter } from "convex/server";
63+
64+
export const val = 1;
65+
`);
66+
expect(result).toBeTruthy();
67+
});
68+
69+
test("returns false when httpRouter is not imported", async () => {
70+
const result = await doesImportConvexHttpRouter(`
71+
export const val = 1;
72+
`);
73+
expect(result).toBeFalsy();
74+
});
75+
76+
test("returns true when multiline import httpRouter found", async () => {
77+
const result = await doesImportConvexHttpRouter(`
78+
import {
79+
httpRouter
80+
} from "convex/server";
81+
82+
export const val = 1;
83+
`);
84+
expect(result).toBeTruthy();
85+
});
86+
87+
test("returns true when httpRouter is imported with alias", async () => {
88+
const result = await doesImportConvexHttpRouter(`
89+
import { httpRouter as router } from "convex/server";
90+
91+
export const val = 1;
92+
`);
93+
expect(result).toBeTruthy();
94+
});
95+
96+
test("returns true when httpRouter is imported with alias and multiline", async () => {
97+
const result = await doesImportConvexHttpRouter(`
98+
import {
99+
httpRouter as router
100+
} from "convex/server";
101+
102+
export const val = 1;
103+
`);
104+
expect(result).toBeTruthy();
105+
});
106+
107+
test("returns true when multiple imports and httpRouter is imported", async () => {
108+
const result = await doesImportConvexHttpRouter(`
109+
import { cronJobs, httpRouter } from "convex/server";
110+
111+
export const val = 1;
112+
`);
113+
expect(result).toBeTruthy();
114+
});
115+
116+
test("bundle warns about https.js|ts at top level", async () => {
117+
const fixtureDir = dirname + "/test_fixtures/js/project_with_https";
118+
const logSpy = jest.spyOn(console, "error");
119+
await entryPoints(oneoffContext, fixtureDir, false);
120+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("https"));
121+
});
122+
123+
test("bundle does not warn about https.js|ts which is not at top level", async () => {
124+
const fixtureDir =
125+
dirname + "/test_fixtures/js/project_with_https_not_at_top_level";
126+
const logSpy = jest.spyOn(console, "error");
127+
await entryPoints(oneoffContext, fixtureDir, false);
128+
expect(logSpy).toHaveBeenCalledTimes(0);
129+
});
130+
131+
test("bundle does not warn about https.js|ts which does not import httpRouter", async () => {
132+
const fixtureDir =
133+
dirname + "/test_fixtures/js/project_with_https_without_router";
134+
const logSpy = jest.spyOn(console, "error");
135+
await entryPoints(oneoffContext, fixtureDir, false);
136+
expect(logSpy).toHaveBeenCalledTimes(0);
137+
});
138+
53139
test("use node regex", () => {
54140
// Double quotes
55141
expect('"use node";').toMatch(useNodeDirectiveRegex);

src/bundler/index.ts

+55-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { parse as parseAST } from "@babel/parser";
21
import path from "path";
32
import chalk from "chalk";
43
import esbuild from "esbuild";
4+
import { parse as parseAST } from "@babel/parser";
5+
import { Identifier, ImportSpecifier } from "@babel/types";
6+
import * as Sentry from "@sentry/node";
57
import { Filesystem } from "./fs.js";
68
import { Context, logFailure, logWarning } from "./context.js";
79
import { wasmPlugin } from "./wasm.js";
@@ -16,20 +18,22 @@ export type { Filesystem } from "./fs.js";
1618

1719
export const actionsDir = "actions";
1820

19-
// Returns a generator of { isDir, path } for all paths
21+
// Returns a generator of { isDir, path, depth } for all paths
2022
// within dirPath in some topological order (not including
2123
// dirPath itself).
2224
export function* walkDir(
2325
fs: Filesystem,
2426
dirPath: string,
25-
): Generator<{ isDir: boolean; path: string }, void, void> {
27+
depth?: number,
28+
): Generator<{ isDir: boolean; path: string; depth: number }, void, void> {
29+
depth = depth ?? 0;
2630
for (const dirEntry of fs.listDir(dirPath).sort()) {
2731
const childPath = path.join(dirPath, dirEntry.name);
2832
if (dirEntry.isDirectory()) {
29-
yield { isDir: true, path: childPath };
30-
yield* walkDir(fs, childPath);
33+
yield { isDir: true, path: childPath, depth };
34+
yield* walkDir(fs, childPath, depth + 1);
3135
} else if (dirEntry.isFile()) {
32-
yield { isDir: false, path: childPath };
36+
yield { isDir: false, path: childPath, depth };
3337
}
3438
}
3539
}
@@ -259,6 +263,29 @@ export async function bundleAuthConfig(ctx: Context, dir: string) {
259263
return result.modules;
260264
}
261265

266+
export async function doesImportConvexHttpRouter(source: string) {
267+
try {
268+
const ast = parseAST(source, {
269+
sourceType: "module",
270+
plugins: ["typescript"],
271+
});
272+
return ast.program.body.some((node) => {
273+
if (node.type !== "ImportDeclaration") return false;
274+
return node.specifiers.some((s) => {
275+
const specifier = s as ImportSpecifier;
276+
const imported = specifier.imported as Identifier;
277+
return imported.name === "httpRouter";
278+
});
279+
});
280+
} catch {
281+
return (
282+
source.match(
283+
/import\s*\{\s*httpRouter.*\}\s*from\s*"\s*convex\/server\s*"/,
284+
) !== null
285+
);
286+
}
287+
}
288+
262289
export async function entryPoints(
263290
ctx: Context,
264291
dir: string,
@@ -272,20 +299,39 @@ export async function entryPoints(
272299
}
273300
};
274301

275-
for (const { isDir, path: fpath } of walkDir(ctx.fs, dir)) {
302+
for (const { isDir, path: fpath, depth } of walkDir(ctx.fs, dir)) {
276303
if (isDir) {
277304
continue;
278305
}
279306
const relPath = path.relative(dir, fpath);
280-
const base = path.parse(fpath).base;
307+
const parsedPath = path.parse(fpath);
308+
const base = parsedPath.base;
309+
const extension = parsedPath.ext.toLowerCase();
281310

282311
if (relPath.startsWith("_deps" + path.sep)) {
283312
logFailure(
284313
ctx,
285314
`The path "${fpath}" is within the "_deps" directory, which is reserved for dependencies. Please move your code to another directory.`,
286315
);
287316
return await ctx.crash(1, "invalid filesystem data");
288-
} else if (relPath.startsWith("_generated" + path.sep)) {
317+
}
318+
319+
if (depth === 0 && base.toLowerCase().startsWith("https.")) {
320+
const source = ctx.fs.readUtf8File(fpath);
321+
if (await doesImportConvexHttpRouter(source))
322+
logWarning(
323+
ctx,
324+
chalk.yellow(
325+
`Found ${fpath}. HTTP action routes will not be imported from this file. Did you mean to include http${extension}?`,
326+
),
327+
);
328+
Sentry.captureMessage(
329+
`User code top level directory contains file ${base} which imports httpRouter.`,
330+
"warning",
331+
);
332+
}
333+
334+
if (relPath.startsWith("_generated" + path.sep)) {
289335
log(chalk.yellow(`Skipping ${fpath}`));
290336
} else if (base.startsWith(".")) {
291337
log(chalk.yellow(`Skipping dotfile ${fpath}`));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { httpRouter } from "convex/server";
2+
3+
export const val = 1;
4+
5+
export let otherHttp = 20;
6+
7+
const http = httpRouter();
8+
9+
export default http;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default async function defaultExport() {
2+
return 1;
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { httpRouter } from "convex/server";
2+
3+
const http = httpRouter();
4+
5+
export default http;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const val = 1;

0 commit comments

Comments
 (0)