diff --git a/.changeset/new-coins-chew.md b/.changeset/new-coins-chew.md new file mode 100644 index 0000000..71c6438 --- /dev/null +++ b/.changeset/new-coins-chew.md @@ -0,0 +1,7 @@ +--- +"sideffect": patch +--- + +- Remove TypeScript from the Sideffect runtime dependency graph to reduce install size. +- Lazy-load the consuming project's TypeScript parser only when the Vite workflow discovery adapter scans workflow files. +- Avoid a TypeScript peer range so beta and RC releases are not blocked by npm prerelease range matching. diff --git a/bun.lock b/bun.lock index e79b1d0..5977588 100644 --- a/bun.lock +++ b/bun.lock @@ -12,9 +12,6 @@ "packages/sideffect": { "name": "sideffect", "version": "0.2.1", - "dependencies": { - "typescript": ">=5.0.0 <7", - }, "devDependencies": { "@cloudflare/vite-plugin": "^1.40.0", "@cloudflare/workers-types": "4.20260605.1", @@ -23,6 +20,7 @@ "bumpp": "11.1.0", "defu": "6.1.7", "effect": "4.0.0-beta.78", + "typescript": "catalog:", "vite-plus": "catalog:", }, "peerDependencies": { diff --git a/packages/sideffect/package.json b/packages/sideffect/package.json index 8594cfc..e867444 100644 --- a/packages/sideffect/package.json +++ b/packages/sideffect/package.json @@ -33,9 +33,6 @@ "prepublishOnly": "vp run build", "prepare": "vp config" }, - "dependencies": { - "typescript": ">=5.0.0 <7" - }, "devDependencies": { "@cloudflare/vite-plugin": "^1.40.0", "@cloudflare/workers-types": "4.20260605.1", @@ -44,6 +41,7 @@ "bumpp": "11.1.0", "defu": "6.1.7", "effect": "4.0.0-beta.78", + "typescript": "catalog:", "vite-plus": "catalog:" }, "peerDependencies": { diff --git a/packages/sideffect/src/vite/workflow-discovery.ts b/packages/sideffect/src/vite/workflow-discovery.ts index f5a5513..f9d5657 100644 --- a/packages/sideffect/src/vite/workflow-discovery.ts +++ b/packages/sideffect/src/vite/workflow-discovery.ts @@ -1,7 +1,9 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { createRequire } from "node:module"; import { dirname, extname, join, resolve } from "node:path"; -import ts from "typescript"; +import { Cause, Effect, Exit } from "effect"; +import type * as TypeScript from "typescript"; import { validateWorkflowExportName } from "../entrypoints.ts"; import type { WorkflowConfigEntry } from "../types.ts"; @@ -18,6 +20,46 @@ const sourceFileExtensions = new Set([ ]); const declarationFileExtensions = [".d.ts", ".d.mts", ".d.cts"]; const identifierName = /^[$A-Z_a-z][$\w]*$/; +type TypeScriptModule = typeof TypeScript; + +class TypeScriptWorkflowDiscoveryError extends Error { + constructor(cause: unknown) { + super( + 'Sideffect workflow discovery requires TypeScript\'s parser, but the "typescript" package could not be resolved. Install TypeScript in the project that uses `sideffect/vite`, for example `npm install -D typescript`. Runtime usage of `sideffect` and `sideffect/cloudflare` is unaffected.', + { cause }, + ); + this.name = "TypeScriptWorkflowDiscoveryError"; + } +} + +const require = createRequire(import.meta.url); +let loadedTypeScript: TypeScriptModule | undefined; + +function loadTypeScript(): TypeScriptModule { + if (loadedTypeScript) { + return loadedTypeScript; + } + + const loaded = Effect.runSyncExit( + Effect.try({ + try: () => require("typescript") as TypeScriptModule, + catch: (cause) => new TypeScriptWorkflowDiscoveryError(cause), + }), + ); + + if (Exit.isSuccess(loaded)) { + loadedTypeScript = loaded.value; + return loaded.value; + } + + throw Cause.squash(loaded.cause); +} + +const ts = new Proxy({} as TypeScriptModule, { + get(_target, property) { + return loadTypeScript()[property as keyof TypeScriptModule]; + }, +}); /** Workflow source paths scanned for static `Workflow.make(...).toLayer(...)` exports. */ export type WorkflowDiscoveryPaths = Array; @@ -407,7 +449,7 @@ function analyzeWorkflowSourceFile( return { exports, definitionExports, reExports }; } -function collectWorkflowBindings(sourceFile: ts.SourceFile): { +function collectWorkflowBindings(sourceFile: TypeScript.SourceFile): { readonly names: Set; readonly namespaces: Set; } { @@ -446,7 +488,7 @@ function collectWorkflowBindings(sourceFile: ts.SourceFile): { } function collectImportedWorkflowDefinitions( - sourceFile: ts.SourceFile, + sourceFile: TypeScript.SourceFile, filePath: string, visited: Set, ): Map { @@ -470,7 +512,7 @@ function collectImportedWorkflowDefinitions( return definitions; } -function collectLocalImports(sourceFile: ts.SourceFile): Array { +function collectLocalImports(sourceFile: TypeScript.SourceFile): Array { return sourceFile.statements.flatMap((statement) => { if ( !ts.isImportDeclaration(statement) || @@ -515,7 +557,7 @@ function collectLocalImports(sourceFile: ts.SourceFile): Array; readonly namespaces: Set }, workflowDefinitions: Map, ): string | undefined { @@ -538,7 +580,7 @@ function workflowNameFromLayerExpression( } function workflowNameFromMakeCall( - expression: ts.Expression, + expression: TypeScript.Expression, workflowBindings: { readonly names: Set; readonly namespaces: Set }, ): string | undefined { const call = skipOuterExpressions(expression); @@ -595,8 +637,8 @@ function workflowNameFromMakeCall( } function exportSpecifierNames( - declaration: ts.ExportDeclaration, - exports: ts.NamedExports, + declaration: TypeScript.ExportDeclaration, + exports: TypeScript.NamedExports, ): Array<{ readonly imported: string; readonly exported: string }> { if (declaration.isTypeOnly) { return []; @@ -626,7 +668,7 @@ function isLocalSpecifier(specifier: string): boolean { return specifier.startsWith(".") || specifier.startsWith("/"); } -function skipOuterExpressions(expression: ts.Expression): ts.Expression { +function skipOuterExpressions(expression: TypeScript.Expression): TypeScript.Expression { let current = expression; while ( ts.isParenthesizedExpression(current) || @@ -640,7 +682,7 @@ function skipOuterExpressions(expression: ts.Expression): ts.Expression { return current; } -function scriptKindForFile(path: string): ts.ScriptKind { +function scriptKindForFile(path: string): TypeScript.ScriptKind { switch (extname(path)) { case ".tsx": return ts.ScriptKind.TSX;