@lvce-editor/eslint-plugin-devcontainer 14.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/dist/index.js +290 -0
- package/package.json +15 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import json from '@eslint/json';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const isObjectNode = node => {
|
|
6
|
+
return node && node.type === 'Object';
|
|
7
|
+
};
|
|
8
|
+
const isStringNode = node => {
|
|
9
|
+
return node && node.type === 'String';
|
|
10
|
+
};
|
|
11
|
+
const getMemberName = member => {
|
|
12
|
+
if (member.name.type === 'String') {
|
|
13
|
+
return member.name.value;
|
|
14
|
+
}
|
|
15
|
+
if (member.name.type === 'Identifier') {
|
|
16
|
+
return member.name.name;
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
};
|
|
20
|
+
const findMember = (node, name) => {
|
|
21
|
+
if (!isObjectNode(node)) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
for (const member of node.members) {
|
|
25
|
+
if (getMemberName(member) === name) {
|
|
26
|
+
return member;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const defaultAllowedImages = ['mcr.microsoft.com/devcontainers/javascript-node:24'];
|
|
33
|
+
const meta$2 = {
|
|
34
|
+
docs: {
|
|
35
|
+
description: 'Ensure that the devcontainer image is allowed'
|
|
36
|
+
},
|
|
37
|
+
messages: {
|
|
38
|
+
imageMustBeString: 'devcontainer image must be a string',
|
|
39
|
+
missingImage: 'devcontainer image must be configured',
|
|
40
|
+
unsupportedImage: 'Unsupported devcontainer image: {{image}}'
|
|
41
|
+
},
|
|
42
|
+
schema: [{
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
properties: {
|
|
45
|
+
allowedImages: {
|
|
46
|
+
items: {
|
|
47
|
+
type: 'string'
|
|
48
|
+
},
|
|
49
|
+
type: 'array',
|
|
50
|
+
uniqueItems: true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
type: 'object'
|
|
54
|
+
}],
|
|
55
|
+
type: 'problem'
|
|
56
|
+
};
|
|
57
|
+
const getAllowedImages = context => {
|
|
58
|
+
const [options] = context.options;
|
|
59
|
+
if (options && typeof options === 'object' && Array.isArray(options.allowedImages)) {
|
|
60
|
+
return options.allowedImages;
|
|
61
|
+
}
|
|
62
|
+
return defaultAllowedImages;
|
|
63
|
+
};
|
|
64
|
+
const create$2 = context => {
|
|
65
|
+
const allowedImages = new Set(getAllowedImages(context));
|
|
66
|
+
return {
|
|
67
|
+
Document(node) {
|
|
68
|
+
if (!isObjectNode(node.body)) {
|
|
69
|
+
context.report({
|
|
70
|
+
loc: node.loc,
|
|
71
|
+
messageId: 'missingImage'
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const image = findMember(node.body, 'image');
|
|
76
|
+
if (!image) {
|
|
77
|
+
context.report({
|
|
78
|
+
loc: node.body.loc,
|
|
79
|
+
messageId: 'missingImage'
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!isStringNode(image.value)) {
|
|
84
|
+
context.report({
|
|
85
|
+
loc: image.name.loc,
|
|
86
|
+
messageId: 'imageMustBeString'
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!allowedImages.has(image.value.value)) {
|
|
91
|
+
context.report({
|
|
92
|
+
data: {
|
|
93
|
+
image: image.value.value
|
|
94
|
+
},
|
|
95
|
+
loc: image.name.loc,
|
|
96
|
+
messageId: 'unsupportedImage'
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const allowedImage = {
|
|
104
|
+
__proto__: null,
|
|
105
|
+
create: create$2,
|
|
106
|
+
defaultAllowedImages,
|
|
107
|
+
meta: meta$2
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const expectedPostCreateCommand = 'npm ci';
|
|
111
|
+
const playwrightInstallDepsPattern = /\bnpx\s+playwright\s+install-deps(?:\s|$|&&|;)/u;
|
|
112
|
+
const playwrightInstallPattern = /\bnpx\s+playwright\s+install(?:\s|$|&&|;)/u;
|
|
113
|
+
const dependencySections = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
114
|
+
const meta$1 = {
|
|
115
|
+
docs: {
|
|
116
|
+
description: 'Ensure that devcontainer postCreateCommand is configured correctly'
|
|
117
|
+
},
|
|
118
|
+
messages: {
|
|
119
|
+
invalidPostCreateCommand: 'devcontainer postCreateCommand must be npm ci',
|
|
120
|
+
missingPlaywrightPostinstall: 'packages/e2e/package.json postinstall must install Playwright dependencies',
|
|
121
|
+
missingPostCreateCommand: 'devcontainer postCreateCommand must be configured',
|
|
122
|
+
postCreateCommandMustBeString: 'devcontainer postCreateCommand must be a string'
|
|
123
|
+
},
|
|
124
|
+
type: 'problem'
|
|
125
|
+
};
|
|
126
|
+
const readJson = path => {
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
129
|
+
} catch {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const hasPlaywrightDependency = packageJson => {
|
|
134
|
+
for (const section of dependencySections) {
|
|
135
|
+
const dependencies = packageJson[section];
|
|
136
|
+
if (!dependencies || typeof dependencies !== 'object') {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if ('playwright' in dependencies || '@playwright/test' in dependencies) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
};
|
|
145
|
+
const hasRequiredPlaywrightPostinstall = packageJson => {
|
|
146
|
+
const postinstall = packageJson.scripts?.postinstall;
|
|
147
|
+
if (typeof postinstall !== 'string') {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
return playwrightInstallDepsPattern.test(postinstall) && playwrightInstallPattern.test(postinstall);
|
|
151
|
+
};
|
|
152
|
+
const shouldReportPlaywrightPostinstall = cwd => {
|
|
153
|
+
const packageJsonPath = join(cwd, 'packages', 'e2e', 'package.json');
|
|
154
|
+
if (!existsSync(packageJsonPath)) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const packageJson = readJson(packageJsonPath);
|
|
158
|
+
if (!packageJson || !hasPlaywrightDependency(packageJson)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
return !hasRequiredPlaywrightPostinstall(packageJson);
|
|
162
|
+
};
|
|
163
|
+
const create$1 = context => {
|
|
164
|
+
return {
|
|
165
|
+
Document(node) {
|
|
166
|
+
if (!isObjectNode(node.body)) {
|
|
167
|
+
context.report({
|
|
168
|
+
loc: node.loc,
|
|
169
|
+
messageId: 'missingPostCreateCommand'
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const postCreateCommand = findMember(node.body, 'postCreateCommand');
|
|
174
|
+
if (!postCreateCommand) {
|
|
175
|
+
context.report({
|
|
176
|
+
loc: node.body.loc,
|
|
177
|
+
messageId: 'missingPostCreateCommand'
|
|
178
|
+
});
|
|
179
|
+
} else if (!isStringNode(postCreateCommand.value)) {
|
|
180
|
+
context.report({
|
|
181
|
+
loc: postCreateCommand.name.loc,
|
|
182
|
+
messageId: 'postCreateCommandMustBeString'
|
|
183
|
+
});
|
|
184
|
+
} else if (postCreateCommand.value.value !== expectedPostCreateCommand) {
|
|
185
|
+
context.report({
|
|
186
|
+
loc: postCreateCommand.name.loc,
|
|
187
|
+
messageId: 'invalidPostCreateCommand'
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (shouldReportPlaywrightPostinstall(context.cwd)) {
|
|
191
|
+
context.report({
|
|
192
|
+
loc: node.body.loc,
|
|
193
|
+
messageId: 'missingPlaywrightPostinstall'
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const postCreateCommand = {
|
|
201
|
+
__proto__: null,
|
|
202
|
+
create: create$1,
|
|
203
|
+
meta: meta$1
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const desktopLiteFeature = 'ghcr.io/devcontainers/features/desktop-lite:1';
|
|
207
|
+
const meta = {
|
|
208
|
+
docs: {
|
|
209
|
+
description: 'Ensure that the desktop-lite devcontainer feature is enabled'
|
|
210
|
+
},
|
|
211
|
+
messages: {
|
|
212
|
+
featuresMustBeObject: 'devcontainer features must be an object',
|
|
213
|
+
missingDesktopLiteFeature: 'desktop-lite devcontainer feature must be enabled',
|
|
214
|
+
missingFeatures: 'devcontainer features must be configured'
|
|
215
|
+
},
|
|
216
|
+
type: 'problem'
|
|
217
|
+
};
|
|
218
|
+
const create = context => {
|
|
219
|
+
return {
|
|
220
|
+
Document(node) {
|
|
221
|
+
if (!isObjectNode(node.body)) {
|
|
222
|
+
context.report({
|
|
223
|
+
loc: node.loc,
|
|
224
|
+
messageId: 'missingFeatures'
|
|
225
|
+
});
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const features = findMember(node.body, 'features');
|
|
229
|
+
if (!features) {
|
|
230
|
+
context.report({
|
|
231
|
+
loc: node.body.loc,
|
|
232
|
+
messageId: 'missingFeatures'
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (!isObjectNode(features.value)) {
|
|
237
|
+
context.report({
|
|
238
|
+
loc: features.name.loc,
|
|
239
|
+
messageId: 'featuresMustBeObject'
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const feature = findMember(features.value, desktopLiteFeature);
|
|
244
|
+
if (!feature) {
|
|
245
|
+
context.report({
|
|
246
|
+
loc: features.name.loc,
|
|
247
|
+
messageId: 'missingDesktopLiteFeature'
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const requireDesktopLiteFeature = {
|
|
255
|
+
__proto__: null,
|
|
256
|
+
create,
|
|
257
|
+
meta
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const plugin = {
|
|
261
|
+
configs: {},
|
|
262
|
+
meta: {
|
|
263
|
+
name: 'devcontainer',
|
|
264
|
+
version: '0.0.1'
|
|
265
|
+
},
|
|
266
|
+
rules: {
|
|
267
|
+
'allowed-image': allowedImage,
|
|
268
|
+
'post-create-command': postCreateCommand,
|
|
269
|
+
'require-desktop-lite-feature': requireDesktopLiteFeature
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
const recommended = [{
|
|
273
|
+
files: ['**/.devcontainer/devcontainer.json'],
|
|
274
|
+
language: 'json/jsonc',
|
|
275
|
+
languageOptions: {
|
|
276
|
+
allowTrailingCommas: true
|
|
277
|
+
},
|
|
278
|
+
plugins: {
|
|
279
|
+
devcontainer: plugin,
|
|
280
|
+
// @ts-ignore
|
|
281
|
+
json
|
|
282
|
+
},
|
|
283
|
+
rules: {
|
|
284
|
+
'devcontainer/allowed-image': 'error',
|
|
285
|
+
'devcontainer/post-create-command': 'error',
|
|
286
|
+
'devcontainer/require-desktop-lite-feature': 'error'
|
|
287
|
+
}
|
|
288
|
+
}];
|
|
289
|
+
|
|
290
|
+
export { recommended as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lvce-editor/eslint-plugin-devcontainer",
|
|
3
|
+
"version": "14.0.1",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"description": "ESLint rules for devcontainer configuration files.",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/lvce-editor/eslint-config.git"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@eslint/json": "2.0.0"
|
|
14
|
+
}
|
|
15
|
+
}
|