feature: add no-trigger-core-import (#1175)
* feature: add no-trigger-core-import * fix: handle aliases and limit to exported modules * fix: use longest export map name * fix: support export type and interface * fix: simplify message when export map is needed * feature: fine grained modules for core * fix: add prerequisite lint plugins * fix: why do I have to fix these components again??? * fix: tests * Add eslint config and vscode settings * Create yellow-roses-arrive.md * Remove the vscode linting This is temporary so no one fixes these until we merge the remix branch --------- Co-authored-by: Eric Allam <eric@trigger.dev> Co-authored-by: Eric Allam <eallam@icloud.com> Co-authored-by: Matt Aitken <matt@mattaitken.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Add more package exports that can be used from the web app
|
||||
+33
-10
@@ -1,13 +1,36 @@
|
||||
{
|
||||
"extends": ["@remix-run/eslint-config", "@remix-run/eslint-config/node", "prettier"],
|
||||
"rules": {
|
||||
"@typescript-eslint/strict-boolean-expressions": [
|
||||
"error",
|
||||
{
|
||||
"allowNullableBoolean": true,
|
||||
"allowNullableString": true,
|
||||
"allowNullableNumber": true
|
||||
"plugins": [
|
||||
"@trigger.dev/eslint-plugin",
|
||||
"react-hooks",
|
||||
"@typescript-eslint/eslint-plugin",
|
||||
"import"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {
|
||||
// Autofixes imports from "@trigger.dev/core" to fine grained modules
|
||||
"@trigger.dev/no-trigger-core-import": "error",
|
||||
// Normalize `import type {}` and `import { type }`
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"warn",
|
||||
{
|
||||
// the "type" annotation can get tangled and cause syntax errors
|
||||
// during some autofixes, so easier to just turn it off
|
||||
"prefer": "type-imports",
|
||||
"disallowTypeAnnotations": true,
|
||||
"fixStyle": "inline-type-imports"
|
||||
}
|
||||
],
|
||||
// no-trigger-core-import splits imports into multiple lines
|
||||
// this one merges them back into a single line
|
||||
// if they still import from the same module
|
||||
"import/no-duplicates": ["warn", { "prefer-inline": true }],
|
||||
// lots of undeclared vars, enable this rule if you want to clean them up
|
||||
"turbo/no-undeclared-env-vars": "off"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignorePatterns": ["seed.js", "seedCloud.ts", "populate.js"]
|
||||
}
|
||||
|
||||
@@ -8,12 +8,8 @@ const Alert = AlertDialogPrimitive.Root;
|
||||
|
||||
const AlertTrigger = AlertDialogPrimitive.Trigger;
|
||||
|
||||
const AlertPortal = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: AlertDialogPrimitive.AlertDialogPortalProps) => (
|
||||
<AlertDialogPrimitive.Portal className={cn(className)} {...props}>
|
||||
const AlertPortal = ({ children, ...props }: AlertDialogPrimitive.AlertDialogPortalProps) => (
|
||||
<AlertDialogPrimitive.Portal {...props}>
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -10,8 +10,8 @@ const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = ({ className, children, ...props }: DialogPrimitive.DialogPortalProps) => (
|
||||
<DialogPrimitive.Portal className={cn(className)} {...props}>
|
||||
const DialogPortal = ({ children, ...props }: DialogPrimitive.DialogPortalProps) => (
|
||||
<DialogPrimitive.Portal {...props}>
|
||||
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -28,8 +28,8 @@ interface SheetPortalProps
|
||||
extends SheetPrimitive.DialogPortalProps,
|
||||
VariantProps<typeof portalVariants> {}
|
||||
|
||||
const SheetPortal = ({ position, className, children, ...props }: SheetPortalProps) => (
|
||||
<SheetPrimitive.Portal className={cn(className)} {...props}>
|
||||
const SheetPortal = ({ position, children, ...props }: SheetPortalProps) => (
|
||||
<SheetPrimitive.Portal {...props}>
|
||||
<div className={portalVariants({ position })}>{children}</div>
|
||||
</SheetPrimitive.Portal>
|
||||
);
|
||||
|
||||
@@ -188,6 +188,7 @@
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@total-typescript/ts-reset": "^0.4.2",
|
||||
"@trigger.dev/eslint-plugin": "workspace:*",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/eslint": "^8.4.6",
|
||||
@@ -221,6 +222,9 @@
|
||||
"esbuild": "^0.15.10",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-turbo": "^2.0.4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss-import": "^16.0.1",
|
||||
"postcss-loader": "^8.1.1",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"@types/node": "18.17.1",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"eslint-plugin-turbo": "^2.0.4",
|
||||
"prettier": "^3.0.0",
|
||||
"tsx": "^3.7.1",
|
||||
"turbo": "^1.10.3",
|
||||
|
||||
+136
-16
@@ -21,6 +21,70 @@
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./eventFilterMatches": {
|
||||
"import": {
|
||||
"types": "./dist/eventFilterMatches.d.mts",
|
||||
"default": "./dist/eventFilterMatches.mjs"
|
||||
},
|
||||
"require": "./dist/eventFilterMatches.js",
|
||||
"types": "./dist/eventFilterMatches.d.ts"
|
||||
},
|
||||
"./replacements": {
|
||||
"import": {
|
||||
"types": "./dist/replacements.d.mts",
|
||||
"default": "./dist/replacements.mjs"
|
||||
},
|
||||
"require": "./dist/replacements.js",
|
||||
"types": "./dist/replacements.d.ts"
|
||||
},
|
||||
"./requestFilterMatches": {
|
||||
"import": {
|
||||
"types": "./dist/requestFilterMatches.d.mts",
|
||||
"default": "./dist/requestFilterMatches.mjs"
|
||||
},
|
||||
"require": "./dist/requestFilterMatches.js",
|
||||
"types": "./dist/requestFilterMatches.d.ts"
|
||||
},
|
||||
"./retry": {
|
||||
"import": {
|
||||
"types": "./dist/retry.d.mts",
|
||||
"default": "./dist/retry.mjs"
|
||||
},
|
||||
"require": "./dist/retry.js",
|
||||
"types": "./dist/retry.d.ts"
|
||||
},
|
||||
"./utils": {
|
||||
"import": {
|
||||
"types": "./dist/utils.d.mts",
|
||||
"default": "./dist/utils.mjs"
|
||||
},
|
||||
"require": "./dist/utils.js",
|
||||
"types": "./dist/utils.d.ts"
|
||||
},
|
||||
"./schemas": {
|
||||
"import": {
|
||||
"types": "./dist/schemas/index.d.mts",
|
||||
"default": "./dist/schemas/index.mjs"
|
||||
},
|
||||
"require": "./dist/schemas/index.js",
|
||||
"types": "./dist/schemas/index.d.ts"
|
||||
},
|
||||
"./types": {
|
||||
"import": {
|
||||
"types": "./dist/types.d.mts",
|
||||
"default": "./dist/types.mjs"
|
||||
},
|
||||
"require": "./dist/types.js",
|
||||
"types": "./dist/types.d.ts"
|
||||
},
|
||||
"./versions": {
|
||||
"import": {
|
||||
"types": "./dist/versions.d.mts",
|
||||
"default": "./dist/versions.mjs"
|
||||
},
|
||||
"require": "./dist/versions.js",
|
||||
"types": "./dist/versions.d.ts"
|
||||
},
|
||||
"./v3": {
|
||||
"import": {
|
||||
"types": "./dist/v3/index.d.mts",
|
||||
@@ -29,6 +93,22 @@
|
||||
"require": "./dist/v3/index.js",
|
||||
"types": "./dist/v3/index.d.ts"
|
||||
},
|
||||
"./v3/errors": {
|
||||
"import": {
|
||||
"types": "./dist/v3/errors.d.mts",
|
||||
"default": "./dist/v3/errors.mjs"
|
||||
},
|
||||
"require": "./dist/v3/errors.js",
|
||||
"types": "./dist/v3/errors.d.ts"
|
||||
},
|
||||
"./v3/logger-api": {
|
||||
"import": {
|
||||
"types": "./dist/v3/logger-api.d.mts",
|
||||
"default": "./dist/v3/logger-api.mjs"
|
||||
},
|
||||
"require": "./dist/v3/logger-api.js",
|
||||
"types": "./dist/v3/logger-api.d.ts"
|
||||
},
|
||||
"./v3/otel": {
|
||||
"import": {
|
||||
"types": "./dist/v3/otel/index.d.mts",
|
||||
@@ -37,6 +117,62 @@
|
||||
"require": "./dist/v3/otel/index.js",
|
||||
"types": "./dist/v3/otel/index.d.ts"
|
||||
},
|
||||
"./v3/semanticInternalAttributes": {
|
||||
"import": {
|
||||
"types": "./dist/v3/semanticInternalAttributes.d.mts",
|
||||
"default": "./dist/v3/semanticInternalAttributes.mjs"
|
||||
},
|
||||
"require": "./dist/v3/semanticInternalAttributes.js",
|
||||
"types": "./dist/v3/semanticInternalAttributes.d.ts"
|
||||
},
|
||||
"./v3/utils/durations": {
|
||||
"import": {
|
||||
"types": "./dist/v3/utils/durations.d.mts",
|
||||
"default": "./dist/v3/utils/durations.mjs"
|
||||
},
|
||||
"require": "./dist/v3/utils/durations.js",
|
||||
"types": "./dist/v3/utils/durations.d.ts"
|
||||
},
|
||||
"./v3/utils/flattenAttributes": {
|
||||
"import": {
|
||||
"types": "./dist/v3/utils/flattenAttributes.d.mts",
|
||||
"default": "./dist/v3/utils/flattenAttributes.mjs"
|
||||
},
|
||||
"require": "./dist/v3/utils/flattenAttributes.js",
|
||||
"types": "./dist/v3/utils/flattenAttributes.d.ts"
|
||||
},
|
||||
"./v3/utils/ioSerialization": {
|
||||
"import": {
|
||||
"types": "./dist/v3/utils/ioSerialization.d.mts",
|
||||
"default": "./dist/v3/utils/ioSerialization.mjs"
|
||||
},
|
||||
"require": "./dist/v3/utils/ioSerialization.js",
|
||||
"types": "./dist/v3/utils/ioSerialization.d.ts"
|
||||
},
|
||||
"./v3/utils/omit": {
|
||||
"import": {
|
||||
"types": "./dist/v3/utils/omit.d.mts",
|
||||
"default": "./dist/v3/utils/omit.mjs"
|
||||
},
|
||||
"require": "./dist/v3/utils/omit.js",
|
||||
"types": "./dist/v3/utils/omit.d.ts"
|
||||
},
|
||||
"./v3/utils/retries": {
|
||||
"import": {
|
||||
"types": "./dist/v3/utils/retries.d.mts",
|
||||
"default": "./dist/v3/utils/retries.mjs"
|
||||
},
|
||||
"require": "./dist/v3/utils/retries.js",
|
||||
"types": "./dist/v3/utils/retries.d.ts"
|
||||
},
|
||||
"./v3/utils/structuredLogger": {
|
||||
"import": {
|
||||
"types": "./dist/v3/utils/structuredLogger.d.mts",
|
||||
"default": "./dist/v3/utils/structuredLogger.mjs"
|
||||
},
|
||||
"require": "./dist/v3/utils/structuredLogger.js",
|
||||
"types": "./dist/v3/utils/structuredLogger.d.ts"
|
||||
},
|
||||
"./v3/zodfetch": {
|
||||
"import": {
|
||||
"types": "./dist/v3/zodfetch.d.mts",
|
||||
@@ -77,22 +213,6 @@
|
||||
"require": "./dist/v3/zodIpc.js",
|
||||
"types": "./dist/v3/zodIpc.d.ts"
|
||||
},
|
||||
"./v3/utils/structuredLogger": {
|
||||
"import": {
|
||||
"types": "./dist/v3/utils/structuredLogger.d.mts",
|
||||
"default": "./dist/v3/utils/structuredLogger.mjs"
|
||||
},
|
||||
"require": "./dist/v3/utils/structuredLogger.js",
|
||||
"types": "./dist/v3/utils/structuredLogger.d.ts"
|
||||
},
|
||||
"./v3/utils/durations": {
|
||||
"import": {
|
||||
"types": "./dist/v3/utils/durations.d.mts",
|
||||
"default": "./dist/v3/utils/durations.mjs"
|
||||
},
|
||||
"require": "./dist/v3/utils/durations.js",
|
||||
"types": "./dist/v3/utils/durations.d.ts"
|
||||
},
|
||||
"./v3/utils/timers": {
|
||||
"import": {
|
||||
"types": "./dist/v3/utils/timers.d.mts",
|
||||
|
||||
@@ -6,30 +6,4 @@ export * from "./replacements";
|
||||
export * from "./searchParams";
|
||||
export * from "./eventFilterMatches";
|
||||
export * from "./requestFilterMatches";
|
||||
|
||||
export const API_VERSIONS = {
|
||||
LAZY_LOADED_CACHED_TASKS: "2023-09-29",
|
||||
SERIALIZED_TASK_OUTPUT: "2023-11-01",
|
||||
} as const;
|
||||
|
||||
export const PLATFORM_FEATURES = {
|
||||
yieldExecution: API_VERSIONS.LAZY_LOADED_CACHED_TASKS,
|
||||
lazyLoadedCachedTasks: API_VERSIONS.LAZY_LOADED_CACHED_TASKS,
|
||||
};
|
||||
|
||||
export function supportsFeature<TFeatureName extends keyof typeof PLATFORM_FEATURES>(
|
||||
featureName: TFeatureName,
|
||||
version: string
|
||||
): boolean {
|
||||
if (version === "unversioned" || version === "unknown") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const supportedVersion = PLATFORM_FEATURES[featureName];
|
||||
|
||||
if (!supportedVersion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return version >= supportedVersion;
|
||||
}
|
||||
export * from "./versions";
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
export const API_VERSIONS = {
|
||||
LAZY_LOADED_CACHED_TASKS: "2023-09-29",
|
||||
SERIALIZED_TASK_OUTPUT: "2023-11-01",
|
||||
} as const;
|
||||
|
||||
export const PLATFORM_FEATURES = {
|
||||
yieldExecution: API_VERSIONS.LAZY_LOADED_CACHED_TASKS,
|
||||
lazyLoadedCachedTasks: API_VERSIONS.LAZY_LOADED_CACHED_TASKS,
|
||||
};
|
||||
|
||||
export function supportsFeature<TFeatureName extends keyof typeof PLATFORM_FEATURES>(
|
||||
featureName: TFeatureName,
|
||||
version: string
|
||||
): boolean {
|
||||
if (version === "unversioned" || version === "unknown") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const supportedVersion = PLATFORM_FEATURES[featureName];
|
||||
|
||||
if (!supportedVersion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return version >= supportedVersion;
|
||||
}
|
||||
@@ -4,21 +4,36 @@ export default defineConfig({
|
||||
...packageOptions,
|
||||
config: "tsconfig.build.json",
|
||||
entry: [
|
||||
"./src/eventFilterMatches.ts",
|
||||
"./src/index.ts",
|
||||
"./src/replacements.ts",
|
||||
"./src/requestFilterMatches.ts",
|
||||
"./src/retry.ts",
|
||||
"./src/schemas/index.ts",
|
||||
"./src/types.ts",
|
||||
"./src/utils.ts",
|
||||
"./src/versions.ts",
|
||||
"./src/v3/dev/index.ts",
|
||||
"./src/v3/errors.ts",
|
||||
"./src/v3/index.ts",
|
||||
"./src/v3/logger-api.ts",
|
||||
"./src/v3/otel/index.ts",
|
||||
"./src/v3/prod/index.ts",
|
||||
"./src/v3/schemas/index.ts",
|
||||
"./src/v3/semanticInternalAttributes.ts",
|
||||
"./src/v3/utils/durations.ts",
|
||||
"./src/v3/utils/flattenAttributes.ts",
|
||||
"./src/v3/utils/ioSerialization.ts",
|
||||
"./src/v3/utils/omit.ts",
|
||||
"./src/v3/utils/retries.ts",
|
||||
"./src/v3/utils/structuredLogger.ts",
|
||||
"./src/v3/workers/index.ts",
|
||||
"./src/v3/zodfetch.ts",
|
||||
"./src/v3/zodIpc.ts",
|
||||
"./src/v3/zodMessageHandler.ts",
|
||||
"./src/v3/zodNamespace.ts",
|
||||
"./src/v3/zodSocket.ts",
|
||||
"./src/v3/zodIpc.ts",
|
||||
"./src/v3/utils/structuredLogger.ts",
|
||||
"./src/v3/utils/durations.ts",
|
||||
"./src/v3/utils/timers.ts",
|
||||
"./src/v3/dev/index.ts",
|
||||
"./src/v3/prod/index.ts",
|
||||
"./src/v3/workers/index.ts",
|
||||
"./src/v3/zodfetch.ts",
|
||||
"./src/v3/schemas/index.ts",
|
||||
],
|
||||
external: ["node:timers/promises"],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Prevent importing from `@trigger.dev/core` directly (`trigger-dev/no-trigger-core-import`)
|
||||
|
||||
<!-- end auto-generated rule header -->
|
||||
|
||||
Due to [this Remix bug](https://github.com/remix-run/remix/issues/9597), the web app is very sensitive to importing server-side code when it's bundling for the client side. If a route imports from a barrel file that ALSO exports server-side code, it will break the webapp's client side navigation and the page simply refreshes. This only happens during development.
|
||||
|
||||
## Rule Details
|
||||
|
||||
This rule prevents importing from `@trigger.dev/core` and `@trigger.dev/core/v3` directly, which are barrel files that export server-side code.
|
||||
|
||||
It forces direct imports from the most specific file that exports a particular module and will autocorrect it.
|
||||
|
||||
Examples of **incorrect** code for this rule:
|
||||
|
||||
```ts
|
||||
import { ScheduledTaskPayload, parsePacket, prettyPrintPacket } from "@trigger.dev/core/v3";
|
||||
```
|
||||
|
||||
Examples of **correct** code for this rule:
|
||||
|
||||
```ts
|
||||
import { ScheduledTaskPayload } from "@trigger.dev/core/v3/schemas";
|
||||
import { parsePacket, prettyPrintPacket } from "@trigger.dev/core/v3/utils/ioSerialization";
|
||||
```
|
||||
|
||||
## When Not To Use It
|
||||
|
||||
This rule prevents issues when client-side navigating to routes that import from `@trigger.dev/core` or `@trigger.dev/core/v3`. If there will be no client-side navigation during development, this rule is not needed.
|
||||
|
||||
If [this bug](https://github.com/remix-run/remix/issues/9597) is fixed, this rule can be removed.
|
||||
|
||||
## Workflow
|
||||
|
||||
This rule runs in several steps
|
||||
|
||||
```ts
|
||||
import { ScheduledTaskPayload, parsePacket, prettyPrintPacket } from "@trigger.dev/core/v3";
|
||||
```
|
||||
|
||||
First it splits Core imports into one line per import
|
||||
|
||||
```ts
|
||||
import { ScheduledTaskPayload } from "@trigger.dev/core/v3";
|
||||
import { parsePacket } from "@trigger.dev/core/v3";
|
||||
import { prettyPrintPacket } from "@trigger.dev/core/v3";
|
||||
```
|
||||
|
||||
Then refines each down to their most specific exported file
|
||||
|
||||
```ts
|
||||
import { ScheduledTaskPayload } from "@trigger.dev/core/v3/schemas/api";
|
||||
import { parsePacket } from "@trigger.dev/core/v3/utils/ioSerialization";
|
||||
import { prettyPrintPacket } from "@trigger.dev/core/v3/utils/ioSerialization";
|
||||
```
|
||||
|
||||
if that exported file is downstream of an allowed barrel file (set to the schemas folders right now), it returns the export from the barrel instead
|
||||
|
||||
```ts
|
||||
import { ScheduledTaskPayload } from "@trigger.dev/core/v3/schemas";
|
||||
import { parsePacket } from "@trigger.dev/core/v3/utils/ioSerialization";
|
||||
import { prettyPrintPacket } from "@trigger.dev/core/v3/utils/ioSerialization";
|
||||
```
|
||||
|
||||
then the normal lint plugin for merging multiple imports from the same file will run and merge any that are the same
|
||||
|
||||
```ts
|
||||
import { ScheduledTaskPayload } from "@trigger.dev/core/v3/schemas";
|
||||
import { parsePacket, prettyPrintPacket } from "@trigger.dev/core/v3/utils/ioSerialization";
|
||||
```
|
||||
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* @fileoverview Prevent importing from `@trigger.dev/core` directly
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const parser = require("@babel/parser");
|
||||
const traverse = require("@babel/traverse").default;
|
||||
const tsconfigPaths = require("tsconfig-paths");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const blockedImportSources = ["@trigger.dev/core", "@trigger.dev/core/v3"];
|
||||
const allowedBarrelFiles = getAllowedBarrelFiles();
|
||||
|
||||
function getAllowedBarrelFiles() {
|
||||
const packageJsonPath = path.resolve(
|
||||
process.cwd().replace("apps/webapp", "").replace("packages/eslint-plugin", ""),
|
||||
"packages/core/package.json"
|
||||
);
|
||||
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||
const exports = packageJson.exports;
|
||||
|
||||
let allowedFiles = [];
|
||||
for (let key in exports) {
|
||||
if (exports.hasOwnProperty(key)) {
|
||||
key = key.replace(/^\.\/?/, "");
|
||||
if (key === "package.json") continue;
|
||||
|
||||
allowedFiles.push(["@trigger.dev/core", key].filter(Boolean).join("/"));
|
||||
}
|
||||
}
|
||||
|
||||
// sort by length so that longest path is first
|
||||
allowedFiles.sort((a, b) => b.length - a.length);
|
||||
|
||||
// Filter out the blocked import sources
|
||||
return allowedFiles.filter((file) => !blockedImportSources.includes(file));
|
||||
}
|
||||
|
||||
function pathToCorePath(filePath) {
|
||||
const baseName = "@trigger.dev/core";
|
||||
const relativePath = filePath
|
||||
.split("packages/core/src/")[1]
|
||||
.replace(/\\/g, "/")
|
||||
.replace("/index.ts", "");
|
||||
return `${baseName}/${relativePath}`;
|
||||
}
|
||||
|
||||
function resolveSpecifier(importSource, specifier, context) {
|
||||
const corePath = resolveModulePath(importSource);
|
||||
const coreDir = path.dirname(corePath);
|
||||
|
||||
const resolvedPath = resolveExport(coreDir, specifier);
|
||||
if (resolvedPath) {
|
||||
for (const allowedBarrelFile of allowedBarrelFiles) {
|
||||
if (pathToCorePath(resolvedPath).startsWith(allowedBarrelFile)) {
|
||||
return allowedBarrelFile;
|
||||
}
|
||||
}
|
||||
|
||||
return pathToCorePath(resolvedPath);
|
||||
}
|
||||
return null;
|
||||
|
||||
function resolveExport(fileOrDir, specifier) {
|
||||
const filePath = fs.lstatSync(fileOrDir).isDirectory()
|
||||
? path.join(fileOrDir, "index.ts")
|
||||
: fileOrDir;
|
||||
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
|
||||
const code = fs.readFileSync(filePath, "utf8");
|
||||
const ast = parser.parse(code, { sourceType: "module", plugins: ["typescript"] });
|
||||
|
||||
let foundPath = null;
|
||||
|
||||
traverse(ast, {
|
||||
ExportNamedDeclaration({ node }) {
|
||||
if (node.declaration) {
|
||||
if (
|
||||
// export const foo = 'bar';
|
||||
(node.declaration.type === "VariableDeclaration" &&
|
||||
node.declaration.declarations.some((decl) => decl.id.name === specifier)) ||
|
||||
// export function foo() {}
|
||||
(node.declaration.type === "FunctionDeclaration" &&
|
||||
node.declaration.id.name === specifier) ||
|
||||
// export class Foo {}
|
||||
(node.declaration.type === "ClassDeclaration" &&
|
||||
node.declaration.id.name === specifier) ||
|
||||
// export type Foo = {};
|
||||
(node.declaration.type === "TSTypeAliasDeclaration" &&
|
||||
node.declaration.id.name === specifier) ||
|
||||
// export interface Foo {}
|
||||
(node.declaration.type === "TSInterfaceDeclaration" &&
|
||||
node.declaration.id.name === specifier)
|
||||
) {
|
||||
foundPath = filePath;
|
||||
return;
|
||||
}
|
||||
} else if (node.specifiers) {
|
||||
for (const exportSpecifier of node.specifiers) {
|
||||
if (exportSpecifier.exported.name === specifier) {
|
||||
const sourcePath = node.source.value;
|
||||
const dir = fs.lstatSync(fileOrDir).isDirectory()
|
||||
? fileOrDir
|
||||
: path.dirname(fileOrDir);
|
||||
const resolvedSourcePath = tryResolveSourcePath(dir, sourcePath);
|
||||
|
||||
if (resolvedSourcePath) {
|
||||
const resolvedExport = resolveExport(resolvedSourcePath, specifier);
|
||||
if (resolvedExport) {
|
||||
foundPath = resolvedExport;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ExportAllDeclaration({ node }) {
|
||||
const sourcePath = node.source.value;
|
||||
const dir = fs.lstatSync(fileOrDir).isDirectory() ? fileOrDir : path.dirname(fileOrDir);
|
||||
const resolvedSourcePath = tryResolveSourcePath(dir, sourcePath);
|
||||
|
||||
if (resolvedSourcePath) {
|
||||
const resolvedExport = resolveExport(resolvedSourcePath, specifier);
|
||||
if (resolvedExport) {
|
||||
foundPath = resolvedExport;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return foundPath ? foundPath : null;
|
||||
}
|
||||
}
|
||||
|
||||
function tryResolveSourcePath(baseDir, sourcePath) {
|
||||
const possibleExtensions = ["", ".ts", ".tsx", ".js", ".jsx"];
|
||||
const possibleIndexFiles = ["index.ts", "index.tsx", "index.js", "index.jsx"];
|
||||
|
||||
for (const ext of possibleExtensions) {
|
||||
const fullPath = path.resolve(baseDir, `${sourcePath}${ext}`);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
|
||||
const dirPath = path.resolve(baseDir, sourcePath);
|
||||
if (fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory()) {
|
||||
for (const indexFile of possibleIndexFiles) {
|
||||
const indexPath = path.join(dirPath, indexFile);
|
||||
if (fs.existsSync(indexPath)) {
|
||||
return indexPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Configure tsconfig-paths
|
||||
function resolveModulePath(sourcePath) {
|
||||
const cwd = path.resolve(process.cwd());
|
||||
const tsconfigPath = path.resolve(cwd, "tsconfig.json");
|
||||
const { absoluteBaseUrl, paths } = tsconfigPaths.loadConfig(tsconfigPath);
|
||||
|
||||
if (!absoluteBaseUrl || !paths) {
|
||||
throw new Error("Could not load tsconfig paths");
|
||||
}
|
||||
|
||||
const matchPath = tsconfigPaths.createMatchPath(absoluteBaseUrl, paths);
|
||||
|
||||
let resolvedPath = matchPath(sourcePath, undefined, undefined, [".ts", ".tsx", ".js", ".jsx"]);
|
||||
|
||||
if (resolvedPath) {
|
||||
if (fs.existsSync(resolvedPath) && fs.lstatSync(resolvedPath).isDirectory()) {
|
||||
const indexResolvedPath = path.join(resolvedPath, "index.ts");
|
||||
if (fs.existsSync(indexResolvedPath)) {
|
||||
resolvedPath = indexResolvedPath;
|
||||
}
|
||||
}
|
||||
|
||||
return path.resolve(resolvedPath);
|
||||
}
|
||||
|
||||
return path.resolve(sourcePath);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Prevent importing from `@trigger.dev/core` or `@trigger.dev/core/v3` directly",
|
||||
recommended: true,
|
||||
url: null,
|
||||
},
|
||||
fixable: "code",
|
||||
schema: [],
|
||||
messages: {
|
||||
noTriggerCoreImportFixable: "Use specific import from '{{resolvedPath}}'",
|
||||
noTriggerCoreImportNeedsExport:
|
||||
"Should export from {{resolvedPath}} but not covered by @trigger.dev/core/package.json#exports",
|
||||
noTriggerCoreImport:
|
||||
"Cannot import from {{importSource}} but no specific import is available",
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importSource = node.source.value;
|
||||
|
||||
if (blockedImportSources.includes(importSource)) {
|
||||
const specifierFixes = node.specifiers
|
||||
.map((specifier) => {
|
||||
const resolvedPath = resolveSpecifier(importSource, specifier.imported.name, context);
|
||||
if (
|
||||
resolvedPath &&
|
||||
!blockedImportSources.includes(resolvedPath.replace("/index.ts", ""))
|
||||
) {
|
||||
return {
|
||||
original: context.getSourceCode().getText(specifier),
|
||||
path: resolvedPath,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (specifierFixes.length > 0) {
|
||||
const fixes = specifierFixes
|
||||
.map((fix) => {
|
||||
return `import { ${fix.original.replace("type ", "")} } from '${fix.path}';`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
if (specifierFixes.every((fix) => allowedBarrelFiles.includes(fix.path))) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noTriggerCoreImportFixable",
|
||||
data: {
|
||||
importSource,
|
||||
name: node.specifiers.map((spec) => spec.local.name).join(", "),
|
||||
resolvedPath: Array.from(new Set(specifierFixes.map((fix) => fix.path))).join(
|
||||
", "
|
||||
),
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(node, fixes);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noTriggerCoreImportNeedsExport",
|
||||
data: {
|
||||
importSource,
|
||||
name: node.specifiers.map((spec) => spec.local.name).join(", "),
|
||||
resolvedPath: Array.from(
|
||||
new Set(
|
||||
specifierFixes
|
||||
.filter((fix) => !allowedBarrelFiles.includes(fix.path))
|
||||
.map((fix) => fix.path)
|
||||
)
|
||||
).join(", "),
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noTriggerCoreImport",
|
||||
data: {
|
||||
importSource,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -18,9 +18,13 @@
|
||||
"update:eslint-docs": "eslint-doc-generator"
|
||||
},
|
||||
"dependencies": {
|
||||
"requireindex": "^1.2.0"
|
||||
"requireindex": "^1.2.0",
|
||||
"tsconfig-paths": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/parser": "^7.24.7",
|
||||
"@babel/traverse": "^7.24.7",
|
||||
"@types/babel__traverse": "^7.20.6",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-doc-generator": "^1.0.0",
|
||||
"eslint-plugin-eslint-plugin": "^5.0.0",
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @fileoverview Prevent importing from `@trigger.dev/core` directly
|
||||
* @author
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const rule = require("../../../lib/rules/no-trigger-core-import"),
|
||||
RuleTester = require("eslint").RuleTester;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaVersion: 2020,
|
||||
},
|
||||
});
|
||||
ruleTester.run("no-trigger-core-import", rule, {
|
||||
valid: [
|
||||
{
|
||||
code: `import { conditionallyImportPacket, parsePacket } from "@trigger.dev/core/v3/utils/ioSerialization";`,
|
||||
},
|
||||
],
|
||||
|
||||
invalid: [
|
||||
{
|
||||
code: `import { parsePacket } from '@trigger.dev/core/v3';`,
|
||||
output: `import { parsePacket } from '@trigger.dev/core/v3/utils/ioSerialization';`,
|
||||
errors: [
|
||||
{
|
||||
messageId: "noTriggerCoreImportFixable",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `import { CreateBackgroundWorkerRequestBody, TaskResource } from '@trigger.dev/core/v3';`,
|
||||
output: `import { CreateBackgroundWorkerRequestBody } from '@trigger.dev/core/v3/schemas';
|
||||
import { TaskResource } from '@trigger.dev/core/v3/schemas';`,
|
||||
errors: [
|
||||
{
|
||||
messageId: "noTriggerCoreImportFixable",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `import { CreateBackgroundWorkerRequestBody, stringifyIO } from '@trigger.dev/core/v3';`,
|
||||
output: `import { CreateBackgroundWorkerRequestBody } from '@trigger.dev/core/v3/schemas';
|
||||
import { stringifyIO } from '@trigger.dev/core/v3/utils/ioSerialization';`,
|
||||
errors: [
|
||||
{
|
||||
messageId: "noTriggerCoreImportFixable",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `import {
|
||||
isExceptionSpanEvent,
|
||||
ExceptionEventProperties,
|
||||
SpanEvent as OtelSpanEvent,
|
||||
} from "@trigger.dev/core/v3";`,
|
||||
output: `import { isExceptionSpanEvent } from '@trigger.dev/core/v3/schemas';
|
||||
import { ExceptionEventProperties } from '@trigger.dev/core/v3/schemas';
|
||||
import { SpanEvent as OtelSpanEvent } from '@trigger.dev/core/v3/schemas';`,
|
||||
errors: [
|
||||
{
|
||||
messageId: "noTriggerCoreImportFixable",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals"],
|
||||
"lib": ["ES2019"],
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "NodeNext",
|
||||
"module": "NodeNext",
|
||||
"resolveJsonModule": true,
|
||||
"target": "ES2019",
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"baseUrl": ".",
|
||||
// Need paths for the no-trigger-core-import plugin
|
||||
"paths": {
|
||||
"~/*": ["./app/*"],
|
||||
"@/*": ["./*"],
|
||||
"@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"],
|
||||
"@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"],
|
||||
"@trigger.dev/core": ["../../packages/core/src/index"],
|
||||
"@trigger.dev/core/*": ["../../packages/core/src/*"],
|
||||
"@trigger.dev/core-backend": ["../../packages/core-backend/src/index"],
|
||||
"@trigger.dev/core-backend/*": ["../../packages/core-backend/src/*"],
|
||||
"@trigger.dev/database": ["../../packages/database/src/index"],
|
||||
"@trigger.dev/database/*": ["../../packages/database/src/*"],
|
||||
"@trigger.dev/yalt": ["../../packages/yalt/src/index"],
|
||||
"@trigger.dev/yalt/*": ["../../packages/yalt/src/*"],
|
||||
"@trigger.dev/otlp-importer": ["../../packages/otlp-importer/src/index"],
|
||||
"@trigger.dev/otlp-importer/*": ["../../packages/otlp-importer/src/*"],
|
||||
"emails": ["../../packages/emails/src/index"],
|
||||
"emails/*": ["../../packages/emails/src/*"]
|
||||
},
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
Generated
+848
-121
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user