diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..58d3284 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +lib/ +MyTypeScriptLog.txt \ No newline at end of file diff --git a/contributing.md b/contributing.md new file mode 100644 index 0000000..1fcce95 --- /dev/null +++ b/contributing.md @@ -0,0 +1,10 @@ + +# Contributing + +Run the build: + +``` +npm run build +``` + +Use this test project for development: https://gist.github.com/matthewp/f5033180c65a0e6075aa2aff37f9a6ba \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1432879 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,14 @@ +{ + "name": "@lucy/typescript-plugin", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "typescript": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.2.tgz", + "integrity": "sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw==", + "dev": true + } + } +} diff --git a/package.json b/package.json index d81ddad..cd7d651 100644 --- a/package.json +++ b/package.json @@ -2,19 +2,25 @@ "name": "@lucy/typescript-plugin", "version": "0.0.1", "description": "TypeScript Language Service plugin for Lucy", - "main": "index.js", + "main": "./lib/index.js", "scripts": { + "build": "tsc", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/lucydsl/typescript-plugin.git" }, - "keywords": ["lucy"], + "keywords": [ + "lucy" + ], "author": "Matthew Phillips", "license": "BSD-2-Clause", "bugs": { "url": "https://github.com/lucydsl/typescript-plugin/issues" }, - "homepage": "https://github.com/lucydsl/typescript-plugin#readme" + "homepage": "https://github.com/lucydsl/typescript-plugin#readme", + "devDependencies": { + "typescript": "^4.3.2" + } } diff --git a/readme.md b/readme.md index 78ae215..c8188eb 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,5 @@ +> *Note*: This is a work-in-progress and doesn't work yet. + # TypeScript Language Service Plugin for Lucy ## Installation diff --git a/src/file.ts b/src/file.ts new file mode 100644 index 0000000..d93e6ea --- /dev/null +++ b/src/file.ts @@ -0,0 +1,15 @@ +export function isLucyFilePath(filePath: string) { + return filePath.endsWith('.lucy'); +} + +export function isVirtualLucyFilePath(filePath: string) { + return filePath.endsWith('.lucy.ts'); +} + +export function toRealLucyFilePath(filePath: string) { + return filePath.slice(0, -'.ts'.length); +} + +export function ensureRealLucyFilePath(filePath: string) { + return isVirtualLucyFilePath(filePath) ? toRealLucyFilePath(filePath) : filePath; +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..8670071 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,28 @@ +import { createProgramState } from './state.js'; +import { createResolveModuleNames } from './module_names.js'; +import { createHostReadFile } from './project_service.js'; + +function init(modules: { typescript: typeof import("typescript/lib/tsserverlibrary") }) { + const ts = modules.typescript; + + type LanguageServiceKeys = Array; + + function create(info: ts.server.PluginCreateInfo) { + const state = createProgramState(ts, info); + + const proxy: ts.LanguageService = Object.create(null); + for (let k of Object.keys(info.languageService) as LanguageServiceKeys) { + const x = info.languageService[k]; + proxy[k] = (...args: Array<{}>) => x.apply(info.languageService, args); + } + + info.languageServiceHost.resolveModuleNames = createResolveModuleNames(state); + info.project.projectService.host.readFile = createHostReadFile(state); + + return proxy; + } + + return { create }; +} + +export = init; diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..9f5a2f5 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,20 @@ +import * as ts from 'typescript/lib/tsserverlibrary'; + +function stringify(args: any[]): string { + return args + .map((arg) => { + if (typeof arg === 'object') { + try { + return JSON.stringify(arg); + } catch (e) { + return '[object that cannot by stringified]'; + } + } + return arg; + }) + .join(' '); +} + +export function info(logger: ts.server.Logger, ...args: any[]) { + logger.info(`lucytsplugin: ${stringify(args)}`); +} \ No newline at end of file diff --git a/src/lucy-sys.ts b/src/lucy-sys.ts new file mode 100644 index 0000000..97b5f4f --- /dev/null +++ b/src/lucy-sys.ts @@ -0,0 +1,28 @@ +import * as ts from 'typescript'; +import { ensureRealLucyFilePath, isVirtualLucyFilePath, toRealLucyFilePath } from './file.js'; + +export function createLucySys() { + const lucySys: ts.System = { + ...ts.sys, + fileExists(path: string) { + return ts.sys.fileExists(ensureRealLucyFilePath(path)); + }, + readDirectory(path, extensions, exclude, include, depth) { + const extensionsWithLucy = (extensions ?? []).concat('.lucy'); + + return ts.sys.readDirectory(path, extensionsWithLucy, exclude, include, depth); + } + }; + + if (ts.sys.realpath) { + const realpath = ts.sys.realpath; + lucySys.realpath = function (path) { + if (isVirtualLucyFilePath(path)) { + return realpath(toRealLucyFilePath(path)) + '.ts'; + } + return realpath(path); + }; + } + + return lucySys; +} \ No newline at end of file diff --git a/src/module_names.ts b/src/module_names.ts new file mode 100644 index 0000000..f0b0f39 --- /dev/null +++ b/src/module_names.ts @@ -0,0 +1,80 @@ +import type { ProgramState } from './state'; +import { ensureRealLucyFilePath, isVirtualLucyFilePath } from './file.js'; +import { info } from './logger.js'; +import * as ts from 'typescript/lib/tsserverlibrary'; + +export function createResolveModuleNames(state: ProgramState) { + const { languageServiceHost, logger, projectService, sys, typescript } = state; + const resolveModuleNames: typeof languageServiceHost.resolveModuleNames = languageServiceHost.resolveModuleNames.bind(languageServiceHost); + + function resolveModuleName( + name: string, + containingFile: string, + compilerOptions: ts.CompilerOptions + ): ts.ResolvedModule | undefined { + const lucyResolvedModule = typescript.resolveModuleName( + name, + containingFile, + compilerOptions, + sys + ).resolvedModule; + if ( + !lucyResolvedModule || + !isVirtualLucyFilePath(lucyResolvedModule.resolvedFileName) + ) { + return lucyResolvedModule; + } + + const resolvedFileName = ensureRealLucyFilePath(lucyResolvedModule.resolvedFileName); + info(logger, 'Resolved', name, 'to Lucy file', resolvedFileName); + + // This will trigger projectService.host.readFile which is patched + const scriptInfo = projectService.getOrCreateScriptInfoForNormalizedPath( + typescript.server.toNormalizedPath(resolvedFileName), + false + ); + + if(!scriptInfo) { + info(logger, 'Was able to create scriptInfo'); + return; + } + + const snapshot = scriptInfo.getSnapshot(); + if(!snapshot) { + return undefined; + } + + const resolvedLucyModule: ts.ResolvedModuleFull = { + extension: ts.Extension.Dts, + resolvedFileName + }; + return resolvedLucyModule; + } + + return function resolveLucyModuleNames( + moduleNames: string[], + containingFile: string, + reusedNames: string[] | undefined, + redirectedReference: ts.ResolvedProjectReference | undefined, + compilerOptions: ts.CompilerOptions + ): Array { + info(logger, `Resolving ${moduleNames.join(',')}`); + const r = resolveModuleNames( + moduleNames, + containingFile, + reusedNames, + redirectedReference, + compilerOptions + ) || Array.from(Array(moduleNames.length)); + + return r.map((moduleName, idx) => { + const fileName = moduleNames[idx]; + if (moduleName || !ensureRealLucyFilePath(fileName).endsWith('.lucy')) { + return moduleName; + } + + const resolvedModule = resolveModuleName(fileName, containingFile, compilerOptions); + return resolvedModule; + }); + }; +} \ No newline at end of file diff --git a/src/project_service.ts b/src/project_service.ts new file mode 100644 index 0000000..ad581a9 --- /dev/null +++ b/src/project_service.ts @@ -0,0 +1,29 @@ +import type { ProgramState } from './state'; +import { isLucyFilePath } from './file.js'; +import { info } from './logger.js'; + +export function createHostReadFile(state: ProgramState) { + const { logger, projectService } = state; + + const readFile = projectService.host.readFile; + return function(path: string) { + if(!isLucyFilePath(path)) { + return readFile(path); + } + + const lucyCode = readFile(path) || ''; + try { + // TODO compile + + return ` + interface CreateMachineRet { + one: string; + } + export function createMachine(): CreateMachineRet + `.trim(); + } catch(err) { + info(logger, `Error loading Lucy file ${path}`); + // TODO debug(logger, err) + } + } +} \ No newline at end of file diff --git a/src/state.ts b/src/state.ts new file mode 100644 index 0000000..6511b57 --- /dev/null +++ b/src/state.ts @@ -0,0 +1,25 @@ +import * as ts from 'typescript/lib/tsserverlibrary'; +import { createLucySys } from './lucy-sys.js'; + +export interface ProgramState { + languageServiceHost: ts.LanguageServiceHost; + logger: ts.server.Logger; + moduleCache: Map; + projectService: ts.server.ProjectService; + sys: ts.System; + typescript: typeof ts; +} + +export function createProgramState(typescript: typeof ts, info: ts.server.PluginCreateInfo): ProgramState { + info.project.projectService + const state: ProgramState = { + moduleCache: new Map(), + languageServiceHost: info.languageServiceHost, + logger: info.project.projectService.logger, + projectService: info.project.projectService, + sys: createLucySys(), + typescript + }; + + return state; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6547c7a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "preserveConstEnums": true, + "outDir": "lib", + "sourceMap": true, + "declaration": true, + "moduleResolution": "node" + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] + }