Skip to content

Commit 1046f7f

Browse files
committed
[forgive] Init
Init basic LSP. At the moment the extension doesn't do anything interesting, but it does compile successfully.
1 parent 048ab5c commit 1046f7f

File tree

14 files changed

+663
-22
lines changed

14 files changed

+663
-22
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ chrome-user-data
2323
.vscode
2424
*.swp
2525
*.swo
26+
*.vsix
2627

2728
packages/react-devtools-core/dist
2829
packages/react-devtools-extensions/chrome/build
@@ -37,3 +38,4 @@ packages/react-devtools-fusebox/dist
3738
packages/react-devtools-inline/dist
3839
packages/react-devtools-shell/dist
3940
packages/react-devtools-timeline/dist
41+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
**/node_modules
2+
client
3+
server
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ignore-engines true
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) Meta Platforms, Inc. and affiliates.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

compiler/packages/react-forgive/client/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"repository": {
1212
"type": "git",
1313
"url": "git+https://github.com/facebook/react.git",
14-
"directory": "compiler/packages/react-forgive-client"
14+
"directory": "compiler/packages/react-forgive"
1515
},
1616
"dependencies": {
1717
"vscode-languageclient": "^9.0.1"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import * as path from 'path';
2+
import {ExtensionContext, window as Window} from 'vscode';
3+
4+
import {
5+
LanguageClient,
6+
LanguageClientOptions,
7+
ServerOptions,
8+
TransportKind,
9+
} from 'vscode-languageclient/node';
10+
11+
let client: LanguageClient;
12+
13+
export function activate(context: ExtensionContext) {
14+
const serverModule = context.asAbsolutePath(path.join('dist', 'server.js'));
15+
16+
// If the extension is launched in debug mode then the debug server options are used
17+
// Otherwise the run options are used
18+
const serverOptions: ServerOptions = {
19+
run: {
20+
module: serverModule,
21+
transport: TransportKind.ipc,
22+
options: {cwd: process.cwd()},
23+
},
24+
debug: {
25+
module: serverModule,
26+
transport: TransportKind.ipc,
27+
options: {cwd: process.cwd()},
28+
},
29+
};
30+
31+
const clientOptions: LanguageClientOptions = {
32+
documentSelector: [
33+
{scheme: 'file', language: 'javascriptreact'},
34+
{scheme: 'file', language: 'typescriptreact'},
35+
],
36+
progressOnInitialization: true,
37+
};
38+
39+
// Create the language client and start the client.
40+
try {
41+
client = new LanguageClient(
42+
'react-forgive',
43+
'React Analyzer',
44+
serverOptions,
45+
clientOptions,
46+
);
47+
} catch {
48+
Window.showErrorMessage(
49+
`React Analyzer couldn't be started. See the output channel for details.`,
50+
);
51+
return;
52+
}
53+
54+
client.registerProposedFeatures();
55+
client.start();
56+
}
57+
58+
export function deactivate(): Thenable<void> | undefined {
59+
if (client !== undefined) {
60+
return client.stop();
61+
}
62+
}

compiler/packages/react-forgive/package.json

+7-8
Original file line numberDiff line numberDiff line change
@@ -35,25 +35,24 @@
3535
]
3636
},
3737
"scripts": {
38-
"compile": "yarn run esbuild-base -- --sourcemap",
38+
"compile": "rimraf dist && concurrently -n server,client \"yarn run esbuild:server --sourcemap\" \"yarn run esbuild:client --sourcemap\"",
3939
"dev": "yarn run package && yarn run install-ext",
40-
"esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node",
41-
"install-ext": "code --install-extension vscode-react-compiler-0.0.1.vsix",
40+
"esbuild:client": "esbuild ./client/src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node",
41+
"esbuild:server": "esbuild ./server/src/index.ts --bundle --outfile=dist/server.js --external:vscode --format=cjs --platform=node",
42+
"install-ext": "code --install-extension react-forgive-0.0.0.vsix",
4243
"lint": "eslint src --ext ts",
43-
"package": "vsce package",
44+
"package": "rm -f react-forgive-0.0.0.vsix && vsce package --yarn",
4445
"postinstall": "cd client && yarn install && cd ../server && yarn install && cd ..",
4546
"pretest": "yarn run compile && yarn run lint",
4647
"test": "vscode-test",
47-
"test-compile": "tsc -p ./",
48-
"vscode:prepublish": "yarn run esbuild-base -- --minify",
49-
"watch": "yarn run esbuild-base -- --sourcemap --watch"
48+
"vscode:prepublish": "yarn run compile",
49+
"watch": "concurrently --kill-others -n server,client \"run esbuild:server --sourcemap --watch\" \"run esbuild:client --sourcemap --watch\""
5050
},
5151
"devDependencies": {
5252
"@eslint/js": "^9.13.0",
5353
"@types/node": "^20",
5454
"esbuild": "^0.24.0",
5555
"eslint": "^9.13.0",
56-
"typescript": "^5.7.2",
5756
"typescript-eslint": "^8.16.0"
5857
}
5958
}

compiler/packages/react-forgive/server/package.json

+9-2
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,23 @@
44
"version": "0.0.0",
55
"description": "Experimental LSP server",
66
"license": "MIT",
7+
"main": "dist/index.js",
78
"scripts": {
8-
"build": "echo 'no build'",
9+
"build": "rimraf dist && rollup --config --bundleConfigAsCjs",
910
"test": "echo 'no tests'"
1011
},
1112
"repository": {
1213
"type": "git",
1314
"url": "git+https://github.com/facebook/react.git",
14-
"directory": "compiler/packages/react-forgive-server"
15+
"directory": "compiler/packages/react-forgive"
1516
},
1617
"dependencies": {
18+
"@babel/core": "^7.26.0",
19+
"@babel/parser": "^7.26.0",
20+
"@babel/plugin-syntax-typescript": "^7.25.9",
21+
"@babel/types": "^7.26.0",
22+
"cosmiconfig": "^9.0.0",
23+
"prettier": "^3.3.3",
1724
"vscode-languageserver": "^9.0.1",
1825
"vscode-languageserver-textdocument": "^1.0.12"
1926
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import type * as BabelCore from '@babel/core';
9+
import {parseAsync, transformFromAstAsync} from '@babel/core';
10+
import BabelPluginReactCompiler, {
11+
type PluginOptions,
12+
} from 'babel-plugin-react-compiler/src';
13+
import * as babelParser from 'prettier/plugins/babel.js';
14+
import estreeParser from 'prettier/plugins/estree';
15+
import * as typescriptParser from 'prettier/plugins/typescript';
16+
import * as prettier from 'prettier/standalone';
17+
18+
type CompileOptions = {
19+
text: string;
20+
file: string;
21+
options: PluginOptions | null;
22+
};
23+
export async function compile({
24+
text,
25+
file,
26+
options,
27+
}: CompileOptions): Promise<BabelCore.BabelFileResult> {
28+
const ast = await parseAsync(text, {
29+
sourceFileName: file,
30+
parserOpts: {
31+
plugins: ['typescript', 'jsx'],
32+
},
33+
sourceType: 'module',
34+
});
35+
const plugins =
36+
options != null
37+
? [[BabelPluginReactCompiler, options]]
38+
: [[BabelPluginReactCompiler]];
39+
const result = await transformFromAstAsync(ast, text, {
40+
filename: file,
41+
highlightCode: false,
42+
retainLines: true,
43+
plugins,
44+
sourceType: 'module',
45+
sourceFileName: file,
46+
});
47+
if (result?.code == null) {
48+
throw new Error(
49+
`Expected BabelPluginReactCompiler to compile successfully, got ${result}`,
50+
);
51+
}
52+
result.code = await prettier.format(result.code, {
53+
semi: false,
54+
parser: 'babel-ts',
55+
plugins: [babelParser, estreeParser, typescriptParser],
56+
});
57+
return result;
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {
9+
parsePluginOptions,
10+
type PluginOptions,
11+
} from 'babel-plugin-react-compiler/src';
12+
import {cosmiconfigSync} from 'cosmiconfig';
13+
14+
export function resolveReactConfig(projectPath: string): PluginOptions | null {
15+
const explorerSync = cosmiconfigSync('react', {
16+
searchStrategy: 'project',
17+
cache: true,
18+
});
19+
const result = explorerSync.search(projectPath);
20+
if (result != null) {
21+
return parsePluginOptions(result.config);
22+
} else {
23+
return null;
24+
}
25+
}

compiler/packages/react-forgive/server/src/index.ts

+87
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,90 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*/
7+
8+
import {TextDocument} from 'vscode-languageserver-textdocument';
9+
import {
10+
createConnection,
11+
type InitializeParams,
12+
type InitializeResult,
13+
ProposedFeatures,
14+
TextDocuments,
15+
TextDocumentSyncKind,
16+
} from 'vscode-languageserver/node';
17+
import {compile} from './compiler';
18+
import {type PluginOptions} from 'babel-plugin-react-compiler/src';
19+
import {resolveReactConfig} from './compiler/options';
20+
21+
const SUPPORTED_LANGUAGE_IDS = new Set([
22+
'javascript',
23+
'javascriptreact',
24+
'typescript',
25+
'typescriptreact',
26+
]);
27+
28+
const connection = createConnection(ProposedFeatures.all);
29+
connection.console.info(`React Analyzer running in node ${process.version}`);
30+
31+
const compiledCache = new WeakMap<TextDocument, string>();
32+
33+
const documents = new TextDocuments(TextDocument);
34+
documents.listen(connection);
35+
36+
let compilerOptions: PluginOptions | null = null;
37+
38+
connection.onInitialize((_params: InitializeParams) => {
39+
// TODO(@poteto) get config fr
40+
compilerOptions = resolveReactConfig('.');
41+
const result: InitializeResult = {
42+
capabilities: {
43+
textDocumentSync: TextDocumentSyncKind.Full,
44+
codeLensProvider: {resolveProvider: true},
45+
},
46+
};
47+
return result;
48+
});
49+
50+
connection.onInitialized(() => {
51+
connection.console.log('initialized');
52+
});
53+
54+
documents.onDidOpen(async event => {
55+
if (SUPPORTED_LANGUAGE_IDS.has(event.document.languageId)) {
56+
const result = await compile({
57+
text: event.document.getText(),
58+
file: event.document.uri,
59+
options: compilerOptions,
60+
});
61+
if (result.code != null) {
62+
compiledCache.set(event.document, result.code);
63+
connection.console.log(result.code);
64+
}
65+
}
66+
});
67+
68+
documents.onDidChangeContent(async event => {
69+
if (SUPPORTED_LANGUAGE_IDS.has(event.document.languageId)) {
70+
const result = await compile({
71+
text: event.document.getText(),
72+
file: event.document.uri,
73+
options: compilerOptions,
74+
});
75+
if (result.code != null) {
76+
compiledCache.set(event.document, result.code);
77+
connection.console.log(result.code);
78+
}
79+
}
80+
});
81+
82+
connection.onDidChangeWatchedFiles(change => {
83+
connection.console.log(
84+
change.changes.map(c => `File changed: ${c.uri}`).join('\n'),
85+
);
86+
});
87+
88+
connection.onCodeLens(params => {
89+
connection.console.log(JSON.stringify(params, null, 2));
90+
return [];
91+
});
92+
93+
connection.listen();
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
{
22
"extends": "@tsconfig/strictest/tsconfig.json",
33
"compilerOptions": {
4-
"module": "CommonJS",
5-
"moduleResolution": "node",
6-
"outDir": "dist",
4+
"module": "ES2015",
5+
"moduleResolution": "Bundler",
6+
"rootDir": "../../..",
7+
"noEmit": true,
78
"jsx": "react-jsxdev",
8-
"lib": ["ES2020"],
9-
"target": "ES2020",
9+
"target": "ES2015",
10+
"sourceMap": false,
11+
"removeComments": true,
12+
13+
"strictNullChecks": false
1014
},
1115
"exclude": ["node_modules", ".vscode-test"],
12-
"include": ["src/**/*.ts"],
16+
"include": ["src/**/*.ts"]
1317
}

0 commit comments

Comments
 (0)