Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 1 | load("@aspect_rules_js//js:providers.bzl", "JsInfo") |
| 2 | load("@bazel_skylib//rules:write_file.bzl", "write_file") |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 3 | load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory") |
| 4 | load("@aspect_bazel_lib//lib:copy_file.bzl", "copy_file") |
| 5 | load("@aspect_rules_esbuild//esbuild:defs.bzl", "esbuild") |
Philipp Schrader | 3de4dfc | 2023-02-15 20:18:25 -0800 | [diff] [blame] | 6 | |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 7 | #load("@npm//:history-server/package_json.bzl", history_server_bin = "bin") |
| 8 | load("@npm//:html-insert-assets/package_json.bzl", html_insert_assets_bin = "bin") |
| 9 | load("//tools/build_rules/js:ng.bzl", "ng_esbuild", "ng_project") |
| 10 | load("//tools/build_rules/js:ts.bzl", _ts_project = "ts_project") |
Philipp Schrader | ba072d9 | 2024-02-21 17:00:37 -0800 | [diff] [blame] | 11 | load("@aspect_rules_rollup//rollup:defs.bzl", upstream_rollup_bundle = "rollup") |
| 12 | load("@aspect_rules_terser//terser:defs.bzl", terser_minified = "terser") |
Philipp Schrader | 155e76c | 2023-02-25 18:42:31 -0800 | [diff] [blame] | 13 | load("@aspect_rules_cypress//cypress:defs.bzl", "cypress_module_test") |
Philipp Schrader | 3de4dfc | 2023-02-15 20:18:25 -0800 | [diff] [blame] | 14 | |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 15 | ts_project = _ts_project |
| 16 | |
| 17 | # Common dependencies of Angular applications |
| 18 | APPLICATION_DEPS = [ |
| 19 | "//:node_modules/@angular/common", |
| 20 | "//:node_modules/@angular/core", |
| 21 | #"//:node_modules/@angular/router", |
| 22 | "//:node_modules/@angular/platform-browser", |
| 23 | "//:node_modules/@types/node", |
| 24 | "//:node_modules/rxjs", |
| 25 | #"//:node_modules/tslib", |
| 26 | ] |
| 27 | |
| 28 | APPLICATION_HTML_ASSETS = ["styles.css", "favicon.ico"] |
| 29 | |
| 30 | # Common dependencies of Angular packages |
| 31 | PACKAGE_DEPS = [ |
| 32 | "//:node_modules/@angular/common", |
| 33 | "//:node_modules/@angular/core", |
| 34 | #"//:node_modules/@angular/router", |
| 35 | "//:node_modules/@types/node", |
| 36 | "//:node_modules/rxjs", |
| 37 | #"//:node_modules/tslib", |
| 38 | ] |
| 39 | |
| 40 | TEST_DEPS = APPLICATION_DEPS + [ |
| 41 | "//:node_modules/@angular/compiler", |
| 42 | "//:node_modules/@types/jasmine", |
| 43 | "//:node_modules/jasmine-core", |
| 44 | "//:node_modules/@angular/platform-browser-dynamic", |
| 45 | ] |
| 46 | |
| 47 | NG_DEV_DEFINE = { |
| 48 | "process.env.NODE_ENV": "'development'", |
| 49 | "ngJitMode": "false", |
| 50 | } |
| 51 | NG_PROD_DEFINE = { |
| 52 | "process.env.NODE_ENV": "'production'", |
| 53 | "ngDevMode": "false", |
| 54 | "ngJitMode": "false", |
| 55 | } |
| 56 | |
| 57 | def ng_application( |
| 58 | name, |
| 59 | deps = [], |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 60 | extra_srcs = [], |
| 61 | assets = None, |
| 62 | html_assets = APPLICATION_HTML_ASSETS, |
Adam Snaider | 13d48d9 | 2023-08-03 12:20:15 -0700 | [diff] [blame] | 63 | visibility = ["//visibility:public"]): |
Philipp Schrader | 3de4dfc | 2023-02-15 20:18:25 -0800 | [diff] [blame] | 64 | """ |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 65 | Bazel macro for compiling an Angular application. Creates {name}, test, serve targets. |
Philipp Schrader | 87277f4 | 2022-01-01 07:45:12 -0800 | [diff] [blame] | 66 | |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 67 | Projects structure: |
| 68 | main.ts |
| 69 | index.html |
| 70 | polyfills.ts |
| 71 | styles.css, favicon.ico (defaults, can be overriden) |
| 72 | app/ |
| 73 | **/*.{ts,css,html} |
| 74 | |
| 75 | Tests: |
| 76 | app/ |
| 77 | **/*.spec.ts |
| 78 | |
| 79 | Args: |
| 80 | name: the rule name |
| 81 | deps: direct dependencies of the application |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 82 | html_assets: assets to insert into the index.html, [styles.css, favicon.ico] by default |
| 83 | assets: assets to include in the file bundle |
| 84 | visibility: visibility of the primary targets ({name}, 'test', 'serve') |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 85 | """ |
| 86 | assets = assets if assets else native.glob(["assets/**/*"]) |
| 87 | html_assets = html_assets if html_assets else [] |
| 88 | |
| 89 | test_spec_srcs = native.glob(["app/**/*.spec.ts"]) |
| 90 | |
| 91 | srcs = native.glob( |
| 92 | ["main.ts", "app/**/*", "package.json"], |
| 93 | exclude = test_spec_srcs, |
| 94 | ) + extra_srcs |
| 95 | |
| 96 | # Primary app source |
| 97 | ng_project( |
| 98 | name = "_app", |
| 99 | srcs = srcs, |
| 100 | deps = deps + APPLICATION_DEPS, |
Austin Schuh | f737d47 | 2023-07-29 17:35:59 -0700 | [diff] [blame] | 101 | tags = [ |
| 102 | "no-remote-cache", |
| 103 | ], |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 104 | visibility = ["//visibility:private"], |
| 105 | ) |
| 106 | |
| 107 | # App polyfills source + bundle. |
| 108 | ng_project( |
| 109 | name = "_polyfills", |
| 110 | srcs = ["polyfills.ts"], |
| 111 | deps = ["//:node_modules/zone.js"], |
Austin Schuh | f737d47 | 2023-07-29 17:35:59 -0700 | [diff] [blame] | 112 | tags = [ |
| 113 | "no-remote-cache", |
| 114 | ], |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 115 | visibility = ["//visibility:private"], |
| 116 | ) |
| 117 | esbuild( |
| 118 | name = "polyfills-bundle", |
| 119 | entry_point = "polyfills.js", |
| 120 | srcs = [":_polyfills"], |
| 121 | define = {"process.env.NODE_ENV": "'production'"}, |
| 122 | config = { |
| 123 | "resolveExtensions": [".mjs", ".js"], |
| 124 | }, |
| 125 | metafile = False, |
| 126 | format = "esm", |
| 127 | minify = True, |
Austin Schuh | f737d47 | 2023-07-29 17:35:59 -0700 | [diff] [blame] | 128 | tags = [ |
| 129 | "no-remote-cache", |
| 130 | ], |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 131 | visibility = ["//visibility:private"], |
| 132 | ) |
| 133 | |
| 134 | _pkg_web( |
| 135 | name = "prod", |
| 136 | entry_point = "main.js", |
| 137 | entry_deps = [":_app"], |
| 138 | html_assets = html_assets, |
| 139 | assets = assets, |
| 140 | production = True, |
| 141 | visibility = ["//visibility:private"], |
| 142 | ) |
| 143 | |
| 144 | _pkg_web( |
| 145 | name = "dev", |
| 146 | entry_point = "main.js", |
| 147 | entry_deps = [":_app"], |
| 148 | html_assets = html_assets, |
| 149 | assets = assets, |
| 150 | production = False, |
| 151 | visibility = ["//visibility:private"], |
| 152 | ) |
| 153 | |
| 154 | # The default target: the prod package |
| 155 | native.alias( |
| 156 | name = name, |
| 157 | actual = "prod", |
| 158 | visibility = visibility, |
| 159 | ) |
| 160 | |
| 161 | def _pkg_web(name, entry_point, entry_deps, html_assets, assets, production, visibility): |
| 162 | """ Bundle and create runnable web package. |
| 163 | |
| 164 | For a given application entry_point, assets and defined constants... generate |
| 165 | a bundle using that entry and constants, an index.html referencing the bundle and |
| 166 | providated assets, package all content into a resulting directory of the given name. |
| 167 | """ |
| 168 | |
| 169 | bundle = "bundle-%s" % name |
| 170 | |
| 171 | ng_esbuild( |
| 172 | name = bundle, |
| 173 | entry_points = [entry_point], |
| 174 | srcs = entry_deps, |
| 175 | define = NG_PROD_DEFINE if production else NG_DEV_DEFINE, |
| 176 | format = "esm", |
| 177 | output_dir = True, |
| 178 | splitting = True, |
| 179 | metafile = False, |
| 180 | minify = production, |
| 181 | visibility = ["//visibility:private"], |
| 182 | ) |
| 183 | |
| 184 | html_out = "_%s_html" % name |
| 185 | |
| 186 | html_insert_assets_bin.html_insert_assets( |
| 187 | name = html_out, |
| 188 | outs = ["%s/index.html" % html_out], |
| 189 | args = [ |
| 190 | # Template HTML file. |
| 191 | "--html", |
| 192 | "$(location :index.html)", |
| 193 | # Output HTML file. |
| 194 | "--out", |
| 195 | "%s/%s/index.html" % (native.package_name(), html_out), |
| 196 | # Root directory prefixes to strip from asset paths. |
| 197 | "--roots", |
| 198 | native.package_name(), |
| 199 | "%s/%s" % (native.package_name(), html_out), |
| 200 | ] + |
| 201 | # Generic Assets |
| 202 | ["--assets"] + ["$(execpath %s)" % s for s in html_assets] + |
| 203 | ["--scripts", "--module", "polyfills-bundle.js"] + |
| 204 | # Main bundle to bootstrap the app last |
| 205 | ["--scripts", "--module", "%s/main.js" % bundle], |
| 206 | # The input HTML template, all assets for potential access for stamping |
| 207 | srcs = [":index.html", ":%s" % bundle, ":polyfills-bundle"] + html_assets, |
Austin Schuh | f737d47 | 2023-07-29 17:35:59 -0700 | [diff] [blame] | 208 | tags = [ |
| 209 | "no-remote-cache", |
| 210 | ], |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 211 | visibility = ["//visibility:private"], |
| 212 | ) |
| 213 | |
| 214 | copy_to_directory( |
| 215 | name = name, |
| 216 | srcs = [":%s" % bundle, ":polyfills-bundle", ":%s" % html_out] + html_assets + assets, |
| 217 | root_paths = [".", "%s/%s" % (native.package_name(), html_out)], |
| 218 | visibility = visibility, |
| 219 | ) |
| 220 | |
| 221 | # http server serving the bundle |
| 222 | # TODO(phil): Get this working. |
| 223 | #history_server_bin.history_server_binary( |
| 224 | # name = "serve" + ("-prod" if production else ""), |
| 225 | # args = ["$(location :%s)" % name], |
| 226 | # data = [":%s" % name], |
| 227 | # visibility = visibility, |
| 228 | #) |
| 229 | |
Adam Snaider | 13d48d9 | 2023-08-03 12:20:15 -0700 | [diff] [blame] | 230 | def ng_pkg(name, generate_public_api = True, extra_srcs = [], deps = [], visibility = ["//visibility:public"], **kwargs): |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 231 | """ |
| 232 | Bazel macro for compiling an npm-like Angular package project. Creates '{name}' and 'test' targets. |
| 233 | |
| 234 | Projects structure: |
| 235 | src/ |
| 236 | public-api.ts |
| 237 | **/*.{ts,css,html} |
| 238 | |
| 239 | Tests: |
| 240 | src/ |
| 241 | **/*.spec.ts |
| 242 | |
| 243 | Args: |
| 244 | name: the rule name |
| 245 | deps: package dependencies |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 246 | visibility: visibility of the primary targets ('{name}', 'test') |
| 247 | """ |
| 248 | |
| 249 | test_spec_srcs = native.glob(["**/*.spec.ts"]) |
| 250 | |
| 251 | srcs = native.glob( |
| 252 | ["**/*.ts", "**/*.css", "**/*.html"], |
| 253 | exclude = test_spec_srcs + [ |
| 254 | "public-api.ts", |
| 255 | ], |
| 256 | ) + extra_srcs |
| 257 | |
| 258 | # An index file to allow direct imports of the directory similar to a package.json "main" |
| 259 | write_file( |
| 260 | name = "_index", |
| 261 | out = "index.ts", |
| 262 | content = ["export * from \"./public-api\";"], |
| 263 | visibility = ["//visibility:private"], |
| 264 | ) |
| 265 | |
| 266 | if generate_public_api: |
| 267 | write_file( |
| 268 | name = "_public_api", |
| 269 | out = "public-api.ts", |
| 270 | content = [ |
| 271 | "export * from './%s.component';" % name, |
| 272 | "export * from './%s.module';" % name, |
| 273 | ], |
| 274 | visibility = ["//visibility:private"], |
| 275 | ) |
| 276 | srcs.append(":_public_api") |
| 277 | |
| 278 | ng_project( |
Philipp Schrader | 89342e5 | 2023-03-03 20:47:21 -0800 | [diff] [blame] | 279 | name = name, |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 280 | srcs = srcs + [":_index"], |
| 281 | deps = deps + PACKAGE_DEPS, |
Adam Snaider | 13d48d9 | 2023-08-03 12:20:15 -0700 | [diff] [blame] | 282 | visibility = visibility, |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 283 | **kwargs |
| 284 | ) |
| 285 | |
Philipp Schrader | ba072d9 | 2024-02-21 17:00:37 -0800 | [diff] [blame] | 286 | def rollup_bundle(name, entry_point, node_modules = "//:node_modules", deps = [], visibility = None, **kwargs): |
Philipp Schrader | 87277f4 | 2022-01-01 07:45:12 -0800 | [diff] [blame] | 287 | """Calls the upstream rollup_bundle() and exposes a .min.js file. |
| 288 | |
| 289 | Legacy version of rollup_bundle() used to provide the .min.js file. This |
| 290 | wrapper provides the same interface by explicitly exposing a .min.js file. |
| 291 | """ |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 292 | copy_file( |
| 293 | name = name + "__rollup_config", |
| 294 | src = "//:rollup.config.js", |
| 295 | out = name + "__rollup_config.js", |
| 296 | ) |
| 297 | |
Philipp Schrader | 87277f4 | 2022-01-01 07:45:12 -0800 | [diff] [blame] | 298 | upstream_rollup_bundle( |
| 299 | name = name, |
| 300 | visibility = visibility, |
| 301 | deps = deps + [ |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 302 | "//:node_modules/@rollup/plugin-node-resolve", |
Philipp Schrader | 87277f4 | 2022-01-01 07:45:12 -0800 | [diff] [blame] | 303 | ], |
Philipp Schrader | ba072d9 | 2024-02-21 17:00:37 -0800 | [diff] [blame] | 304 | node_modules = node_modules, |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 305 | sourcemap = "false", |
| 306 | config_file = ":%s__rollup_config.js" % name, |
| 307 | entry_point = entry_point, |
Philipp Schrader | 87277f4 | 2022-01-01 07:45:12 -0800 | [diff] [blame] | 308 | **kwargs |
| 309 | ) |
| 310 | |
| 311 | terser_minified( |
| 312 | name = name + "__min", |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 313 | srcs = [name + ".js"], |
Philipp Schrader | ba072d9 | 2024-02-21 17:00:37 -0800 | [diff] [blame] | 314 | node_modules = node_modules, |
Austin Schuh | f737d47 | 2023-07-29 17:35:59 -0700 | [diff] [blame] | 315 | tags = [ |
| 316 | "no-remote-cache", |
| 317 | ], |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 318 | sourcemap = False, |
Philipp Schrader | 87277f4 | 2022-01-01 07:45:12 -0800 | [diff] [blame] | 319 | ) |
| 320 | |
| 321 | # Copy the __min.js file (a declared output inside the rule) so that it's a |
| 322 | # pre-declared output and publicly visible. I.e. via attr.output() below. |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 323 | _expose_file_with_suffix( |
Philipp Schrader | 87277f4 | 2022-01-01 07:45:12 -0800 | [diff] [blame] | 324 | name = name + "__min_exposed", |
| 325 | src = ":%s__min" % name, |
| 326 | out = name + ".min.js", |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 327 | suffix = "__min.js", |
Philipp Schrader | 87277f4 | 2022-01-01 07:45:12 -0800 | [diff] [blame] | 328 | visibility = visibility, |
| 329 | ) |
| 330 | |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 331 | def _expose_file_with_suffix_impl(ctx): |
Philipp Schrader | 87277f4 | 2022-01-01 07:45:12 -0800 | [diff] [blame] | 332 | """Copies the .min.js file in order to make it publicly accessible.""" |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 333 | sources = ctx.attr.src[JsInfo].sources.to_list() |
Philipp Schrader | 87277f4 | 2022-01-01 07:45:12 -0800 | [diff] [blame] | 334 | min_js = None |
| 335 | for src in sources: |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 336 | if src.basename.endswith(ctx.attr.suffix): |
Philipp Schrader | 87277f4 | 2022-01-01 07:45:12 -0800 | [diff] [blame] | 337 | min_js = src |
| 338 | break |
| 339 | |
| 340 | if min_js == None: |
| 341 | fail("Couldn't find .min.js in " + str(ctx.attr.src)) |
| 342 | |
| 343 | ctx.actions.run( |
| 344 | inputs = [min_js], |
| 345 | outputs = [ctx.outputs.out], |
| 346 | executable = "cp", |
| 347 | arguments = [min_js.path, ctx.outputs.out.path], |
| 348 | ) |
| 349 | |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 350 | _expose_file_with_suffix = rule( |
| 351 | implementation = _expose_file_with_suffix_impl, |
Philipp Schrader | 87277f4 | 2022-01-01 07:45:12 -0800 | [diff] [blame] | 352 | attrs = { |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 353 | "src": attr.label(providers = [JsInfo]), |
Philipp Schrader | 87277f4 | 2022-01-01 07:45:12 -0800 | [diff] [blame] | 354 | "out": attr.output(mandatory = True), |
Philipp Schrader | 175a93c | 2023-02-19 13:13:40 -0800 | [diff] [blame] | 355 | "suffix": attr.string(mandatory = True), |
Philipp Schrader | 87277f4 | 2022-01-01 07:45:12 -0800 | [diff] [blame] | 356 | }, |
| 357 | ) |
Philipp Schrader | 155e76c | 2023-02-25 18:42:31 -0800 | [diff] [blame] | 358 | |
Philipp Schrader | ba072d9 | 2024-02-21 17:00:37 -0800 | [diff] [blame] | 359 | def cypress_test(name, runner, data = None, **kwargs): |
Philipp Schrader | 155e76c | 2023-02-25 18:42:31 -0800 | [diff] [blame] | 360 | """Runs a cypress test with the specified runner. |
| 361 | |
| 362 | Args: |
| 363 | runner: The runner that starts up any necessary servers and then |
| 364 | invokes Cypress itself. See the Module API documentation for more |
| 365 | information: https://docs.cypress.io/guides/guides/module-api |
| 366 | data: The spec files (*.cy.js) and the servers under test. Also any |
| 367 | other files needed at runtime. |
| 368 | kwargs: Arguments forwarded to the upstream cypress_module_test(). |
| 369 | """ |
| 370 | |
| 371 | # Figure out how many directories deep this package is relative to the |
| 372 | # workspace root. |
| 373 | package_depth = len(native.package_name().split("/")) |
| 374 | |
| 375 | # Chrome is located at the runfiles root. So we need to go up one more |
| 376 | # directory than the workspace root. |
| 377 | chrome_location = "../" * (package_depth + 1) + "chrome_linux/chrome" |
Philipp Schrader | ba072d9 | 2024-02-21 17:00:37 -0800 | [diff] [blame] | 378 | |
| 379 | copy_file( |
| 380 | name = name + "_config", |
| 381 | out = "cypress.config.js", |
| 382 | src = "//tools/build_rules/js:cypress.config.js", |
| 383 | visibility = ["//visibility:private"], |
| 384 | ) |
Philipp Schrader | 155e76c | 2023-02-25 18:42:31 -0800 | [diff] [blame] | 385 | |
| 386 | data = data or [] |
Philipp Schrader | ba072d9 | 2024-02-21 17:00:37 -0800 | [diff] [blame] | 387 | data.append(":%s_config" % name) |
Philipp Schrader | 155e76c | 2023-02-25 18:42:31 -0800 | [diff] [blame] | 388 | data.append("@xvfb_amd64//:wrapped_bin/Xvfb") |
Philipp Schrader | ba072d9 | 2024-02-21 17:00:37 -0800 | [diff] [blame] | 389 | data.append("//:node_modules") |
Philipp Schrader | 155e76c | 2023-02-25 18:42:31 -0800 | [diff] [blame] | 390 | |
| 391 | cypress_module_test( |
Philipp Schrader | ba072d9 | 2024-02-21 17:00:37 -0800 | [diff] [blame] | 392 | name = name, |
Philipp Schrader | 155e76c | 2023-02-25 18:42:31 -0800 | [diff] [blame] | 393 | args = [ |
| 394 | "run", |
Philipp Schrader | ba072d9 | 2024-02-21 17:00:37 -0800 | [diff] [blame] | 395 | "--config-file=cypress.config.js", |
Philipp Schrader | 155e76c | 2023-02-25 18:42:31 -0800 | [diff] [blame] | 396 | "--browser=" + chrome_location, |
| 397 | ], |
| 398 | browsers = ["@chrome_linux//:all"], |
| 399 | copy_data_to_bin = False, |
| 400 | cypress = "//:node_modules/cypress", |
| 401 | data = data, |
| 402 | runner = runner, |
| 403 | **kwargs |
| 404 | ) |