Skip to content

Commit 3ddf10f

Browse files
Reworks Twoslasher instance creation (#103)
* refactor: streamline Twoslash instance creation and caching logic * chore: add changeset for fixing Twoslasher instance creation * refactor: initialize Twoslash-related variables and improve module resolution logic * refactor: remove unused variables and simplify Twoslash function logic * refactor: enhance Twoslasher instance management and caching logic * docs: add note on compilerOptions.lib for Astro build errors * refactor: update tsdown configuration to use deps for onlyAllowBundle * docs: update installation guide to address memory issues during Astro builds * refactor: improve ecEngine initialization and clarify singleton usage in ecTwoSlash * refactor: initialize includesMap only if it is not already set * refactor: lazy load createTwoslasher to improve performance * refactor: update file patterns in labeler.yml for improved matching
1 parent b0691dc commit 3ddf10f

7 files changed

Lines changed: 173 additions & 64 deletions

File tree

.changeset/light-months-doubt.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"expressive-code-twoslash": patch
3+
---
4+
5+
Fixes Twoslasher instance creation

.github/labeler.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,29 @@
33
css-js-gen:
44
- changed-files:
55
- any-glob-to-any-file:
6-
- packages/css-js-gen/*
6+
- packages/css-js-gen/**
77

88
docs:
99
- changed-files:
1010
- any-glob-to-any-file:
11-
- docs/*
11+
- docs/**
1212

1313
ec-ts:twoslash:
1414
- changed-files:
1515
- any-glob-to-any-file:
16-
- packages/@ec-ts/twoslash/*
16+
- packages/@ec-ts/twoslash/**
1717

1818
ec-ts:twoslash-vue:
1919
- changed-files:
2020
- any-glob-to-any-file:
21-
- packages/@ec-ts/twoslash-vue/*
21+
- packages/@ec-ts/twoslash-vue/**
2222

2323
ec-ts:vfs:
2424
- changed-files:
2525
- any-glob-to-any-file:
26-
- packages/@ec-ts/vfs/*
26+
- packages/@ec-ts/vfs/**
2727

2828
twoslash:
2929
- changed-files:
3030
- any-glob-to-any-file:
31-
- packages/expressive-code-twoslash/*
31+
- packages/expressive-code-twoslash/**

docs/src/content/docs/getting-started/installation.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,4 +176,16 @@ ecTwoSlash({
176176
});
177177
```
178178

179+
#### A note about Memory Issues
179180

181+
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:
182+
183+
```ts
184+
lib: ["lib.es2022.d.ts", "lib.dom.d.ts", "lib.dom.iterable.d.ts"],
185+
```
186+
187+
Another option is to set the `NODE_OPTIONS` environment variable to increase the memory limit for Node.js. For example:
188+
189+
```bash
190+
NODE_OPTIONS="--max-old-space-size=8192" astro build
191+
```

packages/@ec-ts/twoslash/tsdown.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@ import { sharedConfig } from "../../../tsdown.shared.ts";
44
export default defineConfig({
55
...sharedConfig,
66
entry: ["./src/index.ts", "./src/core.ts", "./src/fallback.ts"],
7-
inlineOnly: ["ohash"],
7+
deps: {
8+
onlyAllowBundle: ["ohash"],
9+
},
810
});

packages/css-js-gen/tsdown.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@ import { sharedConfig } from "../../tsdown.shared.ts";
44
export default defineConfig({
55
...sharedConfig,
66
entry: ["./src/index.ts"],
7-
inlineOnly: ["csstype"],
7+
deps: {
8+
onlyAllowBundle: ["csstype"],
9+
},
810
});

packages/expressive-code-twoslash/src/helpers/utils.ts

Lines changed: 108 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,9 @@ import type {
66
TwoslashInstance,
77
TwoslashOptions,
88
} from "@ec-ts/twoslash";
9-
import { createTwoslasher } from "@ec-ts/twoslash";
10-
import { createTwoslasher as createTwoslasherVue } from "@ec-ts/twoslash-vue";
9+
import type { CreateTwoslashVueOptions } from "@ec-ts/twoslash-vue";
1110
import type { ExpressiveCodeBlock } from "@expressive-code/core";
12-
import {
13-
type CreateTwoslashESLintOptions,
14-
createTwoslasher as createTwoslasherEslint,
15-
} from "twoslash-eslint";
11+
import type { CreateTwoslashESLintOptions } from "twoslash-eslint";
1612
import ts from "typescript";
1713
import type { PluginTwoslashOptions } from "../types.ts";
1814
import { reTrigger, twoslashDefaultTags } from "./regex.ts";
@@ -54,50 +50,121 @@ export function checkForCustomTagsAndMerge(twoslashOptions: TwoslashOptions | un
5450
} as TwoslashOptions;
5551
}
5652

53+
export type TwoslasherThunk = () => Promise<
54+
TwoslashInstance | TwoslashGenericFunction<TwoslashExecuteOptions>
55+
>;
56+
5757
/**
5858
* Interface representing the data structure for a Twoslash instance, including its trigger pattern, supported languages, and the corresponding twoslasher functions.
5959
*/
6060
export interface TwoslashMapData {
6161
trigger: RegExp;
6262
languages: readonly string[];
6363
twoslashers: (options: TwoslashOptions) => {
64-
default: TwoslashGenericFunction<TwoslashExecuteOptions>;
65-
[key: string]: TwoslashGenericFunction<TwoslashExecuteOptions>;
64+
default: TwoslasherThunk;
65+
[key: string]: TwoslasherThunk;
6666
};
6767
}
6868

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

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

73+
type TwoslashInstanceMap = Map<string, TwoslasherThunk>;
74+
75+
/**
76+
* 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.
77+
*/
78+
const getBaseTwoslasher = (
79+
opts: TwoslashOptions | undefined,
80+
TwoslasherMap: TwoslashInstanceMap,
81+
): TwoslasherThunk => {
82+
const key = "twoslash";
83+
const twoslasher = TwoslasherMap.get(key);
84+
if (!twoslasher) {
85+
const instance = async () => (await import("@ec-ts/twoslash")).createTwoslasher(opts);
86+
TwoslasherMap.set(key, instance);
87+
return instance;
88+
}
89+
return twoslasher;
90+
};
91+
92+
/**
93+
* 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.
94+
*/
95+
const getVueTwoslasher = (
96+
opts: CreateTwoslashVueOptions | undefined,
97+
TwoslasherMap: TwoslashInstanceMap,
98+
): TwoslasherThunk => {
99+
const key = "twoslash-vue";
100+
const twoslasher = TwoslasherMap.get(key);
101+
if (!twoslasher) {
102+
try {
103+
const instance = async () => (await import("@ec-ts/twoslash-vue")).createTwoslasher(opts);
104+
TwoslasherMap.set(key, instance);
105+
return instance;
106+
} catch (error) {
107+
console.error("Failed to load twoslash-vue:", error);
108+
throw new Error("Failed to load twoslash-vue. Please ensure vue is installed and try again.");
109+
}
110+
}
111+
return twoslasher;
112+
};
113+
114+
/**
115+
* 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.
116+
*/
117+
const getEslintTwoslasher = (
118+
opts: CreateTwoslashVueOptions | undefined,
119+
TwoslasherMap: TwoslashInstanceMap,
120+
): TwoslasherThunk => {
121+
const key = "eslint";
122+
const twoslasher = TwoslasherMap.get(key);
123+
if (!twoslasher) {
124+
try {
125+
const instance = async () =>
126+
(await import("twoslash-eslint")).createTwoslasher(
127+
opts as CreateTwoslashESLintOptions,
128+
) as TwoslashGenericFunction<TwoslashExecuteOptions>;
129+
TwoslasherMap.set(key, instance);
130+
return instance;
131+
} catch (error) {
132+
console.error("Failed to load twoslash-eslint:", error);
133+
throw new Error(
134+
"Failed to load twoslash-eslint. Please ensure eslint is installed and try again.",
135+
);
136+
}
137+
}
138+
return twoslasher;
139+
};
140+
73141
/**
74142
* A map that holds the configuration for built-in twoslash instances, including their trigger patterns, supported languages, and the corresponding twoslasher functions.
75143
*/
76-
export const TwoslashInstanceMap = new Map<BuiltInTwoslashers, TwoslashMapData>([
77-
[
78-
"twoslash",
79-
{
80-
trigger: reTrigger,
81-
languages: ["ts", "tsx", "vue"],
82-
twoslashers: (options: TwoslashOptions) => ({
83-
default: createTwoslasher(options),
84-
vue: createTwoslasherVue(options),
85-
}),
86-
},
87-
],
88-
[
89-
"eslint",
90-
{
91-
trigger: /\beslint\b/,
92-
languages: ["ts", "tsx"],
93-
twoslashers: (options: TwoslashOptions) => ({
94-
default: createTwoslasherEslint(
95-
options as CreateTwoslashESLintOptions,
96-
) as TwoslashGenericFunction<TwoslashExecuteOptions>,
97-
}),
98-
},
99-
],
100-
]);
144+
export const TwoslashInstanceMap = (TwoslasherMap: TwoslashInstanceMap) =>
145+
new Map<BuiltInTwoslashers, TwoslashMapData>([
146+
[
147+
"twoslash",
148+
{
149+
trigger: reTrigger,
150+
languages: ["ts", "tsx", "vue"],
151+
twoslashers: (options: TwoslashOptions) => ({
152+
default: getBaseTwoslasher(options, TwoslasherMap),
153+
vue: getVueTwoslasher(options, TwoslasherMap),
154+
}),
155+
},
156+
],
157+
[
158+
"eslint",
159+
{
160+
trigger: /\beslint\b/,
161+
languages: ["ts", "tsx"],
162+
twoslashers: (options: TwoslashOptions) => ({
163+
default: getEslintTwoslasher(options, TwoslasherMap),
164+
}),
165+
},
166+
],
167+
]);
101168

102169
/**
103170
* Retrieves a twoslasher instance configuration and applies optional overrides from plugin options.
@@ -114,8 +181,11 @@ export const TwoslashInstanceMap = new Map<BuiltInTwoslashers, TwoslashMapData>(
114181
const __getTwoslasherAndOverride = (
115182
key: BuiltInTwoslashers,
116183
opts: PluginTwoslashOptions["instanceConfigs"],
184+
TwoslasherMap: TwoslashInstanceMap,
117185
) => {
118-
const data = TwoslashInstanceMap.get(key);
186+
const _TwoslashInstanceMap = TwoslashInstanceMap(TwoslasherMap);
187+
188+
const data = _TwoslashInstanceMap.get(key);
119189

120190
if (!data) {
121191
throw new Error(`No twoslash instance found for key: ${key}`);
@@ -152,17 +222,18 @@ const __getTwoslasherAndOverride = (
152222
export const getTwoslasher = (
153223
opts: PluginTwoslashOptions["instanceConfigs"],
154224
options: TwoslashOptions,
225+
TwoslasherMap: TwoslashInstanceMap,
155226
) => {
156227
const twoslashersMap = BuiltInTwoslashers.reduce(
157228
(acc, key) => {
158-
const twoslasher = __getTwoslasherAndOverride(key, opts);
229+
const twoslasher = __getTwoslasherAndOverride(key, opts, TwoslasherMap);
159230
acc[key] = twoslasher;
160231
return acc;
161232
},
162233
{} as Record<string, TwoslashMapData>,
163234
);
164235

165-
return <A>(
236+
return async <A>(
166237
codeBlock: ExpressiveCodeBlock,
167238
fn: (
168239
transformer: TwoslashInstance | TwoslashGenericFunction<TwoslashExecuteOptions>,
@@ -175,7 +246,7 @@ export const getTwoslasher = (
175246
if (languages.includes(codeBlock.language) && trigger.test(codeBlock.meta)) {
176247
const transformer =
177248
twoslashers(options)[codeBlock.language] ?? twoslashers(options).default;
178-
return fn(transformer, key);
249+
return fn(await transformer(), key);
179250
}
180251
}
181252
return null;

packages/expressive-code-twoslash/src/index.ts

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
renderJSDocs,
3434
renderType,
3535
resolveTsconfigPath,
36+
type TwoslasherThunk,
3637
TwoslashIncludesManager,
3738
} from "./helpers/index.ts";
3839
import floatingUiCore from "./module-code/floating-ui-core.min.ts";
@@ -69,6 +70,14 @@ const defaultCompilerOptions: CompilerOptions = {
6970
lib: ["lib.es2022.d.ts", "lib.dom.d.ts", "lib.dom.iterable.d.ts"],
7071
};
7172

73+
// Singletons to hold tsLibDirectory, includesMap, and the Expressive Code Engine instance for use across the plugin
74+
let tsLibDirectory: string;
75+
let includesMap: Map<string, string>;
76+
let ecEngine: ExpressiveCode;
77+
78+
// Map to hold the Twoslasher functions for different triggers, keyed by the trigger name (e.g., "default", "vue", "eslint")
79+
const TwoslasherMap = new Map<string, TwoslasherThunk>();
80+
7281
/**
7382
* Add Twoslash support to your Expressive Code TypeScript code blocks.
7483
*
@@ -99,20 +108,31 @@ export default function ecTwoSlash(options: PluginTwoslashOptions = {}): Express
99108
} = options;
100109

101110
// Get the Twoslash transformation function based on the provided instance configurations and options
102-
const shouldTransform = getTwoslasher(instanceConfigs, {
103-
...twoslashOptions,
104-
...twoslashVueOptions,
105-
...twoslashEslintOptions,
106-
});
111+
const shouldTransform = getTwoslasher(
112+
instanceConfigs,
113+
{
114+
...twoslashOptions,
115+
...twoslashVueOptions,
116+
...twoslashEslintOptions,
117+
},
118+
TwoslasherMap,
119+
);
107120

108-
// Get the TSConfig path for getting the default library files for Twoslash
109-
const _TsConfigPath = resolveTsconfigPath(cwd, tsConfigPath);
121+
if (!tsLibDirectory) {
122+
// Get the TSConfig path for getting the default library files for Twoslash
123+
const _TsConfigPath = resolveTsconfigPath(cwd, tsConfigPath);
110124

111-
// Get the default compiler options from the parsed TSConfig, which includes the default library files for Twoslash
112-
const { options: baseCompilerOptions } = parseSnippetTsconfig(_TsConfigPath);
125+
// Get the default compiler options from the parsed TSConfig, which includes the default library files for Twoslash
126+
const { options: baseCompilerOptions } = parseSnippetTsconfig(_TsConfigPath);
113127

114-
// Get the directory of the default library files for Twoslash, which is needed for proper module resolution in Twoslash
115-
const tsLibDirectory = path.dirname(ts.getDefaultLibFilePath(baseCompilerOptions));
128+
// Get the directory of the default library files for Twoslash, which is needed for proper module resolution in Twoslash
129+
tsLibDirectory = path.dirname(ts.getDefaultLibFilePath(baseCompilerOptions));
130+
}
131+
132+
// Map to hold the includes for Twoslash code blocks, keyed by the include name
133+
if (!includesMap) {
134+
includesMap = new Map<string, string>();
135+
}
116136

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

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

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

150169
// Twoslash the code block
151170
const twoslash = twoslasher(codeWithIncludes, extension, {
152-
cache: twoslashCache,
153171
tsLibDirectory,
154-
fsMap,
155172
...twoslashOptions,
156173
compilerOptions: {
157174
...defaultCompilerOptions,

0 commit comments

Comments
 (0)