diff --git a/lerna.json b/lerna.json index 1dcaa256..3c17d635 100644 --- a/lerna.json +++ b/lerna.json @@ -4,5 +4,5 @@ ], "npmClient": "yarn", "useWorkspaces": true, - "version": "4.9.0" + "version": "4.10.0" } diff --git a/packages/aura-language-server/package.json b/packages/aura-language-server/package.json index 3b18f34a..810eca6a 100644 --- a/packages/aura-language-server/package.json +++ b/packages/aura-language-server/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/aura-language-server", - "version": "4.9.0", + "version": "4.10.0", "description": "Language server for Aura components.", "main": "lib/aura-indexer/indexer.js", "typings": "lib/shared.d.ts", @@ -27,7 +27,7 @@ "package": "yarn pack" }, "dependencies": { - "@salesforce/lightning-lsp-common": "4.9.0", + "@salesforce/lightning-lsp-common": "4.10.0", "acorn": "^6.0.0", "acorn-loose": "^6.0.0", "acorn-walk": "^6.0.0", diff --git a/packages/lightning-lsp-common/package.json b/packages/lightning-lsp-common/package.json index f8ed7736..c401e964 100644 --- a/packages/lightning-lsp-common/package.json +++ b/packages/lightning-lsp-common/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/lightning-lsp-common", - "version": "4.9.0", + "version": "4.10.0", "description": "Common components for lwc-language-server and aura-language-server.", "main": "lib/index.js", "typings": "lib/index.d.ts", diff --git a/packages/lightning-lsp-common/src/__tests__/context.test.ts b/packages/lightning-lsp-common/src/__tests__/context.test.ts index d7f26b84..d2b82f08 100644 --- a/packages/lightning-lsp-common/src/__tests__/context.test.ts +++ b/packages/lightning-lsp-common/src/__tests__/context.test.ts @@ -336,3 +336,41 @@ it('configureCoreAll()', async () => { // const launchContent = fs.readFileSync(launchPath, 'utf8'); // expect(launchContent).toContain('"name": "SFDC (attach)"'); }); + +it('configureProjectForTs()', async () => { + const context = new WorkspaceContext('test-workspaces/sfdx-workspace'); + const baseTsconfigPathForceApp = 'test-workspaces/sfdx-workspace/.sfdx/tsconfig.sfdx.json'; + const tsconfigPathForceApp = FORCE_APP_ROOT + '/lwc/tsconfig.json'; + const tsconfigPathUtils = UTILS_ROOT + '/lwc/tsconfig.json'; + const tsconfigPathRegisteredEmpty = REGISTERED_EMPTY_FOLDER_ROOT + '/lwc/tsconfig.json'; + const forceignorePath = 'test-workspaces/sfdx-workspace/.forceignore'; + + // configure and verify typings/jsconfig after configuration: + await context.configureProjectForTs(); + + // verify tsconfig.sfdx.json + const baseTsConfigForceAppContent = fs.readJsonSync(baseTsconfigPathForceApp); + expect(baseTsConfigForceAppContent).toEqual({ + compilerOptions: { + target: 'ESNext', + paths: { + 'c/*': [], + }, + }, + }); + + //verify newly create tsconfig.json + const tsconfigForceAppContent = fs.readJsonSync(tsconfigPathForceApp); + expect(tsconfigForceAppContent).toEqual({ + extends: '../../../../.sfdx/tsconfig.sfdx.json', + include: ['**/*.ts', '../../../../.sfdx/typings/lwc/**/*.d.ts'], + exclude: ['**/__tests__/**'], + }); + + // clean up artifacts + fs.removeSync(baseTsconfigPathForceApp); + fs.removeSync(tsconfigPathForceApp); + fs.removeSync(tsconfigPathUtils); + fs.removeSync(tsconfigPathRegisteredEmpty); + fs.removeSync(forceignorePath); +}); diff --git a/packages/lightning-lsp-common/src/context.ts b/packages/lightning-lsp-common/src/context.ts index 3fd64ac2..12f0564b 100644 --- a/packages/lightning-lsp-common/src/context.ts +++ b/packages/lightning-lsp-common/src/context.ts @@ -307,6 +307,14 @@ export class WorkspaceContext { await this.writeTypings(); } + /** + * Configures LWC project to support TypeScript + */ + public async configureProjectForTs(): Promise { + // TODO: This should be moved into configureProject after dev preview + await this.writeTsconfigJson(); + } + /** * Acquires list of absolute modules directories, optimizing for workspace type * @returns Promise @@ -449,6 +457,29 @@ export class WorkspaceContext { } } + private async writeTsconfigJson(): Promise { + switch (this.type) { + case WorkspaceType.SFDX: + // Write tsconfig.sfdx.json first + const baseTsConfigPath = path.join(this.workspaceRoots[0], '.sfdx', 'tsconfig.sfdx.json'); + const baseTsConfig = await fs.readFile(utils.getSfdxResource('tsconfig-sfdx.base.json'), 'utf8'); + this.updateConfigFile(baseTsConfigPath, baseTsConfig); + // Write to the tsconfig.json in each module subdirectory + const tsConfigTemplate = await fs.readFile(utils.getSfdxResource('tsconfig-sfdx.json'), 'utf8'); + const forceignore = path.join(this.workspaceRoots[0], '.forceignore'); + // TODO: We should only be looking through modules that have TS files + const modulesDirs = await this.getModulesDirs(); + for (const modulesDir of modulesDirs) { + const tsConfigPath = path.join(modulesDir, 'tsconfig.json'); + const relativeWorkspaceRoot = utils.relativePath(path.dirname(tsConfigPath), this.workspaceRoots[0]); + const tsConfigContent = this.processTemplate(tsConfigTemplate, { project_root: relativeWorkspaceRoot }); + this.updateConfigFile(tsConfigPath, tsConfigContent); + await this.updateForceIgnoreFile(forceignore); + } + break; + } + } + private async writeSettings(): Promise { switch (this.type) { case WorkspaceType.CORE_ALL: diff --git a/packages/lightning-lsp-common/src/resources/sfdx/tsconfig-sfdx.base.json b/packages/lightning-lsp-common/src/resources/sfdx/tsconfig-sfdx.base.json new file mode 100644 index 00000000..0d69adf6 --- /dev/null +++ b/packages/lightning-lsp-common/src/resources/sfdx/tsconfig-sfdx.base.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "ESNext", + "paths": { + "c/*": [] + } + } +} \ No newline at end of file diff --git a/packages/lightning-lsp-common/src/resources/sfdx/tsconfig-sfdx.json b/packages/lightning-lsp-common/src/resources/sfdx/tsconfig-sfdx.json new file mode 100644 index 00000000..fda3564b --- /dev/null +++ b/packages/lightning-lsp-common/src/resources/sfdx/tsconfig-sfdx.json @@ -0,0 +1,10 @@ +{ + "extends": "${project_root}/.sfdx/tsconfig.sfdx.json", + "include": [ + "**/*.ts", + "${project_root}/.sfdx/typings/lwc/**/*.d.ts" + ], + "exclude": [ + "**/__tests__/**" + ] +} \ No newline at end of file diff --git a/packages/lwc-language-server/package.json b/packages/lwc-language-server/package.json index cba8350f..b58abcd4 100644 --- a/packages/lwc-language-server/package.json +++ b/packages/lwc-language-server/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/lwc-language-server", - "version": "4.9.0", + "version": "4.10.0", "description": "Language server for Lightning Web Components.", "main": "lib/indexer.js", "license": "BSD-3-Clause", @@ -34,7 +34,7 @@ "@lwc/template-compiler": "2.50.0", "@salesforce/apex": "0.0.12", "@salesforce/label": "0.0.12", - "@salesforce/lightning-lsp-common": "4.9.0", + "@salesforce/lightning-lsp-common": "4.10.0", "@salesforce/resourceurl": "0.0.12", "@salesforce/schema": "0.0.12", "@salesforce/user": "0.0.12", diff --git a/packages/lwc-language-server/src/__tests__/lwc-server.test.ts b/packages/lwc-language-server/src/__tests__/lwc-server.test.ts index 9d188ddf..b6376a6f 100644 --- a/packages/lwc-language-server/src/__tests__/lwc-server.test.ts +++ b/packages/lwc-language-server/src/__tests__/lwc-server.test.ts @@ -12,27 +12,31 @@ import { } from 'vscode-languageserver'; import { URI } from 'vscode-uri'; +import { sync } from 'fast-glob'; import * as fsExtra from 'fs-extra'; import * as path from 'path'; -const filename = path.resolve('../../test-workspaces/sfdx-workspace/force-app/main/default/lwc/todo/todo.html'); +const SFDX_WORKSPACE_ROOT = '../../test-workspaces/sfdx-workspace'; +const filename = path.resolve(SFDX_WORKSPACE_ROOT + '/force-app/main/default/lwc/todo/todo.html'); const uri = URI.file(filename).toString(); const document: TextDocument = TextDocument.create(uri, 'html', 0, fsExtra.readFileSync(filename).toString()); -const jsFilename = path.resolve('../../test-workspaces/sfdx-workspace/force-app/main/default/lwc/todo/todo.js'); +const jsFilename = path.resolve(SFDX_WORKSPACE_ROOT + '/force-app/main/default/lwc/todo/todo.js'); const jsUri = URI.file(jsFilename).toString(); const jsDocument: TextDocument = TextDocument.create(uri, 'javascript', 0, fsExtra.readFileSync(jsFilename).toString()); -const auraFilename = path.resolve('../../test-workspaces/sfdx-workspace/force-app/main/default/aura/todoApp/todoApp.app'); +const auraFilename = path.resolve(SFDX_WORKSPACE_ROOT + '/force-app/main/default/aura/todoApp/todoApp.app'); const auraUri = URI.file(auraFilename).toString(); const auraDocument: TextDocument = TextDocument.create(auraFilename, 'html', 0, fsExtra.readFileSync(auraFilename).toString()); -const hoverFilename = path.resolve('../../test-workspaces/sfdx-workspace/force-app/main/default/lwc/lightning_tree_example/lightning_tree_example.html'); +const hoverFilename = path.resolve(SFDX_WORKSPACE_ROOT + '/force-app/main/default/lwc/lightning_tree_example/lightning_tree_example.html'); const hoverUri = URI.file(hoverFilename).toString(); const hoverDocument: TextDocument = TextDocument.create(hoverFilename, 'html', 0, fsExtra.readFileSync(hoverFilename).toString()); const server: Server = new Server(); +let mockTypeScriptSupportConfig = false; + jest.mock('vscode-languageserver', () => { const actual = jest.requireActual('vscode-languageserver'); return { @@ -40,11 +44,15 @@ jest.mock('vscode-languageserver', () => { createConnection: jest.fn().mockImplementation(() => { return { onInitialize: (): boolean => true, + onInitialized: (): boolean => true, onCompletion: (): boolean => true, onCompletionResolve: (): boolean => true, onHover: (): boolean => true, onShutdown: (): boolean => true, onDefinition: (): boolean => true, + workspace: { + getConfiguration: (): boolean => mockTypeScriptSupportConfig, + }, }; }), TextDocuments: jest.fn().mockImplementation(() => { @@ -81,8 +89,8 @@ describe('handlers', () => { capabilities: {}, workspaceFolders: [ { - uri: URI.file(path.resolve('../../test-workspaces/sfdx-workspace/')).toString(), - name: path.resolve('../../test-workspaces/sfdx-workspace/'), + uri: URI.file(path.resolve(SFDX_WORKSPACE_ROOT)).toString(), + name: path.resolve(SFDX_WORKSPACE_ROOT), }, ], }; @@ -324,6 +332,36 @@ describe('handlers', () => { expect(location.range.start.character).toEqual(60); }); }); + + describe('onInitialized()', () => { + it('skip tsconfig initialization when salesforcedx-vscode-lwc.preview.typeScriptSupport = false', async () => { + await server.onInitialize(initializeParams); + await server.onInitialized(); + + const baseTsconfigPath = SFDX_WORKSPACE_ROOT + '/.sfdx/tsconfig.sfdx.json'; + const tsconfigPaths = sync(SFDX_WORKSPACE_ROOT + '/**/lwc/tsconfig.json'); + expect(fsExtra.existsSync(baseTsconfigPath)).toBe(false); + expect(tsconfigPaths.length).toBe(0); + }); + + it('skip tsconfig initialization when salesforcedx-vscode-lwc.preview.typeScriptSupport = true', async () => { + // Enable feature flag + mockTypeScriptSupportConfig = true; + await server.onInitialize(initializeParams); + await server.onInitialized(); + + const baseTsconfigPath = SFDX_WORKSPACE_ROOT + '/.sfdx/tsconfig.sfdx.json'; + const tsconfigPaths = sync(SFDX_WORKSPACE_ROOT + '/**/lwc/tsconfig.json'); + expect(fsExtra.existsSync(baseTsconfigPath)).toBe(true); + // There are currently 3 lwc subdirectories under SFDX_WORKSPACE_ROOT + expect(tsconfigPaths.length).toBe(3); + + // Clean up after test run + fsExtra.removeSync(baseTsconfigPath); + await Promise.all(tsconfigPaths.map(tsconfig => fsExtra.remove(tsconfig))); + mockTypeScriptSupportConfig = false; + }); + }); }); describe('#capabilities', () => { diff --git a/packages/lwc-language-server/src/lwc-server.ts b/packages/lwc-language-server/src/lwc-server.ts index 3a046b75..89e1b87b 100644 --- a/packages/lwc-language-server/src/lwc-server.ts +++ b/packages/lwc-language-server/src/lwc-server.ts @@ -80,6 +80,7 @@ export default class Server { constructor() { this.connection.onInitialize(this.onInitialize.bind(this)); + this.connection.onInitialized(this.onInitialized.bind(this)); this.connection.onCompletion(this.onCompletion.bind(this)); this.connection.onCompletionResolve(this.onCompletionResolve.bind(this)); this.connection.onHover(this.onHover.bind(this)); @@ -130,6 +131,15 @@ export default class Server { }; } + async onInitialized(): Promise { + // The config value comes from salesforcedx-vscode/salesforcedx-vscode-lwc + const hasTsEnabled = await this.connection.workspace.getConfiguration('salesforcedx-vscode-lwc.preview.typeScriptSupport'); + // TODO: add onDidChangeConfiguration handler to detect changes in config value + if (hasTsEnabled) { + await this.context.configureProjectForTs(); + } + } + async onCompletion(params: CompletionParams): Promise { const { position,