@tsrx/mcp 0.0.0
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.
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/package.json +51 -0
- package/src/analyze.js +195 -0
- package/src/compile.js +235 -0
- package/src/docs.js +50 -0
- package/src/format.js +138 -0
- package/src/generated/docs.js +79 -0
- package/src/http.js +99 -0
- package/src/index.js +25 -0
- package/src/inspect.js +235 -0
- package/src/schemas.js +141 -0
- package/src/server.js +629 -0
- package/src/stdio.js +8 -0
- package/src/target.js +216 -0
- package/src/validate.js +112 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Dominic Gannaway
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# @tsrx/mcp
|
|
2
|
+
|
|
3
|
+
MCP server for TSRX language documentation and project context.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
Run the server over stdio:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx -y @tsrx/mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Generic MCP client config:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"tsrx": {
|
|
19
|
+
"command": "npx",
|
|
20
|
+
"args": ["-y", "@tsrx/mcp"]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Hosted HTTP
|
|
27
|
+
|
|
28
|
+
Remote MCP clients need a hosted Streamable HTTP endpoint rather than a local
|
|
29
|
+
stdio command. This monorepo includes a deployment-neutral endpoint app in
|
|
30
|
+
`mcp-endpoint-website` that serves the same MCP server at `/mcp`.
|
|
31
|
+
|
|
32
|
+
The hosted endpoint runs in remote-safe mode. It exposes documentation, prompts,
|
|
33
|
+
`format-tsrx`, `compile-tsrx`, and `analyze-tsrx`, but omits local filesystem
|
|
34
|
+
tools such as `inspect-project`, `detect-target`, and `validate-tsrx-file`.
|
|
35
|
+
|
|
36
|
+
Set `TSRX_MCP_BEARER_TOKEN` in the endpoint environment to require bearer-token
|
|
37
|
+
auth. Set `TSRX_MCP_CORS_ORIGIN` to restrict CORS for browser-based clients.
|
|
38
|
+
|
|
39
|
+
For local development in this monorepo, point at the source entrypoint:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"mcpServers": {
|
|
44
|
+
"tsrx": {
|
|
45
|
+
"command": "node",
|
|
46
|
+
"args": ["/absolute/path/to/ripple/packages/tsrx-mcp/src/stdio.js"]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Claude Desktop
|
|
53
|
+
|
|
54
|
+
Add the generic config above to `claude_desktop_config.json`.
|
|
55
|
+
|
|
56
|
+
### Claude Code
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
claude mcp add tsrx -- npx -y @tsrx/mcp
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For local development:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
claude mcp add tsrx-local -- node /absolute/path/to/ripple/packages/tsrx-mcp/src/stdio.js
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Cursor
|
|
69
|
+
|
|
70
|
+
Add the generic config above to your Cursor MCP settings.
|
|
71
|
+
|
|
72
|
+
### Codex
|
|
73
|
+
|
|
74
|
+
Add the generic config above to your Codex MCP configuration.
|
|
75
|
+
|
|
76
|
+
## Tools
|
|
77
|
+
|
|
78
|
+
- `list-sections` - list target-neutral TSRX documentation sections.
|
|
79
|
+
- `get-documentation` - fetch one or more TSRX documentation sections.
|
|
80
|
+
- `detect-target` - infer the active TSRX runtime target from project files.
|
|
81
|
+
- `inspect-project` - inspect target signals, TSRX packages, tooling, scripts, and
|
|
82
|
+
likely project commands.
|
|
83
|
+
- `compile-tsrx` - compile TSRX code with the inferred or explicit target compiler
|
|
84
|
+
and return diagnostics.
|
|
85
|
+
- `format-tsrx` - format TSRX code using the official Prettier plugin.
|
|
86
|
+
- `analyze-tsrx` - compile TSRX code and convert common diagnostics into
|
|
87
|
+
target-neutral authoring advice with linked docs resources.
|
|
88
|
+
- `validate-tsrx-file` - read a `.tsrx` file and run formatting, compilation, and
|
|
89
|
+
diagnostic advice in one read-only pass.
|
|
90
|
+
|
|
91
|
+
## Agent Workflows
|
|
92
|
+
|
|
93
|
+
For an existing project, start with `inspect-project` to identify the TSRX target,
|
|
94
|
+
installed tooling, and likely validation commands. Use `detect-target` when only
|
|
95
|
+
the runtime target is needed.
|
|
96
|
+
|
|
97
|
+
For generated code, run `format-tsrx` first, then `compile-tsrx` with the inferred
|
|
98
|
+
or explicit target. If compilation fails, run `analyze-tsrx`, apply the advice,
|
|
99
|
+
format again, and compile again.
|
|
100
|
+
|
|
101
|
+
For an existing `.tsrx` file, prefer `validate-tsrx-file`. It reads the file and
|
|
102
|
+
runs formatting, compilation, and diagnostic advice in one read-only pass.
|
|
103
|
+
|
|
104
|
+
## Resources
|
|
105
|
+
|
|
106
|
+
- `tsrx://docs/{slug}.md` - target-neutral TSRX documentation sections.
|
|
107
|
+
- `tsrx://targets/{target}.md` - handoff guidance for target-specific layers.
|
|
108
|
+
|
|
109
|
+
## Prompts
|
|
110
|
+
|
|
111
|
+
- `tsrx-task` - target-aware workflow for TSRX coding tasks.
|
|
112
|
+
|
|
113
|
+
The core server stays target-neutral. Runtime-specific imports, bundler setup, and
|
|
114
|
+
framework semantics should live in target-specific skills, prompts, or resources
|
|
115
|
+
layered on top.
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tsrx/mcp",
|
|
3
|
+
"description": "MCP server for TSRX documentation and project context",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"author": "Dominic Gannaway",
|
|
6
|
+
"version": "0.0.0",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Ripple-TS/ripple.git",
|
|
11
|
+
"directory": "packages/tsrx-mcp"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Ripple-TS/ripple/issues"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://tsrx.dev",
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./src/index.js",
|
|
23
|
+
"default": "./src/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"README.md",
|
|
28
|
+
"src"
|
|
29
|
+
],
|
|
30
|
+
"bin": {
|
|
31
|
+
"tsrx-mcp": "./src/stdio.js"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=22.0.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38
|
+
"prettier": "^3.8.3",
|
|
39
|
+
"zod": "^4.3.6",
|
|
40
|
+
"@tsrx/core": "0.0.19",
|
|
41
|
+
"@tsrx/prettier-plugin": "0.3.39"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^24.3.0",
|
|
45
|
+
"typescript": "^5.9.3",
|
|
46
|
+
"vitest": "^4.1.4"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"generate:docs": "node scripts/generate-docs-index.js"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/analyze.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { DIAGNOSTIC_CODES } from '@tsrx/core';
|
|
2
|
+
import { compile_tsrx } from './compile.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {{
|
|
6
|
+
* kind: string,
|
|
7
|
+
* severity: 'error' | 'warning' | 'info',
|
|
8
|
+
* title: string,
|
|
9
|
+
* message: string,
|
|
10
|
+
* documentation: string[],
|
|
11
|
+
* }} TSRXAdvice
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {{
|
|
16
|
+
* ok: boolean,
|
|
17
|
+
* target: string | null,
|
|
18
|
+
* errors: Array<{ message: string, code?: string | null }>,
|
|
19
|
+
* }} TSRXCompileSummary
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {{ compileResult: TSRXCompileSummary }} input
|
|
24
|
+
* @returns {TSRXAdvice[]}
|
|
25
|
+
*/
|
|
26
|
+
function create_advice(input) {
|
|
27
|
+
const { compileResult } = input;
|
|
28
|
+
/** @type {TSRXAdvice[]} */
|
|
29
|
+
const advice = [];
|
|
30
|
+
const error_codes = new Set(compileResult.errors.map((error) => error.code).filter(Boolean));
|
|
31
|
+
|
|
32
|
+
if (!compileResult.target) {
|
|
33
|
+
advice.push({
|
|
34
|
+
kind: 'missing-target',
|
|
35
|
+
severity: 'error',
|
|
36
|
+
title: 'Select a TSRX runtime target',
|
|
37
|
+
message:
|
|
38
|
+
'The compiler could not infer a runtime target. Call detect-target with a project cwd, or pass target as ripple, react, preact, solid, or vue.',
|
|
39
|
+
documentation: ['tsrx://docs/target-integration.md'],
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const has_function_component_syntax = error_codes.has(DIAGNOSTIC_CODES.FUNCTION_COMPONENT_SYNTAX);
|
|
44
|
+
if (has_function_component_syntax) {
|
|
45
|
+
advice.push({
|
|
46
|
+
kind: 'function-component-syntax',
|
|
47
|
+
severity: 'warning',
|
|
48
|
+
title: 'Use TSRX component declarations',
|
|
49
|
+
message:
|
|
50
|
+
'.tsrx component files should use the component keyword. A React-style function returning JSX is usually the wrong authoring shape for TSRX.',
|
|
51
|
+
documentation: ['tsrx://docs/components.md'],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const fired_jsx_return = error_codes.has(DIAGNOSTIC_CODES.JSX_RETURN_IN_COMPONENT);
|
|
56
|
+
|
|
57
|
+
if (fired_jsx_return) {
|
|
58
|
+
advice.push({
|
|
59
|
+
kind: 'jsx-return-in-component',
|
|
60
|
+
severity: 'error',
|
|
61
|
+
title: 'Do not return JSX from component bodies',
|
|
62
|
+
message:
|
|
63
|
+
'Inside a TSRX component body, template elements are statements. Replace `return <div />` with a template statement like `<div />`; use bare `return;` only for guard exits.',
|
|
64
|
+
documentation: ['tsrx://docs/components.md', 'tsrx://docs/tsx-expression-values.md'],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (error_codes.has(DIAGNOSTIC_CODES.UNCLOSED_TAG)) {
|
|
69
|
+
advice.push({
|
|
70
|
+
kind: 'unclosed-tag',
|
|
71
|
+
severity: 'error',
|
|
72
|
+
title: 'Close template tags',
|
|
73
|
+
message:
|
|
74
|
+
'The compiler found a template tag without a matching closing tag. Add the missing closing tag before changing target-specific code.',
|
|
75
|
+
documentation: ['tsrx://docs/components.md'],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (error_codes.has(DIAGNOSTIC_CODES.MISMATCHED_CLOSING_TAG)) {
|
|
80
|
+
advice.push({
|
|
81
|
+
kind: 'mismatched-closing-tag',
|
|
82
|
+
severity: 'error',
|
|
83
|
+
title: 'Match closing tags',
|
|
84
|
+
message:
|
|
85
|
+
'The compiler found a closing tag that does not match the current open template tag. Align the tag names or close the inner tag first.',
|
|
86
|
+
documentation: ['tsrx://docs/components.md'],
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (error_codes.has(DIAGNOSTIC_CODES.JSX_EXPRESSION_VALUE)) {
|
|
91
|
+
advice.push({
|
|
92
|
+
kind: 'jsx-expression-value',
|
|
93
|
+
severity: 'info',
|
|
94
|
+
title: 'Wrap expression-position JSX',
|
|
95
|
+
message:
|
|
96
|
+
'When JSX is needed as a value, wrap it in a fragment `<>...</>` or `<tsx>...</tsx>` so TSRX knows it is an expression rather than a template statement.',
|
|
97
|
+
documentation: ['tsrx://docs/tsx-expression-values.md'],
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (advice.length === 0 && compileResult.ok) {
|
|
102
|
+
advice.push({
|
|
103
|
+
kind: 'compile-clean',
|
|
104
|
+
severity: 'info',
|
|
105
|
+
title: 'TSRX compiled successfully',
|
|
106
|
+
message:
|
|
107
|
+
'No target-neutral TSRX issues were found. For runtime API or bundler details, continue with the target-specific guidance for the detected target.',
|
|
108
|
+
documentation: ['tsrx://docs/target-integration.md'],
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (advice.length === 0) {
|
|
113
|
+
advice.push({
|
|
114
|
+
kind: 'compiler-diagnostic',
|
|
115
|
+
severity: 'error',
|
|
116
|
+
title: 'Compiler diagnostics need review',
|
|
117
|
+
message:
|
|
118
|
+
'The TSRX compiler returned diagnostics that do not match a known MCP hint yet. Use the normalized compiler errors and relevant docs sections to revise the source.',
|
|
119
|
+
documentation: ['tsrx://docs/overview.md'],
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return advice;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {{
|
|
128
|
+
* code: string,
|
|
129
|
+
* compileResult: {
|
|
130
|
+
* ok: boolean,
|
|
131
|
+
* target: string | null,
|
|
132
|
+
* compilerPackage: string | null,
|
|
133
|
+
* filename: string,
|
|
134
|
+
* cwd: string,
|
|
135
|
+
* errors: Array<{
|
|
136
|
+
* message: string,
|
|
137
|
+
* code: string | null,
|
|
138
|
+
* type: string | null,
|
|
139
|
+
* fileName: string | null,
|
|
140
|
+
* pos: number | null,
|
|
141
|
+
* end: number | null,
|
|
142
|
+
* raisedAt: number | null,
|
|
143
|
+
* loc: unknown
|
|
144
|
+
* }>
|
|
145
|
+
* }
|
|
146
|
+
* }} input
|
|
147
|
+
*/
|
|
148
|
+
export function analyze_tsrx_result(input) {
|
|
149
|
+
const { compileResult } = input;
|
|
150
|
+
const advice = create_advice({
|
|
151
|
+
compileResult,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
ok: compileResult.ok,
|
|
156
|
+
target: compileResult.target,
|
|
157
|
+
compilerPackage: compileResult.compilerPackage,
|
|
158
|
+
filename: compileResult.filename,
|
|
159
|
+
cwd: compileResult.cwd,
|
|
160
|
+
errors: compileResult.errors,
|
|
161
|
+
advice,
|
|
162
|
+
nextSteps: compileResult.ok
|
|
163
|
+
? [
|
|
164
|
+
'Use target-specific resources for runtime semantics.',
|
|
165
|
+
'Compile again after changing generated TSRX.',
|
|
166
|
+
]
|
|
167
|
+
: [
|
|
168
|
+
'Apply the highest-severity advice first.',
|
|
169
|
+
'Fetch the linked docs resources for syntax details.',
|
|
170
|
+
'Run compile-tsrx again after revising the source.',
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {{
|
|
177
|
+
* code: string,
|
|
178
|
+
* filename?: string,
|
|
179
|
+
* target?: string,
|
|
180
|
+
* cwd?: string,
|
|
181
|
+
* collect?: boolean,
|
|
182
|
+
* loose?: boolean,
|
|
183
|
+
* mode?: 'client' | 'server'
|
|
184
|
+
* }} input
|
|
185
|
+
*/
|
|
186
|
+
export async function analyze_tsrx(input) {
|
|
187
|
+
const compileResult = await compile_tsrx({
|
|
188
|
+
...input,
|
|
189
|
+
includeCode: false,
|
|
190
|
+
});
|
|
191
|
+
return analyze_tsrx_result({
|
|
192
|
+
code: input.code,
|
|
193
|
+
compileResult,
|
|
194
|
+
});
|
|
195
|
+
}
|
package/src/compile.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { detect_target, TARGET_CANDIDATES } from './target.js';
|
|
5
|
+
|
|
6
|
+
const VALID_TARGETS = new Set(TARGET_CANDIDATES.map((candidate) => candidate.target));
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {{
|
|
10
|
+
* message: string,
|
|
11
|
+
* code: string | null,
|
|
12
|
+
* type: string | null,
|
|
13
|
+
* fileName: string | null,
|
|
14
|
+
* pos: number | null,
|
|
15
|
+
* end: number | null,
|
|
16
|
+
* raisedAt: number | null,
|
|
17
|
+
* loc: unknown
|
|
18
|
+
* }} NormalizedCompileError
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string | null | undefined} target
|
|
23
|
+
*/
|
|
24
|
+
function get_target_candidate(target) {
|
|
25
|
+
if (!target) return null;
|
|
26
|
+
return TARGET_CANDIDATES.find((candidate) => candidate.target === target) ?? null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {unknown} error
|
|
31
|
+
* @param {string} filename
|
|
32
|
+
* @returns {NormalizedCompileError}
|
|
33
|
+
*/
|
|
34
|
+
function normalize_error(error, filename) {
|
|
35
|
+
if (error && typeof error === 'object') {
|
|
36
|
+
const candidate = /** @type {Record<string, unknown>} */ (error);
|
|
37
|
+
return {
|
|
38
|
+
message: candidate.message ? String(candidate.message) : String(error),
|
|
39
|
+
code: candidate.code ? String(candidate.code) : null,
|
|
40
|
+
type: candidate.type ? String(candidate.type) : null,
|
|
41
|
+
fileName: candidate.fileName ? String(candidate.fileName) : filename,
|
|
42
|
+
pos: typeof candidate.pos === 'number' ? candidate.pos : null,
|
|
43
|
+
end: typeof candidate.end === 'number' ? candidate.end : null,
|
|
44
|
+
raisedAt: typeof candidate.raisedAt === 'number' ? candidate.raisedAt : null,
|
|
45
|
+
loc: candidate.loc ?? null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
message: String(error),
|
|
50
|
+
code: null,
|
|
51
|
+
type: null,
|
|
52
|
+
fileName: filename,
|
|
53
|
+
pos: null,
|
|
54
|
+
end: null,
|
|
55
|
+
raisedAt: null,
|
|
56
|
+
loc: null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {unknown} errors
|
|
62
|
+
* @param {string} filename
|
|
63
|
+
*/
|
|
64
|
+
function normalize_errors(errors, filename) {
|
|
65
|
+
return Array.isArray(errors) ? errors.map((error) => normalize_error(error, filename)) : [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {string} cwd
|
|
70
|
+
* @param {string | null} package_json_path
|
|
71
|
+
*/
|
|
72
|
+
function create_project_require(cwd, package_json_path) {
|
|
73
|
+
return createRequire(package_json_path ?? path.join(path.resolve(cwd), 'package.json'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {string} compiler_package
|
|
78
|
+
* @param {string} cwd
|
|
79
|
+
* @param {string | null} package_json_path
|
|
80
|
+
*/
|
|
81
|
+
async function import_compiler(compiler_package, cwd, package_json_path) {
|
|
82
|
+
const project_require = create_project_require(cwd, package_json_path);
|
|
83
|
+
const resolved = project_require.resolve(compiler_package);
|
|
84
|
+
return import(pathToFileURL(resolved).href);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {unknown} result
|
|
89
|
+
*/
|
|
90
|
+
function get_generated_code(result) {
|
|
91
|
+
if (!result || typeof result !== 'object') return null;
|
|
92
|
+
const output = /** @type {Record<string, unknown>} */ (result);
|
|
93
|
+
if (typeof output.code === 'string') return output.code;
|
|
94
|
+
if (
|
|
95
|
+
output.js &&
|
|
96
|
+
typeof output.js === 'object' &&
|
|
97
|
+
typeof (/** @type {Record<string, unknown>} */ (output.js).code) === 'string'
|
|
98
|
+
) {
|
|
99
|
+
return /** @type {string} */ (/** @type {Record<string, unknown>} */ (output.js).code);
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {unknown} result
|
|
106
|
+
*/
|
|
107
|
+
function get_generated_css(result) {
|
|
108
|
+
if (!result || typeof result !== 'object') return null;
|
|
109
|
+
const output = /** @type {Record<string, unknown>} */ (result);
|
|
110
|
+
if (
|
|
111
|
+
output.css &&
|
|
112
|
+
typeof output.css === 'object' &&
|
|
113
|
+
typeof (/** @type {Record<string, unknown>} */ (output.css).code) === 'string'
|
|
114
|
+
) {
|
|
115
|
+
return /** @type {string} */ (/** @type {Record<string, unknown>} */ (output.css).code);
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {{
|
|
122
|
+
* code: string,
|
|
123
|
+
* filename?: string,
|
|
124
|
+
* target?: string,
|
|
125
|
+
* cwd?: string,
|
|
126
|
+
* collect?: boolean,
|
|
127
|
+
* loose?: boolean,
|
|
128
|
+
* includeCode?: boolean,
|
|
129
|
+
* mode?: 'client' | 'server'
|
|
130
|
+
* }} input
|
|
131
|
+
*/
|
|
132
|
+
export async function compile_tsrx(input) {
|
|
133
|
+
const filename = input.filename ?? 'Component.tsrx';
|
|
134
|
+
const detection = detect_target(input.cwd);
|
|
135
|
+
const cwd = detection.cwd;
|
|
136
|
+
const target = input.target ?? detection.detectedTarget;
|
|
137
|
+
|
|
138
|
+
if (!target) {
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
target: null,
|
|
142
|
+
compilerPackage: null,
|
|
143
|
+
filename,
|
|
144
|
+
cwd,
|
|
145
|
+
errors: [
|
|
146
|
+
{
|
|
147
|
+
message:
|
|
148
|
+
detection.confidence === 'ambiguous'
|
|
149
|
+
? detection.message
|
|
150
|
+
: `Could not infer a TSRX target. Pass target explicitly. ${detection.message}`,
|
|
151
|
+
code: null,
|
|
152
|
+
type: null,
|
|
153
|
+
fileName: filename,
|
|
154
|
+
pos: null,
|
|
155
|
+
end: null,
|
|
156
|
+
raisedAt: null,
|
|
157
|
+
loc: null,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
code: null,
|
|
161
|
+
css: null,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!VALID_TARGETS.has(target)) {
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
target,
|
|
169
|
+
compilerPackage: null,
|
|
170
|
+
filename,
|
|
171
|
+
cwd,
|
|
172
|
+
errors: [
|
|
173
|
+
{
|
|
174
|
+
message: `Unknown TSRX target "${target}".`,
|
|
175
|
+
code: null,
|
|
176
|
+
type: null,
|
|
177
|
+
fileName: filename,
|
|
178
|
+
pos: null,
|
|
179
|
+
end: null,
|
|
180
|
+
raisedAt: null,
|
|
181
|
+
loc: null,
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
code: null,
|
|
185
|
+
css: null,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const candidate = get_target_candidate(target);
|
|
190
|
+
if (!candidate) {
|
|
191
|
+
throw new Error(`Missing compiler candidate for target "${target}".`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const compiler = await import_compiler(
|
|
196
|
+
candidate.compilerPackage,
|
|
197
|
+
cwd,
|
|
198
|
+
detection.packageJsonPath,
|
|
199
|
+
);
|
|
200
|
+
if (typeof compiler.compile !== 'function') {
|
|
201
|
+
throw new Error(`${candidate.compilerPackage} does not export a compile() function.`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const result = compiler.compile(input.code, filename, {
|
|
205
|
+
collect: input.collect ?? true,
|
|
206
|
+
loose: input.loose,
|
|
207
|
+
mode: input.mode,
|
|
208
|
+
});
|
|
209
|
+
const errors = normalize_errors(result?.errors, filename);
|
|
210
|
+
const code = get_generated_code(result);
|
|
211
|
+
const css = get_generated_css(result);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
ok: errors.length === 0,
|
|
215
|
+
target,
|
|
216
|
+
compilerPackage: candidate.compilerPackage,
|
|
217
|
+
filename,
|
|
218
|
+
cwd,
|
|
219
|
+
errors,
|
|
220
|
+
code: input.includeCode ? code : null,
|
|
221
|
+
css: input.includeCode ? css : null,
|
|
222
|
+
};
|
|
223
|
+
} catch (error) {
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
target,
|
|
227
|
+
compilerPackage: candidate.compilerPackage,
|
|
228
|
+
filename,
|
|
229
|
+
cwd,
|
|
230
|
+
errors: [normalize_error(error, filename)],
|
|
231
|
+
code: null,
|
|
232
|
+
css: null,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
package/src/docs.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export { documentation_sections } from './generated/docs.js';
|
|
2
|
+
|
|
3
|
+
import { documentation_sections } from './generated/docs.js';
|
|
4
|
+
|
|
5
|
+
/** @typedef {{ slug: string, title: string, use_cases: string, content: string }} DocumentationSection */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} value
|
|
9
|
+
*/
|
|
10
|
+
function normalize(value) {
|
|
11
|
+
return value
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
14
|
+
.replace(/^-|-$/g, '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @returns {DocumentationSection[]}
|
|
19
|
+
*/
|
|
20
|
+
export function list_documentation_sections() {
|
|
21
|
+
return documentation_sections;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} section
|
|
26
|
+
*/
|
|
27
|
+
export function find_documentation_section(section) {
|
|
28
|
+
const normalized = normalize(section);
|
|
29
|
+
return (
|
|
30
|
+
documentation_sections.find(
|
|
31
|
+
(candidate) =>
|
|
32
|
+
candidate.slug === normalized ||
|
|
33
|
+
normalize(candidate.title) === normalized ||
|
|
34
|
+
candidate.slug === section,
|
|
35
|
+
) ?? null
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} section
|
|
41
|
+
*/
|
|
42
|
+
export function find_similar_documentation_sections(section) {
|
|
43
|
+
const normalized = normalize(section);
|
|
44
|
+
return documentation_sections.filter(
|
|
45
|
+
(candidate) =>
|
|
46
|
+
candidate.slug.includes(normalized) ||
|
|
47
|
+
normalize(candidate.title).includes(normalized) ||
|
|
48
|
+
candidate.use_cases.toLowerCase().includes(section.toLowerCase()),
|
|
49
|
+
);
|
|
50
|
+
}
|