Compare commits

...

27 Commits

Author SHA1 Message Date
Jacob Paris 92ee40afd5 feature: add no-trigger-core-import 2024-06-21 14:38:17 -04:00
Matt Aitken 61c5cfc4d6 Added denyImports to vite.config 2024-06-19 10:28:38 +01:00
Matt Aitken d38d420ac6 Changed some core imports to type only 2024-06-19 09:48:49 +01:00
Matt Aitken 473b603763 Latest lockfile 2024-06-18 16:26:06 +01:00
Matt Aitken b32a1ae501 Merge remote-tracking branch 'origin/main' into pullrequests/jacobparis/switch-to-vite
# Conflicts:
#	apps/webapp/package.json
#	pnpm-lock.yaml
2024-06-18 16:25:28 +01:00
Matt Aitken 6ffdbc2489 Tidied up imports 2024-06-14 17:12:03 +01:00
Matt Aitken e24e0da52a Merge remote-tracking branch 'origin/main' into pullrequests/jacobparis/switch-to-vite
# Conflicts:
#	apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.schedules.new/route.tsx
#	apps/webapp/tsconfig.json
2024-06-14 17:07:42 +01:00
Matt Aitken 826cbaebc1 Upgrade Vite to 5.3.1 2024-06-14 17:03:16 +01:00
Matt Aitken 86ddd0b053 Latest lockfile 2024-06-14 12:26:01 +01:00
Matt Aitken 9e4d8b2103 Merge remote-tracking branch 'origin/main' into pullrequests/jacobparis/switch-to-vite
# Conflicts:
#	pnpm-lock.yaml
2024-06-14 12:25:27 +01:00
Matt Aitken 51f7466173 Uncomment the TasksTreeView 2024-06-14 12:25:08 +01:00
Matt Aitken 3d6a713e1f Fix for eventRepository import and added removeListener 2024-06-14 12:24:59 +01:00
Matt Aitken 6acdc1297f Set a strict port in vite config 2024-06-14 12:21:38 +01:00
Matt Aitken e01b5bac93 Merge branch 'main' into switch-to-vite 2024-06-14 10:49:23 +01:00
Jacob Paris c02954cd3c Merge branch 'main' into switch-to-vite 2024-06-11 15:06:01 -04:00
Jacob Paris 2d24b758d1 Merge branch 'main' into switch-to-vite 2024-06-10 15:56:36 -04:00
Jacob Paris 5fc5006543 fix: add typings to core 2024-06-10 15:53:56 -04:00
Jacob Paris feba1c9381 fix: add to tsconfig 2024-06-10 15:12:18 -04:00
Jacob Paris 6150efe52e Merge branch 'main' into switch-to-vite 2024-06-10 14:54:04 -04:00
Jacob Paris 8d1cc30c75 fix: import durations from their own file 2024-06-10 14:53:15 -04:00
Matt Aitken fef7bbe031 Merge branch 'main' into switch-to-vite 2024-06-07 11:39:03 +01:00
Jacob Paris 224c7186d5 fix: portals dont support classname 2024-06-05 21:31:40 -04:00
Jacob Paris 673d7cb6eb fix: force include server runtime 2024-06-05 21:19:20 -04:00
Jacob Paris cb6c54936e fix: automate include deps 2024-06-05 21:19:20 -04:00
Jacob Paris 4fa3137872 use remix with vite
fix: delete old server.ts

fix: remove unused imports
2024-06-05 21:06:52 -04:00
Jacob Paris e7ecb43f02 fix: use default import for cjs modules 2024-06-05 21:06:52 -04:00
Jacob Paris 47ac5b8df5 update deps and switch to esm 2024-06-05 21:06:52 -04:00
54 changed files with 2133 additions and 1504 deletions
@@ -9,11 +9,10 @@ const Alert = AlertDialogPrimitive.Root;
const AlertTrigger = AlertDialogPrimitive.Trigger;
const AlertPortal = ({
className,
children,
...props
}: AlertDialogPrimitive.AlertDialogPortalProps) => (
<AlertDialogPrimitive.Portal className={cn(className)} {...props}>
<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>
);
@@ -2,7 +2,7 @@ import type { VirtualElement as IVirtualElement } from "@popperjs/core";
import { ReactNode, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { usePopper } from "react-popper";
import { useEvent } from "react-use";
import useEvent from "react-use/esm/useEvent";
import useLazyRef from "~/hooks/useLazyRef";
// Recharts 3.x will have portal support, but until then we're using this:
+1 -1
View File
@@ -1,5 +1,5 @@
import type { DisplayProperty, StyleName } from "@trigger.dev/core";
import { formatDuration } from "@trigger.dev/core/v3";
import { formatDuration } from "@trigger.dev/core/v3/utils/durations";
import { motion } from "framer-motion";
import { HourglassIcon } from "lucide-react";
import { ReactNode, useEffect, useState } from "react";
@@ -12,7 +12,7 @@ import {
RunPanelIconProperty,
RunPanelIconSection,
} from "./RunCard";
import { formatDuration } from "@trigger.dev/core/v3";
import { formatDuration } from "@trigger.dev/core/v3/utils/durations";
export function RunCompletedDetail({ run }: { run: MatchedRun }) {
return (
@@ -51,7 +51,7 @@ import {
} from "./RunCard";
import { TaskCard } from "./TaskCard";
import { TaskCardSkeleton } from "./TaskCardSkeleton";
import { formatDuration, formatDurationMilliseconds } from "@trigger.dev/core/v3";
import { formatDuration, formatDurationMilliseconds } from "@trigger.dev/core/v3/utils/durations";
type RunOverviewProps = {
run: ViewRun;
+1 -1
View File
@@ -20,7 +20,7 @@ import {
UpdatingDuration,
} from "./RunCard";
import { TaskStatusIcon } from "./TaskStatus";
import { formatDuration } from "@trigger.dev/core/v3";
import { formatDuration } from "@trigger.dev/core/v3/utils/durations";
type TaskCardProps = ViewTask & {
selectedId?: string;
@@ -30,7 +30,7 @@ import { TaskStatusIcon } from "./TaskStatus";
import { ClientOnly } from "remix-utils/client-only";
import { Spinner } from "../primitives/Spinner";
import type { DetailedTask } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam.tasks.$taskParam/route";
import { formatDuration } from "@trigger.dev/core/v3";
import { formatDuration } from "@trigger.dev/core/v3/utils/durations";
export function TaskDetail({ task }: { task: DetailedTask }) {
const {
@@ -11,7 +11,7 @@ import {
RunPanelIconSection,
RunPanelProperties,
} from "./RunCard";
import { DisplayProperty } from "@trigger.dev/core";
import type { DisplayProperty } from "@trigger.dev/core";
export function TriggerDetail({
trigger,
@@ -16,7 +16,7 @@ import {
TableRow,
} from "../primitives/Table";
import { RunStatus } from "./RunStatuses";
import { formatDuration, formatDurationMilliseconds } from "@trigger.dev/core/v3";
import { formatDuration, formatDurationMilliseconds } from "@trigger.dev/core/v3/utils/durations";
type RunTableItem = {
id: string;
@@ -15,7 +15,7 @@ import {
TableRow,
} from "../primitives/Table";
import { RunStatus } from "./RunStatuses";
import { formatDuration } from "@trigger.dev/core/v3";
import { formatDuration } from "@trigger.dev/core/v3/utils/durations";
type RunTableItem = {
id: string;
@@ -1,4 +1,4 @@
import { formatDuration } from "@trigger.dev/core/v3";
import { formatDuration } from "@trigger.dev/core/v3/utils/durations";
import { useEffect, useState } from "react";
export function LiveTimer({
@@ -2,7 +2,7 @@ import {
isExceptionSpanEvent,
type ExceptionEventProperties,
type SpanEvent as OtelSpanEvent,
} from "@trigger.dev/core/v3";
} from "@trigger.dev/core/v3/schemas";
import { CodeBlock } from "~/components/code/CodeBlock";
import { Callout } from "~/components/primitives/Callout";
import { DateTimeAccurate } from "~/components/primitives/DateTime";
@@ -1,5 +1,5 @@
import { ChevronRightIcon } from "@heroicons/react/20/solid";
import { TaskEventStyle } from "@trigger.dev/core/v3";
import type { TaskEventStyle } from "@trigger.dev/core/v3";
import type { TaskEventLevel } from "@trigger.dev/database";
import { Fragment } from "react";
import { RunEvent } from "~/presenters/v3/RunPresenter.server";
@@ -2,7 +2,7 @@ import { ArrowPathIcon, StopCircleIcon } from "@heroicons/react/20/solid";
import { StopIcon } from "@heroicons/react/24/outline";
import { BeakerIcon, BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid";
import { useLocation } from "@remix-run/react";
import { formatDuration } from "@trigger.dev/core/v3";
import { formatDuration } from "@trigger.dev/core/v3/utils/durations";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
import { useEnvironments } from "~/hooks/useEnvironments";
+4 -9
View File
@@ -14,9 +14,9 @@ import {
OperatingSystemContextProvider,
OperatingSystemPlatform,
} from "./components/primitives/OperatingSystemProvider";
import { getSharedSqsEventConsumer } from "./services/events/sqsEventConsumer";
import { singleton } from "./utils/singleton";
import { logger } from "./services/logger.server";
import { initializeWebSocketServer } from "./v3/handleWebsockets.server";
initializeWebSocketServer();
const ABORT_DELAY = 30000;
@@ -180,9 +180,4 @@ function logError(error: unknown, request?: Request) {
}
}
const sqsEventConsumer = singleton("sqsEventConsumer", getSharedSqsEventConsumer);
export { apiRateLimiter } from "./services/apiRateLimit.server";
export { socketIo } from "./v3/handleSocketIo.server";
export { wss } from "./v3/handleWebsockets.server";
export { registryProxy } from "./v3/registryProxy.server";
export { express } from "./express.server";
+66
View File
@@ -0,0 +1,66 @@
import { createExpressApp } from "remix-create-express-app";
import http from "http";
import morgan from "morgan";
import compression from "compression";
import { registryProxy } from "./v3/registryProxy.server";
import { apiRateLimiter } from "./services/apiRateLimit.server";
import { registerSocketIo } from "./socket.server";
export const express = createExpressApp({
configure(app) {
if (process.env.DISABLE_COMPRESSION !== "1") {
app.use(compression());
}
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
app.disable("x-powered-by");
app.use(morgan("tiny"));
process.title = "node webapp-server";
if (!process.env.PORT) {
// TODO: Test REMIX_APP_PORT
process.env.PORT = process.env.REMIX_APP_PORT || "3000";
}
if (process.env.HTTP_SERVER_DISABLED !== "true") {
if (registryProxy && process.env.ENABLE_REGISTRY_PROXY === "true") {
console.log(`🐳 Enabling container registry proxy to ${registryProxy.origin}`);
// Adjusted to match /v2 and any subpath under /v2
app.all("/v2/*", async (req, res) => {
await registryProxy?.call(req, res);
});
// This might also be necessary if you need to explicitly match /v2 as well
app.all("/v2", async (req, res) => {
await registryProxy?.call(req, res);
});
}
app.use((req, res, next) => {
// helpful headers:
res.set("Strict-Transport-Security", `max-age=${60 * 60 * 24 * 365 * 100}`);
// /clean-urls/ -> /clean-urls
if (req.path.endsWith("/") && req.path.length > 1) {
const query = req.url.slice(req.path.length);
const safepath = req.path.slice(0, -1).replace(/\/+/g, "/");
res.redirect(301, safepath + query);
return;
}
next();
});
}
app.use(apiRateLimiter);
},
createServer(app) {
const server = http.createServer(app);
registerSocketIo(server);
return server;
},
// TODO: MISSING if (process.env.DASHBOARD_AND_API_DISABLED !== "true") {
});
@@ -1,5 +1,5 @@
import { ExternalAccount, Integration, TriggerSource } from "@trigger.dev/database";
import { ConnectionAuth } from "@trigger.dev/core";
import type { ConnectionAuth } from "@trigger.dev/core";
import { PrismaClientOrTransaction } from "~/db.server";
import { integrationAuthRepository } from "~/services/externalApis/integrationAuthRepository.server";
import { logger } from "~/services/logger.server";
+2 -2
View File
@@ -1,5 +1,5 @@
import type { JobRun, Task, TaskAttempt } from "@trigger.dev/database";
import { CachedTask, ServerTask } from "@trigger.dev/core";
import type { CachedTask, ServerTask } from "@trigger.dev/core";
import type { Task, TaskAttempt } from "@trigger.dev/database";
export type TaskWithAttempts = Task & {
attempts: TaskAttempt[];
@@ -1,4 +1,4 @@
import { millisecondsToNanoseconds } from "@trigger.dev/core/v3";
import { millisecondsToNanoseconds } from "@trigger.dev/core/v3/utils/durations";
import { createTreeFromFlatItems, flattenTree } from "~/components/primitives/TreeView/TreeView";
import { PrismaClient, prisma } from "~/db.server";
import { getUsername } from "~/utils/username";
+2 -3
View File
@@ -1,11 +1,11 @@
import type { LinksFunction, LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import type { ShouldRevalidateFunction } from "@remix-run/react";
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
import { UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-typedjson";
import { ExternalScripts } from "remix-utils/external-scripts";
import type { ToastMessage } from "~/models/message.server";
import { commitSession, getSession } from "~/models/message.server";
import tailwindStylesheetUrl from "~/tailwind.css";
import tailwindStylesheetUrl from "~/tailwind.css?url";
import { RouteErrorDisplay } from "./components/ErrorDisplay";
import { HighlightInit } from "./components/HighlightInit";
import { AppContainer, MainCenteredContainer } from "./components/layout/AppLayout";
@@ -115,7 +115,6 @@ function App() {
<ScrollRestoration />
<ExternalScripts />
<Scripts />
<LiveReload />
</body>
</html>
</>
@@ -2,7 +2,7 @@ import { CalendarDaysIcon, ReceiptRefundIcon } from "@heroicons/react/20/solid";
import { ArrowUpCircleIcon } from "@heroicons/react/24/outline";
import { Outlet } from "@remix-run/react";
import { ActiveSubscription } from "@trigger.dev/billing";
import { formatDurationInDays } from "@trigger.dev/core/v3";
import { formatDurationInDays } from "@trigger.dev/core/v3/utils/durations";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { LinkButton } from "~/components/primitives/Buttons";
import { DateTime } from "~/components/primitives/DateTime";
@@ -1,7 +1,7 @@
import { ChatBubbleLeftRightIcon, ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/20/solid";
import { useRevalidator } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { formatDurationMilliseconds } from "@trigger.dev/core/v3";
import { formatDurationMilliseconds } from "@trigger.dev/core/v3/utils/durations";
import { TaskRunStatus } from "@trigger.dev/database";
import { Fragment, Suspense, useEffect, useState } from "react";
import { Bar, BarChart, ResponsiveContainer, Tooltip, TooltipProps } from "recharts";
@@ -12,7 +12,7 @@ import {
formatDurationMilliseconds,
millisecondsToNanoseconds,
nanosecondsToMilliseconds,
} from "@trigger.dev/core/v3";
} from "@trigger.dev/core/v3/utils/durations";
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { motion } from "framer-motion";
import { useCallback, useEffect, useRef, useState } from "react";
@@ -3,10 +3,9 @@ import { parse } from "@conform-to/zod";
import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid";
import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react";
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
import { useVirtualizer } from "@tanstack/react-virtual";
import { parseExpression } from "cron-parser";
import cronParser from "cron-parser";
import cronstrue from "cronstrue";
import { useRef, useState } from "react";
import { useState } from "react";
import {
environmentTextClassName,
environmentTitle,
@@ -32,6 +31,7 @@ import {
TableRow,
} from "~/components/primitives/Table";
import { TextLink } from "~/components/primitives/TextLink";
import { TimezoneList } from "~/components/scheduled/timezones";
import { prisma } from "~/db.server";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
@@ -43,8 +43,8 @@ import { ProjectParamSchema, docsPath, v3SchedulesPath } from "~/utils/pathBuild
import { CronPattern, UpsertSchedule } from "~/v3/schedules";
import { UpsertTaskScheduleService } from "~/v3/services/upsertTaskSchedule.server";
import { AIGeneratedCronField } from "../resources.orgs.$organizationSlug.projects.$projectParam.schedules.new.natural-language";
import { TimezoneList } from "~/components/scheduled/timezones";
const { parseExpression } = cronParser;
const cronFormat = `* * * * *
┬ ┬ ┬ ┬ ┬
│ │ │ │ |
@@ -6,7 +6,10 @@ import {
} from "@heroicons/react/20/solid";
import { useParams } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { formatDurationNanoseconds, nanosecondsToMilliseconds } from "@trigger.dev/core/v3";
import {
formatDurationNanoseconds,
nanosecondsToMilliseconds,
} from "@trigger.dev/core/v3/utils/durations";
import { useEffect } from "react";
import { typedjson, useTypedFetcher } from "remix-typedjson";
import { ExitIcon } from "~/assets/icons/ExitIcon";
@@ -1,5 +1,5 @@
import * as Slider from "@radix-ui/react-slider";
import { formatDurationMilliseconds } from "@trigger.dev/core/v3";
import { formatDurationMilliseconds } from "@trigger.dev/core/v3/utils/durations";
import { useState } from "react";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Switch } from "~/components/primitives/Switch";
+1 -1
View File
@@ -1,4 +1,4 @@
import { Prettify } from "@trigger.dev/core";
import type { Prettify } from "@trigger.dev/core";
import { z } from "zod";
import {
RuntimeEnvironment,
@@ -5,10 +5,10 @@ import {
ScheduleMetadataSchema,
} from "@trigger.dev/core";
import { $transaction, PrismaClientOrTransaction, prisma } from "~/db.server";
import { parseExpression } from "cron-parser";
import cronParser from "cron-parser";
import { logger } from "../logger.server";
import { DeliverScheduledEventService } from "./deliverScheduledEvent.server";
const { parseExpression } = cronParser;
export class NextScheduledEventService {
#prismaClient: PrismaClientOrTransaction;
+108
View File
@@ -0,0 +1,108 @@
import type http from "http";
import type { Server as EngineServer } from "engine.io";
import { singleton } from "./utils/singleton";
import { env } from "./env.server";
import { Redis } from "ioredis";
import { createAdapter } from "@socket.io/redis-adapter";
import { Server } from "socket.io";
import { WebSocketServer } from "ws";
// wss and io are used directly in express.server.ts without bundling
// so don't import from /app here
// entry.server.ts gets bundled, so see initializeWebSocketServer() there
// to add extra handlers that register on first request
export const wss = singleton("wss", () => {
return new WebSocketServer({ noServer: true });
});
export const io = singleton("socketIo", () => {
if (!env.REDIS_HOST || !env.REDIS_PORT) {
console.warn("No redis config found, skipping socket.io");
return new Server();
}
const pubClient = new Redis({
port: env.REDIS_PORT,
host: env.REDIS_HOST,
username: env.REDIS_USERNAME,
password: env.REDIS_PASSWORD,
enableAutoPipelining: true,
...(env.REDIS_TLS_DISABLED === "true" ? {} : { tls: {} }),
});
const subClient = pubClient.duplicate();
const io = new Server({
adapter: createAdapter(pubClient, subClient, {
key: "tr:socket.io:",
publishOnSpecificResponseChannel: true,
}),
});
return io;
});
export function registerSocketIo(server: http.Server) {
server.keepAliveTimeout = 65 * 1000;
process.on("SIGTERM", () => {
server.close((err) => {
if (err) {
console.error("Error closing express server:", err);
} else {
console.log("Express server closed gracefully.");
}
});
});
io.on("connection", (socket) => {
console.log(`[socket.io][${socket.id}] connection at url: ${socket.request.url}`);
});
console.log("Attaching socket.io");
io.attach(server);
// prevent duplicate upgrades from listeners created by io.attach()
server.removeAllListeners("upgrade");
server.on("upgrade", async (req, socket, head) => {
console.log(
`Attemping to upgrade connection at url ${req.url} with headers: ${JSON.stringify(
req.headers
)}`
);
socket.on("error", (err) => {
console.error("Connection upgrade error:", err);
});
const url = new URL(req.url ?? "", "http://localhost");
// Upgrade socket.io connection
if (url.pathname.startsWith("/socket.io/")) {
console.log(`Socket.io client connected, upgrading their connection...`);
// https://github.com/socketio/socket.io/issues/4693
(io.engine as EngineServer).handleUpgrade(req, socket, head);
return;
}
// Only upgrade the connecting if the path is `/ws`
if (url.pathname !== "/ws") {
// Setting the socket.destroy() error param causes an error event to be emitted which needs to be handled with socket.on("error") to prevent uncaught exceptions.
socket.destroy(
new Error(
"Cannot connect because of invalid path: Please include `/ws` in the path of your upgrade request."
)
);
return;
}
console.log(`Client connected, upgrading their connection...`);
// Handle the WebSocket connection
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit("connection", ws, req);
});
});
}
+2 -1
View File
@@ -22,7 +22,7 @@ import {
import { Prisma, TaskEvent, TaskEventStatus, type TaskEventKind } from "@trigger.dev/database";
import Redis, { RedisOptions } from "ioredis";
import { createHash } from "node:crypto";
import { EventEmitter } from "node:stream";
import { EventEmitter } from "node:events";
import { $replica, PrismaClient, PrismaReplicaClient, prisma } from "~/db.server";
import { env } from "~/env.server";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
@@ -781,6 +781,7 @@ export class EventRepository {
const unsubscribe = async () => {
await redis.punsubscribe(channel);
redis.quit();
eventEmitter.removeAllListeners();
this._subscriberCount--;
};
+1 -33
View File
@@ -19,20 +19,13 @@ import { findEnvironmentById } from "~/models/runtimeEnvironment.server";
import { CreateDeployedBackgroundWorkerService } from "./services/createDeployedBackgroundWorker.server";
import { ResumeAttemptService } from "./services/resumeAttempt.server";
import { DeploymentIndexFailed } from "./services/deploymentIndexFailed.server";
import { Redis } from "ioredis";
import { createAdapter } from "@socket.io/redis-adapter";
import { CrashTaskRunService } from "./services/crashTaskRun.server";
import { CreateTaskRunAttemptService } from "./services/createTaskRunAttempt.server";
import { io } from "~/socket.server";
export const socketIo = singleton("socketIo", initalizeIoServer);
function initalizeIoServer() {
const io = initializeSocketIOServerInstance();
io.on("connection", (socket) => {
logger.log(`[socket.io][${socket.id}] connection at url: ${socket.request.url}`);
});
const coordinatorNamespace = createCoordinatorNamespace(io);
const providerNamespace = createProviderNamespace(io);
const sharedQueueConsumerNamespace = createSharedQueueConsumerNamespace(io);
@@ -45,31 +38,6 @@ function initalizeIoServer() {
};
}
function initializeSocketIOServerInstance() {
if (env.REDIS_HOST && env.REDIS_PORT) {
const pubClient = new Redis({
port: env.REDIS_PORT,
host: env.REDIS_HOST,
username: env.REDIS_USERNAME,
password: env.REDIS_PASSWORD,
enableAutoPipelining: true,
...(env.REDIS_TLS_DISABLED === "true" ? {} : { tls: {} }),
});
const subClient = pubClient.duplicate();
const io = new Server({
adapter: createAdapter(pubClient, subClient, {
key: "tr:socket.io:",
publishOnSpecificResponseChannel: true,
}),
});
return io;
}
return new Server();
}
function createCoordinatorNamespace(io: Server) {
const coordinator = new ZodNamespace({
// @ts-ignore - for some reason the built ZodNamespace Server type is not compatible with the Server type here, but only when doing typechecking
+14 -15
View File
@@ -7,27 +7,26 @@ import { AuthenticatedSocketConnection } from "./authenticatedSocketConnection.s
import { Gauge } from "prom-client";
import { metricsRegister } from "~/metrics.server";
export const wss = singleton("wss", initalizeWebSocketServer);
import { wss } from "~/socket.server";
let authenticatedConnections: Map<string, AuthenticatedSocketConnection>;
function initalizeWebSocketServer() {
const server = new WebSocketServer({ noServer: true });
export function initializeWebSocketServer() {
return singleton("wss:register", () => {
wss.on("connection", handleWebSocketConnection);
server.on("connection", handleWebSocketConnection);
authenticatedConnections = new Map();
authenticatedConnections = new Map();
new Gauge({
name: "dev_authenticated_connections",
help: "Number of authenticated dev connections",
collect() {
this.set(authenticatedConnections.size);
},
registers: [metricsRegister],
});
new Gauge({
name: "dev_authenticated_connections",
help: "Number of authenticated dev connections",
collect() {
this.set(authenticatedConnections.size);
},
registers: [metricsRegister],
return wss;
});
return server;
}
async function handleWebSocketConnection(ws: WebSocket, req: IncomingMessage) {
+2 -1
View File
@@ -1,6 +1,7 @@
import { parseExpression } from "cron-parser";
import cronParser from "cron-parser";
import { z } from "zod";
const { parseExpression } = cronParser;
export const CronPattern = z.string().refine(
(val) => {
//only allow CRON expressions that don't include seconds (they have 5 parts)
+2 -1
View File
@@ -24,12 +24,13 @@ import {
} from "@opentelemetry/sdk-trace-base";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { SEMRESATTRS_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
import { PrismaInstrumentation } from "@prisma/instrumentation";
import prismaInstrumentation from "@prisma/instrumentation";
import { env } from "~/env.server";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { singleton } from "~/utils/singleton";
import { LoggerSpanExporter } from "./telemetry/loggerExporter.server";
const { PrismaInstrumentation } = prismaInstrumentation;
export const SEMINTATTRS_FORCE_RECORDING = "forceRecording";
class CustomWebappSampler implements Sampler {
@@ -1,5 +1,6 @@
import { parseExpression } from "cron-parser";
import cronParser from "cron-parser";
const { parseExpression } = cronParser;
export function calculateNextScheduledTimestamp(
schedule: string,
timezone: string | null,
+2
View File
@@ -0,0 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="@remix-run/node" />
+35 -24
View File
@@ -3,12 +3,12 @@
"name": "webapp",
"version": "1.0.0",
"sideEffects": false,
"type": "module",
"scripts": {
"build": "run-s build:**",
"build:db:seed": "esbuild --platform=node --bundle --minify --format=cjs ./prisma/seed.ts --outdir=prisma",
"build:remix": "remix build",
"build:server": "esbuild --platform=node --format=cjs ./server.ts --outdir=build",
"dev": "cross-env PORT=3030 remix dev -c \"node ./build/server.js\"",
"build:db:seed": "esbuild --platform=node --bundle --minify --format=esm ./prisma/seed.ts --outdir=prisma",
"build:remix": "remix vite:build",
"dev": "cross-env PORT=3030 vite",
"dev:worker": "cross-env NODE_PATH=../../node_modules/.pnpm/node_modules node ./build/server.js",
"format": "prettier --write .",
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
@@ -17,7 +17,7 @@
"typecheck": "tsc -p ./tsconfig.check.json",
"db:seed": "node prisma/seed.js",
"db:seed:local": "ts-node prisma/seed.ts",
"build:db:populate": "esbuild --platform=node --bundle --minify --format=cjs ./prisma/populate.ts --outdir=prisma",
"build:db:populate": "esbuild --platform=node --bundle --minify --format=esm ./prisma/populate.ts --outdir=prisma",
"db:populate": "node prisma/populate.js --",
"generate:sourcemaps": "remix build --sourcemap",
"clean:sourcemaps": "run-s clean:sourcemaps:*",
@@ -43,6 +43,7 @@
"@codemirror/search": "^6.2.3",
"@codemirror/state": "^6.1.3",
"@codemirror/view": "^6.5.0",
"@conform-to/dom": "0.6.1",
"@conform-to/react": "^0.6.1",
"@conform-to/zod": "^0.6.1",
"@depot/sdk-node": "^0.5.0",
@@ -65,25 +66,25 @@
"@opentelemetry/semantic-conventions": "^1.22.0",
"@popperjs/core": "^2.11.8",
"@prisma/instrumentation": "^5.11.0",
"@radix-ui/react-alert-dialog": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.3",
"@radix-ui/react-label": "^2.0.1",
"@radix-ui/react-popover": "^1.0.5",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-select": "^1.2.1",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.5",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@react-aria/datepicker": "^3.9.1",
"@react-stately/datepicker": "^3.9.1",
"@react-types/datepicker": "^3.7.1",
"@remix-run/express": "2.1.0",
"@remix-run/node": "2.1.0",
"@remix-run/react": "2.1.0",
"@remix-run/router": "^1.15.3",
"@remix-run/serve": "2.1.0",
"@remix-run/server-runtime": "2.1.0",
"@remix-run/express": "2.9.2",
"@remix-run/node": "2.9.2",
"@remix-run/react": "2.9.2",
"@remix-run/router": "^1.16.1",
"@remix-run/serve": "2.9.2",
"@remix-run/server-runtime": "2.9.2",
"@remix-run/v1-meta": "^0.1.3",
"@slack/web-api": "^6.8.1",
"@socket.io/redis-adapter": "^8.3.0",
@@ -108,11 +109,13 @@
"class-variance-authority": "^0.5.2",
"clsx": "^1.2.1",
"compression": "^1.7.4",
"cookie": "^0.6.0",
"cron-parser": "^4.9.0",
"cronstrue": "^2.21.0",
"cross-env": "^7.0.3",
"cuid": "^2.1.8",
"dotenv": "^16.4.5",
"dotenv-expand": "^11.0.6",
"emails": "workspace:*",
"evt": "^2.4.13",
"express": "^4.18.1",
@@ -124,6 +127,7 @@
"ioredis": "^5.3.2",
"isbot": "^3.6.5",
"jose": "^5.4.0",
"js-beautify": "^1.15.1",
"jsonpointer": "^5.0.1",
"jwt-decode": "^4.0.0",
"lodash.omit": "^4.5.0",
@@ -155,10 +159,12 @@
"remix-auth": "^3.6.0",
"remix-auth-email-link": "2.0.2",
"remix-auth-github": "^1.6.0",
"remix-create-express-app": "^0.3.7",
"remix-typedjson": "0.3.1",
"remix-utils": "^7.1.0",
"seedrandom": "^3.0.5",
"semver": "^7.5.0",
"set-cookie-parser": "^2.6.0",
"simple-oauth2": "^5.0.0",
"simplur": "^3.0.1",
"slug": "^6.0.0",
@@ -174,15 +180,16 @@
"tiny-invariant": "^1.2.0",
"ulid": "^2.3.0",
"ulidx": "^2.2.1",
"vite-env-only": "^3.0.1",
"ws": "^8.11.0",
"zod": "3.22.3",
"zod-error": "1.5.0",
"zod-validation-error": "^1.5.0"
},
"devDependencies": {
"@remix-run/dev": "2.1.0",
"@remix-run/eslint-config": "2.1.0",
"@remix-run/testing": "^2.1.0",
"@remix-run/dev": "2.9.2",
"@remix-run/eslint-config": "2.9.2",
"@remix-run/testing": "^2.9.2",
"@swc/core": "^1.3.4",
"@swc/helpers": "^0.4.11",
"@tailwindcss/forms": "^0.5.3",
@@ -217,8 +224,9 @@
"babel-preset-react-app": "^10.0.1",
"css-loader": "^6.10.0",
"datepicker": "link:@types/@react-aria/datepicker",
"deepmerge": "^4.3.1",
"engine.io": "^6.5.4",
"esbuild": "^0.15.10",
"esbuild": "^0.15.18",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"npm-run-all": "^4.1.5",
@@ -227,16 +235,19 @@
"prettier": "^2.8.8",
"prettier-plugin-tailwindcss": "^0.3.0",
"prop-types": "^15.8.1",
"remix-express-dev-server": "^0.2.5",
"rimraf": "^3.0.2",
"style-loader": "^3.3.4",
"tailwind-scrollbar": "^3.0.1",
"tailwindcss": "3.4.1",
"ts-node": "^10.7.0",
"tsconfig-paths": "^3.14.1",
"typescript": "^5.1.6",
"typescript": "^5.4.5",
"vite": "^5.3.1",
"vite-tsconfig-paths": "^4.0.5",
"vitest": "^1.4.0"
},
"engines": {
"node": ">=16.0.0"
}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
module.exports = {
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
-2
View File
@@ -1,2 +0,0 @@
/// <reference types="@remix-run/dev" />
/// <reference types="@remix-run/node/globals" />
-159
View File
@@ -1,159 +0,0 @@
import path from "path";
import express from "express";
import compression from "compression";
import morgan from "morgan";
import { createRequestHandler } from "@remix-run/express";
import { WebSocketServer } from "ws";
import { broadcastDevReady, logDevReady } from "@remix-run/server-runtime";
import type { Server as IoServer } from "socket.io";
import type { Server as EngineServer } from "engine.io";
import { RegistryProxy } from "~/v3/registryProxy.server";
import { RateLimitMiddleware, apiRateLimiter } from "~/services/apiRateLimit.server";
const app = express();
if (process.env.DISABLE_COMPRESSION !== "1") {
app.use(compression());
}
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
app.disable("x-powered-by");
// Remix fingerprints its assets so we can cache forever.
app.use("/build", express.static("public/build", { immutable: true, maxAge: "1y" }));
// Everything else (like favicon.ico) is cached for an hour. You may want to be
// more aggressive with this caching.
app.use(express.static("public", { maxAge: "1h" }));
app.use(morgan("tiny"));
process.title = "node webapp-server";
const MODE = process.env.NODE_ENV;
const BUILD_DIR = path.join(process.cwd(), "build");
const build = require(BUILD_DIR);
const port = process.env.REMIX_APP_PORT || process.env.PORT || 3000;
if (process.env.HTTP_SERVER_DISABLED !== "true") {
const socketIo: { io: IoServer } | undefined = build.entry.module.socketIo;
const wss: WebSocketServer | undefined = build.entry.module.wss;
const registryProxy: RegistryProxy | undefined = build.entry.module.registryProxy;
const apiRateLimiter: RateLimitMiddleware = build.entry.module.apiRateLimiter;
if (registryProxy && process.env.ENABLE_REGISTRY_PROXY === "true") {
console.log(`🐳 Enabling container registry proxy to ${registryProxy.origin}`);
// Adjusted to match /v2 and any subpath under /v2
app.all("/v2/*", async (req, res) => {
await registryProxy.call(req, res);
});
// This might also be necessary if you need to explicitly match /v2 as well
app.all("/v2", async (req, res) => {
await registryProxy.call(req, res);
});
}
app.use((req, res, next) => {
// helpful headers:
res.set("Strict-Transport-Security", `max-age=${60 * 60 * 24 * 365 * 100}`);
// /clean-urls/ -> /clean-urls
if (req.path.endsWith("/") && req.path.length > 1) {
const query = req.url.slice(req.path.length);
const safepath = req.path.slice(0, -1).replace(/\/+/g, "/");
res.redirect(301, safepath + query);
return;
}
next();
});
if (process.env.DASHBOARD_AND_API_DISABLED !== "true") {
app.use(apiRateLimiter);
app.all(
"*",
// @ts-ignore
createRequestHandler({
build,
mode: MODE,
})
);
} else {
// we need to do the health check here at /healthcheck
app.get("/healthcheck", (req, res) => {
res.status(200).send("OK");
});
}
const server = app.listen(port, () => {
console.log(`✅ server ready: http://localhost:${port} [NODE_ENV: ${MODE}]`);
if (MODE === "development") {
broadcastDevReady(build)
.then(() => logDevReady(build))
.catch(console.error);
}
});
server.keepAliveTimeout = 65 * 1000;
process.on("SIGTERM", () => {
server.close((err) => {
if (err) {
console.error("Error closing express server:", err);
} else {
console.log("Express server closed gracefully.");
}
});
});
socketIo?.io.attach(server);
server.removeAllListeners("upgrade"); // prevent duplicate upgrades from listeners created by io.attach()
server.on("upgrade", async (req, socket, head) => {
console.log(
`Attemping to upgrade connection at url ${req.url} with headers: ${JSON.stringify(
req.headers
)}`
);
socket.on("error", (err) => {
console.error("Connection upgrade error:", err);
});
const url = new URL(req.url ?? "", "http://localhost");
// Upgrade socket.io connection
if (url.pathname.startsWith("/socket.io/")) {
console.log(`Socket.io client connected, upgrading their connection...`);
// https://github.com/socketio/socket.io/issues/4693
(socketIo?.io.engine as EngineServer).handleUpgrade(req, socket, head);
return;
}
// Only upgrade the connecting if the path is `/ws`
if (url.pathname !== "/ws") {
// Setting the socket.destroy() error param causes an error event to be emitted which needs to be handled with socket.on("error") to prevent uncaught exceptions.
socket.destroy(
new Error(
"Cannot connect because of invalid path: Please include `/ws` in the path of your upgrade request."
)
);
return;
}
console.log(`Client connected, upgrading their connection...`);
// Handle the WebSocket connection
wss?.handleUpgrade(req, socket, head, (ws) => {
wss?.emit("connection", ws, req);
});
});
} else {
require(BUILD_DIR);
console.log(`✅ app ready (skipping http server)`);
}
+2 -1
View File
@@ -1,6 +1,6 @@
{
"exclude": ["./cypress", "./cypress.config.ts"],
"include": ["remix.env.d.ts", "global.d.ts", "**/*.ts", "**/*.tsx"],
"include": ["env.d.ts", "global.d.ts", "**/*.ts", "**/*.tsx"],
"compilerOptions": {
"types": ["vitest/globals"],
"lib": ["DOM", "DOM.Iterable", "ES2019"],
@@ -8,6 +8,7 @@
"esModuleInterop": true,
"jsx": "react-jsx",
"moduleResolution": "Bundler",
"module": "ESNext",
"resolveJsonModule": true,
"target": "ES2019",
"strict": true,
+150
View File
@@ -0,0 +1,150 @@
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig, Plugin } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { expressDevServer } from "remix-express-dev-server";
import { installGlobals } from "@remix-run/node";
import { config } from "dotenv";
import { expand } from "dotenv-expand";
import { denyImports } from "vite-env-only";
expand(
config({
path: "../../.env",
})
);
installGlobals({ nativeFetch: true });
const MODE = process.env.NODE_ENV;
export default defineConfig({
ssr: {
noExternal: [
// Fixes Error when evaluating SSR module
"react-use",
],
},
optimizeDeps: {
entries: ["./app/entry.client.tsx", "./app/root.tsx", "./app/routes/**/*"],
include: [
// Has cookie and set-cookie-parser deps that need to be optimized
"@remix-run/server-runtime",
],
// Exclude if dep is ESM or runs server-side (just speeds up Vite on navigations)
exclude: [
// Include @prisma/client until @trigger-dev/database is ESM
// https://github.com/prisma/prisma/issues/21474
"@prisma/client",
"@opentelemetry/api-logs",
"@opentelemetry/api",
"@opentelemetry/core",
"@opentelemetry/exporter-trace-otlp-http",
"@opentelemetry/instrumentation-express",
"@opentelemetry/instrumentation-http",
"@opentelemetry/instrumentation",
"@opentelemetry/resources",
"@opentelemetry/sdk-trace-base",
"@opentelemetry/sdk-trace-node",
"@opentelemetry/semantic-conventions",
"@prisma/instrumentation",
"@radix-ui/react-alert-dialog",
"@radix-ui/react-dialog",
"@radix-ui/react-label",
"@radix-ui/react-popover",
"@radix-ui/react-radio-group",
"@radix-ui/react-select",
"@radix-ui/react-slider",
"@radix-ui/react-switch",
"@radix-ui/react-tabs",
"@radix-ui/react-tooltip",
"@react-email/components",
"@react-email/render",
"@remix-run/node",
"@remix-run/server-runtime",
"@slack/web-api",
"@socket.io/redis-adapter",
"@tanstack/react-virtual",
"@team-plain/typescript-sdk",
"@whatwg-node/fetch",
"aws4fetch",
"cron-parser",
"cronstrue",
"evt",
"fsevents",
"graphile-worker",
"https",
"ioredis",
"jsonpointer",
"lodash.omit",
"nanoid",
"ohash",
"parse-duration",
"posthog-node",
"prism-react-renderer",
"prom-client",
"random-words",
"react-use/esm/useEvent",
"remix-auth-email-link",
"remix-auth-github",
"remix-auth",
"resend",
"seedrandom",
"simple-oauth2",
"slug",
"socket.io-client",
"socket.io",
"superjson",
"ulid",
"ulidx",
"util",
"ws",
"zod-error",
],
},
build: {
minify: true,
cssCodeSplit: false,
target: "esnext",
cssMinify: MODE === "production",
rollupOptions: {
external: [/node:.*/, /.*\.node$/, "https", "stream", "crypto", "fsevents", "deepmerge"],
},
},
server: {
hmr: {
port: 8002,
},
strictPort: true,
port: Number(process.env.PORT),
warmup: {
clientFiles: ["./app/entry.client.tsx", "./app/root.tsx", "./app/routes/**/*"],
},
},
plugins: [
expressDevServer({
exportName: "express",
async configureServer(server) {
if (!server) return;
const { registerSocketIo } = await import("./app/socket.server");
registerSocketIo(server);
},
}),
remix({
ignoredRouteFiles: ["**/.*"],
serverModuleFormat: "esm",
}).filter((plugin) => plugin.name !== "remix-dot-server"),
tsconfigPaths() as Plugin,
denyImports({
client: {
specifiers: ["node:stream", "@trigger.dev/core"],
},
server: {
specifiers: ["@trigger.dev/core"],
},
}),
],
});
+5 -1
View File
@@ -56,7 +56,7 @@
"prettier": "^3.0.0",
"tsx": "^3.7.1",
"turbo": "^1.10.3",
"vite": "^4.1.1",
"vite": "^5.3.1",
"vite-tsconfig-paths": "^4.0.5",
"vitest": "^0.28.4"
},
@@ -72,6 +72,10 @@
"tsup@8.0.1": "patches/tsup@8.0.1.patch",
"engine.io-parser@5.2.2": "patches/engine.io-parser@5.2.2.patch",
"graphile-worker@0.16.6": "patches/graphile-worker@0.16.6.patch"
},
"overrides": {
"@vanilla-extract/integration>vite": "5.2.12",
"@vanilla-extract/integration>vite-node": "1.4.0"
}
}
}
+17 -1
View File
@@ -37,6 +37,22 @@
"require": "./dist/v3/otel/index.js",
"types": "./dist/v3/otel/index.d.ts"
},
"./v3/schemas": {
"import": {
"types": "./dist/v3/schemas/index.d.mts",
"default": "./dist/v3/schemas/index.mjs"
},
"require": "./dist/v3/schemas/index.js",
"types": "./dist/v3/schemas/index.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/zodfetch": {
"import": {
"types": "./dist/v3/zodfetch.d.mts",
@@ -167,4 +183,4 @@
"engines": {
"node": ">=18.0.0"
}
}
}
+2
View File
@@ -6,6 +6,8 @@ export default defineConfig({
entry: [
"./src/index.ts",
"./src/v3/index.ts",
"./src/v3/utils/durations.ts",
"./src/v3/schemas/index.ts",
"./src/v3/otel/index.ts",
"./src/v3/zodMessageHandler.ts",
"./src/v3/zodNamespace.ts",
@@ -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,235 @@
/**
* @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 = ["@trigger.dev/core/schemas", "@trigger.dev/core/v3/schemas"];
function resolveSpecifier(importSource, specifier, context) {
const corePath = resolveModulePath(importSource);
const coreDir = path.dirname(corePath);
const resolvedPath = resolveExport(coreDir, specifier);
if (resolvedPath) {
const baseName = "@trigger.dev/core";
const relativePath = resolvedPath.split("packages/core/src/")[1].replace(/\\/g, "/");
const finalPath = `${baseName}/${relativePath}`;
// Check if the resolved path should map to an allowed barrel file
for (const barrelFile of allowedBarrelFiles) {
if (finalPath.startsWith(barrelFile)) {
return barrelFile;
}
}
return removeExtensionAndIndex(finalPath);
}
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 (
node.declaration.type === "VariableDeclaration" &&
node.declaration.declarations.some((decl) => decl.id.name === specifier)
) {
foundPath = filePath;
return;
}
if (
node.declaration.type === "FunctionDeclaration" &&
node.declaration.id.name === specifier
) {
foundPath = filePath;
return;
}
if (
node.declaration.type === "ClassDeclaration" &&
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;
}
function removeExtensionAndIndex(filePath) {
return filePath
.replace(/\/index(\.ts|\.tsx|\.js|\.jsx)$/, "")
.replace(/(\.ts|\.tsx|\.js|\.jsx)$/, "");
}
// Configure tsconfig-paths
function resolveModulePath(sourcePath) {
const cwd = path.resolve(process.cwd());
const tsconfigPath = path.resolve(cwd, "tsconfig.json");
const tsconfig = require(tsconfigPath);
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: {
noTriggerCoreImport: "{{name}} should be imported from '{{resolvedPath}}'",
},
},
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.local.name, context);
if (resolvedPath) {
return {
name: specifier.local.name,
path: resolvedPath,
};
}
return null;
})
.filter(Boolean);
if (specifierFixes.length > 0) {
const fixes = specifierFixes
.map((fix) => {
return `import { ${fix.name} } from '${fix.path}';`;
})
.join("\n");
context.report({
node,
messageId: "noTriggerCoreImport",
data: {
name: node.specifiers.map((spec) => spec.local.name).join(", "),
resolvedPath: fixes,
},
fix(fixer) {
return fixer.replaceText(node, fixes);
},
});
}
}
},
};
},
};
+5 -1
View File
@@ -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,62 @@
/**
* @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: "noTriggerCoreImport",
},
],
},
{
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: "noTriggerCoreImport",
},
],
},
{
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: "noTriggerCoreImport",
},
],
},
],
});
+41
View File
@@ -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
}
}
+1267 -1215
View File
File diff suppressed because it is too large Load Diff