generated from bazel-contrib/rules-template
-
-
Notifications
You must be signed in to change notification settings - Fork 60
/
Copy pathstylelint.bzl
244 lines (210 loc) · 9.24 KB
/
stylelint.bzl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
"""Configures [Stylelint](https://stylelint.io/) to run as a Bazel aspect
First, all CSS sources must be the srcs of some Bazel rule.
You can use a `filegroup` with `lint-with-stylelint` in the `tags`:
```python
filegroup(
name = "css",
srcs = glob(["*.css"]),
tags = ["lint-with-stylelint"],
)
```
See the `filegroup_tags` and `rule_kinds` attributes below to customize this behavior.
## Usage
Add `stylelint` as a `devDependency` in your `package.json`, and declare a binary target for Bazel to execute it.
For example in `tools/lint/BUILD.bazel`:
```starlark
load("@npm//:stylelint/package_json.bzl", stylelint_bin = "bin")
stylelint_bin.stylelint_binary(name = "stylelint")
```
Then declare the linter aspect, typically in `tools/lint/linters.bzl`:
```starlark
load("@aspect_rules_lint//lint:stylelint.bzl", "lint_stylelint_aspect")
stylelint = lint_stylelint_aspect(
binary = "@@//tools/lint:stylelint",
config = "@@//:stylelintrc",
)
```
Finally, register the aspect with your linting workflow, such as in `.aspect/cli/config.yaml` for `aspect lint`.
"""
load("@aspect_bazel_lib//lib:copy_to_bin.bzl", "COPY_FILE_TO_BIN_TOOLCHAINS", "copy_files_to_bin_actions")
load("@aspect_rules_js//js:libs.bzl", "js_lib_helpers")
load("//lint/private:lint_aspect.bzl", "LintOptionsInfo", "filter_srcs", "output_files", "patch_and_output_files", "should_visit")
_MNEMONIC = "AspectRulesLintStylelint"
def _gather_inputs(ctx, srcs, files = []):
inputs = copy_files_to_bin_actions(ctx, srcs)
# Add the config file along with any deps it has on npm packages
if "gather_files_from_js_providers" in dir(js_lib_helpers):
# rules_js 1.x
js_inputs = js_lib_helpers.gather_files_from_js_providers(
[ctx.attr._config_file] + files,
include_transitive_sources = True,
include_declarations = False,
include_npm_linked_packages = True,
)
else:
# rules_js 2.x
js_inputs = js_lib_helpers.gather_files_from_js_infos(
[ctx.attr._config_file] + files,
include_sources = True,
include_transitive_sources = True,
include_types = False,
include_transitive_types = False,
include_npm_sources = True,
)
return depset(inputs, transitive = [js_inputs])
def stylelint_action(ctx, executable, srcs, stderr, exit_code = None, env = {}, options = [], format = None):
"""Spawn stylelint as a Bazel action
Args:
ctx: an action context OR aspect context
executable: struct with an _stylelint field
srcs: list of file objects to lint
config: js_library representing the config file (and its dependencies)
stderr: output file containing the stderr or --output-file of stylelint
exit_code: output file containing the exit code of stylelint.
If None, then fail the build when stylelint exits non-zero.
Exit codes may be:
1 - fatal error
2 - lint problem
64 - invalid CLI usage
78 - invalid configuration file
env: environment variables for stylelint
options: additional command-line arguments
format: a formatter to add as a command line argument
"""
outputs = [stderr]
# Wire command-line options, see https://stylelint.io/user-guide/cli#options
args = ctx.actions.args()
args.add_all(options)
args.add_all(srcs)
if exit_code:
command = "{stylelint} $@ 2>{stderr}; echo $? >" + exit_code.path
outputs.append(exit_code)
else:
# Create empty file on success, as Bazel expects one
command = "{stylelint} $@ && touch {stderr}"
file_inputs = []
if type(format) == "string":
args.add_all(["--formatter", format])
elif format != None:
args.add_all(["--custom-formatter", "../../../" + format.files.to_list()[0].path])
file_inputs.append(format)
ctx.actions.run_shell(
inputs = _gather_inputs(ctx, srcs, file_inputs),
outputs = outputs,
command = command.format(stylelint = executable._stylelint.path, stderr = stderr.path),
arguments = [args],
mnemonic = _MNEMONIC,
env = dict(env, **{
"BAZEL_BINDIR": ctx.bin_dir.path,
}),
progress_message = "Linting %{label} with Stylelint",
tools = [executable._stylelint],
)
def stylelint_fix(ctx, executable, srcs, patch, stderr, exit_code, env = {}, options = []):
"""Create a Bazel Action that spawns stylelint with --fix.
Args:
ctx: an action context OR aspect context
executable: struct with a _stylelint field
srcs: list of file objects to lint
config: js_library representing the config file (and its dependencies)
patch: output file containing the applied fixes that can be applied with the patch(1) command.
stderr: output file containing the stderr or --output-file of stylelint
exit_code: output file containing the exit code of stylelint
env: environment variables for stylelint
options: additional command line options
"""
patch_cfg = ctx.actions.declare_file("_{}.patch_cfg".format(ctx.label.name))
args = ["--fix"]
args.extend(options)
args.extend([s.short_path for s in srcs])
ctx.actions.write(
output = patch_cfg,
content = json.encode({
"linter": executable._stylelint.path,
"args": args,
"env": dict(env, **{"BAZEL_BINDIR": ctx.bin_dir.path}),
"files_to_diff": [s.path for s in srcs],
"output": patch.path,
}),
)
ctx.actions.run(
inputs = depset([patch_cfg], transitive = [_gather_inputs(ctx, srcs)]),
outputs = [patch, stderr, exit_code],
executable = executable._patcher,
arguments = [patch_cfg.path],
env = dict(env, **{
"BAZEL_BINDIR": ".",
"JS_BINARY__EXIT_CODE_OUTPUT_FILE": exit_code.path,
"JS_BINARY__STDERR_OUTPUT_FILE": stderr.path,
"JS_BINARY__SILENT_ON_SUCCESS": "1",
}),
tools = [executable._stylelint],
mnemonic = _MNEMONIC,
progress_message = "Linting %{label} with Stylelint",
)
# buildifier: disable=function-docstring
def _stylelint_aspect_impl(target, ctx):
if not should_visit(ctx.rule, ctx.attr._rule_kinds, ctx.attr._filegroup_tags):
return []
files_to_lint = filter_srcs(ctx.rule)
if ctx.attr._options[LintOptionsInfo].fix:
outputs, info = patch_and_output_files(_MNEMONIC, target, ctx)
else:
outputs, info = output_files(_MNEMONIC, target, ctx)
# https://stylelint.io/user-guide/cli#--color---no-color
color_options = ["--color"] if ctx.attr._options[LintOptionsInfo].color else ["--no-color"]
# stylelint can produce a patch file at the same time it reports the unpatched violations
if hasattr(outputs, "patch"):
stylelint_fix(ctx, ctx.executable, files_to_lint, outputs.patch, outputs.human.out, outputs.human.exit_code, options = color_options)
else:
stylelint_action(ctx, ctx.executable, files_to_lint, outputs.human.out, outputs.human.exit_code, options = color_options)
# TODO(alex): if we run with --fix, this will report the issues that were fixed. Does a machine reader want to know about them?
stylelint_action(ctx, ctx.executable, files_to_lint, outputs.machine.out, outputs.machine.exit_code, format = ctx.attr._compact_formatter)
return [info]
def lint_stylelint_aspect(binary, config, rule_kinds = ["css_library"], filegroup_tags = ["lint-with-stylelint"]):
"""A factory function to create a linter aspect.
Args:
binary: the stylelint binary, typically a rule like
```
load("@npm//:stylelint/package_json.bzl", stylelint_bin = "bin")
stylelint_bin.stylelint_binary(name = "stylelint")
```
config: label(s) of the stylelint config file
rule_kinds: which [kinds](https://bazel.build/query/language#kind) of rules should be visited by the aspect
filegroup_tags: which tags on a `filegroup` indicate that it should be visited by the aspect
"""
return aspect(
implementation = _stylelint_aspect_impl,
attrs = {
"_options": attr.label(
default = "//lint:options",
providers = [LintOptionsInfo],
),
"_stylelint": attr.label(
default = binary,
executable = True,
cfg = "exec",
),
"_config_file": attr.label(
default = config,
allow_files = True,
),
"_compact_formatter": attr.label(
default = "@aspect_rules_lint//lint:stylelint.compact-formatter",
allow_single_file = True,
cfg = "exec",
),
"_patcher": attr.label(
default = "@aspect_rules_lint//lint/private:patcher",
executable = True,
cfg = "exec",
),
"_filegroup_tags": attr.string_list(
default = filegroup_tags,
),
"_rule_kinds": attr.string_list(
default = rule_kinds,
),
},
toolchains = COPY_FILE_TO_BIN_TOOLCHAINS,
)