@joinezco/codeblock 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/assets/fs.worker-DfanUHpQ.js +21 -0
  2. package/dist/assets/{index-MGle_v2x.js → index-BAnLzvMk.js} +1 -1
  3. package/dist/assets/{index-as7ELo0J.js → index-BBC9WDX6.js} +1 -1
  4. package/dist/assets/{index-Dx_VuNNd.js → index-BEXYxRro.js} +1 -1
  5. package/dist/assets/{index-pGm0qkrJ.js → index-BfYmUKH9.js} +1 -1
  6. package/dist/assets/{index-CXFONXS8.js → index-BhaTNAWE.js} +1 -1
  7. package/dist/assets/{index-D5Z27j1C.js → index-CCbYDSng.js} +1 -1
  8. package/dist/assets/{index-Dvu-FFzd.js → index-CIi8tLT6.js} +1 -1
  9. package/dist/assets/{index-C-QhPFHP.js → index-CaANcgI2.js} +1 -1
  10. package/dist/assets/index-CkWzFNzm.js +208 -0
  11. package/dist/assets/{index-N-GE7HTU.js → index-D_XGv9QZ.js} +1 -1
  12. package/dist/assets/{index-DWOBdRjn.js → index-DkmiPfkD.js} +1 -1
  13. package/dist/assets/{index-CGx5MZO7.js → index-DmNlLMQ4.js} +1 -1
  14. package/dist/assets/{index-I0dlv-r3.js → index-DmX_vI7D.js} +1 -1
  15. package/dist/assets/{index-9HdhmM_Y.js → index-DogEEevD.js} +1 -1
  16. package/dist/assets/{index-aEsF5o-7.js → index-DsDl5qZV.js} +1 -1
  17. package/dist/assets/{index-gUUzXNuP.js → index-gAy5mDg-.js} +1 -1
  18. package/dist/assets/{index-CIuq3uTk.js → index-i5qJLB2h.js} +1 -1
  19. package/dist/assets/javascript.worker-ClsyHOLi.js +552 -0
  20. package/dist/e2e/editor.spec.d.ts +1 -0
  21. package/dist/e2e/editor.spec.js +309 -0
  22. package/dist/editor.d.ts +9 -3
  23. package/dist/editor.js +83 -15
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.html +2 -3
  26. package/dist/index.js +3 -0
  27. package/dist/lsps/typescript.d.ts +3 -1
  28. package/dist/lsps/typescript.js +8 -17
  29. package/dist/panels/footer.d.ts +16 -0
  30. package/dist/panels/footer.js +258 -0
  31. package/dist/panels/toolbar.d.ts +15 -2
  32. package/dist/panels/toolbar.js +528 -115
  33. package/dist/panels/toolbar.test.js +20 -14
  34. package/dist/rpc/transport.d.ts +2 -11
  35. package/dist/rpc/transport.js +19 -35
  36. package/dist/themes/index.js +181 -14
  37. package/dist/themes/vscode.js +3 -2
  38. package/dist/utils/fs.d.ts +15 -3
  39. package/dist/utils/fs.js +85 -6
  40. package/dist/utils/lsp.d.ts +26 -15
  41. package/dist/utils/lsp.js +79 -44
  42. package/dist/utils/search.d.ts +2 -0
  43. package/dist/utils/search.js +13 -4
  44. package/dist/utils/typescript-defaults.d.ts +57 -0
  45. package/dist/utils/typescript-defaults.js +208 -0
  46. package/dist/utils/typescript-defaults.test.d.ts +1 -0
  47. package/dist/utils/typescript-defaults.test.js +197 -0
  48. package/dist/workers/fs.worker.js +14 -18
  49. package/dist/workers/javascript.worker.js +11 -9
  50. package/package.json +4 -3
  51. package/dist/assets/fs.worker-BwEqZcql.ts +0 -109
  52. package/dist/assets/index-C3BnE2cG.js +0 -222
  53. package/dist/assets/javascript.worker-C1zGArKk.js +0 -527
  54. package/dist/snapshot.bin +0 -0
package/dist/utils/lsp.js CHANGED
@@ -1,74 +1,109 @@
1
1
  import * as Comlink from 'comlink';
2
- import { LanguageServerClient, languageServerWithClient } from "@marimo-team/codemirror-languageserver";
3
- import MessagePortTransport from "../rpc/transport";
4
- import { HighlightStyle } from "@codemirror/language";
5
- import { languageSupportCompartment, renderMarkdownCode } from "../editor";
6
- import markdownit from 'markdown-it';
7
- import { vscodeLightDark } from "../themes/vscode";
2
+ import { LSPClient, languageServerExtensions } from "@codemirror/lsp-client";
3
+ import { messagePortTransport } from "../rpc/transport";
8
4
  const clients = new Map();
9
- // TODO: better fix for this reference sticking around to prevent Comlink from releasing the port
10
- export const languageServerFactory = new Map();
5
+ // FileChangeType from LSP spec
6
+ export const FileChangeType = { Created: 1, Changed: 2, Deleted: 3 };
7
+ const MAX_LOG_ENTRIES = 200;
8
+ const lspLogBuffer = [];
9
+ const lspLogListeners = new Set();
10
+ export var LspLog;
11
+ (function (LspLog) {
12
+ function entries() {
13
+ return lspLogBuffer;
14
+ }
15
+ LspLog.entries = entries;
16
+ function push(level, message) {
17
+ lspLogBuffer.push({ timestamp: Date.now(), level, message });
18
+ if (lspLogBuffer.length > MAX_LOG_ENTRIES) {
19
+ lspLogBuffer.splice(0, lspLogBuffer.length - MAX_LOG_ENTRIES);
20
+ }
21
+ for (const listener of lspLogListeners)
22
+ listener();
23
+ }
24
+ LspLog.push = push;
25
+ function clear() {
26
+ lspLogBuffer.length = 0;
27
+ for (const listener of lspLogListeners)
28
+ listener();
29
+ }
30
+ LspLog.clear = clear;
31
+ function subscribe(fn) {
32
+ lspLogListeners.add(fn);
33
+ return () => { lspLogListeners.delete(fn); };
34
+ }
35
+ LspLog.subscribe = subscribe;
36
+ })(LspLog || (LspLog = {}));
37
+ // Cached factory (Comlink-wrapped) and LSP port per language
38
+ const languageServerFactory = new Map();
39
+ const lspPorts = new Map();
11
40
  export const lspWorkers = new Map();
12
41
  export var LSP;
13
42
  (function (LSP) {
14
- async function worker(language, fs) {
15
- let factory, worker;
16
- console.debug('language', { language });
43
+ async function worker(language, fs, libFiles) {
44
+ let factory;
45
+ let worker;
17
46
  switch (language) {
18
47
  case 'javascript':
19
48
  case 'typescript':
20
49
  factory = languageServerFactory.get('javascript');
21
50
  worker = lspWorkers.get('javascript');
22
- console.debug('got worker', { worker, factory });
23
51
  if (!factory) {
24
52
  worker = new SharedWorker(new URL('../workers/javascript.worker.js', import.meta.url), { type: 'module' });
25
53
  worker.port.start();
26
54
  lspWorkers.set('javascript', worker);
27
- const { createLanguageServer } = Comlink.wrap(worker.port);
28
- factory = createLanguageServer;
55
+ const wrapped = Comlink.wrap(worker.port);
56
+ factory = wrapped.createLanguageServer;
29
57
  languageServerFactory.set('javascript', factory);
30
58
  }
31
59
  break;
60
+ default:
61
+ return null;
32
62
  }
33
- await factory?.(Comlink.proxy({ fs }));
34
- return { worker };
63
+ // fs is proxied (has methods), libFiles is plain data (structured clone)
64
+ // The factory returns a MessagePort for the LSP connection (separate from Comlink's port)
65
+ const lspPort = await factory(Comlink.proxy(fs), libFiles);
66
+ lspPort.start();
67
+ lspPorts.set(language, lspPort);
68
+ return { worker: worker, lspPort };
35
69
  }
36
70
  LSP.worker = worker;
37
- async function client({ fs, language, path, view }) {
71
+ async function client({ fs, language, path, libFiles }) {
38
72
  let client = clients.get(language);
39
- let clientExtension;
40
73
  const uri = `file:///${path}`;
41
74
  if (!client) {
42
- const { worker } = await LSP.worker(language, fs);
43
- if (!worker)
75
+ const result = await LSP.worker(language, fs, libFiles);
76
+ if (!result)
44
77
  return null;
45
- console.debug('got worker', { worker });
46
- client = new LanguageServerClient({
47
- transport: new MessagePortTransport(worker.port),
78
+ const { lspPort } = result;
79
+ client = new LSPClient({
48
80
  rootUri: 'file:///',
49
- workspaceFolders: [{ name: 'workspace', uri: 'file:///' }]
81
+ extensions: languageServerExtensions(),
82
+ notificationHandlers: {
83
+ "window/logMessage": (_client, params) => {
84
+ const level = params.type === 1 ? 'error' : params.type === 2 ? 'warn' : params.type === 3 ? 'info' : 'log';
85
+ LspLog.push(level, params.message);
86
+ return false; // fall through to default handler (console)
87
+ }
88
+ },
50
89
  });
90
+ client.connect(messagePortTransport(lspPort));
91
+ clients.set(language, client);
51
92
  }
52
- clients.set(language, client);
53
- clientExtension = { client, extension: [] };
54
- clientExtension.extension = languageServerWithClient({
55
- client: clientExtension.client,
56
- documentUri: uri,
57
- languageId: language,
58
- allowHTMLContent: true,
59
- markdownRenderer(markdown) {
60
- const support = languageSupportCompartment.get(view.state);
61
- const highlighter = vscodeLightDark[1].find(item => item.value instanceof HighlightStyle)?.value;
62
- const parser = support.language?.parser;
63
- const md = markdownit({
64
- highlight: (str) => {
65
- return renderMarkdownCode(str, parser, highlighter);
66
- }
67
- });
68
- return md.render(markdown);
69
- },
70
- });
71
- return clientExtension;
93
+ return client.plugin(uri, language);
72
94
  }
73
95
  LSP.client = client;
96
+ /**
97
+ * Notify all connected LSP clients that a file was created, changed, or deleted.
98
+ * This sends workspace/didChangeWatchedFiles so the server re-evaluates the project.
99
+ */
100
+ function notifyFileChanged(path, type = FileChangeType.Changed) {
101
+ const uri = `file:///${path}`;
102
+ for (const client of clients.values()) {
103
+ client.notification("workspace/didChangeWatchedFiles", {
104
+ changes: [{ uri, type }]
105
+ });
106
+ }
107
+ }
108
+ LSP.notifyFileChanged = notifyFileChanged;
74
109
  })(LSP || (LSP = {}));
@@ -15,7 +15,9 @@ export type HighlightedSearch = SearchResult & {
15
15
  };
16
16
  export declare class SearchIndex {
17
17
  index: MiniSearch;
18
+ savePath?: string;
18
19
  constructor(index: MiniSearch);
20
+ add(path: string): void;
19
21
  search(...params: Parameters<MiniSearch['search']>): HighlightedSearch[];
20
22
  /**
21
23
  *
@@ -7,9 +7,16 @@ export const defaultFilter = (path) => {
7
7
  };
8
8
  export class SearchIndex {
9
9
  index;
10
+ /// The VFS path this index was loaded from / saved to, if known.
11
+ savePath;
10
12
  constructor(index) {
11
13
  this.index = index;
12
14
  }
15
+ add(path) {
16
+ if (!this.index.has(path)) {
17
+ this.index.add({ path });
18
+ }
19
+ }
13
20
  search(...params) {
14
21
  const results = this.index.search(...params);
15
22
  const highlights = this.highlight(results);
@@ -48,10 +55,12 @@ export class SearchIndex {
48
55
  return new SearchIndex(index);
49
56
  }
50
57
  static async get(fs, path, fields = defaultFields) {
51
- const index = await fs.exists(path) ? await fs.readFile(path) : null;
52
- return index ?
53
- SearchIndex.from(index, fields) :
54
- SearchIndex.build(fs, { fields, idField: 'path' }).then(index => index.save(fs, path));
58
+ const data = await fs.exists(path) ? await fs.readFile(path) : null;
59
+ let index = data
60
+ ? SearchIndex.from(data, fields)
61
+ : await SearchIndex.build(fs, { fields, idField: 'path' }).then(idx => idx.save(fs, path));
62
+ index.savePath = path;
63
+ return index;
55
64
  }
56
65
  static async build(fs, { filter = defaultFilter, ...rest }) {
57
66
  const index = new MiniSearch({ ...rest });
@@ -0,0 +1,57 @@
1
+ import { VfsInterface } from "../types";
2
+ export type TypescriptDefaultsConfig = {
3
+ /** ES target, determines which lib files are needed. Default: "ES2020" */
4
+ target?: string;
5
+ /** Additional lib names to load beyond the ES target libs.
6
+ * Defaults to TypeScript's `.full` environment libs (dom, dom.iterable,
7
+ * dom.asynciterable, webworker.importscripts, scripthost).
8
+ * Pass `[]` to disable. */
9
+ additionalLibs?: string[];
10
+ /** Custom tsconfig compilerOptions merged with defaults */
11
+ compilerOptions?: Record<string, any>;
12
+ };
13
+ /**
14
+ * Returns the list of TypeScript lib file names required for a given ES target,
15
+ * plus any additional libs (DOM by default).
16
+ * Names are without the `lib.` prefix and `.d.ts` suffix (e.g. "es5", "dom").
17
+ */
18
+ export declare function getRequiredLibs(target?: string, additionalLibs?: string[]): string[];
19
+ /**
20
+ * Returns all individual lib names for the tsconfig `lib` field.
21
+ *
22
+ * Lists every individual lib file (e.g. "es5", "es2015.promise", "dom") instead of
23
+ * just the top-level entry (e.g. "ES2020"). This is critical for browser-based
24
+ * TypeScript via Volar: the virtual filesystem is async, so each
25
+ * `/// <reference lib="..." />` chain level requires a separate async round-trip.
26
+ * By listing all libs explicitly, TypeScript loads them all in a single pass.
27
+ */
28
+ export declare function getLibFieldForTarget(target?: string, additionalLibs?: string[]): string[];
29
+ /**
30
+ * Returns the cached lib file contents from the last prefill, if available.
31
+ * These are keyed by full path (e.g. "/node_modules/typescript/lib/lib.es5.d.ts").
32
+ * Includes the tsconfig.json content as well.
33
+ */
34
+ export declare function getCachedLibFiles(): Record<string, string> | undefined;
35
+ /**
36
+ * Pre-fills the virtual filesystem with TypeScript default lib definitions and tsconfig.
37
+ * Writes to `/node_modules/typescript/lib/` where Volar's TypeScript language server
38
+ * expects to find them in a browser environment.
39
+ *
40
+ * - Skips files that already exist on the filesystem
41
+ * - Only runs once per session (subsequent calls are no-ops)
42
+ * - Should be called lazily when a TypeScript file is first opened
43
+ *
44
+ * Returns a map of file paths to their contents for direct use by the LSP worker,
45
+ * bypassing the need for the worker to read through nested Comlink proxies.
46
+ *
47
+ * @param fs - Virtual filesystem to write to
48
+ * @param resolveLib - Function that resolves a lib name to its `.d.ts` content.
49
+ * Receives names like "es5", "es2015.collection".
50
+ * In Vite, use `import.meta.glob('typescript/lib/*.d.ts', { query: '?raw' })`.
51
+ * @param config - Optional target and tsconfig overrides
52
+ */
53
+ export declare function prefillTypescriptDefaults(fs: VfsInterface, resolveLib: (name: string) => Promise<string>, config?: TypescriptDefaultsConfig): Promise<Record<string, string>>;
54
+ /**
55
+ * Resets the prefilled state. Primarily useful for testing.
56
+ */
57
+ export declare function resetPrefillState(): void;
@@ -0,0 +1,208 @@
1
+ const LIB_DIR = '/node_modules/typescript/lib';
2
+ const TSCONFIG_PATH = '/tsconfig.json';
3
+ /**
4
+ * Maps ES target names to the list of TypeScript lib files they require.
5
+ * Each target includes all libs from previous targets plus its own additions.
6
+ * Derived from the `/// <reference lib="..." />` chains in TypeScript's lib files.
7
+ */
8
+ const TARGET_LIBS = {
9
+ es5: [
10
+ 'es5',
11
+ 'decorators',
12
+ 'decorators.legacy',
13
+ ],
14
+ es2015: [
15
+ 'es5', 'decorators', 'decorators.legacy',
16
+ 'es2015',
17
+ 'es2015.core', 'es2015.collection', 'es2015.iterable',
18
+ 'es2015.generator', 'es2015.promise', 'es2015.proxy',
19
+ 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown',
20
+ ],
21
+ es2016: [
22
+ 'es5', 'decorators', 'decorators.legacy',
23
+ 'es2015',
24
+ 'es2015.core', 'es2015.collection', 'es2015.iterable',
25
+ 'es2015.generator', 'es2015.promise', 'es2015.proxy',
26
+ 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown',
27
+ 'es2016',
28
+ 'es2016.array.include', 'es2016.intl',
29
+ ],
30
+ es2017: [
31
+ 'es5', 'decorators', 'decorators.legacy',
32
+ 'es2015',
33
+ 'es2015.core', 'es2015.collection', 'es2015.iterable',
34
+ 'es2015.generator', 'es2015.promise', 'es2015.proxy',
35
+ 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown',
36
+ 'es2016',
37
+ 'es2016.array.include', 'es2016.intl',
38
+ 'es2017',
39
+ 'es2017.arraybuffer', 'es2017.date', 'es2017.intl',
40
+ 'es2017.object', 'es2017.sharedmemory', 'es2017.string',
41
+ 'es2017.typedarrays',
42
+ ],
43
+ es2018: [
44
+ 'es5', 'decorators', 'decorators.legacy',
45
+ 'es2015',
46
+ 'es2015.core', 'es2015.collection', 'es2015.iterable',
47
+ 'es2015.generator', 'es2015.promise', 'es2015.proxy',
48
+ 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown',
49
+ 'es2016',
50
+ 'es2016.array.include', 'es2016.intl',
51
+ 'es2017',
52
+ 'es2017.arraybuffer', 'es2017.date', 'es2017.intl',
53
+ 'es2017.object', 'es2017.sharedmemory', 'es2017.string',
54
+ 'es2017.typedarrays',
55
+ 'es2018',
56
+ 'es2018.asynciterable', 'es2018.asyncgenerator',
57
+ 'es2018.promise', 'es2018.regexp', 'es2018.intl',
58
+ ],
59
+ es2019: [
60
+ 'es5', 'decorators', 'decorators.legacy',
61
+ 'es2015',
62
+ 'es2015.core', 'es2015.collection', 'es2015.iterable',
63
+ 'es2015.generator', 'es2015.promise', 'es2015.proxy',
64
+ 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown',
65
+ 'es2016',
66
+ 'es2016.array.include', 'es2016.intl',
67
+ 'es2017',
68
+ 'es2017.arraybuffer', 'es2017.date', 'es2017.intl',
69
+ 'es2017.object', 'es2017.sharedmemory', 'es2017.string',
70
+ 'es2017.typedarrays',
71
+ 'es2018',
72
+ 'es2018.asynciterable', 'es2018.asyncgenerator',
73
+ 'es2018.promise', 'es2018.regexp', 'es2018.intl',
74
+ 'es2019',
75
+ 'es2019.array', 'es2019.object', 'es2019.string',
76
+ 'es2019.symbol', 'es2019.intl',
77
+ ],
78
+ es2020: [
79
+ 'es5', 'decorators', 'decorators.legacy',
80
+ 'es2015',
81
+ 'es2015.core', 'es2015.collection', 'es2015.iterable',
82
+ 'es2015.generator', 'es2015.promise', 'es2015.proxy',
83
+ 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown',
84
+ 'es2016',
85
+ 'es2016.array.include', 'es2016.intl',
86
+ 'es2017',
87
+ 'es2017.arraybuffer', 'es2017.date', 'es2017.intl',
88
+ 'es2017.object', 'es2017.sharedmemory', 'es2017.string',
89
+ 'es2017.typedarrays',
90
+ 'es2018',
91
+ 'es2018.asynciterable', 'es2018.asyncgenerator',
92
+ 'es2018.promise', 'es2018.regexp', 'es2018.intl',
93
+ 'es2019',
94
+ 'es2019.array', 'es2019.object', 'es2019.string',
95
+ 'es2019.symbol', 'es2019.intl',
96
+ 'es2020',
97
+ 'es2020.bigint', 'es2020.date', 'es2020.number',
98
+ 'es2020.promise', 'es2020.sharedmemory', 'es2020.string',
99
+ 'es2020.symbol.wellknown', 'es2020.intl',
100
+ ],
101
+ };
102
+ /**
103
+ * Environment lib files included by TypeScript's `.full` variants (e.g. lib.es2020.full.d.ts).
104
+ * TypeScript includes these by default when no explicit `lib` is specified in tsconfig.
105
+ * We list them explicitly to match VS Code's default behavior.
106
+ */
107
+ const DEFAULT_ENV_LIBS = ['dom', 'dom.iterable', 'dom.asynciterable', 'webworker.importscripts', 'scripthost'];
108
+ /**
109
+ * Returns the list of TypeScript lib file names required for a given ES target,
110
+ * plus any additional libs (DOM by default).
111
+ * Names are without the `lib.` prefix and `.d.ts` suffix (e.g. "es5", "dom").
112
+ */
113
+ export function getRequiredLibs(target = 'es2020', additionalLibs = DEFAULT_ENV_LIBS) {
114
+ const targetLibs = TARGET_LIBS[target.toLowerCase()] || TARGET_LIBS.es2020;
115
+ return [...targetLibs, ...additionalLibs];
116
+ }
117
+ /**
118
+ * Returns all individual lib names for the tsconfig `lib` field.
119
+ *
120
+ * Lists every individual lib file (e.g. "es5", "es2015.promise", "dom") instead of
121
+ * just the top-level entry (e.g. "ES2020"). This is critical for browser-based
122
+ * TypeScript via Volar: the virtual filesystem is async, so each
123
+ * `/// <reference lib="..." />` chain level requires a separate async round-trip.
124
+ * By listing all libs explicitly, TypeScript loads them all in a single pass.
125
+ */
126
+ export function getLibFieldForTarget(target = 'es2020', additionalLibs = DEFAULT_ENV_LIBS) {
127
+ const t = target.toLowerCase();
128
+ const targetLibs = TARGET_LIBS[t] || TARGET_LIBS.es2020;
129
+ return [...targetLibs, ...additionalLibs];
130
+ }
131
+ let prefilled = false;
132
+ let cachedLibFiles;
133
+ /**
134
+ * Returns the cached lib file contents from the last prefill, if available.
135
+ * These are keyed by full path (e.g. "/node_modules/typescript/lib/lib.es5.d.ts").
136
+ * Includes the tsconfig.json content as well.
137
+ */
138
+ export function getCachedLibFiles() {
139
+ return cachedLibFiles;
140
+ }
141
+ /**
142
+ * Pre-fills the virtual filesystem with TypeScript default lib definitions and tsconfig.
143
+ * Writes to `/node_modules/typescript/lib/` where Volar's TypeScript language server
144
+ * expects to find them in a browser environment.
145
+ *
146
+ * - Skips files that already exist on the filesystem
147
+ * - Only runs once per session (subsequent calls are no-ops)
148
+ * - Should be called lazily when a TypeScript file is first opened
149
+ *
150
+ * Returns a map of file paths to their contents for direct use by the LSP worker,
151
+ * bypassing the need for the worker to read through nested Comlink proxies.
152
+ *
153
+ * @param fs - Virtual filesystem to write to
154
+ * @param resolveLib - Function that resolves a lib name to its `.d.ts` content.
155
+ * Receives names like "es5", "es2015.collection".
156
+ * In Vite, use `import.meta.glob('typescript/lib/*.d.ts', { query: '?raw' })`.
157
+ * @param config - Optional target and tsconfig overrides
158
+ */
159
+ export async function prefillTypescriptDefaults(fs, resolveLib, config = {}) {
160
+ if (prefilled) {
161
+ return cachedLibFiles || {};
162
+ }
163
+ prefilled = true;
164
+ const target = config.target || 'ES2020';
165
+ const additionalLibs = config.additionalLibs ?? DEFAULT_ENV_LIBS;
166
+ const fileContents = {};
167
+ // Write tsconfig.json if it doesn't exist
168
+ const tsconfigExists = await fs.exists(TSCONFIG_PATH);
169
+ const tsconfigContent = JSON.stringify({
170
+ compilerOptions: {
171
+ target,
172
+ lib: getLibFieldForTarget(target, additionalLibs),
173
+ module: "ESNext",
174
+ moduleResolution: "bundler",
175
+ strict: true,
176
+ skipLibCheck: true,
177
+ ...config.compilerOptions,
178
+ }
179
+ }, null, 2);
180
+ fileContents[TSCONFIG_PATH] = tsconfigContent;
181
+ if (!tsconfigExists) {
182
+ await fs.writeFile(TSCONFIG_PATH, tsconfigContent);
183
+ }
184
+ // Write lib files to the path Volar expects in browser environments
185
+ const libs = getRequiredLibs(target, additionalLibs);
186
+ await fs.mkdir(LIB_DIR, { recursive: true });
187
+ await Promise.all(libs.map(async (name) => {
188
+ const path = `${LIB_DIR}/lib.${name}.d.ts`;
189
+ try {
190
+ const content = await resolveLib(name);
191
+ fileContents[path] = content;
192
+ if (!(await fs.exists(path))) {
193
+ await fs.writeFile(path, content);
194
+ }
195
+ }
196
+ catch (e) {
197
+ console.error(`Failed to load TypeScript lib: ${name}`, e);
198
+ }
199
+ }));
200
+ cachedLibFiles = fileContents;
201
+ return fileContents;
202
+ }
203
+ /**
204
+ * Resets the prefilled state. Primarily useful for testing.
205
+ */
206
+ export function resetPrefillState() {
207
+ prefilled = false;
208
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,197 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { getRequiredLibs, getLibFieldForTarget, prefillTypescriptDefaults, resetPrefillState } from './typescript-defaults';
3
+ function createMockFs() {
4
+ const files = new Map();
5
+ return {
6
+ readFile: vi.fn(async (path) => {
7
+ if (!files.has(path))
8
+ throw new Error(`ENOENT: ${path}`);
9
+ return files.get(path);
10
+ }),
11
+ writeFile: vi.fn(async (path, data) => {
12
+ files.set(path, data);
13
+ }),
14
+ exists: vi.fn(async (path) => files.has(path)),
15
+ mkdir: vi.fn(async () => { }),
16
+ readDir: vi.fn(async () => []),
17
+ stat: vi.fn(async () => null),
18
+ watch: vi.fn(),
19
+ };
20
+ }
21
+ describe('getRequiredLibs', () => {
22
+ it('returns ES5 libs for es5 target', () => {
23
+ const libs = getRequiredLibs('es5');
24
+ expect(libs).toContain('es5');
25
+ expect(libs).toContain('decorators');
26
+ expect(libs).toContain('decorators.legacy');
27
+ expect(libs).not.toContain('es2015');
28
+ });
29
+ it('returns ES2015 libs including ES5 for es2015 target', () => {
30
+ const libs = getRequiredLibs('es2015');
31
+ expect(libs).toContain('es5');
32
+ expect(libs).toContain('es2015');
33
+ expect(libs).toContain('es2015.promise');
34
+ expect(libs).toContain('es2015.collection');
35
+ expect(libs).not.toContain('es2016');
36
+ });
37
+ it('returns ES2020 libs for es2020 target', () => {
38
+ const libs = getRequiredLibs('es2020');
39
+ expect(libs).toContain('es5');
40
+ expect(libs).toContain('es2015');
41
+ expect(libs).toContain('es2020');
42
+ expect(libs).toContain('es2020.bigint');
43
+ expect(libs).toContain('es2020.promise');
44
+ });
45
+ it('is case-insensitive', () => {
46
+ expect(getRequiredLibs('ES2020')).toEqual(getRequiredLibs('es2020'));
47
+ });
48
+ it('defaults to ES2020 for unknown targets', () => {
49
+ expect(getRequiredLibs('unknown')).toEqual(getRequiredLibs('es2020'));
50
+ });
51
+ it('defaults to ES2020 when no target is provided', () => {
52
+ expect(getRequiredLibs()).toEqual(getRequiredLibs('es2020'));
53
+ });
54
+ });
55
+ describe('getLibFieldForTarget', () => {
56
+ it('returns all individual lib names for a target', () => {
57
+ const libs = getLibFieldForTarget('es2020');
58
+ expect(libs).toContain('es5');
59
+ expect(libs).toContain('es2015');
60
+ expect(libs).toContain('es2015.promise');
61
+ expect(libs).toContain('es2020');
62
+ expect(libs).toContain('es2020.bigint');
63
+ expect(libs).toEqual(getRequiredLibs('es2020'));
64
+ });
65
+ it('returns es2015 libs for es2015 target', () => {
66
+ const libs = getLibFieldForTarget('ES2015');
67
+ expect(libs).toContain('es5');
68
+ expect(libs).toContain('es2015');
69
+ expect(libs).not.toContain('es2016');
70
+ });
71
+ it('defaults to es2020 libs for unknown targets', () => {
72
+ expect(getLibFieldForTarget('unknown')).toEqual(getRequiredLibs('es2020'));
73
+ });
74
+ it('defaults to es2020 libs when no target is provided', () => {
75
+ expect(getLibFieldForTarget()).toEqual(getRequiredLibs('es2020'));
76
+ });
77
+ });
78
+ describe('prefillTypescriptDefaults', () => {
79
+ let mockFs;
80
+ const mockResolveLib = vi.fn(async (name) => `// lib.${name}.d.ts content`);
81
+ beforeEach(() => {
82
+ mockFs = createMockFs();
83
+ mockResolveLib.mockClear();
84
+ resetPrefillState();
85
+ });
86
+ it('writes tsconfig.json with default settings', async () => {
87
+ await prefillTypescriptDefaults(mockFs, mockResolveLib);
88
+ expect(mockFs.writeFile).toHaveBeenCalledWith('/tsconfig.json', expect.stringContaining('"target"'));
89
+ const tsconfigCall = vi.mocked(mockFs.writeFile).mock.calls.find(([path]) => path === '/tsconfig.json');
90
+ const tsconfig = JSON.parse(tsconfigCall[1]);
91
+ expect(tsconfig.compilerOptions.target).toBe('ES2020');
92
+ expect(tsconfig.compilerOptions.lib).toEqual(getRequiredLibs('es2020'));
93
+ expect(tsconfig.compilerOptions.module).toBe('ESNext');
94
+ expect(tsconfig.compilerOptions.strict).toBe(true);
95
+ });
96
+ it('does not overwrite existing tsconfig.json', async () => {
97
+ // Pre-create tsconfig.json
98
+ await mockFs.writeFile('/tsconfig.json', '{"existing": true}');
99
+ vi.mocked(mockFs.writeFile).mockClear();
100
+ await prefillTypescriptDefaults(mockFs, mockResolveLib);
101
+ const tsconfigWrites = vi.mocked(mockFs.writeFile).mock.calls.filter(([path]) => path === '/tsconfig.json');
102
+ expect(tsconfigWrites).toHaveLength(0);
103
+ });
104
+ it('merges custom compilerOptions into tsconfig', async () => {
105
+ await prefillTypescriptDefaults(mockFs, mockResolveLib, {
106
+ compilerOptions: { jsx: 'react-jsx', strict: false },
107
+ });
108
+ const tsconfigCall = vi.mocked(mockFs.writeFile).mock.calls.find(([path]) => path === '/tsconfig.json');
109
+ const tsconfig = JSON.parse(tsconfigCall[1]);
110
+ expect(tsconfig.compilerOptions.jsx).toBe('react-jsx');
111
+ expect(tsconfig.compilerOptions.strict).toBe(false);
112
+ });
113
+ it('writes TypeScript lib files to /node_modules/typescript/lib/', async () => {
114
+ await prefillTypescriptDefaults(mockFs, mockResolveLib, { target: 'es5' });
115
+ expect(mockFs.mkdir).toHaveBeenCalledWith('/node_modules/typescript/lib', { recursive: true });
116
+ // ES5 needs 3 target libs + 5 default env libs (dom, dom.iterable, etc.)
117
+ const libWrites = vi.mocked(mockFs.writeFile).mock.calls.filter(([path]) => path.startsWith('/node_modules/typescript/lib/'));
118
+ expect(libWrites).toHaveLength(8);
119
+ expect(libWrites.map(([path]) => path)).toEqual(expect.arrayContaining([
120
+ '/node_modules/typescript/lib/lib.es5.d.ts',
121
+ '/node_modules/typescript/lib/lib.decorators.d.ts',
122
+ '/node_modules/typescript/lib/lib.decorators.legacy.d.ts',
123
+ '/node_modules/typescript/lib/lib.dom.d.ts',
124
+ '/node_modules/typescript/lib/lib.dom.iterable.d.ts',
125
+ '/node_modules/typescript/lib/lib.dom.asynciterable.d.ts',
126
+ '/node_modules/typescript/lib/lib.webworker.importscripts.d.ts',
127
+ '/node_modules/typescript/lib/lib.scripthost.d.ts',
128
+ ]));
129
+ });
130
+ it('calls resolveLib for each lib file', async () => {
131
+ await prefillTypescriptDefaults(mockFs, mockResolveLib, { target: 'es5' });
132
+ expect(mockResolveLib).toHaveBeenCalledWith('es5');
133
+ expect(mockResolveLib).toHaveBeenCalledWith('decorators');
134
+ expect(mockResolveLib).toHaveBeenCalledWith('decorators.legacy');
135
+ expect(mockResolveLib).toHaveBeenCalledWith('dom');
136
+ expect(mockResolveLib).toHaveBeenCalledWith('dom.iterable');
137
+ expect(mockResolveLib).toHaveBeenCalledWith('dom.asynciterable');
138
+ expect(mockResolveLib).toHaveBeenCalledWith('webworker.importscripts');
139
+ expect(mockResolveLib).toHaveBeenCalledWith('scripthost');
140
+ });
141
+ it('does not overwrite existing lib files', async () => {
142
+ // Pre-create one lib file
143
+ await mockFs.writeFile('/node_modules/typescript/lib/lib.es5.d.ts', '// existing');
144
+ vi.mocked(mockFs.writeFile).mockClear();
145
+ mockResolveLib.mockClear();
146
+ await prefillTypescriptDefaults(mockFs, mockResolveLib, { target: 'es5' });
147
+ // resolveLib is called for all libs (to populate the return cache)
148
+ expect(mockResolveLib).toHaveBeenCalledWith('es5');
149
+ // But the existing file should NOT be overwritten in the VFS
150
+ const es5Writes = vi.mocked(mockFs.writeFile).mock.calls.filter(([path]) => path === '/node_modules/typescript/lib/lib.es5.d.ts');
151
+ expect(es5Writes).toHaveLength(0);
152
+ // Other libs should still be written
153
+ expect(mockResolveLib).toHaveBeenCalledWith('decorators');
154
+ expect(mockResolveLib).toHaveBeenCalledWith('decorators.legacy');
155
+ });
156
+ it('only runs once per session', async () => {
157
+ const result1 = await prefillTypescriptDefaults(mockFs, mockResolveLib, { target: 'es5' });
158
+ const firstCallCount = mockResolveLib.mock.calls.length;
159
+ const result2 = await prefillTypescriptDefaults(mockFs, mockResolveLib, { target: 'es5' });
160
+ // Should not have been called again
161
+ expect(mockResolveLib).toHaveBeenCalledTimes(firstCallCount);
162
+ // Second call returns cached result
163
+ expect(result2).toEqual(result1);
164
+ });
165
+ it('returns lib file contents keyed by path', async () => {
166
+ const result = await prefillTypescriptDefaults(mockFs, mockResolveLib, { target: 'es5' });
167
+ expect(result['/tsconfig.json']).toBeDefined();
168
+ expect(result['/node_modules/typescript/lib/lib.es5.d.ts']).toBe('// lib.es5.d.ts content');
169
+ expect(result['/node_modules/typescript/lib/lib.decorators.d.ts']).toBe('// lib.decorators.d.ts content');
170
+ expect(result['/node_modules/typescript/lib/lib.decorators.legacy.d.ts']).toBe('// lib.decorators.legacy.d.ts content');
171
+ expect(result['/node_modules/typescript/lib/lib.dom.d.ts']).toBe('// lib.dom.d.ts content');
172
+ expect(result['/node_modules/typescript/lib/lib.dom.iterable.d.ts']).toBe('// lib.dom.iterable.d.ts content');
173
+ });
174
+ it('handles resolveLib errors gracefully', async () => {
175
+ const errorResolve = vi.fn(async (name) => {
176
+ if (name === 'decorators')
177
+ throw new Error('Network error');
178
+ return `// lib.${name}.d.ts`;
179
+ });
180
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
181
+ await prefillTypescriptDefaults(mockFs, errorResolve, { target: 'es5' });
182
+ // Should have logged the error
183
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('decorators'), expect.any(Error));
184
+ // Other libs should still have been written
185
+ const es5Write = vi.mocked(mockFs.writeFile).mock.calls.find(([path]) => path === '/node_modules/typescript/lib/lib.es5.d.ts');
186
+ expect(es5Write).toBeTruthy();
187
+ consoleSpy.mockRestore();
188
+ });
189
+ it('uses custom target when specified', async () => {
190
+ await prefillTypescriptDefaults(mockFs, mockResolveLib, { target: 'ES2015' });
191
+ // Should include ES2015-specific libs
192
+ expect(mockResolveLib).toHaveBeenCalledWith('es2015.promise');
193
+ expect(mockResolveLib).toHaveBeenCalledWith('es2015.collection');
194
+ // Should not include ES2016+ libs
195
+ expect(mockResolveLib).not.toHaveBeenCalledWith('es2016');
196
+ });
197
+ });