Skip to content

feat: enhance file resolution logic for Convex #127

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 38 additions & 13 deletions npm-packages/convex/src/bundler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,10 +297,26 @@ export async function bundleSchema(
dir: string,
extraConditions: string[],
) {
let target = path.resolve(dir, "schema.ts");
if (!ctx.fs.exists(target)) {
target = path.resolve(dir, "schema.js");
const candidates = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would love to hear if people are using these extensions and why, but if this is helpful we can make this change. I'd guess the various TypeScript extensions would be more important? .mts, .cts, etc than .js, since most users use TypeScript? But I haven't heard requests for either

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didnt see a lot of developer using .mts and .cts for typescript files.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine if we remove .cjs, we never want to bundle a commonjs schema.

"schema.ts",
"schema.mjs",
"schema.cjs",
"schema.js",
];

let target = "";
for (const filename of candidates) {
const candidatePath = path.resolve(dir, filename);
if (ctx.fs.exists(candidatePath)) {
target = candidatePath;
break;
}
}

if (!target) {
return [];
}

const result = await bundle(
ctx,
dir,
Expand All @@ -314,21 +330,30 @@ export async function bundleSchema(
}

export async function bundleAuthConfig(ctx: Context, dir: string) {
const authConfigPath = path.resolve(dir, "auth.config.js");
const authConfigTsPath = path.resolve(dir, "auth.config.ts");
if (ctx.fs.exists(authConfigPath) && ctx.fs.exists(authConfigTsPath)) {
const candidates = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auth.config.ts doesn't exist in components so there's no need to change this

"auth.config.ts",
"auth.config.mjs",
"auth.config.cjs",
"auth.config.js"
];

const existingPaths = candidates
.map(filename => path.resolve(dir, filename))
.filter(filepath => ctx.fs.exists(filepath));

if (existingPaths.length > 1) {
return await ctx.crash({
exitCode: 1,
errorType: "invalid filesystem data",
printedMessage: `Found both ${authConfigPath} and ${authConfigTsPath}, choose one.`,
printedMessage: `Found multiple auth config files: ${existingPaths.join(", ")}, choose one.`,
});
}
const chosenPath = ctx.fs.exists(authConfigTsPath)
? authConfigTsPath
: authConfigPath;
if (!ctx.fs.exists(chosenPath)) {

if (existingPaths.length === 0) {
return [];
}

const chosenPath = existingPaths[0];
const result = await bundle(ctx, dir, [chosenPath], true, "browser");
return result.modules;
}
Expand Down Expand Up @@ -419,10 +444,10 @@ export async function entryPoints(
logVerbose(ctx, chalk.yellow(`Skipping dotfile ${fpath}`));
} else if (base.startsWith("#")) {
logVerbose(ctx, chalk.yellow(`Skipping likely emacs tempfile ${fpath}`));
} else if (base === "schema.ts" || base === "schema.js") {
} else if (base === "schema.mjs" || base === "schema.cjs" || base === "schema.js" || base === "schema.ts") {
logVerbose(ctx, chalk.yellow(`Skipping ${fpath}`));
} else if ((base.match(/\./g) || []).length > 1) {
// `auth.config.ts` and `convex.config.ts` are important not to bundle.
// `auth.config.*` and `convex.config.*` files are important not to bundle.
// `*.test.ts` `*.spec.ts` are common in developer code.
logVerbose(
ctx,
Expand Down
26 changes: 17 additions & 9 deletions npm-packages/convex/src/cli/lib/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,27 @@ import { Reporter, Span } from "./tracing.js";
import {
DEFINITION_FILENAME_JS,
DEFINITION_FILENAME_TS,
DEFINITION_FILENAME_MJS,
DEFINITION_FILENAME_CJS,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the component changes I do not want to make without understanding the use cases. The spec for components isn't well-defined yet but I wouldn't want to make what's accepted anyy more broad than necessary without specifics use cases

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a response in #126 :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not want the CJS one. I don't know that we really need a .mjs extension either, but if this is a difficult limitation for a packaging tool we can add .mjs if we also add a component that works like this to make sure we don't regress the behavior.

If you'd like to use packem to build convex components, would you consider adding the ability to produce .js files for ESM-only builds of a package with "type": "module"? e.g. with tsdown, an ESM-only build configured like below produces ESM files with .js extensions. Component definitions are already always ESM, so it's never necessary to distinguish between these.

import { defineConfig } from "tsdown";

export default [
  defineConfig({
    entry: ["src/index.ts", "src/another.ts"],
    format: ["cjs", "esm"],
    dts: {
      tsconfig: "./tsconfig.json",
      sourcemap: true,
    },
  }),
  defineConfig({
    entry: ["src/server.ts"],
    format: ["esm"],
    dts: {
      tsconfig: "./tsconfig.json",
      sourcemap: true,
    },
  }),
];

} from "./components/constants.js";
import { DeploymentSelection } from "./deploymentSelection.js";
async function findComponentRootPath(ctx: Context, functionsDir: string) {
// Default to `.ts` but fallback to `.js` if not present.
let componentRootPath = path.resolve(
path.join(functionsDir, DEFINITION_FILENAME_TS),
);
if (!ctx.fs.exists(componentRootPath)) {
componentRootPath = path.resolve(
path.join(functionsDir, DEFINITION_FILENAME_JS),
);
const candidates = [
DEFINITION_FILENAME_MJS,
DEFINITION_FILENAME_CJS,
DEFINITION_FILENAME_JS,
DEFINITION_FILENAME_TS,
];

for (const filename of candidates) {
const componentRootPath = path.resolve(path.join(functionsDir, filename));
if (ctx.fs.exists(componentRootPath)) {
return componentRootPath;
}
}
return componentRootPath;

// Default fallback to .ts for backward compatibility
return path.resolve(path.join(functionsDir, DEFINITION_FILENAME_TS));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a behavior change, previously the default was the JS path

}

export async function runCodegen(
Expand Down
2 changes: 2 additions & 0 deletions npm-packages/convex/src/cli/lib/components/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export const DEFINITION_FILENAME_TS = "convex.config.ts";
export const DEFINITION_FILENAME_JS = "convex.config.js";
export const DEFINITION_FILENAME_MJS = "convex.config.mjs";
export const DEFINITION_FILENAME_CJS = "convex.config.cjs";
25 changes: 15 additions & 10 deletions npm-packages/convex/src/cli/lib/components/definition/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function componentPlugin({
name: `convex-${mode === "discover" ? "discover-components" : "bundle-components"}`,
async setup(build) {
// This regex can't be really precise since developers could import
// "convex.config", "convex.config.js", "convex.config.ts", etc.
// "convex.config", "convex.config.mjs", "convex.config.cjs", "convex.config.js", "convex.config.ts", etc.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, throughout the PR remove .cjs

build.onResolve({ filter: /.*convex.config.*/ }, async (args) => {
verbose && logMessage(ctx, "esbuild resolving import:", args);
if (args.namespace !== "file") {
Expand Down Expand Up @@ -83,12 +83,14 @@ function componentPlugin({

const candidates = [args.path];
const ext = path.extname(args.path);
if (ext === ".js") {
candidates.push(args.path.slice(0, -".js".length) + ".ts");

if (ext === ".mjs" || ext === ".cjs" || ext === ".js") {
candidates.push(args.path.slice(0, -ext.length) + ".ts");
}
if (ext !== ".js" && ext !== ".ts") {
candidates.push(args.path + ".js");
candidates.push(args.path + ".ts");

// If no extension or unrecognized extension, try all in priority order
if (!ext || ![".mjs", ".cjs", ".js", ".ts"].includes(ext)) {
candidates.push(args.path + ".mjs", args.path + ".cjs", args.path + ".js", args.path + ".ts");
}
let resolvedPath = undefined;
for (const candidate of candidates) {
Expand Down Expand Up @@ -497,11 +499,14 @@ export async function bundleImplementations(
rootComponentDirectory.path,
directory.path,
);
// Check for schema files in priority order: .mjs, .cjs, .js, .ts
const schemaCandidates = ["schema.mjs", "schema.cjs", "schema.js", "schema.ts"];
const schemaExists = schemaCandidates.some(filename =>
ctx.fs.exists(path.resolve(resolvedPath, filename))
);

let schema;
if (ctx.fs.exists(path.resolve(resolvedPath, "schema.ts"))) {
schema =
(await bundleSchema(ctx, resolvedPath, extraConditions))[0] || null;
} else if (ctx.fs.exists(path.resolve(resolvedPath, "schema.js"))) {
if (schemaExists) {
schema =
(await bundleSchema(ctx, resolvedPath, extraConditions))[0] || null;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Context } from "../../../../bundler/context.js";
import {
DEFINITION_FILENAME_JS,
DEFINITION_FILENAME_TS,
DEFINITION_FILENAME_MJS,
DEFINITION_FILENAME_CJS,
} from "../constants.js";
import { getFunctionsDirectoryPath } from "../../config.js";

Expand All @@ -29,7 +31,7 @@ export type ComponentDirectory = {
path: string;

/**
* Absolute local filesystem path to the `convex.config.{ts,js}` file within the component definition.
* Absolute local filesystem path to the `convex.config.{mjs,cjs,js,ts}` file within the component definition.
*/
definitionPath: string;
};
Expand Down Expand Up @@ -67,17 +69,30 @@ export function isComponentDirectory(
return { kind: "err", why: `Not a directory` };
}

// Check that we have a definition file, defaulting to `.ts` but falling back to `.js`.
let filename = DEFINITION_FILENAME_TS;
let definitionPath = path.resolve(path.join(directory, filename));
if (!ctx.fs.exists(definitionPath)) {
filename = DEFINITION_FILENAME_JS;
definitionPath = path.resolve(path.join(directory, filename));
// Check that we have a definition file, using priority order: .mjs, .cjs, .js, .ts
const candidates = [
DEFINITION_FILENAME_MJS,
DEFINITION_FILENAME_CJS,
DEFINITION_FILENAME_JS,
DEFINITION_FILENAME_TS,
];

let filename = "";
let definitionPath = "";

for (const candidate of candidates) {
const candidatePath = path.resolve(path.join(directory, candidate));
if (ctx.fs.exists(candidatePath)) {
filename = candidate;
definitionPath = candidatePath;
break;
}
}
if (!ctx.fs.exists(definitionPath)) {

if (!filename) {
return {
kind: "err",
why: `Directory doesn't contain a ${filename} file`,
why: `Directory doesn't contain any of the supported definition files: ${candidates.join(", ")}`,
};
}
const definitionStat = ctx.fs.stat(definitionPath);
Expand Down
31 changes: 28 additions & 3 deletions npm-packages/dashboard-common/scripts/build-convexServerTypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
RELATIVE_PATH_TO_OUTPUT_FILE = "../src/lib/generated/convexServerTypes.json"

# The entrypoints and helpers from `convex` NPM package used in Convex server functions
SERVER_ENTRYPOINTS = ["server", "values", "type_utils.d.ts"]
SERVER_ENTRYPOINTS = ["server", "values", "type_utils"]

# For VS Code
PATH_PREFIX = "file:///convex/"
Expand Down Expand Up @@ -36,10 +36,35 @@ def build_entrypoint(convex_build_directory, entry_point):
def find_dts_files(path, base_path):
dts_files = {}
if os.path.isdir(path):
# Collect all .d.ts, .d.cts, .d.mts files in this directory
dir_files = {}
for item in os.listdir(path):
item_path = os.path.join(path, item)
dts_files.update(find_dts_files(item_path, base_path))
elif path.endswith(".d.ts"):
if os.path.isdir(item_path):
dts_files.update(find_dts_files(item_path, base_path))
elif item.endswith((".d.mts", ".d.cts", ".d.ts")):
# Extract base name (e.g., "a" from "a.d.mts")
if item.endswith(".d.mts"):
base_name = item[:-6] # Remove ".d.mts"
priority = 0 # Highest priority
elif item.endswith(".d.cts"):
base_name = item[:-6] # Remove ".d.cts"
priority = 1
else: # .d.ts
base_name = item[:-5] # Remove ".d.ts"
priority = 2 # Lowest priority

if base_name not in dir_files or priority < dir_files[base_name][1]:
dir_files[base_name] = (item_path, priority)

# Process the selected files from this directory
for item_path, _ in dir_files.values():
relative_path = os.path.relpath(item_path, base_path)
with open(item_path, "r", encoding="utf-8") as file:
dts_files[PATH_PREFIX + relative_path] = strip_source_map_suffix(
file.read()
)
elif path.endswith((".d.mts", ".d.cts", ".d.ts")):
relative_path = os.path.relpath(path, base_path)
with open(path, "r", encoding="utf-8") as file:
dts_files[PATH_PREFIX + relative_path] = strip_source_map_suffix(
Expand Down