@cockernutx/language-plugin-brace 0.1.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 +128 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +242 -0
- package/dist/package.json +3 -0
- package/index.cjs +11 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vue-brace contributors
|
|
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,128 @@
|
|
|
1
|
+
# @cockernutx/language-plugin-brace
|
|
2
|
+
|
|
3
|
+
Volar / Vue language tools plugin for `lang="brace"` templates.
|
|
4
|
+
|
|
5
|
+
Without it, Volar hands the brace source to the HTML parser: template expressions are never
|
|
6
|
+
*checked*, so `@if (…)` / `@for (…)` conditions are seen as plain text and the whole block
|
|
7
|
+
produces spurious errors and no hover.
|
|
8
|
+
|
|
9
|
+
## Build before you use it
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
deno task build:plugin # from the repo root
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Volar loads a language plugin by resolving it and calling `require()` on the result**, in
|
|
16
|
+
`vue-tsc` and in the editor's language server — both Node processes. Serving
|
|
17
|
+
`src/index.ts` works only under Deno, which can `require()` TypeScript directly. In Node it
|
|
18
|
+
is a `SyntaxError`; Volar catches it and carries on **without the plugin**, so every brace
|
|
19
|
+
template loses hover, completions and diagnostics, and the only trace is a warning in the
|
|
20
|
+
Vue Language Server output channel:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
[Vue] Resolve plugin path failed: @cockernutx/language-plugin-brace SyntaxError: …
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This is the failure mode this package was written in, and it is worth knowing because it
|
|
27
|
+
looks like "the plugin does nothing" rather than "the plugin did not load". `index.cjs`
|
|
28
|
+
therefore loads `dist/index.js`, and `deno task test` rebuilds it; a test fails if `dist` is
|
|
29
|
+
missing or older than the source it came from.
|
|
30
|
+
|
|
31
|
+
**How to tell it loaded:** open a brace template and hover a variable in a `@try` block. With
|
|
32
|
+
the plugin loaded you get a type; without it, `any` (or nothing) — and Volar will be reporting
|
|
33
|
+
errors on lines of brace syntax that are perfectly valid. **Reload the window** after building:
|
|
34
|
+
the language server only loads plugins at startup.
|
|
35
|
+
|
|
36
|
+
## Setup
|
|
37
|
+
|
|
38
|
+
```jsonc
|
|
39
|
+
// tsconfig.app.json
|
|
40
|
+
{
|
|
41
|
+
"vueCompilerOptions": {
|
|
42
|
+
"plugins": ["@cockernutx/language-plugin-brace"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
|
|
49
|
+
Volar reaches a custom template language through three hooks. The plugin implements all
|
|
50
|
+
three:
|
|
51
|
+
|
|
52
|
+
1. **`getEmbeddedCodes` / `resolveEmbeddedCode`** — publish the raw brace block as an
|
|
53
|
+
embedded document with `lang: 'brace'`. Volar's built-in `vue-sfc-template` plugin only
|
|
54
|
+
does this for `lang === 'html'`, so without these hooks a brace template has no virtual
|
|
55
|
+
file at all: no highlighting and no in-template features.
|
|
56
|
+
2. **`compileSFCTemplate`** — compiles the generated template with language-core's
|
|
57
|
+
`compileTemplate` and supplies the AST that template type checking works from.
|
|
58
|
+
3. **Offset remapping** — every location in that AST is rewritten back into brace-source
|
|
59
|
+
coordinates.
|
|
60
|
+
|
|
61
|
+
Step 3 is why `compileBrace` returns a map. Because the transform emits one line per input
|
|
62
|
+
line, a generated line belongs to exactly one source line, and within it every copied
|
|
63
|
+
fragment is recorded as a `{ gen, src, length }` segment. A generated position inside one of
|
|
64
|
+
those segments maps to the exact source character; anything else is synthesised markup
|
|
65
|
+
(`<template v-if="…">`) and falls back to the end of the nearest copied run, or to the end of
|
|
66
|
+
the source line when it has none.
|
|
67
|
+
|
|
68
|
+
Per-fragment mapping matters more than it sounds. One `@for` line becomes
|
|
69
|
+
`<template v-for="item in items" :key="item.id">`, so `item`, `items` and `item.id` all sit
|
|
70
|
+
at different offsets from their generated counterparts; a single shift per line gets only
|
|
71
|
+
the first of them right, and the rest — the ones outside an `@if` line, which is most of a
|
|
72
|
+
template — point at the wrong token, which is what makes hover and go-to-definition fail.
|
|
73
|
+
|
|
74
|
+
The embedded content pushed in step 1 is the *raw brace source*, which is the same
|
|
75
|
+
coordinate space the remapped AST uses. Keeping those two in step is the whole trick, and
|
|
76
|
+
it is why the two hooks cannot be implemented independently.
|
|
77
|
+
|
|
78
|
+
A malformed block is reported through `onError` rather than thrown, so a bad template
|
|
79
|
+
produces a diagnostic instead of a dead language server.
|
|
80
|
+
|
|
81
|
+
### Why the AST must stay in brace coordinates
|
|
82
|
+
|
|
83
|
+
Volar does not only read offsets out of this AST — it also slices text:
|
|
84
|
+
|
|
85
|
+
- `options.template` (the brace source) is sliced using AST offsets, so offsets have to be
|
|
86
|
+
brace coordinates for expressions, props and children to map anywhere sensible.
|
|
87
|
+
- `parseVForNode` slices `node.loc.source` — the *generated* text — using offsets relative to
|
|
88
|
+
the node. That is the one place where the two coordinate spaces collide, and it is what used
|
|
89
|
+
to make `@for` with `index` / `key` report two TypeScript syntax errors; the alignment pass
|
|
90
|
+
described under Caveats is what reconciles them. Everything else is consistent because both
|
|
91
|
+
the offsets and the document they describe end up in brace coordinates.
|
|
92
|
+
|
|
93
|
+
## Highlighting
|
|
94
|
+
|
|
95
|
+
The `lang: 'brace'` embedded document needs a grammar to be useful, which VS Code can only
|
|
96
|
+
get from an extension. `packages/vscode-vue-brace` registers that language id and a TextMate
|
|
97
|
+
grammar — install it as described in its README. Without it, brace templates render as
|
|
98
|
+
plain text.
|
|
99
|
+
|
|
100
|
+
## Entry point
|
|
101
|
+
|
|
102
|
+
Volar resolves plugin names with `require()` and then calls the resolved module *itself* as
|
|
103
|
+
the factory (no `.default` unwrapping), so the entry has to be a callable CommonJS module
|
|
104
|
+
that **Node** can load. `index.cjs` re-exports the default from `dist/index.js`, which
|
|
105
|
+
`deno task build:plugin` produces. Pointing it at `src/index.ts` instead is the trap
|
|
106
|
+
described at the top of this file: Deno loads it happily and the editor never does.
|
|
107
|
+
|
|
108
|
+
## Caveats
|
|
109
|
+
|
|
110
|
+
- The plugin API version is pinned to `2.2`. language-core's `validVersions` is
|
|
111
|
+
`[2, 2.1, 2.2]`; anything else makes it drop the plugin with only a console warning.
|
|
112
|
+
- **`@for` with `index` or `key` needs the element aligned to the author's column.** Volar
|
|
113
|
+
rebuilds the
|
|
114
|
+
`v-for` binding by slicing the generated `<template v-for="(item, i) in …">` text with
|
|
115
|
+
offsets *relative to the element*, so those deltas have to come out as the generated ones.
|
|
116
|
+
`collectVForPatterns` reads the binding names with generated deltas *before* the remap;
|
|
117
|
+
`alignVForPatterns` then anchors the element on the `@for` column the author wrote and
|
|
118
|
+
rebuilds `loc.source` as a padded pattern occupying the author's own columns. Before that,
|
|
119
|
+
the slice produced garbage: the errors above, plus garbage binding names whose semantic
|
|
120
|
+
tokens landed across the template. (An earlier attempt moved the element's start onto the
|
|
121
|
+
generated geometry instead. That repairs the slice and corrupts every other consumer of that
|
|
122
|
+
offset, which reads it as a *source* position — do not reintroduce it.)
|
|
123
|
+
- **`vue-tsc` needs a Node runtime.** It cannot run under Deno — Volar registers `.vue`
|
|
124
|
+
support by rewriting `tsc.js` through an `fs.readFileSync` hook that Deno bypasses — so a
|
|
125
|
+
Node binary has to be present to type-check templates. That is how the loader bug above was
|
|
126
|
+
found: `node …/vue-tsc.js --noEmit` reported `Resolve plugin path failed`, and
|
|
127
|
+
`--listFiles` showed no `.vue` files in the program at all.
|
|
128
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { VueLanguagePlugin } from '@vue/language-core';
|
|
2
|
+
/**
|
|
3
|
+
* Volar plugin for `lang="brace"`.
|
|
4
|
+
*
|
|
5
|
+
* Volar only reaches a custom template language through `compileSFCTemplate`; without it
|
|
6
|
+
* the brace source is handed to the HTML parser, so template expressions are never
|
|
7
|
+
* type-checked. Wire it up with:
|
|
8
|
+
*
|
|
9
|
+
* ```jsonc
|
|
10
|
+
* // tsconfig.json
|
|
11
|
+
* { "vueCompilerOptions": { "plugins": ["@cockernutx/language-plugin-brace"] } }
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
declare const plugin: VueLanguagePlugin;
|
|
15
|
+
export default plugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const compile_1 = require("@cockernutx/brace-template/compile");
|
|
4
|
+
const mapper_1 = require("@cockernutx/brace-template/mapper");
|
|
5
|
+
const preprocessor_1 = require("@cockernutx/brace-template/preprocessor");
|
|
6
|
+
/**
|
|
7
|
+
* Apply `visit` to every object in the tree, once.
|
|
8
|
+
*
|
|
9
|
+
* Every *object*, not every AST node: `remapOffsets` has to reach the `{ start, end }`
|
|
10
|
+
* locations nested inside `loc`, which carry no `type` of their own, so the filter has to be
|
|
11
|
+
* the callback's business.
|
|
12
|
+
*/
|
|
13
|
+
function walk(value, visit, seen = new Set()) {
|
|
14
|
+
if (!value || typeof value !== 'object' || seen.has(value))
|
|
15
|
+
return;
|
|
16
|
+
seen.add(value);
|
|
17
|
+
const record = value;
|
|
18
|
+
visit(record);
|
|
19
|
+
for (const key in record)
|
|
20
|
+
walk(record[key], visit, seen);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Rewrite `offset` on every node of the tree, in place.
|
|
24
|
+
*
|
|
25
|
+
* The compiler's AST carries offsets on expression nodes and locations; Volar derives lines
|
|
26
|
+
* and columns from them, so every one of them has to move into brace-source coordinates.
|
|
27
|
+
*/
|
|
28
|
+
function remapOffsets(value, toSourceOffset) {
|
|
29
|
+
walk(value, (node) => {
|
|
30
|
+
if (typeof node.offset === 'number')
|
|
31
|
+
node.offset = toSourceOffset(node.offset);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Point each element's `loc.start` at its tag name rather than at the `<`.
|
|
36
|
+
*
|
|
37
|
+
* Volar's component-name tokens (`vue-component-semantic-tokens` in the language server) compute
|
|
38
|
+
* the range like this:
|
|
39
|
+
*
|
|
40
|
+
* ```js
|
|
41
|
+
* let start = element.loc.start.offset
|
|
42
|
+
* if (template.lang === 'html') start += 1 // skip the `<`
|
|
43
|
+
* push({ start, length: element.tag.length })
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* So for `lang === 'html'` the AST is assumed to start an element at the `<` and the provider
|
|
47
|
+
* compensates; for any other lang it takes `loc.start` to be the *name*. Our brace AST follows
|
|
48
|
+
* the html convention, so the token came out covering `<FragileChil` — one character wide of the
|
|
49
|
+
* name, leaving the last character to the grammar's colour. Volar's own mappings were exact
|
|
50
|
+
* throughout, which is what made this look like a mapping bug on our side; it is a convention
|
|
51
|
+
* mismatch in a branch keyed on the template's `lang`.
|
|
52
|
+
*
|
|
53
|
+
* Shifting by one keeps every other consumer working: `getElementTagOffsets` searches with
|
|
54
|
+
* `indexOf(node.tag, node.loc.start.offset)`, and the name starts exactly at that position, so
|
|
55
|
+
* the offset it returns is unchanged.
|
|
56
|
+
*/
|
|
57
|
+
function alignElementStartsToTagNames(ast) {
|
|
58
|
+
walk(ast, (node) => {
|
|
59
|
+
if (node.type !== 1 || typeof node.tag !== 'string' || !node.loc?.start)
|
|
60
|
+
return;
|
|
61
|
+
node.loc.start.offset += 1;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A `v-for`'s binding pattern, as Volar reads it.
|
|
66
|
+
*
|
|
67
|
+
* `codegen/template/vFor.js` (`parseVForNode`) rebuilds the pattern by slicing the element's
|
|
68
|
+
* own text between two offsets, relative to the element:
|
|
69
|
+
*
|
|
70
|
+
* ```js
|
|
71
|
+
* node.loc.source.slice(value.loc.start.offset - node.loc.start.offset,
|
|
72
|
+
* (index ?? key ?? value).loc.end.offset - node.loc.start.offset)
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* It then emits `for (const [<slice>] of …)` and maps the slice 1:1 onto the source range
|
|
76
|
+
* `[value.loc.start, (index ?? key ?? value).loc.end)`. Two coordinate spaces meet here:
|
|
77
|
+
*
|
|
78
|
+
* - `loc.source` is the element's text as parsed from the *generated* markup, so it has to be
|
|
79
|
+
* read with **generated** offsets;
|
|
80
|
+
* - the offsets themselves are remapped to **source** coordinates, because the same `loc` drives
|
|
81
|
+
* hover and navigation for the author's file.
|
|
82
|
+
*
|
|
83
|
+
* Reading the names with remapped offsets silently returned `"(it` instead of `item`. And
|
|
84
|
+
* because the emitted pattern is only as long as the slice, the range it maps onto has to be
|
|
85
|
+
* the author's clause — otherwise the two bindings land on the wrong columns (`index` used to
|
|
86
|
+
* be mapped onto `of`, and the whole range was scaled down to seven characters).
|
|
87
|
+
*/
|
|
88
|
+
function collectVForPatterns(ast) {
|
|
89
|
+
const found = [];
|
|
90
|
+
walk(ast, (node) => {
|
|
91
|
+
const parseResult = node.parseResult;
|
|
92
|
+
const { value, index, key } = parseResult ?? {};
|
|
93
|
+
if (!value?.loc || !node.loc)
|
|
94
|
+
return;
|
|
95
|
+
const last = index ?? key ?? value;
|
|
96
|
+
const pattern = node.loc.source;
|
|
97
|
+
if (typeof pattern !== 'string')
|
|
98
|
+
return;
|
|
99
|
+
const relative = (offset) => offset - node.loc.start.offset;
|
|
100
|
+
const names = [value, last].filter((name, i, all) => all.indexOf(name) === i);
|
|
101
|
+
found.push({
|
|
102
|
+
node,
|
|
103
|
+
value,
|
|
104
|
+
names: names.map((name) => [
|
|
105
|
+
name,
|
|
106
|
+
pattern.slice(relative(name.loc.start.offset), relative(name.loc.end.offset)),
|
|
107
|
+
]),
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
return found;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Give the element a source position, and build the pattern text Volar will slice out of it.
|
|
114
|
+
*
|
|
115
|
+
* The element is synthesised (`@for (…) {` becomes `<template v-for="(item, i) in …">`), so it
|
|
116
|
+
* is anchored to the directive the author wrote. The names are then placed at the columns they
|
|
117
|
+
* occupy in the author's clause, and the pattern is padded to the clause's length: the slice is
|
|
118
|
+
* a valid binding pattern, and because it is exactly as long as the source range it maps onto,
|
|
119
|
+
* each binding lands on the name that was typed. The padding is whitespace inside `[…]`, which
|
|
120
|
+
* is legal and inert.
|
|
121
|
+
*/
|
|
122
|
+
function alignVForPatterns(patterns, template) {
|
|
123
|
+
for (const { node, value, names } of patterns) {
|
|
124
|
+
const sourceLineStart = template.lastIndexOf('\n', node.loc.end.offset) + 1;
|
|
125
|
+
const indent = /^[ \t]*/.exec(template.slice(sourceLineStart))?.[0].length ?? 0;
|
|
126
|
+
node.loc.start.offset = sourceLineStart + indent;
|
|
127
|
+
const end = names.at(-1)[0].loc.end.offset - node.loc.start.offset;
|
|
128
|
+
if (end <= 0)
|
|
129
|
+
continue;
|
|
130
|
+
const chars = Array.from({ length: end }, () => ' ');
|
|
131
|
+
for (const [name, text] of names) {
|
|
132
|
+
const at = name.loc.start.offset - node.loc.start.offset;
|
|
133
|
+
if (at < 0)
|
|
134
|
+
continue;
|
|
135
|
+
for (let i = 0; i < text.length && at + i < end; i += 1)
|
|
136
|
+
chars[at + i] = text[i];
|
|
137
|
+
}
|
|
138
|
+
// Separate the names, so the slice parses as a two-element binding pattern.
|
|
139
|
+
if (names.length > 1) {
|
|
140
|
+
chars[value.loc.end.offset - node.loc.start.offset] = ',';
|
|
141
|
+
}
|
|
142
|
+
node.loc.source = chars.join('');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Volar plugin for `lang="brace"`.
|
|
147
|
+
*
|
|
148
|
+
* Volar only reaches a custom template language through `compileSFCTemplate`; without it
|
|
149
|
+
* the brace source is handed to the HTML parser, so template expressions are never
|
|
150
|
+
* type-checked. Wire it up with:
|
|
151
|
+
*
|
|
152
|
+
* ```jsonc
|
|
153
|
+
* // tsconfig.json
|
|
154
|
+
* { "vueCompilerOptions": { "plugins": ["@cockernutx/language-plugin-brace"] } }
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
const plugin = ({ modules }) => {
|
|
158
|
+
const { codeFeatures, compileTemplate } = modules['@vue/language-core'];
|
|
159
|
+
return {
|
|
160
|
+
name: '@cockernutx/language-plugin-brace',
|
|
161
|
+
// `validVersions` is [2, 2.1, 2.2]; anything else makes language-core drop the plugin
|
|
162
|
+
// with only a console warning.
|
|
163
|
+
version: 2.2,
|
|
164
|
+
/**
|
|
165
|
+
* language-core's built-in `vue-sfc-template` plugin only produces an embedded document
|
|
166
|
+
* when `lang === 'html'`, so without this a brace template has no virtual file in the
|
|
167
|
+
* editor at all: no highlighting, and no completions, hover, navigation or diagnostics
|
|
168
|
+
* inside it.
|
|
169
|
+
*
|
|
170
|
+
* The content pushed in `resolveEmbeddedCode` is the *raw brace source*, which is
|
|
171
|
+
* exactly the coordinate space the remapped AST uses — the two have to agree for any of
|
|
172
|
+
* this to line up, and keeping them in step is why `compileBraceWithMap` exists.
|
|
173
|
+
*/
|
|
174
|
+
getEmbeddedCodes(_fileName, ir) {
|
|
175
|
+
const template = ir.template;
|
|
176
|
+
if (!template || template.lang !== preprocessor_1.BRACE_LANG)
|
|
177
|
+
return [];
|
|
178
|
+
return [{ id: 'template', lang: preprocessor_1.BRACE_LANG }];
|
|
179
|
+
},
|
|
180
|
+
resolveEmbeddedCode(_fileName, ir, embeddedFile) {
|
|
181
|
+
const template = ir.template;
|
|
182
|
+
if (embeddedFile.id !== 'template' || !template || template.lang !== preprocessor_1.BRACE_LANG)
|
|
183
|
+
return;
|
|
184
|
+
embeddedFile.content.push([
|
|
185
|
+
template.content,
|
|
186
|
+
template.name,
|
|
187
|
+
0,
|
|
188
|
+
codeFeatures.full,
|
|
189
|
+
]);
|
|
190
|
+
},
|
|
191
|
+
compileSFCTemplate(lang, template, options) {
|
|
192
|
+
if (lang !== preprocessor_1.BRACE_LANG)
|
|
193
|
+
return;
|
|
194
|
+
let compiled;
|
|
195
|
+
try {
|
|
196
|
+
compiled = (0, compile_1.compileBraceWithMap)(template);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
// A malformed block must surface as a diagnostic, never as a dead language server. The
|
|
200
|
+
// cast names the error type of *this* options object rather than one imported from
|
|
201
|
+
// `@vue/compiler-dom`, for the reason explained at `TemplateAst`.
|
|
202
|
+
options.onError?.({
|
|
203
|
+
name: 'BraceCompileError',
|
|
204
|
+
message: error instanceof Error ? error.message : String(error),
|
|
205
|
+
code: 0,
|
|
206
|
+
});
|
|
207
|
+
return { ast: compileTemplate('', options), code: '', preamble: '' };
|
|
208
|
+
}
|
|
209
|
+
const toSourceOffset = (0, mapper_1.createOffsetMapper)(template, compiled.code, compiled.lines);
|
|
210
|
+
/**
|
|
211
|
+
* Move a diagnostic's location into brace coordinates.
|
|
212
|
+
*
|
|
213
|
+
* Generic on purpose: the identity of Volar's warning and error types is Volar's business,
|
|
214
|
+
* not ours, and a signature naming them would reintroduce the coupling this file avoids.
|
|
215
|
+
*/
|
|
216
|
+
const remap = (diagnostic) => {
|
|
217
|
+
if (diagnostic.loc) {
|
|
218
|
+
diagnostic.loc.start.offset = toSourceOffset(diagnostic.loc.start.offset);
|
|
219
|
+
diagnostic.loc.end.offset = toSourceOffset(diagnostic.loc.end.offset);
|
|
220
|
+
}
|
|
221
|
+
return diagnostic;
|
|
222
|
+
};
|
|
223
|
+
const ast = compileTemplate(compiled.code, {
|
|
224
|
+
...options,
|
|
225
|
+
onWarn: (warning) => options.onWarn?.(remap(warning)),
|
|
226
|
+
onError: (error) => options.onError?.(remap(error)),
|
|
227
|
+
});
|
|
228
|
+
// The `v-for` pattern needs both coordinate spaces: the names are read from the generated
|
|
229
|
+
// markup *before* the remap, and placed into the author's columns *after* it.
|
|
230
|
+
const vForPatterns = collectVForPatterns(ast);
|
|
231
|
+
remapOffsets(ast, toSourceOffset);
|
|
232
|
+
// Volar's component-token provider reads an element's `loc.start` as its tag *name* for
|
|
233
|
+
// any template whose lang is not `html` (see `alignElementStartsToTagNames`), so that has
|
|
234
|
+
// to be true of the offsets we hand over — and after the remap, so the shift applies to a
|
|
235
|
+
// source offset.
|
|
236
|
+
alignElementStartsToTagNames(ast);
|
|
237
|
+
alignVForPatterns(vForPatterns, template);
|
|
238
|
+
return { ast, code: '', preamble: '' };
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
exports.default = plugin;
|
package/index.cjs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Volar loads language plugins by resolving a specifier and calling `require()` on the
|
|
2
|
+
// result, with no `.default` unwrapping — see how `@vue/language-plugin-pug` ships as
|
|
3
|
+
// CommonJS.
|
|
4
|
+
//
|
|
5
|
+
// This file used to require `./src/index.ts`, which only works because Deno can `require()`
|
|
6
|
+
// TypeScript. The editor's language server and `vue-tsc` are Node processes, where that is a
|
|
7
|
+
// `SyntaxError`: Volar catches it and carries on without the plugin, so every brace template
|
|
8
|
+
// silently loses brace syntax, hover and diagnostics, and the only trace is a warning in the
|
|
9
|
+
// language server's output channel. `deno task build:plugin` compiles the plugin to `dist/`,
|
|
10
|
+
// which is what Node loads.
|
|
11
|
+
module.exports = require('./dist/index.js').default
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cockernutx/language-plugin-brace",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Volar / Vue language tools plugin for lang=\"brace\" SFC templates",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"require": "./index.cjs",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist", "index.cjs", "README.md", "LICENSE"],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"prepack": "deno task --cwd=../.. build:plugin",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest"
|
|
20
|
+
},
|
|
21
|
+
"keywords": ["vue", "volar", "language-server", "plugin", "template"],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/strawberyy-coconut/vue-brace.git",
|
|
25
|
+
"directory": "packages/language-plugin-brace"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/strawberyy-coconut/vue-brace#readme",
|
|
28
|
+
"bugs": "https://github.com/strawberyy-coconut/vue-brace/issues",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@cockernutx/brace-template": "^0.1.0",
|
|
31
|
+
"@vue/language-core": "^3.3.11"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"vue": "^3.5.0 || >=3.6.0-0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@vue/compiler-dom": "catalog:",
|
|
38
|
+
"vitest": "^5.0.2",
|
|
39
|
+
"vue": "catalog:"
|
|
40
|
+
}
|
|
41
|
+
}
|