extension is working!

This commit is contained in:
Corey Johnson 2025-11-04 13:59:22 -08:00
parent f4cbe54a88
commit 2d7f0dbe25
13 changed files with 476 additions and 174 deletions

View File

@ -280,13 +280,27 @@ export class Compiler {
const opValue = input.slice(operator.from, operator.to)
switch (opValue) {
case '+=': instructions.push(['ADD']); break
case '-=': instructions.push(['SUB']); break
case '*=': instructions.push(['MUL']); break
case '/=': instructions.push(['DIV']); break
case '%=': instructions.push(['MOD']); break
case '+=':
instructions.push(['ADD'])
break
case '-=':
instructions.push(['SUB'])
break
case '*=':
instructions.push(['MUL'])
break
case '/=':
instructions.push(['DIV'])
break
case '%=':
instructions.push(['MOD'])
break
default:
throw new CompilerError(`Unknown compound operator: ${opValue}`, operator.from, operator.to)
throw new CompilerError(
`Unknown compound operator: ${opValue}`,
operator.from,
operator.to
)
}
// DUP and store (same as regular assignment)
@ -304,10 +318,8 @@ export class Compiler {
}
case terms.FunctionDef: {
const { paramNames, bodyNodes, catchVariable, catchBody, finallyBody } = getFunctionDefParts(
node,
input
)
const { paramNames, bodyNodes, catchVariable, catchBody, finallyBody } =
getFunctionDefParts(node, input)
const instructions: ProgramItem[] = []
const functionLabel: Label = `.func_${this.fnLabelCount++}`
const afterLabel: Label = `.after_${functionLabel}`
@ -330,7 +342,13 @@ export class Compiler {
if (catchVariable || finallyBody) {
// If function has catch or finally, wrap body in try/catch/finally
instructions.push(
...this.#compileTryCatchFinally(compileFunctionBody, catchVariable, catchBody, finallyBody, input)
...this.#compileTryCatchFinally(
compileFunctionBody,
catchVariable,
catchBody,
finallyBody,
input
)
)
} else {
instructions.push(...compileFunctionBody())

View File

@ -304,9 +304,12 @@ describe('default params', () => {
})
test.skip('dict default', () => {
expect('make-person = do person=[name=Bob age=60]: person end; make-person')
.toEvaluateTo({ name: 'Bob', age: 60 })
expect('make-person = do person=[name=Bob age=60]: person end; make-person [name=Jon age=21]')
.toEvaluateTo({ name: 'Jon', age: 21 })
expect('make-person = do person=[name=Bob age=60]: person end; make-person').toEvaluateTo({
name: 'Bob',
age: 60,
})
expect(
'make-person = do person=[name=Bob age=60]: person end; make-person [name=Jon age=21]'
).toEvaluateTo({ name: 'Jon', age: 21 })
})
})

View File

@ -1,3 +1,4 @@
node_modules
dist
client/dist
server/dist
*.vsix

View File

@ -9,7 +9,8 @@
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
"${workspaceFolder}/client/dist/**/*.js",
"${workspaceFolder}/server/dist/**/*.js"
],
"preLaunchTask": "bun: compile"
}

View File

@ -3,6 +3,11 @@
"workspaces": {
"": {
"name": "shrimp",
"dependencies": {
"vscode-languageclient": "^9.0.1",
"vscode-languageserver": "^9.0.1",
"vscode-languageserver-textdocument": "^1.0.12",
},
"devDependencies": {
"@types/node": "22.x",
"@types/vscode": "^1.105.0",
@ -15,8 +20,28 @@
"@types/vscode": ["@types/vscode@1.105.0", "", {}, "sha512-Lotk3CTFlGZN8ray4VxJE7axIyLZZETQJVWi/lYoUVQuqfRxlQhVOfoejsD2V3dVXPSbS15ov5ZyowMAzgUqcw=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
"vscode-languageclient": ["vscode-languageclient@9.0.1", "", { "dependencies": { "minimatch": "^5.1.0", "semver": "^7.3.7", "vscode-languageserver-protocol": "3.17.5" } }, "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA=="],
"vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
"vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="],
"vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="],
"vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
}
}

View File

@ -0,0 +1,74 @@
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
TransportKind,
} from 'vscode-languageclient/node'
import * as vscode from 'vscode'
export function activate(context: vscode.ExtensionContext) {
const serverModule = context.asAbsolutePath('server/dist/server.js')
const serverOptions: ServerOptions = {
run: { module: serverModule, transport: TransportKind.ipc },
debug: { module: serverModule, transport: TransportKind.ipc },
}
const clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: 'file', language: 'shrimp' }],
}
const client = new LanguageClient(
'shrimpLanguageServer',
'Shrimp Language Server',
serverOptions,
clientOptions
)
client.start()
context.subscriptions.push(client)
// Command: Show Parse Tree
context.subscriptions.push(
vscode.commands.registerCommand('shrimp.showParseTree', async () => {
const editor = vscode.window.activeTextEditor
if (!editor || editor.document.languageId !== 'shrimp') {
vscode.window.showErrorMessage('No active Shrimp file')
return
}
const result = await client.sendRequest<string>('shrimp/parseTree', {
uri: editor.document.uri.toString(),
})
const doc = await vscode.workspace.openTextDocument({
content: result,
language: 'text',
})
await vscode.window.showTextDocument(doc, { preview: false })
})
)
// Command: Show Bytecode
context.subscriptions.push(
vscode.commands.registerCommand('shrimp.showBytecode', async () => {
const editor = vscode.window.activeTextEditor
if (!editor || editor.document.languageId !== 'shrimp') {
vscode.window.showErrorMessage('No active Shrimp file')
return
}
const result = await client.sendRequest<string>('shrimp/bytecode', {
uri: editor.document.uri.toString(),
})
const doc = await vscode.workspace.openTextDocument({
content: result,
language: 'text',
})
await vscode.window.showTextDocument(doc, { preview: false })
})
)
}
export function deactivate() {}

View File

@ -1,7 +1,7 @@
{
"name": "shrimp",
"version": "0.0.1",
"main": "./dist/extension.js",
"main": "./client/dist/extension.js",
"devDependencies": {
"@types/vscode": "^1.105.0",
"@types/node": "22.x",
@ -28,7 +28,29 @@
"[shrimp]": {
"editor.semanticHighlighting.enabled": true
}
},
"commands": [
{
"command": "shrimp.showParseTree",
"title": "Shrimp: Show Parse Tree"
},
{
"command": "shrimp.showBytecode",
"title": "Shrimp: Show Bytecode"
}
],
"keybindings": [
{
"command": "shrimp.showParseTree",
"key": "alt+k alt+i",
"when": "editorLangId == shrimp"
},
{
"command": "shrimp.showBytecode",
"key": "alt+k alt+,",
"when": "editorLangId == shrimp"
}
]
},
"description": "Language support for Shrimp shell scripting language",
"displayName": "Shrimp",
@ -39,9 +61,16 @@
"publisher": "shrimp-lang",
"scripts": {
"vscode:prepublish": "bun run package",
"compile": "bun build src/extension.ts --outdir dist --target node --format cjs --external vscode",
"watch": "bun build src/extension.ts --outdir dist --target node --format cjs --external vscode --watch",
"package": "bun build src/extension.ts --outdir dist --target node --format cjs --external vscode --minify",
"compile": "bun run compile:client && bun run compile:server",
"compile:client": "bun build client/src/extension.ts --outdir client/dist --target node --format cjs --external vscode",
"compile:server": "bun build server/src/server.ts --outdir server/dist --target node --format cjs --external vscode-languageserver --external vscode-languageserver-textdocument",
"watch": "bun run compile:client --watch",
"package": "bun run compile:client --minify && bun run compile:server --minify",
"check-types": "tsc --noEmit"
},
"dependencies": {
"vscode-languageclient": "^9.0.1",
"vscode-languageserver": "^9.0.1",
"vscode-languageserver-textdocument": "^1.0.12"
}
}

View File

@ -0,0 +1,93 @@
import { TextDocument, Position } from 'vscode-languageserver-textdocument'
import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver/node'
import { parser } from '../../../src/parser/shrimp'
import { Compiler } from '../../../src/compiler/compiler'
import { CompilerError } from '../../../src/compiler/compilerError'
export const buildDiagnostics = (textDocument: TextDocument): Diagnostic[] => {
const text = textDocument.getText()
const diagnostics = getParseErrors(textDocument)
if (diagnostics.length > 0) {
return diagnostics
}
const diagnostic = getCompilerError(text)
if (diagnostic) return [diagnostic]
return []
}
const getCompilerError = (text: string): Diagnostic | undefined => {
try {
new Compiler(text)
} catch (e) {
if (!(e instanceof CompilerError)) {
return unknownDiagnostic(getErrorMessage(e))
}
const lineInfo = e.lineAtPosition(text)!
const cause = e.cause ? ` Cause: ${e.cause}` : ''
const message = e.message
if (!lineInfo) {
return unknownDiagnostic(message + cause)
}
const diagnostic: Diagnostic = {
severity: DiagnosticSeverity.Error,
range: {
start: { line: lineInfo.lineNumber, character: lineInfo.columnStart },
end: { line: lineInfo.lineNumber, character: lineInfo.columnEnd },
},
message: `Compiler error: ${message}${cause}`,
source: 'shrimp',
}
return diagnostic
}
}
const unknownDiagnostic = (message: string): Diagnostic => {
const diagnostic: Diagnostic = {
severity: DiagnosticSeverity.Error,
range: {
start: { line: 0, character: 0 },
end: { line: -1, character: -1 },
},
message,
source: 'shrimp',
}
return diagnostic
}
const getParseErrors = (textDocument: TextDocument): Diagnostic[] => {
const tree = parser.parse(textDocument.getText())
const ranges: { start: Position; end: Position }[] = []
tree.iterate({
enter(n) {
if (n.type.isError) {
ranges.push({
start: textDocument.positionAt(n.from),
end: textDocument.positionAt(n.to),
})
return false
}
},
})
return ranges.map((range) => {
return {
range,
severity: DiagnosticSeverity.Error,
message: 'Parse error: Invalid syntax',
source: 'shrimp',
}
})
}
const getErrorMessage = (error: unknown): string => {
if (error instanceof Error) {
return error.message
}
return String(error)
}

View File

@ -0,0 +1,101 @@
import { parser } from '../../../src/parser/shrimp'
import * as Terms from '../../../src/parser/shrimp.terms'
import { SyntaxNode } from '@lezer/common'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { SemanticTokensBuilder, SemanticTokenTypes } from 'vscode-languageserver/node'
export const TOKEN_TYPES = [
SemanticTokenTypes.function,
SemanticTokenTypes.variable,
SemanticTokenTypes.string,
SemanticTokenTypes.number,
SemanticTokenTypes.operator,
SemanticTokenTypes.keyword,
SemanticTokenTypes.parameter,
SemanticTokenTypes.property,
SemanticTokenTypes.regexp,
]
export const TOKEN_MODIFIERS: string[] = []
export function buildSemanticTokens(document: TextDocument): number[] {
const text = document.getText()
const tree = parser.parse(text)
const builder = new SemanticTokensBuilder()
walkTree(tree.topNode, document, builder)
return builder.build().data
}
// Walk the tree and collect tokens
function walkTree(node: SyntaxNode, document: TextDocument, builder: SemanticTokensBuilder) {
const tokenType = getTokenType(node.type.id)
if (tokenType !== undefined) {
const start = document.positionAt(node.from)
const length = node.to - node.from
builder.push(start.line, start.character, length, tokenType, 0)
}
let child = node.firstChild
while (child) {
walkTree(child, document, builder)
child = child.nextSibling
}
}
// Map Lezer node IDs to semantic token type indices
function getTokenType(nodeTypeId: number): number | undefined {
switch (nodeTypeId) {
case Terms.FunctionCall:
case Terms.FunctionDef:
return TOKEN_TYPES.indexOf(SemanticTokenTypes.function)
case Terms.Identifier:
case Terms.AssignableIdentifier:
case Terms.FunctionCallOrIdentifier:
return TOKEN_TYPES.indexOf(SemanticTokenTypes.variable)
case Terms.String:
case Terms.StringFragment:
case Terms.Word:
return TOKEN_TYPES.indexOf(SemanticTokenTypes.string)
case Terms.Number:
return TOKEN_TYPES.indexOf(SemanticTokenTypes.number)
case Terms.Plus:
case Terms.Minus:
case Terms.Star:
case Terms.Slash:
case Terms.Eq:
case Terms.EqEq:
case Terms.Neq:
case Terms.Lt:
case Terms.Lte:
case Terms.Gt:
case Terms.Gte:
case Terms.Modulo:
case Terms.And:
case Terms.Or:
return TOKEN_TYPES.indexOf(SemanticTokenTypes.operator)
case Terms.keyword:
case Terms.Do:
return TOKEN_TYPES.indexOf(SemanticTokenTypes.keyword)
case Terms.Params:
case Terms.NamedParam:
return TOKEN_TYPES.indexOf(SemanticTokenTypes.parameter)
case Terms.DotGet:
return TOKEN_TYPES.indexOf(SemanticTokenTypes.property)
case Terms.Regex:
return TOKEN_TYPES.indexOf(SemanticTokenTypes.regexp)
default:
return undefined
}
}

View File

@ -0,0 +1,105 @@
import { TextDocuments } from 'vscode-languageserver/node'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { createConnection, ProposedFeatures } from 'vscode-languageserver/node'
import { buildDiagnostics } from './diagnostics'
import { buildSemanticTokens, TOKEN_MODIFIERS, TOKEN_TYPES } from './semanticTokens'
import { parser } from '../../../src/parser/shrimp'
import { Compiler } from '../../../src/compiler/compiler'
const connection = createConnection(ProposedFeatures.all)
const documents = new TextDocuments(TextDocument)
documents.listen(connection)
connection.onInitialize(() => {
connection.console.log('🦐 Server initialized with capabilities')
return {
capabilities: {
textDocumentSync: 1,
semanticTokensProvider: {
legend: {
tokenTypes: TOKEN_TYPES,
tokenModifiers: TOKEN_MODIFIERS,
},
full: true,
},
},
}
})
connection.languages.semanticTokens.on((params) => {
const document = documents.get(params.textDocument.uri)
if (!document) {
return { data: [] }
}
const data = buildSemanticTokens(document)
return { data }
})
documents.onDidChangeContent((change) => {
const textDocument = change.document
const diagnostics = buildDiagnostics(textDocument)
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics })
})
connection.onRequest('shrimp/parseTree', (params: { uri: string }) => {
connection.console.log(`🦐 Parse tree requested for: ${params.uri}`)
const document = documents.get(params.uri)
if (!document) return 'Document not found'
const text = document.getText()
const tree = parser.parse(text)
const treeString = tree.toString()
// Format with indentation, without parentheses
let formatted = ''
let indent = 0
for (let i = 0; i < treeString.length; i++) {
const char = treeString[i]
if (char === '(') {
formatted += '\n'
indent++
formatted += ' '.repeat(indent)
} else if (char === ')') {
indent--
} else if (char === ',') {
formatted += '\n'
formatted += ' '.repeat(indent)
} else {
formatted += char
}
}
return formatted
})
connection.onRequest('shrimp/bytecode', (params: { uri: string }) => {
connection.console.log(`🦐 Bytecode requested for: ${params.uri}`)
const document = documents.get(params.uri)
if (!document) return 'Document not found'
try {
const text = document.getText()
const compiler = new Compiler(text)
// Format bytecode as readable string
let output = 'Bytecode:\n\n'
const bytecode = compiler.bytecode
output += bytecode.instructions
.map((op, i) => `${i.toString().padStart(4)}: ${JSON.stringify(op)}`)
.join('\n')
// Strip ANSI color codes
output = output.replace(/\x1b\[[0-9;]*m/g, '')
return output
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
// Strip ANSI color codes from error message too
return `Compilation failed: ${errorMsg.replace(/\x1b\[[0-9;]*m/g, '')}`
}
})
connection.listen()

View File

@ -1,30 +0,0 @@
import * as vscode from 'vscode'
import { ShrimpSemanticTokensProvider, legend } from './semanticTokens'
import { parser } from '../../src/parser/shrimp'
// This method is called when your extension is activated
export function activate(context: vscode.ExtensionContext) {
console.log('Shrimp extension is now active!')
console.log('Parser loaded:', typeof parser, parser)
// Test the parser
try {
const testTree = parser.parse('x = 42')
console.log('Parser test successful:', testTree.topNode.toString())
} catch (error) {
console.error('Parser test failed:', error)
}
// Register semantic tokens provider for Shrimp language
const provider = new ShrimpSemanticTokensProvider()
const selector: vscode.DocumentSelector = { language: 'shrimp', scheme: 'file' }
const disposable = vscode.languages.registerDocumentSemanticTokensProvider(selector, provider, legend)
console.log('Registered semantic tokens provider:', disposable)
context.subscriptions.push(disposable)
console.log('Legend token types:', legend.tokenTypes)
}
// This method is called when your extension is deactivated
export function deactivate() {}

View File

@ -1,118 +0,0 @@
import * as vscode from 'vscode'
import { parser } from '../../src/parser/shrimp'
import { Tree, SyntaxNode } from '@lezer/common'
// Define the token types we'll use
const tokenTypes = [
'function',
'variable',
'string',
'number',
'operator',
'keyword',
'parameter',
'property',
'regexp',
]
const tokenModifiers: string[] = []
export const legend = new vscode.SemanticTokensLegend(tokenTypes, tokenModifiers)
export class ShrimpSemanticTokensProvider implements vscode.DocumentSemanticTokensProvider {
async provideDocumentSemanticTokens(
document: vscode.TextDocument,
_token: vscode.CancellationToken
): Promise<vscode.SemanticTokens> {
try {
console.log('provideDocumentSemanticTokens called for:', document.fileName)
const tokensBuilder = new vscode.SemanticTokensBuilder(legend)
const text = document.getText()
console.log('Document text:', text)
console.log('About to parse with parser:', typeof parser)
const tree: Tree = parser.parse(text)
console.log('Parsed tree:', tree.topNode.toString())
this.walkTree(tree.topNode, document, tokensBuilder)
const result = tokensBuilder.build()
console.log('Built tokens, data length:', result.data.length)
return result
} catch (error) {
console.error('Error in provideDocumentSemanticTokens:', error)
throw error
}
}
// Map Lezer node types to semantic token types
walkTree(node: SyntaxNode, document: vscode.TextDocument, builder: vscode.SemanticTokensBuilder) {
const tokenType = this.getTokenType(node.type.name)
if (tokenType !== undefined) {
const start = document.positionAt(node.from)
const length = node.to - node.from
builder.push(start.line, start.character, length, tokenType, 0)
}
// Recursively walk children
let child = node.firstChild
while (child) {
this.walkTree(child, document, builder)
child = child.nextSibling
}
}
getTokenType(nodeTypeName: string): number | undefined {
// Map Lezer node names to VSCode semantic token types
switch (nodeTypeName) {
case 'FunctionCall':
case 'FunctionDef':
return tokenTypes.indexOf('function')
case 'Identifier':
case 'AssignableIdentifier':
case 'FunctionCallOrIdentifier':
return tokenTypes.indexOf('variable')
case 'String':
case 'StringFragment':
case 'Word':
return tokenTypes.indexOf('string')
case 'Number':
return tokenTypes.indexOf('number')
case 'Plus':
case 'Minus':
case 'Star':
case 'Slash':
case 'Eq':
case 'EqEq':
case 'Neq':
case 'Lt':
case 'Lte':
case 'Gt':
case 'Gte':
case 'Modulo':
case 'And':
case 'Or':
return tokenTypes.indexOf('operator')
case 'keyword':
case 'Do':
return tokenTypes.indexOf('keyword')
case 'Params':
case 'NamedParam':
return tokenTypes.indexOf('parameter')
case 'DotGet':
return tokenTypes.indexOf('property')
case 'Regex':
return tokenTypes.indexOf('regexp')
default:
return undefined
}
}
}

View File

@ -3,7 +3,7 @@
"target": "ES2022",
"lib": ["ES2022"],
"module": "commonjs",
"moduleResolution": "node",
"moduleResolution": "bundler",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
@ -11,6 +11,6 @@
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
"include": ["client/src/**/*", "server/src/**/*", "../src/**/*"],
"exclude": ["node_modules", "client/dist", "server/dist"]
}