brighterscript-xml-plugin 0.0.1
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/README.md +35 -0
- package/dist/SGFieldTypes.js +59 -0
- package/dist/SGXmlCompletionProvider.js +243 -0
- package/dist/SystemCompletion.js +24 -0
- package/dist/index.js +22 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# brighterscript-xml-plugin
|
|
2
|
+
## BrighterScript plugin for SceneGraph XML completions
|
|
3
|
+
|
|
4
|
+
### It Does:
|
|
5
|
+
- suggest components
|
|
6
|
+
- suggest component fields
|
|
7
|
+
|
|
8
|
+
### It Does Not:
|
|
9
|
+
- validate field values
|
|
10
|
+
- suggest interface elements like `field` and `function`
|
|
11
|
+
|
|
12
|
+
### Installation
|
|
13
|
+
```
|
|
14
|
+
npm i bsc-xml
|
|
15
|
+
```
|
|
16
|
+
`bsconfig.json`
|
|
17
|
+
```
|
|
18
|
+
{
|
|
19
|
+
...
|
|
20
|
+
plugins: [
|
|
21
|
+
'bsc-xml'
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Usage
|
|
27
|
+
SceneGraph Component completions
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
- provides both built-in Roku SceneGraph components and custom project components
|
|
32
|
+
- provides built-in fields and custom fields
|
|
33
|
+
- auto suggestions for fields when typing inside an element tag
|
|
34
|
+
- use completions shortcut to trigger manually
|
|
35
|
+
- component completions on `<` do not fill automatically, pretty sure this is something that needs to be configured at the vscode extension level.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* unused, just an export of all types for eventual validation
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SGFieldTypes = void 0;
|
|
7
|
+
class SGFieldTypes {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.typeMap = {
|
|
10
|
+
Time: true,
|
|
11
|
+
string: true,
|
|
12
|
+
float: true,
|
|
13
|
+
boolean: true,
|
|
14
|
+
'option string': true,
|
|
15
|
+
time: true,
|
|
16
|
+
Boolean: true,
|
|
17
|
+
integer: true,
|
|
18
|
+
'array of floats': true,
|
|
19
|
+
ContentNode: true,
|
|
20
|
+
color: true,
|
|
21
|
+
uri: true,
|
|
22
|
+
rect2d: true,
|
|
23
|
+
Float: true,
|
|
24
|
+
vector2d: true,
|
|
25
|
+
font: true,
|
|
26
|
+
'N/A': true,
|
|
27
|
+
'array of strings': true,
|
|
28
|
+
'Poster node': true,
|
|
29
|
+
Font: true,
|
|
30
|
+
'array of Boolean': true,
|
|
31
|
+
'array of colors': true,
|
|
32
|
+
ButtonGroup: true,
|
|
33
|
+
Event: true,
|
|
34
|
+
'array of integers': true,
|
|
35
|
+
'RSGPalette node': true,
|
|
36
|
+
"array of float's": true,
|
|
37
|
+
Node: true,
|
|
38
|
+
'option as string': true,
|
|
39
|
+
'array of float': true,
|
|
40
|
+
'URI string': true,
|
|
41
|
+
BusySpinner: true,
|
|
42
|
+
'color (string containing hex value e.g. RGBA)': true,
|
|
43
|
+
'array of integer': true,
|
|
44
|
+
'array of vector2d': true,
|
|
45
|
+
'associative array': true,
|
|
46
|
+
assocarray: true,
|
|
47
|
+
bool: true,
|
|
48
|
+
'associative array of associative arrays': true,
|
|
49
|
+
URL: true,
|
|
50
|
+
TargetSet: true,
|
|
51
|
+
int: true,
|
|
52
|
+
'array of TargetSet nodes': true,
|
|
53
|
+
Color: true,
|
|
54
|
+
'array of rectangles': true,
|
|
55
|
+
'roArray of roAssociativeArrays': true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.SGFieldTypes = SGFieldTypes;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SGXmlCompletionProvider = void 0;
|
|
4
|
+
const brighterscript_1 = require("brighterscript");
|
|
5
|
+
const vscode_languageserver_types_1 = require("vscode-languageserver-types");
|
|
6
|
+
const roku_types_1 = require("brighterscript/dist/roku-types");
|
|
7
|
+
const SystemCompletion_1 = require("./SystemCompletion");
|
|
8
|
+
const OPEN_CLOSE_TAGS = ['<', '</', '>', '/>'];
|
|
9
|
+
const systemNodes = roku_types_1.nodes;
|
|
10
|
+
class SGXmlCompletionProvider {
|
|
11
|
+
constructor(program) {
|
|
12
|
+
this.program = program;
|
|
13
|
+
this.systemCompletions = this.processSystemNodes();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
*
|
|
17
|
+
* @returns system nodes indexed by name {[name]: SystemCompletion}
|
|
18
|
+
*/
|
|
19
|
+
processSystemNodes() {
|
|
20
|
+
const result = {};
|
|
21
|
+
for (const key in systemNodes) {
|
|
22
|
+
const node = systemNodes[key];
|
|
23
|
+
result[key] = new SystemCompletion_1.SystemCompletion(node);
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
getFieldCompletions(componentName) {
|
|
28
|
+
var _a;
|
|
29
|
+
componentName = componentName.toLocaleLowerCase();
|
|
30
|
+
const projectComponent = this.program.getComponent(componentName);
|
|
31
|
+
let result = [];
|
|
32
|
+
if (projectComponent) {
|
|
33
|
+
result.push(...this.getCompletionsFromXmlFile(projectComponent.file));
|
|
34
|
+
const extendsName = (_a = projectComponent.file.ast.component) === null || _a === void 0 ? void 0 : _a.extends;
|
|
35
|
+
if (extendsName) {
|
|
36
|
+
result.push(...this.getAllAvailableFields(extendsName));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (systemNodes[componentName]) {
|
|
40
|
+
const node = this.systemCompletions[componentName];
|
|
41
|
+
result.push(...node.fields);
|
|
42
|
+
if (node.extends) {
|
|
43
|
+
result.push(...this.getAllAvailableFields(node.extends));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
this.program.logger.error(`Unknown component extension: ${componentName}`);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
getCompletionsFromXmlFile(xmlFile) {
|
|
52
|
+
var _a, _b;
|
|
53
|
+
return ((_b = (_a = xmlFile.ast.component) === null || _a === void 0 ? void 0 : _a.api.fields.map((f) => {
|
|
54
|
+
return {
|
|
55
|
+
label: f.id,
|
|
56
|
+
detail: `${f.type}${f.value ? `: ${f.value}` : ''}`,
|
|
57
|
+
};
|
|
58
|
+
})) !== null && _b !== void 0 ? _b : []);
|
|
59
|
+
}
|
|
60
|
+
getComponentCompletions(file, beforeToken) {
|
|
61
|
+
const completions = [];
|
|
62
|
+
// suggest components
|
|
63
|
+
completions.unshift(...Object.entries(this.systemCompletions).map(([key, obj]) => {
|
|
64
|
+
return {
|
|
65
|
+
label: obj.name,
|
|
66
|
+
sortText: '<' + obj.name,
|
|
67
|
+
kind: vscode_languageserver_types_1.CompletionItemKind.Class,
|
|
68
|
+
insertTextFormat: vscode_languageserver_types_1.InsertTextFormat.Snippet,
|
|
69
|
+
insertText: `${beforeToken === '<' ? '' : '<'}${obj.name} $0></${obj.name}>`,
|
|
70
|
+
};
|
|
71
|
+
}), ...this.program
|
|
72
|
+
.getScopes()
|
|
73
|
+
.filter(brighterscript_1.isXmlScope)
|
|
74
|
+
.filter((s) => s.xmlFile.componentName !== file.componentName)
|
|
75
|
+
.map((s) => s.xmlFile.componentName)
|
|
76
|
+
.map((name) => {
|
|
77
|
+
return {
|
|
78
|
+
label: name.text,
|
|
79
|
+
sortText: '<' + name.text,
|
|
80
|
+
kind: vscode_languageserver_types_1.CompletionItemKind.Class,
|
|
81
|
+
insertTextFormat: vscode_languageserver_types_1.InsertTextFormat.Snippet,
|
|
82
|
+
insertText: `${beforeToken === '<' ? '' : '<'}${name.text} $0></${name.text}>`,
|
|
83
|
+
};
|
|
84
|
+
}));
|
|
85
|
+
return completions;
|
|
86
|
+
}
|
|
87
|
+
getAllAvailableFields(componentName) {
|
|
88
|
+
var _a, _b, _c, _d, _e, _f;
|
|
89
|
+
const component = this.program.getComponent(componentName);
|
|
90
|
+
let result = [];
|
|
91
|
+
if (component) {
|
|
92
|
+
result = [
|
|
93
|
+
...((_c = (_b = (_a = component.file.ast.component) === null || _a === void 0 ? void 0 : _a.api) === null || _b === void 0 ? void 0 : _b.fields.map((f) => {
|
|
94
|
+
return {
|
|
95
|
+
name: f.id,
|
|
96
|
+
type: f.type,
|
|
97
|
+
sortText: '0_' + f.id,
|
|
98
|
+
};
|
|
99
|
+
})) !== null && _c !== void 0 ? _c : []),
|
|
100
|
+
...this.getAllAvailableFields((_e = (_d = component.file.ast.component) === null || _d === void 0 ? void 0 : _d.extends) !== null && _e !== void 0 ? _e : '').map((f) => {
|
|
101
|
+
return Object.assign(Object.assign({}, f), { sortText: '1_' + f.sortText });
|
|
102
|
+
}),
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
const rokuComponent = roku_types_1.nodes[componentName.toLowerCase()];
|
|
107
|
+
if (rokuComponent) {
|
|
108
|
+
const rokuComponentFields = (_f = rokuComponent.fields) === null || _f === void 0 ? void 0 : _f.filter((f) => { var _a; return (_a = f.accessPermission) === null || _a === void 0 ? void 0 : _a.includes('WRITE'); }).map((f) => {
|
|
109
|
+
return {
|
|
110
|
+
name: f.name,
|
|
111
|
+
type: f.type,
|
|
112
|
+
sortText: '0_' + f.name,
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
if (rokuComponent.extends) {
|
|
116
|
+
result = [
|
|
117
|
+
...rokuComponentFields,
|
|
118
|
+
...this.getAllAvailableFields(rokuComponent.extends.name).map((f) => {
|
|
119
|
+
return Object.assign(Object.assign({}, f), { sortText: '1_' + f.sortText });
|
|
120
|
+
}),
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
result = rokuComponentFields;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const resultMap = {};
|
|
129
|
+
result.forEach((e) => {
|
|
130
|
+
resultMap[e.name] = e;
|
|
131
|
+
});
|
|
132
|
+
result = Object.values(result);
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
process(event) {
|
|
136
|
+
var _a, _b, _c, _d, _e, _f;
|
|
137
|
+
const { file, position } = event;
|
|
138
|
+
const tokens = event.file.parser.tokens.map((t, i) => {
|
|
139
|
+
return Object.assign({ index: i }, t);
|
|
140
|
+
});
|
|
141
|
+
const cursorToken = tokens.find((t) => {
|
|
142
|
+
(t) => {
|
|
143
|
+
var _a;
|
|
144
|
+
return ((_a = (t.startLine - 1 <= position.line &&
|
|
145
|
+
t.startColumn - 1 < position.character &&
|
|
146
|
+
t.endColumn)) !== null && _a !== void 0 ? _a : 0 > position.character - 1);
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
const beforeToken = cursorToken
|
|
150
|
+
? cursorToken
|
|
151
|
+
: tokens.find((t, i, o) => {
|
|
152
|
+
var _a, _b, _c, _d;
|
|
153
|
+
if (t.endLine <= position.line + 1) {
|
|
154
|
+
const nextStartLine = (_b = (_a = o[i + 1]) === null || _a === void 0 ? void 0 : _a.startLine) !== null && _b !== void 0 ? _b : -1;
|
|
155
|
+
let nextStartColumn = -1;
|
|
156
|
+
if (nextStartLine === position.line + 1) {
|
|
157
|
+
nextStartColumn = (_c = o[i + 1]) === null || _c === void 0 ? void 0 : _c.startColumn;
|
|
158
|
+
}
|
|
159
|
+
else if (nextStartLine > position.line + 1) {
|
|
160
|
+
nextStartColumn = Number.MAX_SAFE_INTEGER;
|
|
161
|
+
}
|
|
162
|
+
return ((t.endLine < position.line + 1 ||
|
|
163
|
+
((_d = t.endColumn) !== null && _d !== void 0 ? _d : 0) < position.character + 1) &&
|
|
164
|
+
nextStartColumn > position.character);
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
});
|
|
168
|
+
if (beforeToken) {
|
|
169
|
+
if (beforeToken.image === '<') {
|
|
170
|
+
event.completions.push(...this.getComponentCompletions(file, beforeToken === null || beforeToken === void 0 ? void 0 : beforeToken.image));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
let openTag = beforeToken;
|
|
175
|
+
// iterate backwards until we find some kind of open or close tag
|
|
176
|
+
while (openTag) {
|
|
177
|
+
if (OPEN_CLOSE_TAGS.includes(openTag.image)) {
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
openTag = tokens[openTag.index - 1];
|
|
181
|
+
}
|
|
182
|
+
switch (openTag === null || openTag === void 0 ? void 0 : openTag.image) {
|
|
183
|
+
case '<': // we are inside an element definition
|
|
184
|
+
const fields = [];
|
|
185
|
+
let nextField = tokens[openTag.index + 1];
|
|
186
|
+
while (nextField) {
|
|
187
|
+
if (OPEN_CLOSE_TAGS.includes(nextField.image)) {
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
else if (nextField.image === '=') {
|
|
191
|
+
fields.push({
|
|
192
|
+
name: (_b = (_a = tokens[nextField.index - 1]) === null || _a === void 0 ? void 0 : _a.image) !== null && _b !== void 0 ? _b : '',
|
|
193
|
+
value: (_d = (_c = tokens[nextField.index + 1]) === null || _c === void 0 ? void 0 : _c.image.replace(/"/g, '')) !== null && _d !== void 0 ? _d : '',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
nextField = tokens[nextField.index + 1];
|
|
197
|
+
}
|
|
198
|
+
const componentName = (_e = tokens[openTag.index + 1]) === null || _e === void 0 ? void 0 : _e.image;
|
|
199
|
+
event.completions.push(...((_f = this.getAllAvailableFields(componentName !== null && componentName !== void 0 ? componentName : '')
|
|
200
|
+
.filter((f) => !fields.map((f) => f.name).includes(f.name))
|
|
201
|
+
.map((field) => {
|
|
202
|
+
var _a;
|
|
203
|
+
return {
|
|
204
|
+
label: field.name,
|
|
205
|
+
insertText: `${field.name}="\${1:${(_a = field.value) !== null && _a !== void 0 ? _a : ''}}"`,
|
|
206
|
+
kind: vscode_languageserver_types_1.CompletionItemKind.Field,
|
|
207
|
+
insertTextFormat: vscode_languageserver_types_1.InsertTextFormat.Snippet,
|
|
208
|
+
detail: field.type,
|
|
209
|
+
sortText: field.sortText,
|
|
210
|
+
};
|
|
211
|
+
})) !== null && _f !== void 0 ? _f : []));
|
|
212
|
+
return;
|
|
213
|
+
case '>':
|
|
214
|
+
case '/>':
|
|
215
|
+
event.completions.push(...this.getComponentCompletions(file, beforeToken === null || beforeToken === void 0 ? void 0 : beforeToken.image));
|
|
216
|
+
return;
|
|
217
|
+
default:
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const childrenOpenToken = tokens.find((token) => {
|
|
224
|
+
return token.image.toLowerCase() === 'children';
|
|
225
|
+
});
|
|
226
|
+
if (childrenOpenToken &&
|
|
227
|
+
(childrenOpenToken.endLine <= position.line ||
|
|
228
|
+
(childrenOpenToken.endLine === position.line + 1 &&
|
|
229
|
+
childrenOpenToken.endColumn < position.character))) {
|
|
230
|
+
event.completions.push(...this.getComponentCompletions(file, undefined));
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
event.completions.push({
|
|
234
|
+
label: 'new component',
|
|
235
|
+
insertText: `<component name="" extends="Group">
|
|
236
|
+
|
|
237
|
+
</component>`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
exports.SGXmlCompletionProvider = SGXmlCompletionProvider;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SystemCompletion = void 0;
|
|
4
|
+
class SystemCompletion {
|
|
5
|
+
constructor(systemNode) {
|
|
6
|
+
var _a;
|
|
7
|
+
this.name = systemNode.name;
|
|
8
|
+
this.component = {
|
|
9
|
+
label: systemNode.name,
|
|
10
|
+
};
|
|
11
|
+
this.extends = (_a = systemNode.extends) === null || _a === void 0 ? void 0 : _a.name;
|
|
12
|
+
this.fields = systemNode.fields
|
|
13
|
+
.filter((f) => f.accessPermission.includes('WRITE'))
|
|
14
|
+
.map((f) => {
|
|
15
|
+
return {
|
|
16
|
+
label: f.name,
|
|
17
|
+
type: f.type,
|
|
18
|
+
detail: f.default,
|
|
19
|
+
description: f.description,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.SystemCompletion = SystemCompletion;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BscXmlPlugin = void 0;
|
|
4
|
+
const brighterscript_1 = require("brighterscript");
|
|
5
|
+
const SGXmlCompletionProvider_1 = require("./SGXmlCompletionProvider");
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
class BscXmlPlugin {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.name = 'bsc-xml-plugin';
|
|
10
|
+
this.id = (0, crypto_1.randomUUID)();
|
|
11
|
+
// console.log('creating bsc xml plugin');
|
|
12
|
+
}
|
|
13
|
+
provideCompletions(completionEvent) {
|
|
14
|
+
if ((0, brighterscript_1.isXmlFile)(completionEvent.file)) {
|
|
15
|
+
return new SGXmlCompletionProvider_1.SGXmlCompletionProvider(completionEvent.program).process(completionEvent);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.BscXmlPlugin = BscXmlPlugin;
|
|
20
|
+
exports.default = () => {
|
|
21
|
+
return new BscXmlPlugin();
|
|
22
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "brighterscript-xml-plugin",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "BrighterScript plugin for SceneGraph XML completions",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"roku",
|
|
8
|
+
"brightscript",
|
|
9
|
+
"scenegraph",
|
|
10
|
+
"brighterscript",
|
|
11
|
+
"bsc"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "npm run clean && tsc",
|
|
15
|
+
"test": "mocha",
|
|
16
|
+
"clean": "rimraf dist",
|
|
17
|
+
"prepack": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"mocha": {
|
|
20
|
+
"spec": "src/**/*.spec.ts",
|
|
21
|
+
"require": [
|
|
22
|
+
"source-map-support/register",
|
|
23
|
+
"ts-node/register"
|
|
24
|
+
],
|
|
25
|
+
"fullTrace": true,
|
|
26
|
+
"timeout": 60000,
|
|
27
|
+
"watchExtensions": [
|
|
28
|
+
"ts"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"author": "Sam Heavner",
|
|
32
|
+
"license": "ISC",
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/chai": "^4.3.4",
|
|
35
|
+
"@types/mocha": "^10.0.1",
|
|
36
|
+
"@types/node": "^18.15.3",
|
|
37
|
+
"brighterscript": "0.65.x",
|
|
38
|
+
"chai": "^4.3.7",
|
|
39
|
+
"mocha": "^10.2.0",
|
|
40
|
+
"rimraf": "^5.0.5",
|
|
41
|
+
"source-map-support": "^0.5.21",
|
|
42
|
+
"ts-node": "^10.9.1",
|
|
43
|
+
"typescript": "^5.0.2",
|
|
44
|
+
"vscode-languageserver-types": "^3.17.3"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"brighterscript": "0.65.x"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"brighterscript": {
|
|
51
|
+
"optional": false
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
"dist/*.js",
|
|
56
|
+
"!dist/*.spec.js"
|
|
57
|
+
]
|
|
58
|
+
}
|