Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/light-months-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"expressive-code-twoslash": patch
---

Fixes Twoslasher instance creation
12 changes: 6 additions & 6 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,29 @@
css-js-gen:
- changed-files:
- any-glob-to-any-file:
- packages/css-js-gen/*
- packages/css-js-gen/**

docs:
- changed-files:
- any-glob-to-any-file:
- docs/*
- docs/**

ec-ts:twoslash:
- changed-files:
- any-glob-to-any-file:
- packages/@ec-ts/twoslash/*
- packages/@ec-ts/twoslash/**

ec-ts:twoslash-vue:
- changed-files:
- any-glob-to-any-file:
- packages/@ec-ts/twoslash-vue/*
- packages/@ec-ts/twoslash-vue/**

ec-ts:vfs:
- changed-files:
- any-glob-to-any-file:
- packages/@ec-ts/vfs/*
- packages/@ec-ts/vfs/**

twoslash:
- changed-files:
- any-glob-to-any-file:
- packages/expressive-code-twoslash/*
- packages/expressive-code-twoslash/**
12 changes: 12 additions & 0 deletions docs/src/content/docs/getting-started/installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,16 @@ ecTwoSlash({
});
```

#### A note about Memory Issues

If you are running Astro, and run into build errors about running out of memory, you may need to change the `lib` option to be more specific or empty. By default, EC Twoslash includes the following libs:

```ts
lib: ["lib.es2022.d.ts", "lib.dom.d.ts", "lib.dom.iterable.d.ts"],
```

Another option is to set the `NODE_OPTIONS` environment variable to increase the memory limit for Node.js. For example:

```bash
NODE_OPTIONS="--max-old-space-size=8192" astro build
```
4 changes: 3 additions & 1 deletion packages/@ec-ts/twoslash/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ import { sharedConfig } from "../../../tsdown.shared.ts";
export default defineConfig({
...sharedConfig,
entry: ["./src/index.ts", "./src/core.ts", "./src/fallback.ts"],
inlineOnly: ["ohash"],
deps: {
onlyAllowBundle: ["ohash"],
},
});
4 changes: 3 additions & 1 deletion packages/css-js-gen/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ import { sharedConfig } from "../../tsdown.shared.ts";
export default defineConfig({
...sharedConfig,
entry: ["./src/index.ts"],
inlineOnly: ["csstype"],
deps: {
onlyAllowBundle: ["csstype"],
},
});
145 changes: 108 additions & 37 deletions packages/expressive-code-twoslash/src/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,9 @@ import type {
TwoslashInstance,
TwoslashOptions,
} from "@ec-ts/twoslash";
import { createTwoslasher } from "@ec-ts/twoslash";
import { createTwoslasher as createTwoslasherVue } from "@ec-ts/twoslash-vue";
import type { CreateTwoslashVueOptions } from "@ec-ts/twoslash-vue";
import type { ExpressiveCodeBlock } from "@expressive-code/core";
import {
type CreateTwoslashESLintOptions,
createTwoslasher as createTwoslasherEslint,
} from "twoslash-eslint";
import type { CreateTwoslashESLintOptions } from "twoslash-eslint";
import ts from "typescript";
import type { PluginTwoslashOptions } from "../types.ts";
import { reTrigger, twoslashDefaultTags } from "./regex.ts";
Expand Down Expand Up @@ -54,50 +50,121 @@ export function checkForCustomTagsAndMerge(twoslashOptions: TwoslashOptions | un
} as TwoslashOptions;
}

export type TwoslasherThunk = () => Promise<
TwoslashInstance | TwoslashGenericFunction<TwoslashExecuteOptions>
>;

/**
* Interface representing the data structure for a Twoslash instance, including its trigger pattern, supported languages, and the corresponding twoslasher functions.
*/
export interface TwoslashMapData {
trigger: RegExp;
languages: readonly string[];
twoslashers: (options: TwoslashOptions) => {
default: TwoslashGenericFunction<TwoslashExecuteOptions>;
[key: string]: TwoslashGenericFunction<TwoslashExecuteOptions>;
default: TwoslasherThunk;
[key: string]: TwoslasherThunk;
};
}

export const BuiltInTwoslashers = ["twoslash", "eslint"] as const;

export type BuiltInTwoslashers = (typeof BuiltInTwoslashers)[number];

type TwoslashInstanceMap = Map<string, TwoslasherThunk>;

/**
* Retrieves or creates a base Twoslash instance and caches it in the `TwoslasherMap` for future use. If an instance already exists for the "twoslash" key, it returns the cached instance; otherwise, it creates a new one using the provided options and stores it in the map before returning it.
*/
const getBaseTwoslasher = (
opts: TwoslashOptions | undefined,
TwoslasherMap: TwoslashInstanceMap,
): TwoslasherThunk => {
const key = "twoslash";
const twoslasher = TwoslasherMap.get(key);
if (!twoslasher) {
const instance = async () => (await import("@ec-ts/twoslash")).createTwoslasher(opts);
TwoslasherMap.set(key, instance);
return instance;
}
return twoslasher;
};

/**
* Retrieves or creates a Twoslash instance specific to Vue and caches it in the `TwoslasherMap` for future use. If an instance already exists for the "twoslash-vue" key, it returns the cached instance; otherwise, it attempts to create a new one using the provided options and stores it in the map before returning it. If the module fails to load, it logs an error and throws a new error with a user-friendly message.
*/
const getVueTwoslasher = (
opts: CreateTwoslashVueOptions | undefined,
TwoslasherMap: TwoslashInstanceMap,
): TwoslasherThunk => {
const key = "twoslash-vue";
const twoslasher = TwoslasherMap.get(key);
if (!twoslasher) {
try {
const instance = async () => (await import("@ec-ts/twoslash-vue")).createTwoslasher(opts);
TwoslasherMap.set(key, instance);
return instance;
} catch (error) {
console.error("Failed to load twoslash-vue:", error);
throw new Error("Failed to load twoslash-vue. Please ensure vue is installed and try again.");
}
}
return twoslasher;
};

/**
* Retrieves or creates a Twoslash instance specific to ESLint and caches it in the `TwoslasherMap` for future use. If an instance already exists for the "eslint" key, it returns the cached instance; otherwise, it attempts to create a new one using the provided options and stores it in the map before returning it. If the module fails to load, it logs an error and throws a new error with a user-friendly message.
*/
const getEslintTwoslasher = (
opts: CreateTwoslashVueOptions | undefined,
TwoslasherMap: TwoslashInstanceMap,
): TwoslasherThunk => {
const key = "eslint";
const twoslasher = TwoslasherMap.get(key);
if (!twoslasher) {
try {
const instance = async () =>
(await import("twoslash-eslint")).createTwoslasher(
opts as CreateTwoslashESLintOptions,
) as TwoslashGenericFunction<TwoslashExecuteOptions>;
TwoslasherMap.set(key, instance);
return instance;
} catch (error) {
console.error("Failed to load twoslash-eslint:", error);
throw new Error(
"Failed to load twoslash-eslint. Please ensure eslint is installed and try again.",
);
}
}
return twoslasher;
};

/**
* A map that holds the configuration for built-in twoslash instances, including their trigger patterns, supported languages, and the corresponding twoslasher functions.
*/
export const TwoslashInstanceMap = new Map<BuiltInTwoslashers, TwoslashMapData>([
[
"twoslash",
{
trigger: reTrigger,
languages: ["ts", "tsx", "vue"],
twoslashers: (options: TwoslashOptions) => ({
default: createTwoslasher(options),
vue: createTwoslasherVue(options),
}),
},
],
[
"eslint",
{
trigger: /\beslint\b/,
languages: ["ts", "tsx"],
twoslashers: (options: TwoslashOptions) => ({
default: createTwoslasherEslint(
options as CreateTwoslashESLintOptions,
) as TwoslashGenericFunction<TwoslashExecuteOptions>,
}),
},
],
]);
export const TwoslashInstanceMap = (TwoslasherMap: TwoslashInstanceMap) =>
new Map<BuiltInTwoslashers, TwoslashMapData>([
[
"twoslash",
{
trigger: reTrigger,
languages: ["ts", "tsx", "vue"],
twoslashers: (options: TwoslashOptions) => ({
default: getBaseTwoslasher(options, TwoslasherMap),
vue: getVueTwoslasher(options, TwoslasherMap),
}),
},
],
[
"eslint",
{
trigger: /\beslint\b/,
languages: ["ts", "tsx"],
twoslashers: (options: TwoslashOptions) => ({
default: getEslintTwoslasher(options, TwoslasherMap),
}),
},
],
]);

/**
* Retrieves a twoslasher instance configuration and applies optional overrides from plugin options.
Expand All @@ -114,8 +181,11 @@ export const TwoslashInstanceMap = new Map<BuiltInTwoslashers, TwoslashMapData>(
const __getTwoslasherAndOverride = (
key: BuiltInTwoslashers,
opts: PluginTwoslashOptions["instanceConfigs"],
TwoslasherMap: TwoslashInstanceMap,
) => {
const data = TwoslashInstanceMap.get(key);
const _TwoslashInstanceMap = TwoslashInstanceMap(TwoslasherMap);

const data = _TwoslashInstanceMap.get(key);

if (!data) {
throw new Error(`No twoslash instance found for key: ${key}`);
Expand Down Expand Up @@ -152,17 +222,18 @@ const __getTwoslasherAndOverride = (
export const getTwoslasher = (
opts: PluginTwoslashOptions["instanceConfigs"],
options: TwoslashOptions,
TwoslasherMap: TwoslashInstanceMap,
) => {
const twoslashersMap = BuiltInTwoslashers.reduce(
(acc, key) => {
const twoslasher = __getTwoslasherAndOverride(key, opts);
const twoslasher = __getTwoslasherAndOverride(key, opts, TwoslasherMap);
acc[key] = twoslasher;
return acc;
},
{} as Record<string, TwoslashMapData>,
);

return <A>(
return async <A>(
codeBlock: ExpressiveCodeBlock,
fn: (
transformer: TwoslashInstance | TwoslashGenericFunction<TwoslashExecuteOptions>,
Expand All @@ -175,7 +246,7 @@ export const getTwoslasher = (
if (languages.includes(codeBlock.language) && trigger.test(codeBlock.meta)) {
const transformer =
twoslashers(options)[codeBlock.language] ?? twoslashers(options).default;
return fn(transformer, key);
return fn(await transformer(), key);
}
}
return null;
Expand Down
55 changes: 36 additions & 19 deletions packages/expressive-code-twoslash/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
renderJSDocs,
renderType,
resolveTsconfigPath,
type TwoslasherThunk,
TwoslashIncludesManager,
} from "./helpers/index.ts";
import floatingUiCore from "./module-code/floating-ui-core.min.ts";
Expand Down Expand Up @@ -69,6 +70,14 @@ const defaultCompilerOptions: CompilerOptions = {
lib: ["lib.es2022.d.ts", "lib.dom.d.ts", "lib.dom.iterable.d.ts"],
};

// Singletons to hold tsLibDirectory, includesMap, and the Expressive Code Engine instance for use across the plugin
let tsLibDirectory: string;
let includesMap: Map<string, string>;
let ecEngine: ExpressiveCode;

// Map to hold the Twoslasher functions for different triggers, keyed by the trigger name (e.g., "default", "vue", "eslint")
const TwoslasherMap = new Map<string, TwoslasherThunk>();

/**
* Add Twoslash support to your Expressive Code TypeScript code blocks.
*
Expand Down Expand Up @@ -99,20 +108,31 @@ export default function ecTwoSlash(options: PluginTwoslashOptions = {}): Express
} = options;

// Get the Twoslash transformation function based on the provided instance configurations and options
const shouldTransform = getTwoslasher(instanceConfigs, {
...twoslashOptions,
...twoslashVueOptions,
...twoslashEslintOptions,
});
const shouldTransform = getTwoslasher(
instanceConfigs,
{
...twoslashOptions,
...twoslashVueOptions,
...twoslashEslintOptions,
},
TwoslasherMap,
);

// Get the TSConfig path for getting the default library files for Twoslash
const _TsConfigPath = resolveTsconfigPath(cwd, tsConfigPath);
if (!tsLibDirectory) {
// Get the TSConfig path for getting the default library files for Twoslash
const _TsConfigPath = resolveTsconfigPath(cwd, tsConfigPath);

// Get the default compiler options from the parsed TSConfig, which includes the default library files for Twoslash
const { options: baseCompilerOptions } = parseSnippetTsconfig(_TsConfigPath);
// Get the default compiler options from the parsed TSConfig, which includes the default library files for Twoslash
const { options: baseCompilerOptions } = parseSnippetTsconfig(_TsConfigPath);

// Get the directory of the default library files for Twoslash, which is needed for proper module resolution in Twoslash
const tsLibDirectory = path.dirname(ts.getDefaultLibFilePath(baseCompilerOptions));
// Get the directory of the default library files for Twoslash, which is needed for proper module resolution in Twoslash
tsLibDirectory = path.dirname(ts.getDefaultLibFilePath(baseCompilerOptions));
}

// Map to hold the includes for Twoslash code blocks, keyed by the include name
if (!includesMap) {
includesMap = new Map<string, string>();
}

return definePlugin({
name: "expressive-code-twoslash",
Expand All @@ -123,15 +143,14 @@ export default function ecTwoSlash(options: PluginTwoslashOptions = {}): Express
async preprocessCode({ codeBlock, config }) {
// Check if the code block should be transformed with Twoslash based on the trigger and language
await shouldTransform(codeBlock, async (twoslasher, trigger) => {
// Map to hold the includes for Twoslash code blocks, keyed by the include name
const includesMap = new Map();
const twoslashCache = new Map();
const fsMap = new Map();
// If the EC Engine is not initialized, create a new instance of the Expressive Code Engine for use in the plugin
if (!ecEngine) {
// Create a new instance of the Expressive Code Engine for use in the plugin
ecEngine = new ExpressiveCode(ecConfig(config));
}

// Create a new instance of the TwoslashIncludesManager
const includes = new TwoslashIncludesManager(includesMap);
// Create a new instance of the Expressive Code Engine for use in the plugin
const ecEngine = new ExpressiveCode(ecConfig(config));

// Apply the includes to the code block
const codeWithIncludes = includes.applyInclude(codeBlock.code);
Expand All @@ -149,9 +168,7 @@ export default function ecTwoSlash(options: PluginTwoslashOptions = {}): Express

// Twoslash the code block
const twoslash = twoslasher(codeWithIncludes, extension, {
cache: twoslashCache,
tsLibDirectory,
fsMap,
...twoslashOptions,
compilerOptions: {
...defaultCompilerOptions,
Expand Down
Loading