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