Skip to content

Instantly share code, notes, and snippets.

@sillvva
Last active November 15, 2025 14:16
Show Gist options
  • Select an option

  • Save sillvva/b820ad178ddd29533b7eb76bd9c0e498 to your computer and use it in GitHub Desktop.

Select an option

Save sillvva/b820ad178ddd29533b7eb76bd9c0e498 to your computer and use it in GitHub Desktop.
eslint: enforce guarded remote functions
export default {
meta: {
type: "problem",
docs: {
description:
"Enforce that exports in .remote.ts files use guardedQuery(), guardedCommand(), or guardedForm(), and only allow type exports",
category: "Best Practices",
recommended: true
},
messages: {
unguardedExport:
"Exports in .remote.ts files must use guardedQuery(), guardedCommand(), or guardedForm(). Direct exports of query(), command(), or form() are not allowed.",
mustBeGuarded: 'Export "{{name}}" must be the return value of guardedQuery(), guardedCommand(), or guardedForm().',
onlyTypesAllowed:
'Only guarded remote functions and type exports are allowed in .remote.ts files. Export "{{name}}" is not allowed.'
},
schema: []
},
create(context) {
const filename = context.getFilename();
// Only apply this rule to files ending in .remote.ts
if (!filename.endsWith(".remote.ts")) {
return {};
}
const guardedFunctions = new Set(["guardedQuery", "guardedCommand", "guardedForm"]);
const unguardedFunctions = new Set(["query", "command", "form"]);
function isGuardedCall(node) {
return node.type === "CallExpression" && node.callee.type === "Identifier" && guardedFunctions.has(node.callee.name);
}
function isUnguardedCall(node) {
return node.type === "CallExpression" && node.callee.type === "Identifier" && unguardedFunctions.has(node.callee.name);
}
function checkExportDeclaration(node) {
// Allow type-only exports
if (node.exportKind === "type") {
return;
}
if (node.type === "ExportNamedDeclaration") {
// Handle type-only named exports: export type { Foo }
if (node.exportKind === "type") {
return;
}
// Handle re-exports without declaration
if (!node.declaration && node.specifiers) {
for (const specifier of node.specifiers) {
// Allow individual type specifiers: export { type Foo }
if (specifier.exportKind === "type") {
continue;
}
context.report({
node: specifier,
messageId: "onlyTypesAllowed",
data: {
name: specifier.exported.name
}
});
}
return;
}
if (node.declaration && node.declaration.type === "VariableDeclaration") {
for (const declarator of node.declaration.declarations) {
if (declarator.init) {
if (isUnguardedCall(declarator.init)) {
context.report({
node: declarator.init,
messageId: "unguardedExport"
});
} else if (!isGuardedCall(declarator.init)) {
context.report({
node: declarator,
messageId: "mustBeGuarded",
data: {
name: declarator.id.name
}
});
}
}
}
} else if (node.declaration) {
// Block any other declaration types (functions, classes, etc.) unless they're type declarations
const name = node.declaration.id?.name || "unknown";
context.report({
node: node.declaration,
messageId: "onlyTypesAllowed",
data: { name }
});
}
}
}
return {
ExportNamedDeclaration: checkExportDeclaration,
// Block default exports entirely (they can't be types in the same way)
ExportDefaultDeclaration(node) {
context.report({
node: node.declaration || node,
loc: node.loc,
messageId: "onlyTypesAllowed",
data: { name: "default" }
});
},
// Block export * statements
ExportAllDeclaration(node) {
context.report({
node,
loc: node.loc,
messageId: "onlyTypesAllowed",
data: { name: "*" }
});
}
};
}
};
// ESLint flat config for SvelteKit + TypeScript + Prettier
// Migrated from .eslintrc.cjs to eslint.config.js (Flat Config)
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier/flat";
import svelte from "eslint-plugin-svelte";
import globals from "globals";
import enforceGuardedExports from "./eslint/enforce-guarded-functions.js";
import svelteConfig from "./svelte.config.js";
// Use the typescript-eslint aggregator for flat config presets
// (requires devDependency: "typescript-eslint")
import tseslint from "typescript-eslint";
export default [
... // existing rules
{
files: ["**/*.remote.ts"],
plugins: {
custom: {
rules: {
"enforce-guarded-functions": enforceGuardedExports
}
}
},
rules: {
"custom/enforce-guarded-functions": "error"
}
}
];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment