Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to @aspect_rules_js #48

Closed
dgp1130 opened this issue Jul 4, 2022 · 37 comments
Closed

Update to @aspect_rules_js #48

dgp1130 opened this issue Jul 4, 2022 · 37 comments
Labels
feature New feature or request
Milestone

Comments

@dgp1130
Copy link
Owner

dgp1130 commented Jul 4, 2022

The future of the Bazel Node ecosystem appears to be rules_js. It is still in RC right now but hopefully coming to stable soon. Would be worth playing around to see if it can improve things. The model of running actions inside a single source tree could smooth over some rough edges with ts_project() and tools which insist on the node_modules/ structure, like Parcel.

@dgp1130 dgp1130 added the feature New feature or request label Jul 4, 2022
@dgp1130 dgp1130 added this to the 1.0.0 milestone Jul 4, 2022
@dgp1130
Copy link
Owner Author

dgp1130 commented Jul 5, 2022

Migration doc - https://docs.aspect.build/aspect-build/rules_js/v1.0.0-rc.1/docs/migrate.html.

Attempting to follow the instructions, switching to pnpm seems easy enough. I needed to disable strict peer deps since @bazel/concatjs has a peer dep on Karma packages, but I don't actually use them so it should be safe to ignore. Apparently I'm also missing a dep on jasmine-core from @bazel/jasmine, but I'll worry about that later.

I'm a bit confused by the docs which indicate that "Link the node modules" comes before "Update WORKSPACE", feels like that order should be swapped? Also "Update WORKSPACE" is pretty lacking on details and how the existing workspace configuration should change. I tried dropping @build_bazel_rules_nodejs as I thought it was a dependency of @rules_js, but it turns out that dependency is actually @rules_nodejs, which is different from @build_bazel_rules_nodejs.

I also forced the Node version to stay the same at v12, though this technically isn't supported by pnpm. As long as pnpm isn't executed by Bazel directly (and I don't think it is), then I can use an out-of-band Node version for that separate from the rest of the repository. After this migration I can hopefully have an easier time swapping out rules_postcss for Parcel, which will unblock me from upgrading Node.

Next problem is load("@npm//@bazel/postcss:package.bzl", "rules_postcss_dependencies"), which doesn't work as that package.bzl file doesn't exist anymore. Commenting it out, I have similar errors for all imported Starlark code. I'm not too sure exactly how this is supposed to work as the migration instructions don't really elaborate. Do I need to update these packages now (ie. replace @bazel/typescript with rules_ts?) or should they still work? So far, I think I've only added @rules_js and updated it to generate @npm instead of @build_bazel_rules_nodejs. It seems these have been restructured as @npm//:@bazel/typescript/package_json.bzl. Rewriting the imports to that format seems to kinda work, but I'm unclear on differences between package.bzl and index.bzl (do they get magically merged together into package_json.bzl?).

Also I still need strict_visibility = False for macros and exports_directories_only = False for PostCSS stuff, so I'm not sure if those are hard blockers for this migration? It seems like ts_project() was, but I also still use --bazel_patch_module_resolver and link_workspace_root = True, which also might need to be removed first? Questions around how I should deploy my ruleset also come up, since rules_prerender currently ships index.bzl and the rest of the Starlark in the NPM rules_prerender package. It seems like @rules_ts no longer does this like @bazel/typescript does. Do I need to do the same and have users set this up via their WORKSPACE? Am I screwed for @bazel/postcss or can I use their WORKSPACE-based setup instructions?

Attempting to switch to @rules_postcss WORKSPACE setup finally passes loading however now I get:

Error: file '@npm//:@bazel/typescript/package_json.bzl' does not contain symbol 'ts_project'

which invalidates my hope of just rewriting imports. AFAICT there is no way to load Starlark code from an NPM package. That implies that I need to migrate all the rulesets I'm using to their rules_js versions in a single commit, which doesn't seem right. Maybe I could have two versions of @npm? Instead of replacing @npm, I added a new @npm_rules_js which managed to not break anything.

Not sure where to go next. The docs suggest replacing any binary tool invocations from node_modules to use @rules_js, but it turns out I don't do any of that. rules_prerender only ever imports hand-written macros / rules from Starlark, not generated binary tool wrappers. I guess the next step might be to replace one of my nodejs_binary() targets with a js_binary()? Not sure how that will play with @bazel/typescript ts_project() targets, but if they use the same providers, I think it should work?

I tried updating //packages/annotation_extractor to use js_binary(). This immediately failed because it can't copy_to_bin() from an external repository @npm (probably for the @npm//yargs dep I had. I tried switching this dependency to @npm_rules_js//:yargs, but this failed with an even more cryptic error.

ERROR: /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/external/npm_rules_js/BUILD.bazel:206:12: no such target '@npm_rules_js__yargs__16.2.0//:yargs': target 'yargs' not declared in package '' defined by /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/external/npm_rules_js__yargs__16.2.0/BUILD.bazel and referenced by '@npm_rules_js//:yargs'

The error is definitely internal to @rules_js, so I'm not sure where I'm going wrong here. It seems that workspace exists, but :yargs doesn't:

$ pnpm run bazel query @npm_rules_js__yargs__16.2.0//:all

> rules_prerender@0.0.0-PLACEHOLDER bazel /home/doug/Source/rules_prerender
> bazel "query" "@npm_rules_js__yargs__16.2.0//:all"

INFO: Invocation ID: 3b5a5313-bb2b-4c44-af46-074d1922620c
@npm_rules_js__yargs__16.2.0//:pkg
@npm_rules_js__yargs__16.2.0//:source_directory
Loading: 0 packages loaded

I think I'm definitely lost on how these two rulesets are supposed to interoperate with each other. I'll reach out to some Aspect folks and see if I can get some direction here. I think some of the questions I have are:

  1. How do @rules_js and @build_bazel_rules_nodejs interoperate? Is it possible to incrementally migrate targets/rulesets from one ecosystem to the other?
  2. Does @rules_postcss have a viable path here? Can I use it via @rules_js, or do I need to switch to something like Parcel as part of this process?
  3. What other migrations need to be landed before @rules_js can be used? In particular, @rules_prerender is still using:
    • strict_visibility = False - Because I export Starlark macros invoked by user code in their repository which calls @rules_prerender dependencies (user transitive dependencies), see Guidance around exporting macros with strict_visibility = True bazel-contrib/rules_nodejs#2393.
    • exports_directories_only = False - This is needed by @rules_postcss for reasons I don't really understand.
    • --bazel_patch_module_resolver - Needed for workspace-relative imports.
    • link_workspace_root = True - Needed for workspace-relative imports.
    • How do these complicate the migration to @rules_js and which of them are hard blockers?
  4. Since @rules_prerender itself exports a ruleset for other workspaces, do I need to change the deployment model? Is it still ok to ship Starlark code in NPM packages, or is that a no-no now?
    • Does this mean I need to use bzlmod? Is that a part of @rules_js or not?

Current process is in the rules-js, not yet convinced this is at all the right direction I should be going.

@dgp1130
Copy link
Owner Author

dgp1130 commented Jul 5, 2022

Looking at some examples, it seems that you're not supposed to depend on @npm_rules_js//:yargs, but rather //:node_modules/yargs from npm_link_all_packages().

Changing that dependency now builds the js_binary() successfully. However workspace-relative imports are busted due to the lack of link_workspace_root and --bazel_patch_module_resolver. Rewriting imports to be relative works and is probably the right solution here. I was able to remove link_workspace_root = True for everything but rollup_bundle(), which is a problem I can deal with separately. I tried to remove --bazel_patch_module_resolver but ran into some challenges with having the rules_prerender NPM package import a dependency in the workspace:

Cannot find module '../../common/models/prerender_resource'
Require stack:
- /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/sandbox/linux-sandbox/2493/execroot/rules_prerender/node_modules/rules_prerender/index.js
- /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/sandbox/linux-sandbox/2493/execroot/rules_prerender/bazel-out/host/bin/examples/minimal/page.js
- /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/sandbox/linux-sandbox/2493/execroot/rules_prerender/bazel-out/host/bin/packages/renderer/dynamic_import.js
- /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/sandbox/linux-sandbox/2493/execroot/rules_prerender/bazel-out/host/bin/packages/renderer/entry_point.js
- /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/sandbox/linux-sandbox/2493/execroot/rules_prerender/bazel-out/host/bin/packages/renderer/renderer.js

I get why that's an issue, resolving the path it would search for prerender_resources.js in /.../execroot/rules_prerender/common/models/prerender_resource.js, when the file is actually under /.../execroot/rules_prerender/bazel-out/host/bin/common/models/prerender_resource.js. I feel like I node to copy_to_bin() the node_modules/ directory, but I'm not sure how feasible that actually is. Fortunately I don't think I need to solve this problem right now, an incremental migration to @rules_js should hopefully address it.

Refocusing on migrating //packages/annotation_extractor, updating the dependencies to //:node_modules/%{dep} seems to be good enough. I also had to add the BAZEL_BINDIR variable to its usage, but after that it worked just fine. The other tools should work similarly, though renderer is a weird one because it has to dynamic import a file from an input, not a dependency.

Trying to migrate renderer struggled a bit with the entry point, since I needed to turn that into a relative import from the packages/renderer/dynamic_import.js file to the user's provided entry point in the package which prerender_resources() is generated. Fixing that seems to have been enough though, and it can load the file correctly. Unfortunately, that file imports the runtime at a bare rules_prerender path, which fails. So the next problem is out to simulate a local package and link it into node_modules/.

I feel like npm_package() and npm_link_package() are the right tools for this, but the docs show a lot of private implementation details which aren't exposed publicly and don't seem to work that way, so I'm a bit confused on how I'm supposed to use it.

I eventually discovered this example which shows how to use npm_link_package() from the root package. Using this strategy I'm able to link rules_prerender under node_modules/ in the runfiles tree. This kind of works, I suspect there's a bug in @aspect_rules_js here as adding/removing :node_modules/some_internal_package requires a bazel clean to take effect. After some fiddling I eventually got it working in a test js_binary(). It's able to import from rules_prerender, however rules_prerender cannot import outside its package:

Error: Cannot find module '../../common/models/prerender_resource'
Require stack:
- /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/execroot/rules_prerender/bazel-out/k8-fastbuild/bin/packages/rules_prerender/test_bin.sh.runfiles/rules_prerender/node_modules/.aspect_rules_js/rules_prerender@0.0.0/node_modules/rules_prerender/index.js
- /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/execroot/rules_prerender/bazel-out/k8-fastbuild/bin/packages/rules_prerender/test_bin.sh.runfiles/rules_prerender/packages/rules_prerender/test.js
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:815:15)
    at Function.Module._load (internal/modules/cjs/loader.js:667:27)
    at Module.require (internal/modules/cjs/loader.js:887:19)
    at require (internal/modules/cjs/helpers.js:74:18)
    at Object.<anonymous> (/home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/execroot/rules_prerender/bazel-out/k8-fastbuild/bin/packages/rules_prerender/test_bin.sh.runfiles/rules_prerender/node_modules/.aspect_rules_js/rules_prerender@0.0.0/node_modules/rules_prerender/index.js:5:28)
    at Module._compile (internal/modules/cjs/loader.js:999:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
    at Module.load (internal/modules/cjs/loader.js:863:32)
    at Function.Module._load (internal/modules/cjs/loader.js:708:14)
    at Module.require (internal/modules/cjs/loader.js:887:19) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [
    '/home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/execroot/rules_prerender/bazel-out/k8-fastbuild/bin/packages/rules_prerender/test_bin.sh.runfiles/rules_prerender/node_modules/.aspect_rules_js/rules_prerender@0.0.0/node_modules/rules_prerender/index.js',
    '/home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/execroot/rules_prerender/bazel-out/k8-fastbuild/bin/packages/rules_prerender/test_bin.sh.runfiles/rules_prerender/packages/rules_prerender/test.js'
  ]
}

This is the same problem removing --bazel_patch_module_resolver ran into. Fortunately this makes a little more sense to me now since this version of rules_prerender is under node_modules/, independent of any other sources present in the workspace. I think the solution here is to make rules_prerender vendor any other dependencies from the workspace in its own package. Looking at the package now I see:

$ bazel build //packages/rules_prerender:rules_prerender_pkg && tree dist/bin/packages/rules_prerender/rules_prerender_pkg/
INFO: Invocation ID: 023ce57c-59f0-4080-8bb9-0a9d9791b9c3
INFO: Analyzed target //packages/rules_prerender:rules_prerender_pkg (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //packages/rules_prerender:rules_prerender_pkg up-to-date:
  dist/bin/packages/rules_prerender/rules_prerender_pkg
INFO: Elapsed time: 0.111s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
dist/bin/packages/rules_prerender/rules_prerender_pkg/
├── index.d.ts
├── index.js
├── index.js.map
└── package.json

0 directories, 4 files

The package should have scripts.js and styles.js, but those aren't present. I assume this is because they are transitive dependencies of the npm_package() target, and I think pkg_npm() or some other @build_bazel_rules_nodejs rule worked that way. An easy solution there is to have some rule which collects all the transitive dependencies and exports them for npm_package() to consume. The bigger challenge is that anything outside of //packages/rules_prerender/... (where this npm_package() is defined, has no clear directory to live in. The issue is that this directory is NPM package-relative, not Bazel workspace-relative.

The very hacky workaround to solve this is to move the npm_package() target to the workspace-root BUILD.bazel file. Since that is at the root location, all the files it depends on will be in nested directories. so "NPM package-relative" and "Bazel workspace-relative" become equivalent. This surprisingly actually worked, though is not a great long-term strategy.

I migrated all the examples to include //:node_modules/rules_prerender and the vast majority of them "just worked". There are two use cases which didn't:

  1. Styles - The renderer is supposed to dump execroot-relative paths into the rendered HTML, which the resource injector reads, finds those files, and then inlines them. However, migrating the renderer to @aspect_rules_js means it only sees root-relative paths, so the resource injector only sees root-relative paths. It needs to somehow resolve these back to their execroot-relative paths to unambiguously inline them.
  2. Declarative shadow DOM - The rules_prerender/declarative_shadow_dom path doesn't import. I didn't get a chance to debug this further, but I'm guessing the problem is that previously I actually had two js_library(package_name = "rules_prerender") targets, one with the typical rules_prerender runtime, and another with the declarative shadow DOM piece. They worked because both happened to exist at the same Bazel package, which worked in @build_bazel_rules_nodejs and was desirable because depending on declarative shadow DOM should also include a client-side polyfill. I'm guessing this won't work in @aspect_rules_js (though I haven't confirmed that). I think the options to fix this are probably 1) split declarative shadow DOM into its own package or 2) put the declarative shadow DOM runtime into the rules_prerender package always. Not sure what the full implications of both of these will be.

Both of these problems feel solvable, so I'll keep hacking on them. At this point I think I have a reasonable understanding of how @aspect_rules_js and @build_bazel_rules_nodejs are supposed to interoperate. I think my remaining questions are:

  1. How is @build_bazel_rules_nodejs's ts_project() supposed to work with @aspect_rules_js? //:node_modules/* dependencies don't really work. Should I be migrating that first?
  2. How should I deploy rules_prerender? Should I stop putting Starlark code in the NPM package?

@dgp1130
Copy link
Owner Author

dgp1130 commented Jul 6, 2022

Fixed the styling issue by having the renderer still generate HTML with execroot-relative paths to CSS files. This is doable because it takes an inline style map as an input from Starlark, which maps "style import specifier" -> "execroot-relative path to CSS file". So even though the renderer is working in a single source tree with all root-relative paths, this map can still use an execroot-relative path and stay compatible with resource injector.

One other challenge I encountered in this process is that the inline style map actually wasn't working. I found that the map would be set correctly in the renderer, but then when it was read it would always be undefined. It seems that the issue here comes back to package management. Since rules_prerender is now its own package in node_modules/, it vendors the inline_style_map.js file. However, other imports of packages/rules_prerender/inline_style_map.js import through the workspace and get a different version of the file. This means the same source content is executed twice at different locations, with a different scope and state.

The solution here is to only use one version of the file. I updated the rules_prerender NPM package to export the inline style map getter and setter under private aliases so that one definition can be used even outside the package. Not elegant, but good enough for now.

Moving on to declarative shadow DOM, merging with the rules_prerender NPM package probably isn't a great idea since only the prerender source code needs to be in that package, meaning I have to break the invariant that a prerender_component() encapsulates all the flavors of its implementation and it is not possible to depend on any one flavor without also depending on the rest. While I could special case it for declarative shadow DOM, there's nothing inherently special about this component, and any use case in which a rules_prerender component is published to NPM will run into the same problem.

Therefore, I think the better solution is to put declarative shadow DOM into its own NPM package. To do that I need to take its published files and dump them into an npm_package() target, which seems easy enough. However it then begs the question of how to use convert that package back into a prerender_component(). This was previously done by manually creating a prerender_component() in the published NPM package's BUILD.bazel file. In theory, I could have a published_prerender_component(dep = ":some_npm_package_target") which converts the npm_package() into a prerender_component(), however I don't know how feasible that is given that an npm_package() packs everything into a TreeArtifact. That basically means I can't have any analysis time knowledge of anything in a prerender_component(). Being unable to ship Starlark in an NPM package also means we lose the context around which sources are for prerendering, which are for client side scripts, which are styles, and which are resources and how they're served.

I think we would need to define an NPM package format for shipping rules_prerender components, maybe something where the package.json defines exactly which files belong to what flavor of the component?

{
  "name": "@rules_prerender/declarative_shadow_dom",
  "rules_prerender": {
    "components": {
      "declarative_shadow_dom": {
        "prerender": ["./index.js", "./index.d.ts"],
        "client": ["./polyfill.js", "./polyfill.d.ts"],
        "styles": ["./styles.css"],
        "resources": {"/declarative_shadow_dom/logo.png": "./dsd_logo.png"}
      },
    }
  }
}

Then you'd need to extract this component from the npm_package():

npm_link_all_packages(name = "node_modules")

published_prerender_component(
    name = "rules_prerender_components/@rules_prerender/declarative_shadow_dom",
    package = ":node_modules/@rules_prerender/declarative_shadow_dom",
    component = "declarative_shadow_dom", # The component extracted from the `package.json`, because it could contain multiple.
)

prerender_pages(
    name = "site",
    # ...
    # Use like `//:node_modules/*`, but where a `prerender_component` is needed.
    deps = [":prerender_components/@rules_prerender/declarative_shadow_dom"],
)

The challenge here is that the implementation of published_prerender_component() requires execution-time only knowledge of source files, which would significantly complicate the implementation. I don't know how well ts_project(), css_library() / css_binaries(), or web_resources() will work without analysis-time knowledge.

Since the core challenge seems to be that @aspect_rules_js doesn't like Starlark code in NPM packages, we could choose to just not fight that. We have the same problem with the rest of the @rules_prerender Starlark rules, and if we have to require users to pull in those rules via a WORKSPACE file like @aspect_rules_ts does, then maybe we should do the same for deployed components. Having a WORKSPACE level dependency on @rules_prerender would enable users to directly depend on the prerender_component(name = "declarative_shadow_dom"). If users have to do this for rules_prerender anyways, then it wouldn't be too much more of an ask for them to do it for any other published components they want to use. It enforces a requirement that those components are built with prerender_component() via @rules_prerender, but I think that's a pretty reasonable requirement.

If we accept that users will depend on the :declarative_shadow_dom target via a WORKSPACE dependency on @rules_prerender, the next question is how they will import it. I'm not sure how to do a relative import across WORKSPACE boundaries, or how cross-WORKSPACE files even work in an @aspect_rules_js world. The most straightforward answer is to import via an NPM package specifier (ie. import { polyfillDeclarativeShadowDom } from '@rules_prerender/declarative_shadow_dom'). However this implies that there is either 1) a single package for both prerender and client scripts or 2) a package for prerender JS and another package for client JS. Neither is ideal since one bends the invariant that you can't import code written for a different platform (browser vs NodeJS) while the other requires multiple NPM packages to ship a single isolated piece of functionality (fortunately these packages don't actually need to be published to NPM, since they would be pulled in via the WORKSPACE).

A middle ground might be to use a single NPM package with two entry points like:

import { prerenderMyComponent } from 'some_component/prerender';
// vs.
import { doSomethingOnTheClinet } from 'some_component/client';

This would still avoid importing across contexts, but you would be depending across contexts, which isn't great either.

I asked on the Bazel Slack and confirmed that publishing Starlark via NPM is no longer supported. So I think exporting a real prerender_component() target and letting users depend on that is fine, however I still need to solve the import problem. Ideally, users would import from a rules_prerender specifier, though it was pointed out on Slack that this could lead to version skew if it uses a real NPM package. Instead, I think the ideal approach is to generate an npm_package() and link it in the user's workspace. I tried to hack through this and managed to make something kinda work for the declarative shadow DOM component. I managed to create a synthetic @rules_prerender/declarative_shadow_dom component containing the prerender and client-side scripts and call it from userspace. However, I couldn't get client-side scripts to build as rollup_bundle() doesn't process //:node_modules/@rules_prerender/declarative_shadow_dom as a dep. I tried upgrading to the @rules_rollup version of this rule which is supposed to be compatible with @aspect_rules_js, however this fails due to some weird fsevents errors in some bundled aspect lifecycle code I can't really debug.

ERROR: /home/doug/Source/rules_prerender/BUILD.bazel:21:22: Running lifecycle hooks on npm package fsevents@2.1.3 failed: (Exit 1): lifecycle-hooks.sh failed: error executing command bazel-out/k8-opt-exec-2B5CBBC6/bin/external/aspect_rules_js/npm/private/lifecycle/lifecycle-hooks.sh fsevents ../../../external/npm_rules_js__fsevents__2.1.3/package ... (remaining 1 argument skipped)

Use --sandbox_debug to see verbose messages from the sandbox
/home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/sandbox/linux-sandbox/2160/execroot/rules_prerender/bazel-out/k8-opt-exec-2B5CBBC6/bin/external/aspect_rules_js/npm/private/lifecycle/lifecycle-hooks.sh.runfiles/aspect_rules_js/npm/private/lifecycle/min/index.min.js:1
(()=>{var __webpack_modules__={7100:(__unused_webpack_module,exports,__nccwpck_require__)=>{"use strict";Object.defineProperty(exports,"__esModule",value:true});exports.codeFrameColumns=codeFrameColumns;exports["default"]=_default;var _highlight=__nccwpck_require__(1420);let deprecationWarningShown=false;function getDefs(chalk)return{gutter:chalk.grey,marker:chalk.red.bold,message:chalk.red.bold}}
// This goes on for quite some time...
9323:module,__unused_webpack_exports,__nccwpck_require__)=>{"use strict";

SyntaxError: Unexpected token '.'
    at wrapSafe (internal/modules/cjs/loader.js:915:16)
    at Module._compile (internal/modules/cjs/loader.js:963:27)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
    at Module.load (internal/modules/cjs/loader.js:863:32)
    at Function.Module._load (internal/modules/cjs/loader.js:708:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
    at internal/main/run_main_module.js:17:47
INFO: Elapsed time: 0.189s, Critical Path: 0.06s
INFO: 2 processes: 2 internal.
FAILED: Build did NOT complete successfully

Not sure what to do with that.

@dgp1130
Copy link
Owner Author

dgp1130 commented Jul 13, 2022

Talked today with @alexeagle and @gregmagolan about some of the challenges (thank you both!). To write down my takeaways while they're still fresh in my mind:

@rules_prerender seems to be a bit unique because it includes both Starlark rules and also a JS runtime which is imported by user code. While both of these problems are solved individually, this seems to be the first ruleset which needs to do both. We discussed a number of solutions and ultimately came up with the following approach:

First, we split JS and Starlark code. Ship only userland rules_prerender JS content to the NPM package and have users set up a WORKSPACE-level dependency on @rules_prerender. Tool-internal code stays in the Bazel workspace. This allows users to easily depend on @rules_prerender Starlark rules and import the rules_prerender NPM package. However the next problem is how does code in the @rules_prerender Bazel workspace depend on the rules_prerender NPM package.

Answer: It doesn't. @rules_prerender//packages/renderer doesn't depend on @user//:node_modules/rules_prerender (because we don't have a label for @user), but instead has a type-only dependency on @rules_prerender//packages/rules_prerender. prerender_resources() is already executed in userland and has a static entry point. We can change this to generate an entry point in the user's Bazel workspace with a dependency on @user//:node_modules/rules_prerender. This generated entry point can then import * as rulesPrerenderLib from 'rules_prerender'; and pass that object down the call stack. This keeps a single definition of rules_prerender NPM used by userland code and @rules_prerender internals.

Splitting the JS and Starlark does mean there is a potential for version skew. I'm thinking prerender_resources() can generate a test which verifies that the //:node_modules/rules_prerender dependency is the same version as hard-coded in Starlark. Need to figure out how to do that without actually hard-coding a version string in the workspace as I've so far managed to avoid bumping any versions as part of releases.

A similar trick can be done for published prerender_component() targets like declarative shadow DOM, though probably less elegantly. I think this would work by publishing the JS code in a @rules_prerender/declarative_shadow_dom package and then using the Starlark dependency on @rules_prerender to depend on a prerender_component() at @rules_prerender//packages/declarative_shadow_dom. The challenge is that @rules_prerender//packages/declarative_shadow_dom cannot depend on @user//:node_modules/@rules_prerender/declarative_shadow_dom. Instead we need to do a similar trick to before. Where prerender_resources() generated a nodejs_binary() in the user workspace with a dependency on @user//:node_modules/rules_prerender, we need some kind of link_prerender_component() rule in the user's workspace which can depend on @user//:node_modules/@rules_prerender/declarative_shadow_dom. This will be a bit extra boilerplate, but I don't think it should be too bad.

There are still a few open questions on the prerender_component() use case. Most notably:

  1. How do we split prerender and client side JS to prevent bad cross dependencies?
    • Could process the package TreeArtifact into two different providers, one for prerender JS and the other for client-side JS, then feed each one into a separate ts_project(). This would type check the build correctly but requires multiple entry points in the NPM package and wouldn't align with intellisense in editors which assume both versions of the JS are always available.
    • Could define the package with two exports fields, one for prerender JS and the other for client-side JS and keep one ts_project() for both. This makes it possible to accidentally cross-import between the two, but hopefully a harder mistake to make. Maybe some stronger conventions plus a lint or conformance check might help protect against that.
    • Could use two NPM packages, one for prerender JS and the other for client-side JS. This feels like a lot of overhead and isn't compatible with the abstraction that an NPM package can fully encapsulate a prerender_component().
  2. Where do CSS or resources live? In the NPM package or the Starlark prerender_component() in the @rules_prerender workspace?
    • Logically I think it makes sense for these resources to be in the NPM package, so it has all the information needed to use the component.
    • This aligns with the mindset of "How would you use a prerender component outside of Bazel?" though that's not really a goal of this project.
    • Putting everything in the NPM package (and thus a TreeArtifact) means we lose analysis-time knowledge of these files though.

I feel confident that I can get the rules_prerender NPM package to work with this model, though I'm a bit hazy on the details with published prerender_component() targets. I'll have to spend some time hacking through it and see what I can get working.

@dgp1130
Copy link
Owner Author

dgp1130 commented Jul 17, 2022

Tried to make something work here. I started by shifting all usages of the rules_prerender runtime to only be imported via the rules_prerender NPM package and passed down to the renderer as an input. This was done using a different entry point for the renderer which looked like:

const { context } = require('rules_prerender');
const { execute } = require('path/to/packages/renderer/renderer.js');

execute(context);

path/to/packages/renderer/renderer.js needs to be dynamically computed based on the Bazel package this entry point is generated in, but I'm hard-coding ./renderer.js which is good enough for the test binary right now.

Despite this, I was still seeing inline_style_map being loaded twice. I eventually traced this back to the fact that even though I didn't actually have any runtime imports of rules_prerender, the JS was still getting pulled in via @rules_prerender//:node_modules/rules_prerender pulled into the renderer runfiles. I found the two files existed at:

.../execroot/rules_prerender/node_modules/rules_prerender/index.js
.../execroot/rules_prerender/bazel-out/k8-dbg/bin/packages/renderer/renderer_test.sh.runfiles/rules_prerender/node_modules/.aspect_rules_js/rules_prerender@0.0.0/node_modules/rules_prerender/packages/rules_prerender/index.js

I knew I would want to drop these JS sources, I was just being lazy about it and didn't expect that to be required. I defined a new types_only() rule which propagates DeclarationInfo and LinkablePackageInfo while dropping the JS providers. This removed the second instance of those files and made inline_style_map only get loaded once.

The next problem is that the renderer test code cannot import rules_prerender:

 Cannot find module 'rules_prerender'
    Require stack:
    - /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/sandbox/linux-sandbox/314/execroot/rules_prerender/_tmp/66ad16056267f243223fe5901a481a06/useTempDir-c3fyuV/foo.js
    - /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/sandbox/linux-sandbox/314/execroot/rules_prerender/bazel-out/k8-fastbuild/bin/packages/renderer/renderer_test.sh.runfiles/rules_prerender/packages/renderer/dynamic_import.js
    - /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/sandbox/linux-sandbox/314/execroot/rules_prerender/bazel-out/k8-fastbuild/bin/packages/renderer/renderer_test.sh.runfiles/rules_prerender/packages/renderer/entry_point.js
    - /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/sandbox/linux-sandbox/314/execroot/rules_prerender/bazel-out/k8-fastbuild/bin/packages/renderer/renderer_test.sh.runfiles/rules_prerender/packages/renderer/renderer.js

This code is generated by the test with a require('rules_prerender') expression. I used to have to manually resolve runfiles in these tests, but I think they should be able to just import rules_prerender.

# Failed import originates from here:
.../execroot/rules_prerender/_tmp/66ad16056267f243223fe5901a481a06/useTempDir-c3fyuV/foo.js

# Parent `node_modules/` directory exists here:
.../execroot/rules_prerender/node_modules/
# which is a symlink to:
.../execroot/rules_prerender/bazel-out/k8-fastbuild/bin/packages/renderer/renderer_test.sh.runfiles/npm/node_modules

# `rules_prerender` is not present in `node_modules/`.
.../execroot/rules_prerender/node_modules/rules_prerender/ # <--- Does not exist.

The test itself does have a dependency on //:node_modules/rules_prerender but this doesn't seem to get reflected in the output tree. The symlink to renderer_test.sh.runfiles/npm/node_modules/ seems to indicate that this is using the @bulid_bazel_rules_nodejs NPM dependencies, not @rules_js. If I add a dependency on //packages/rules_prerender, the import succeeds and .../execroot/rules_prerender/node_modules/rules_prerender/ exists (but inline_style_map gets duplicated). I'm guessing I'll need to migrate the test to the @rules_js form of jasmine_node_test(). Given that this is a test only issue, I'll ignore it for now.

Running bazel test //examples/... I'm able to get everything which doesn't use declarative shadow DOM to pass. @bazel/runfiles didn't work so I had to manually use process.env['RUNFILES'] to get //examples/data/... to pass.

Starting on the declarative shadow DOM problem, I made a new prerender_package() rule which generates an npm_package() which contains a generated /rules_prerender.json file which looks like:

{
  "components": {
    "declarative_shadow_dom": {
      "prerender": [
        "declarative_shadow_dom.js",
        "declarative_shadow_dom.d.ts",
        "declarative_shadow_dom.js.map"
      ],
      "scripts": [
        "declarative_shadow_dom_polyfill.js",
        "declarative_shadow_dom_polyfill.d.ts",
        "declarative_shadow_dom_polyfill.js.map"
      ],
      "styles": [],
      "resources": "declarative_shadow_dom_resources/",
    }
  }
}

Then, I've got a prerender_link_component() which runs in the root of the user's workspace to generate a prerender_component() target from these files. Aliasing :%{component}_prerender and :%{component}_scripts to //:node_modules/@rules_prerender/declarative_shadow_dom actually gets a lot farther than I thought.

TypeScript can't process the import to @rules_prerender/declarative_shadow_dom, because it would need a dependency on the @build_bazel_rules_nodejs ts_project(), which isn't accessible. I'm ignoring with @ts-ignore for now, but hopefully that's a problem which will go away once the ts_project() targets get updated to @rules_js. It also can't process an import from the declarative shadow DOM component onto the rules_prerender NPM package. I'm a bit surprised by that since it does still have a dependency on @npm//rules_prerender in addition to //:node_modules/rules_prerender. Also @ts-ignore-ing this for now, though I don't feel like I should need to.

This is enough for the renderer to successfully invoke the declarative shadow DOM component and render the HTML output 🎉. Unfortunately Rollup fails to bundle as it doesn't seem to be getting the @rules_prerender/declarative_shadow_dom package in its inputs. I think the problem is that @bazel/rollup uses JSModuleInfo / JSEcmaScriptModuleInfo sources if available, and drops DefaultInfo (link). In prerender_pages_unbundled(), a script entry point is generated and the dependencies are added via js_library(). This generates JSModuleInfo and JSEcmaScriptModuleInfo providers, but they do not contain the NPM package, while DefaultInfo does.

$ bazel-pquery //examples/declarative_shadow_dom:site_page_scripts
@build_bazel_rules_nodejs//internal/providers:js_providers.bzl%JSEcmaScriptModuleInfo = struct(
    direct_sources = depset([
        <generated file examples/declarative_shadow_dom/site_page_scripts.js>,
    ]),
    sources = depset([
        <generated file examples/declarative_shadow_dom/site_page_scripts.js>,
    ]),
),
@rules_nodejs//nodejs/private/providers:js_providers.bzl%JSModuleInfo = struct(
    direct_sources = depset([
        <generated file examples/declarative_shadow_dom/site_page_scripts.js>,
    ]),
    sources = depset([
        <generated file examples/declarative_shadow_dom/site_page_scripts.js>,
    ]),
),
@build_bazel_rules_nodejs//internal/providers:js_providers.bzl%JSNamedModuleInfo = struct(
    direct_sources = depset([]),
    sources = depset([]),
),
@rules_nodejs//nodejs/private/providers:linkable_package_info.bzl%LinkablePackageInfo = struct(
    files = depset([
        <generated file examples/declarative_shadow_dom/site_page_scripts.js>,
    ]),
    package_name =     "",
    package_path =     "",
    path =     "bazel-out/k8-fastbuild/bin/examples/declarative_shadow_dom",
),
@rules_nodejs//nodejs/private/providers:declaration_info.bzl%DeclarationInfo = struct(
    declarations = depset([]),
    transitive_declarations = depset([
        <generated file node_modules/.aspect_rules_js/@rules_prerender+declarative_shadow_dom@0.0.0/node_modules/@rules_prerender/declarative_shadow_dom>,
        <generated file node_modules/@rules_prerender/declarative_shadow_dom>,
    ]),
    type_blocklisted_declarations = depset([]),
),
FileProvider = file_provider(
    files_to_build = depset([
        <generated file examples/declarative_shadow_dom/site_page_scripts.js>,
        <generated file node_modules/.aspect_rules_js/@rules_prerender+declarative_shadow_dom@0.0.0/node_modules/@rules_prerender/declarative_shadow_dom>,
        <generated file node_modules/@rules_prerender/declarative_shadow_dom>,
    ]),
),

The @aspect_rules_js version in @aspect_rules_rollup doesn't appear to conditionalize the inputs, so the solution might be to switch to that. I tried this again and ran into the same error as before. After attempting to narrow down the issue, I found that adding @rollup/plugin-node-resolve to the config was the part that was breaking. I tried upgrading the NodeJS version to 16.10.x and saw the error go away. So this should be an easy upgrade, though currently @rules_postcss is blocking that. As a part of this migration I can move to Parcel, which should unblock a Node.js version upgrade. Unfortunately this has the immediate effect of breaking CSS bundling.

I could possibly downgrade Rollup, but apparently @aspect_rules_rollup only supports the latest version. bazelbuild/rules_postcss#73 mentions a few workarounds, and turning off CSS sourcemaps is the easiest option for now. I'm pretty sure they don't really work anyways, so there's no need to keep them on.

A side effect of switching to @aspect_rules_rollup is that link_workspace_root is no longer supported, so all includeScript() calls are relative to the package where prerender_pages() lives, which is super-unintuitive. Fortunately it's pretty easy to generate and prepend some ../ strings to the import specifiers to make everything workspace relative. Still sucks that there's no easy way to make these paths file-relative, but that's a separate problem.

With Node.js updated to version 16.10.0 and using @aspect_rules_rollup, I was finally able to get the declarative shadow DOM component to work in @aspect_rules_js, hooray! Current work is in ref/rules-js. Biggest issue is still typing, but maybe an upgrade to @rules_ts will fix that. I'll keep hacking and try to migrate more of the repository and clean up the work I've already got functional.

@dgp1130
Copy link
Owner Author

dgp1130 commented Jul 18, 2022

I tried migrating some of the ts_project() targets to @aspect_rules_ts. I found the tsconfig behavior confusing given that I would like to generally have one tsconfig.json for the whole repository (with overrides where appropriate). I eventually found:

ts_project(
    name = "proj",
    srcs = ["source.ts"],
    tsconfig = {}, # Removes the requirement for a package-local `tsconfig.json`.
    extends = "//:tsconfig", # Must be `@aspect_rules_ts` version of `ts_config()`.
    declaration = True,
    source_map = True,
    deps = [":dep"],
)

This mostly works the way I want it to. It doesn't require a package-local tsconfig.json and it can compose other ts_project() targets easily. The biggest challenge is that any transitive @npm//... dep will fail the build, meaning all of those dependencies need to be updated to //:node_modules/... at the same time.

jasmine_node_test() targets also fail (can't resolve NPM packages), presumably because they are using nodejs_test() under the hood rather than js_test(). I considered upgrading to @aspect_rules_jasmine before discovering that doesn't exist. The closest equivalent would be @aspect_rules_jest, so I may need to migrate to Jest first. Unfortunately there doesn't seem to be a @bazel/jest package either, so I guess the migration to Jest and @aspect_rules_js have to happen at the same time, though they can probably be done test-by-test.

I tried setting up @aspect_rules_jest and migrated the annotation extractor to use jest_test(). This wasn't too difficult to get working, though I need to remove all my @types/jasmine and import 'jasmine'; deps. Surprisingly, it type checks successfully without any NPM dependency on Jest (or @types/node for that matter). I wonder if a transitive dep is pulling in those sources and ts_project() doesn't apply any strict deps checking? I'm very confused by that and will need to investigate a bit more.

I was also able to move the Jest change before the @aspect_rules_ts migration. So I think it should be possible to migrate all tests to @aspect_rules_jest and then migrate the rest of the toolchain without breaking the tests at the same time. Current snapshot at ref/rules-js-2.

@dgp1130
Copy link
Owner Author

dgp1130 commented Jul 19, 2022

I tried running Jasmine within @aspect_rules_js based on this example for Karma and mostly got it to work without too many issues. I do need to use //:node_modules/* deps, and sometimes I still need to keep the @npm//* deps, which I don't fully understand, but the tests do run as expected.

I did run into one novel issue where @aspect_rules_js failed to install bl@4.0.3 because it has a duplicate key (license) in its package.json. I had to manually bump the version in the lockfile (which was way harder than it really should be, given that pnpm update bl@4.0.4 still updated everything and nothing would easily tell me the integrity hash for a manual edit).

I also discovered that web_test_suite() ignores the args of the wrapped test, so Jasmine's --config argument needs to be specified on both the bin.jasmine_test() as well as the web_test_suite() which wraps it to support both use cases.

I got all the //packages/... tests to pass, but the WebDriverIO tests failed because they can't load debug:

Error: Cannot find module 'debug'
Require stack:
- /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/sandbox/linux-sandbox/19/execroot/rules_prerender/bazel-out/k8-fastbuild/bin/examples/components/test_chromium-local.sh.runfiles/rules_prerender/node_modules/.aspect_rules_js/devtools@7.16.3/node_modules/devtools/build/utils.js
- /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/sandbox/linux-sandbox/19/execroot/rules_prerender/bazel-out/k8-fastbuild/bin/examples/components/test_chromium-local.sh.runfiles/rules_prerender/node_modules/.aspect_rules_js/devtools@7.16.3/node_modules/devtools/build/devtoolsdriver.js
- /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/sandbox/linux-sandbox/19/execroot/rules_prerender/bazel-out/k8-fastbuild/bin/examples/components/test_chromium-local.sh.runfiles/rules_prerender/node_modules/.aspect_rules_js/devtools@7.16.3/node_modules/devtools/build/index.js
- /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/sandbox/linux-sandbox/19/execroot/rules_prerender/bazel-out/k8-fastbuild/bin/examples/components/test_chromium-local.sh.runfiles/rules_prerender/node_modules/.aspect_rules_js/webdriverio@7.16.3/node_modules/webdriverio/build/utils/index.js
- ...

Looking in the test runfiles, I don't see debug, only direct dependencies are present:

$ lla dist/bin/examples/components/test_chromium-local.sh.runfiles/rules_prerender/node_modules/
total 24
drwxr-xr-x 167 doug doug 12288 Jul 18 21:00 .aspect_rules_js
lrwxrwxrwx   1 doug doug   151 Jul 18 20:45 http-status-codes -> /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/execroot/rules_prerender/bazel-out/k8-fastbuild/bin/node_modules/http-status-codes
lrwxrwxrwx   1 doug doug   143 Jul 18 20:45 tree-kill -> /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/execroot/rules_prerender/bazel-out/k8-fastbuild/bin/node_modules/tree-kill
lrwxrwxrwx   1 doug doug   145 Jul 18 21:00 webdriverio -> /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/execroot/rules_prerender/bazel-out/k8-fastbuild/bin/node_modules/webdriverio

Looking in .aspect_rules_js, I do see the relevant debug package:

$ ll dist/bin/examples/components/test_chromium-local.sh.runfiles/rules_prerender/node_modules/.aspect_rules_js/
# ...
drwxr-xr-x 3 doug doug 4096 Jul 18 21:00 debug@2.6.9
drwxr-xr-x 3 doug doug 4096 Jul 18 21:00 debug@4.3.1

I'm unclear how Node is supposed to find those packages though. Interesting, webdriverio was able to find devtools, but devtools failed to find debug. Looking through the file tree, I noticed that each package has its own node_modules/ with only its direct dependencies listed and symlinked to their own packages:

$ ll /home/doug/.cache/bazel/_bazel_doug/0000b3b3fa770637f16a71f2a8299a7e/sandbox/linux-sandbox/23/execroot/rules_prerender/bazel-out/k8-fastbuild/bin/examples/components/test_chromium-local.sh.runfiles/rules_prerender/node_modules/.aspect_rules_js/devtools@7.16.3/node_modules/
total 36
drwxr-xr-x 2 doug doug 4096 Jul 18 21:33 @types
drwxr-xr-x 2 doug doug 4096 Jul 18 21:33 @wdio
lrwxrwxrwx 1 doug doug  195 Jul 18 21:33 chrome-launcher
drwxr-xr-x 3 doug doug 4096 Jul 18 21:33 devtools
lrwxrwxrwx 1 doug doug  190 Jul 18 21:33 edge-paths
lrwxrwxrwx 1 doug doug  194 Jul 18 21:33 puppeteer-core
lrwxrwxrwx 1 doug doug  205 Jul 18 21:33 query-selector-shadow-dom
lrwxrwxrwx 1 doug doug  192 Jul 18 21:33 ua-parser-js
lrwxrwxrwx 1 doug doug  184 Jul 18 21:33 uuid

Note that debug is not present. My guess is that this list is generated from package.json, and devtools does not have a dependency on debug. This seems accurate , yet devtools does require debug, apparently to monkey-patch it. It looks for puppeteer-core/node_modules/debug as a dependency of devtools. puppeteer-core does exist, but it does not contain a node_modules/ tree under it, it actually looks like:

puppeteer-core@10.4.0/
  node_modules/
    puppeteer-core/
      node_modules/ <-- Does not exist.
        debug/ <-- `devtools` looking here.
    debug/ <-- `debug` actually here.

This seems to mostly mirror the pnpm package structure: https://pnpm.io/symlinked-node-modules-structure.

Therefore devtools assumes that puppeteer-core does not have debug installed, and tries to require the global debug dependency, which isn't available because it doesn't declare a dependency on debug.

I figured this might be a pnpm-compatibility bug in devtools, but surprisingly pnpm actually works wither WebDriverIO in a minimal reproduction. require.resolve('debug') from devtools actually finds the debug package. Manually following the Node resolution algorithm, I found:

node_modules/.pnpm/node_modules/debug/...

The equivalent does not exist in @aspect_rules_js:

node_modules/.aspect_rules_js/node_modules/ <-- Does not exist.

Trying to understand why this directory exists, I eventually found this discussion which mentions the hoist option. Setting hoist=false in the .npmrc for my pnpm WebDriverIO repro, I'm able to reproduce the error. I'm guessing @aspect_rules_js disables hoisting by default, which is why this failed. The simplest solution is probably to hoist the debug package. Adding hoist-pattern[]=*debug* is enough for the pnpm repro to pass, but the same solution doesn't work with @aspect_rules_js. I'm guessing it doesn't have a dependency on the .npmrc file. Unfortunately I don't see anything in npm_translate_lock() which fixes this.

The migration guide mentions hooks.readPackage which can apparently allow me to declare this dependency for devtools. I don't have time to test this out right now, but I think there are two possible fixes that could be upstreamed here:

  1. @aspect_rules_js should have some means of changing hoisting behavior (whether through .npmrc or in the WORKSPACE).
  2. devtools shouldn't require hoisting for debug.

@dgp1130
Copy link
Owner Author

dgp1130 commented Jul 20, 2022

I made a fix for WebDriverIO in webdriverio/webdriverio#8542, hopefully it'll land soon.

In the mean time, I discovered public_hoist_packages which seems to be the @aspect_rules_js version of hoisting. I was a bit confused by the usage but I think the right answer is:

npm_translate_lock(
    # ...

    # Links `debug` to the "" (root) Bazel package, so it becomes available at `//:node_modules/debug`.
    public_hoist_packages = {
        "debug@4.3.1": [""],
    },
)

Then I can add //:node_modules/debug as a dependency and WebDriverIO works. Not totally sure if this is easier than hooks.readPackage, but good enough for me.

dgp1130 added a commit that referenced this issue Jul 20, 2022
This makes it available even to packages which don't declare a dependency on it, which is necessary to work around a pnpm-compatibility issue in WebDriverIO.

See: #48 (comment)
dgp1130 added a commit that referenced this issue Jul 20, 2022
Refs #48.

Generated with `pnpm import`. Also needed to disable strict peer deps as `@bazel/concatjs` requires Karma peer deps even though those rules are not used in `rules_prerender`.
dgp1130 added a commit that referenced this issue Jul 20, 2022
Refs #48.

This does not replace the `@npm` workspace, but instead adds a new `@npm_rules_js` so the two can co-exist.
dgp1130 added a commit that referenced this issue Jul 20, 2022
Refs #48.

This is an issue for the `@aspect_rules_js` version of `jasmine_node_test()` and wasn't load-bearing for `@build_bazel_rules_nodejs` anyways, so the simplest solution is to just delete it.

Generated this commit with a simple find/replace in VSCode, looking for `import 'jasmine';\n\n` and replacing with "". Then did another search for `import 'jasmine';` and fixed a few stragglers which fell through the cracks.
dgp1130 added a commit that referenced this issue Jul 20, 2022
Refs #48.

Version `4.0.4` has a bad `package.json` which declares the `license` key twice and breaks `@aspect_rules_js`. This upgrades it to a later version with a fixed `package.json`.
dgp1130 added a commit that referenced this issue Jul 20, 2022
Refs #48.

This makes it available even to packages which don't declare a dependency on it, which is necessary to work around a pnpm-compatibility issue in WebDriverIO.

See: #48 (comment)
dgp1130 added a commit that referenced this issue Jul 20, 2022
Refs #48.

This exposes `//:node_modules/rules_prerender` which can be used like any other NPM package yet is actually built from `//packages/rules_prerender` at HEAD.

This _should_ be defined in `//packages/rules_prerender/...` but can't because it depends on files outside that Bazel package like `//common/...` which can't be pulled into the NPM package. Easiest solution is to move the `npm_package()` target to the root BUILD file, so _everything_ is a subpackage. However this does result in a weird file layout in the NPM package and clutters the BUILD file. Once the repository is reorganized to use internal NPM packages for common code, this can likely be moved to `//packages/rules_prerender/...`. Left a TODO to follow up with that.
dgp1130 added a commit that referenced this issue Jul 20, 2022
Refs #48.

This commit includes a handful of changes which needed to be landed all at once to avoid an awkward migration period.

First we need to actually change the `jasmine_node_test()` macro to use `@aspect_rules_js`. There is no `@aspect_rules_jasmine`, so this needs to be done by manually invoking the `jasmine` binary via the rule generated by `npm_translate_lock()`. It generates a config file which explicitly lists its direct dependencies as test files. This means there is no convention to follow for test files (like `_test.js` or `.spec.js`), the only requirement is that it is a direct dependency of the `jasmine_node_test()` target.

The config file needs to be passed in `args`, however that only works for direct `bazel test` invocations. The `web_test_suite()` target wraps this binary and drops `args`, so they need to be repeated in that context. This breaks abstraction a little bit, but is small and simple enough to not be too big a deal.

Second we need to fix the errors this casues:

1. Adds `//:node_modules/*` dependencies where needed. `@npm//*` deps are still needed to type check things, but won't resolve at runtime. These dependencies are duplicated for now which doesn't seem to cause problems. Eventually the `@npm//*` dependencies will be dropped altogether.
2. Removes `@bazel/runfiles` from `jasmine_node_test()` targets because it fails (throws "`$BAZEL_WORKSPACE` is not set" error) and because it isn't needed. Since everything in `@aspect_rules_js` is in a single file tree, runfiles are just available at their root relative paths. This removes all but one usage of `@bazel/runfiles` which is still needed for a `nodejs_binary()` target.
3. Adds `debug` dependency to `//common/testing:webdriver`. This is necessary so `devtools` (a transitive dep of `webdriverio`) can import `debug` without an explicit dependency on it. This is a workaround for a pnpm-compatibility bug in WebDriverIO which should be removed once [the fix](webdriverio/webdriverio#8542) lands.
@dgp1130
Copy link
Owner Author

dgp1130 commented Jul 20, 2022

After that the @aspect_rules_js Jasmine migration is fairly straightforward. I need to remove @bazel/runfiles dependencies because it complains about $BAZEL_WORKSPACE not being set. I don't fully understand why that doesn't work, but it's not necessary anymore since everything is in a single execution tree, so instead of fs.readFile(runfiles.resolve('foo/bar/baz.js')) we can just do fs.readFile('foo/bar/baz.js').

One challenge is that //examples/site/... has some tests which directly import prerender JS as part of their tests in the same process, meaning that the rules_prerender NPM package needs to be linked in an @aspect_rules_js compatible manner. This pulls on directory layout issues, since rules_prerender depends on //common/... which isn't under //packages/rules_prerender/....

Solution is to move the npm_package() target to the workspace root so everything is a subpackage. This makes the NPM package structure a little weird, but it is able to resolve ../../common/... imports as expected.

I also found that npm_package() only pulls direct source files of a js_library() target, presumably because they are the only ones in DefaultInfo. I wrote a short rule to collect transitive sources and return them in DefaultInfo to feed into npm_package(), which fixed that issue.

With that, I chunked off the Jasmine migration into its own branch, cleaned up the commits, and merged them. I figure that's a safe start to the migration since it's internal changes to testing which don't actually affect the built artifacts.

@dgp1130
Copy link
Owner Author

dgp1130 commented Jul 20, 2022

CI failed after merging because apparently @aspect_rules_js breaks the --incompatible_config_setting_private_default_visibility flag. Sent aspect-build/rules_js#301 to fix it and b30bcfa removes that flag for now to fix CI.

@dgp1130
Copy link
Owner Author

dgp1130 commented Jul 23, 2022

webdriverio/webdriverio#8542 landed but isn't released yet. Should be in v7.20.8 once that is ready.

aspect-build/rules_js#301 also landed but isn't out yet either. Should be in v1.0.0-rc.3.

dgp1130 added a commit that referenced this issue Aug 2, 2022
dgp1130 added a commit that referenced this issue Aug 2, 2022
… for `--config incompatible` builds.

Refs #48.

Since `@aspect_rules_js` has been updated to include aspect-build/rules_js#301, we can now re-enable this flag.
dgp1130 added a commit that referenced this issue Aug 2, 2022
Refs #48.

This is no longer used as it has been replaced with a custom `@aspect_rules_js` implementation.
dgp1130 added a commit that referenced this issue Aug 2, 2022
Refs #48.

This pulls in webdriverio/webdriverio#8542, which is needed to support pnpm natively.
dgp1130 added a commit that referenced this issue Aug 2, 2022
Refs #48.

Now that webdriverio/webdriverio#8542 has been pulled in, this package no longer needs to be hoisted.
@dgp1130
Copy link
Owner Author

dgp1130 commented Aug 2, 2022

The upstream @aspect_rules_js and WebDriverIO fixes have landed, so I was able to pull them in and remove the workarounds.

Moving forward, I think the next incremental step I can do is take a tool and ship it via js_binary() instead of nodejs_binary(). I'll start with not renderer, since that's the most complicated one. This means that user projects need a workspace dependency on @rules_prerender which they haven't had in the past, so I'll need to figure out that story.

Experimenting with this, I updated the ref/external branch to use @aspect_rules_js and added a local_repository() dependency on @rules_prerender (without breaking the existing @npm//rules_prerender dep). This unfortunately fails because Jasmine isn't in the workspace:

ERROR: /home/doug/Source/rules_prerender/user/app/BUILD.bazel:10:16: every rule of type _multi_extract_annotations implicitly depends upon the target '@rules_prerender//packages/annotation_extractor:annotation_extractor', but this target could not be found because of: error loading package '@rules_prerender//packages/annotation_extractor': at /home/doug/.cache/bazel/_bazel_doug/bae30dce5d5ae268b94024c0d79faa02/external/rules_prerender/tools/jasmine.bzl:1:6: at /home/doug/.cache/bazel/_bazel_doug/bae30dce5d5ae268b94024c0d79faa02/external/rules_prerender/tools/jasmine_node_test.bzl:4:6: cannot load '@npm_rules_js//:jasmine/package_json.bzl': no such file

I believe the reason for this is because @npm_rules_js is instantiated by the user workspace which does not have a required dependency on Jasmine, yet I load("@npm_rules_js//:jasmine/package_json.bzl", "jasmine_bin") in the annotation extractor package (because its unit tests coexist with the implementation). We're never actually running the npm_translate_lock() from @rules_prerender, so it's package.json and the associated node_modules/ are never installed.

I think the root cause here is that WORKSPACE files aren't transitively loaded, which is the problem bzlmod is supposed to solve, though I haven't looked into it much and don't think it should be necessary to use @rules_prerender, though WORKSPACE files may get a little hacky.

Updating the user WORKSPACE to call a new rules_prerender_dependencies() which instantiates an npm_translate_lock() using a different name than the user's @npm_rules_js workspace I'm able to get the Jasmine error to go away. It then reveals a new error:

ERROR: /home/doug/.cache/bazel/_bazel_doug/bae30dce5d5ae268b94024c0d79faa02/external/rules_prerender/packages/annotation_extractor/BUILD.bazel:15:11: no such package '@npm//@types/yargs': BUILD file not found in directory '@types/yargs' of external repository @npm. Add a BUILD file to a directory to mark it as a package. and referenced by '@rules_prerender//packages/annotation_extractor:annotation_extractor_lib'

I think this is the same issue but applied to the @npm workspace instead of the @npm_rules_js workspace. Eventually this dependency will go away entirely, however since I'm migrating only a single tool //common/... is used in both @aspect_rules_js and @build_bazel_rules_nodejs contexts, so I can't just delete these deps and/or migrate the ts_project() targets to @aspect_rules_ts.

I updated the user's WORKSPACE to include an npm_install() of @rules_prerender//:package-lock.json. This again required non-conflicting names for the two @npm workspaces. To add to the confusion, the current implementation of @npm//rules_prerender assumes the user has named it @npm. This means all internal dependencies in the shipped NPM package need to use @user_version_of_npm//rules_prerender/... while any external dependencies need to use @internal_version_of_npm//some_dep/....

After working through those issues, I found that @build_bazel_rules_nodejs requires that all deps come from a single NPM workspace:

ERROR: /home/doug/Source/rules_prerender/user/app/component/BUILD.bazel:3:20: in _ts_project rule //app/component:component_prerender_ts: 
Traceback (most recent call last):
        File "/home/doug/.cache/bazel/_bazel_doug/bae30dce5d5ae268b94024c0d79faa02/external/npm/@bazel/typescript/index.bzl", line 44, column 42, in _ts_project_impl
                return _ts_project_lib.implementation(ctx, run_node, ExternalNpmPackageInfo)
        File "/home/doug/.cache/bazel/_bazel_doug/bae30dce5d5ae268b94024c0d79faa02/external/rules_nodejs/nodejs/private/ts_project.bzl", line 169, column 23, in _ts_project_impl
                run_action(
        File "/home/doug/.cache/bazel/_bazel_doug/bae30dce5d5ae268b94024c0d79faa02/external/build_bazel_rules_nodejs/internal/providers/node_runtime_deps_info.bzl", line 106, column 51, in run_node
                modules_manifest = write_node_modules_manifest(
        File "/home/doug/.cache/bazel/_bazel_doug/bae30dce5d5ae268b94024c0d79faa02/external/build_bazel_rules_nodejs/internal/linker/link_node_modules.bzl", line 124, column 25, in write_node_modules_manifest
                fail("All npm dependencies at the path '%s' must come from a single workspace. Found '%s' and '%s'." % (path, other_workspace, workspace))
Error in fail: All npm dependencies at the path '' must come from a single workspace. Found 'npm' and 'npm_user'.
ERROR: /home/doug/Source/rules_prerender/user/app/component/BUILD.bazel:3:20: Analysis of target '//app/component:component_prerender_ts' failed

In theory, I think everything we need to compile a component should be under @npm_user//.... I found that ts_project() has an implicit dependency on @npm//typescript/bin:tsc, rewriting it to @npm_user//typescript/bin:tsc should make it use the peer dep in the user's project, but this doesn't remove the error. I also don't see any other @npm//... dependencies on that target:

$ (cd user/ && bazel query "somepath(//app/component:component_prerender_ts, @npm//...)" --notool_deps)
INFO: Empty results

Printing out more information, I see that this is actually the renderer which is failing to build because of a dependency on @npm//yargs:

DEBUG: /home/doug/.cache/bazel/_bazel_doug/bae30dce5d5ae268b94024c0d79faa02/external/build_bazel_rules_nodejs/internal/linker/link_node_modules.bzl:118:10: From //app:page_page_annotated_binary with deps: [<merged target @npm//yargs:yargs>, <merged target //app:page_page_component_prerender>, <merged target @npm_user//rules_prerender:rules_prerender__files>]
DEBUG: /home/doug/.cache/bazel/_bazel_doug/bae30dce5d5ae268b94024c0d79faa02/external/build_bazel_rules_nodejs/internal/linker/link_node_modules.bzl:123:18: @npm//yargs:yargs
DEBUG: /home/doug/.cache/bazel/_bazel_doug/bae30dce5d5ae268b94024c0d79faa02/external/build_bazel_rules_nodejs/internal/linker/link_node_modules.bzl:123:18: //app:page_page_component_prerender
ERROR: /home/doug/Source/rules_prerender/user/app/BUILD.bazel:10:16: in nodejs_binary rule //app:page_page_annotated_binary: 
Traceback (most recent call last):
        File "/home/doug/.cache/bazel/_bazel_doug/bae30dce5d5ae268b94024c0d79faa02/external/build_bazel_rules_nodejs/internal/node/node.bzl", line 164, column 56, in _nodejs_binary_impl
                node_modules_manifest = write_node_modules_manifest(ctx, link_workspace_root = ctx.attr.link_workspace_root)
        File "/home/doug/.cache/bazel/_bazel_doug/bae30dce5d5ae268b94024c0d79faa02/external/build_bazel_rules_nodejs/internal/linker/link_node_modules.bzl", line 127, column 25, in write_node_modules_manifest
                fail("All npm dependencies at the path '%s' must come from a single workspace. Found '%s' and '%s'." % (path, other_workspace, workspace))
Error in fail: All npm dependencies at the path '' must come from a single workspace. Found 'npm' and 'npm_user'.
ERROR: /home/doug/Source/rules_prerender/user/app/BUILD.bazel:10:16: Analysis of target '//app:page_page_annotated_binary' failed

Rewriting that to @npm_user//yargs fixes the issue, though it requires the user workspace to have a Yargs dependency, which it really shouldn't need. This is because Yargs is a dependency of the renderer binary, but not a peer dep users should be aware of. The easiest solution for now is to just make Yargs an unofficial peer dep, since its the only offender it's not too big a deal. Maybe depending on two npm_translate_lock() workspaces is fine in @aspect_rules_js, even if it isn't with @build_bazel_rules_nodejs. I'll have to tackle that problem later.

Next error is that my ts_project() targets under //packages/annotation_extractor/... can't build because they are executed under external/rules_prerender/... while the host configuration is actually under bazel-out/host/bin/..., not external/rules_prerender/bazel-out/host/bin/.... Adding ../../ prefix to the tsconfig.json rootDirs option is enough to fix this, though it's a bit awkward. I believe this should go away once we move to @aspect_rules_ts.

That's enough to pass tsconfig validation, however the tsc invocation fails due to missing imports:

$ (cd user/ && bazel build @rules_prerender//packages/annotation_extractor:metadata)
external/rules_prerender/packages/annotation_extractor/metadata.ts(1,37): error TS2307: Cannot find module '../../common/models/prerender_annotation' or its corresponding type declarations.
external/rules_prerender/packages/annotation_extractor/metadata.ts(2,51): error TS2307: Cannot find module '../../common/models/prerender_metadata' or its corresponding type declarations.

Building with --sandbox_debug and looking in the file tree, I see that external/rules_prerender/common/... is missing. Checking the inputs of the action:

$ (cd user/ && bazel aquery "mnemonic(TsProject, @rules_prerender//packages/annotation_extractor:metadata)")
# ...
Inputs: [
    bazel-out/k8-fastbuild/bin/external/rules_prerender/common/models/prerender_annotation.d.ts
    # ...
]

Apparently generated inputs are under bazel-out/${cfg}/bin/external/rules_prerender/..., so they also need to be added to rootPaths.

This is enough to build and extract annotations, however client side bundling fails with the error:

[!] Error: 'rules_prerender/packages/rules_prerender/declarative_shadow_dom/declarative_shadow_dom_polyfill' is imported by bazel-out/k8-fastbuild/bin/app/page_page_scripts.js, but could not be resolved – treating it as an external dependency

I'm a bit surprised by this since I would expect Rollup bundling to be unaffected by this change. It has a dependency on @npm_user//rules_prerender:declarative_shadow_dom, which is the same as the old dependency (not using @npm_rules_js yet). Again a bazel aquery shows:

$ (cd user/ && bazel aquery "mnemonic(Rollup, //app:page_bundle)")
# ...
Inputs: [
external/npm_user/node_modules/rules_prerender/packages/rules_prerender/declarative_shadow_dom/declarative_shadow_dom_polyfill.d.ts,
external/npm_user/node_modules/rules_prerender/packages/rules_prerender/declarative_shadow_dom/declarative_shadow_dom_polyfill.js,
# ...
]

However, I think it's expected that the sources are under external/npm_user/node_modules/rules_prerender/..., this shouldn't have meaningfully changed. I do see other sources under external/npm/node_modules/..., so I wonder if that's confusing @rollup/plugin-node-resolve? Users can bundle their own scripts or other NPM deps under @npm_user/..., so I would expect Rollup to only look in that node_modules/ directory.

Digging through --sandbox_debug, it seems that Rollup is looking under @npm//... since that's where all the Rollup dependencies are (rollup, @rollup/plugin-node-resolve, etc.) It makes sense that those are internal deps which should be hidden from the user's workspace. These are present at node_modules/ in the working directory of the sandbox, while the DSD component is under external/npm_user/node_modules/.... I tried adding that to moduleDirectories in the Rollup config, but this doesn't seem to have any effect.

I tried looking through @rollup/plugin-node-resolve to see if there was a way to log all the paths being attempted. I couldn't find any functionality like that, but I did realize the algorithm is dependent on package.json placement, so I added a dependency from the DSD script onto the package.json so it would be included at external/npm_user/node_modules/rules_prerender/package.json and combined with the moduleDirectories change, this fixes Rollup to bundle successfully.

And with that an external application finally works with an @aspect_rules_js annotation extractor. The most awkward part is that it requires a user project to declare @npm_user and @npm_rules_js_user. This was technically already the case with the @npm workspace, but it's awkward that we now have two of each which need specific names. I could invert the convention so users get @npm and @npm_rules_js, though all internal deps then become something like @rules_prerender_npm and @rules_prerender_npm_rules_js. Maybe I can use keep @npm in @rules_prerender source and use repo_mapping to alias it to some @npm_for_rules_prerender in the user's WORKSPACE. That might work for basic deps, but files will be under external/npm_for_rules_prerender/..., so tools like Rollup need to know that name. Any deps in a macro evaluated in the user workspace will also need to use that name.

The other question is: Do I really care about hard-coding @npm_user? After this migration, @npm and @npm_user will go away. The same problem will exist for @npm_rules_js and @npm_rules_js_user. That hasn't really been an issue yet since only annotation extractor uses @aspect_rules_js and it's an internal tool. However, I suspect that migrating the renderer to @aspect_rules_js will convert all my @npm_user problems into @npm_rules_js_user problems. @aspect_rules_js strategy of putting everything into a root-relative path might alleviate some of the issues (for instance, Rollup doesn't need to know about multiple node_modules/ roots), however that's assuming that it supports depending on multiple npm_translate_lock() targets in the same action. I suspect that @rules_prerender//:node_modules/dep will end up at a path like external/rules_prerender/node_modules/..., so it may still be necessary to configure tools like Rollup to look in the right location.

I'm inclined to just hard-code the workspace names for now (maybe @npm for user deps and @npm_for_rules_prerender for internal deps) and come back to the problem once @npm is dropped altogether.

Current snapshot: ref/rules-js-annotation-extractor-external.

@gregmagolan
Copy link

@dgp1130 Heads up that I just landed a big refactor in rules_js that doesn't affect the user facing public API very much but it does affect the API for derivative rules. It adds a common provider (JsInfo) that downstream rules should use, establishes patterns to use and provides helpers for these. The change will go out in rc4 shortly.

You can look at our rulesets for examples of how it affects downstream rules,

aspect-build/rules_esbuild#48
aspect-build/rules_jest#24
aspect-build/rules_ts#113
aspect-build/rules_rollup#26
aspect-build/rules_swc#71
aspect-build/rules_terser#24
aspect-build/rules_webpack#34

LMK if you have a questions or ping me on Bazel slack if you would like some help with the changes required.

@dgp1130
Copy link
Owner Author

dgp1130 commented Aug 2, 2022

Thanks @gregmagolan, I'll take a look once that comes out. Is the core change that there's a (new?) JsInfo provider which needs to be manually propagated in some situations and used where DefaultInfo suffices today?

dgp1130 added a commit that referenced this issue Feb 11, 2023
Refs #48.

This mostly works just fine. I did notice that source maps seem to be broken, though that seems like a problem with the new `@aspect_rules_js` toolchain rather than this specific external example. I left a TODO to follow up with that separately.
@dgp1130
Copy link
Owner Author

dgp1130 commented Feb 11, 2023

I added an actual script to the example and found it bundled successfully, though I noticed two interesting things.

First, includeScript('script') fails to resolve script.js. However includeScript('./script') works. No other script includes use the leading ./. The actual generated Rollup entry point is:

import 'script';

And following Node module resolution, that would be treated as an NPM package, not a relative path. So I can understand why Rollup doesn't resolve that, though I'm surprised that it was an issue with other paths. Eventually I realized the script entry generator adds ../ paths depending on the depth of the prerender_pages() target. In this case the depth was 0, so it didn't prepend anything. I updated it to prepend ./, though arguably this should be authored by the user. I'm not going to worry about it right now, but we should take another look at this in #2.

Second, source maps don't seem to be working anymore. Even in non-external examples sourcemaps don't resolve correctly. It looks like TypeScript and Rollup aren't composing each other correctly given that the JS source files also seem to contain a source map reference at the end. They also reference files like __st_outdir__/script.js.map, and I have no idea where that prefix is coming from. It's not that big a deal for now, but I'll need to debug that eventually.

Otherwise I was able to get client side scripts bundling and executing as expected. Next step is styles and then declarative shadow DOM to tie them all together.

dgp1130 added a commit that referenced this issue Feb 11, 2023
Refs #48.

This mostly works but requires `postcss-import` to be installed in the user's workspace. It doesn't bother me that much right now though, since this is all still on `@build_bazel_rules_nodejs` infra. Once we update to Parcel with `@aspect_rules_js`, this shouldn't be an issue.
dgp1130 added a commit that referenced this issue Feb 11, 2023
Refs #48.

This just worked without any issues. Technically `prerender_pages()` emits a `WebResources` provider and follows the same code path, so this really has been working for a while now.
dgp1130 added a commit that referenced this issue Feb 12, 2023
Refs #48.

I actually want to bump the `@aspect_rules_ts` dependency, but this appears to require `@aspect_rules_js` to be bumped as well, and there seems to be an unintentional breaking change when used with `@aspect_rules_jasmine`, so we need to upgrade this first.

See notes in https://github.com/aspect-build/rules_js/releases/tag/v1.17.0.
dgp1130 added a commit that referenced this issue Feb 12, 2023
Refs #48.

I actually want to bump the `@aspect_rules_ts` dependency, but this appears to require `@aspect_rules_js` to be bumped as well, and there seems to be an unintentional breaking change when used with `@aspect_rules_rollup`, so we need to upgrade this first.

See notes in https://github.com/aspect-build/rules_js/releases/tag/v1.17.0.
dgp1130 added a commit that referenced this issue Feb 12, 2023
Refs #48.

I actually want to bump the `@aspect_rules_ts` dependency, but this appears to require `@aspect_rules_js` to be bumped as well.
dgp1130 added a commit that referenced this issue Feb 12, 2023
Refs #48.

This pulls in aspect-build/rules_ts#310 to fix `ts_project()` builds in external workspaces, meaning we can remove the local patch.
dgp1130 added a commit that referenced this issue Feb 12, 2023
Refs #48.

To use published prerender components from an external workspace (such as linking against the `npm_package()` for `@rules_prerender/declarative_shadow_dom`), that component needs to build within an external workspace. This has unique implications on the file paths since everything goes under `external/my_external_wksp/...`, but this is not reflected in `file.short_path` in a useful way. Instead we need to do a little more work to get the right paths, and `@aspect_bazel_lib's` `to_output_relative_path()` is exactly what we need. Essentially we should always prefer that over direct usage of `file.short_path`.
dgp1130 added a commit that referenced this issue Feb 12, 2023
Refs #48.

This actually worked a lot easier than I expected. `link_prerender_component()` worked just fine and the only real hiccup was adding `include_external_repositories = ["rules_prerender"]` to work around bazel-contrib/bazel-lib#359.
dgp1130 added a commit that referenced this issue Feb 12, 2023
Refs #48.

Need to refactor the way that `tsconfig.json` was managed and also realized that I needed to have a separate `tsconfig.client.json` to make sure it was compiled targetting ESM.
dgp1130 added a commit that referenced this issue Feb 12, 2023
Refs #48.

This the `rules_prerender` and `@rules_prerender/declarative_shadow_dom` `package.json` files which the current version. I initially tried to use [`stamped_package_json()`](https://docs.aspect.build/rules/aspect_rules_js/docs/npm_package#stamped_package_json), but found it insufficient because `@rules_prerender/declarative_shadow_dom` includes a peer dep on `rules_prerender` which was not being stamped. I filed aspect-build/rules_js#866 to suggest this as a general feature, but opted to implement my own version for now. It also uses `jq()` under the hood to transform the input JSON. It stamps the `version` property and any `rules_prerender` or `@rules_prerender/*` dependencies.

I also add a `default_version` attribute. Not so much because it is actually useful, but because it makes the rule easier to test. Technically these tests fail with `--stamp`, but I don't think that should be too much of an issue.
@dgp1130
Copy link
Owner Author

dgp1130 commented Feb 12, 2023

Started looking into CSS in the external example and was able to get it working relatively easily. Needed to add Label() to a dep to get it to resolve correctly. The only other issue is that users need to install postcss-import via the @build_bazel_rules_nodejs toolchain. The import shouldn't be required and the separate JS toolchain shouldn't be needed either. This doesn't bother me too much right now, because we already have a plan to switch to Parcel in #46 after @aspect_rules_js has landed.

I also added an image resource to the external site which worked as expected without any issues. Technically prerender_pages() emits a WebResourceInfo so we were already going through the same code paths, but good to see that the actual use case works.

While I was working on that, aspect-build/rules_ts#310 landed with the fix for building ts_project() targets in external workspaces. I upgraded all the @aspect_rules_* dependencies to accommodate and was able to drop my local patch. 😁

The final problem is getting declarative shadow DOM working. I started by adding a npm_link_package() for @rules_prerender/declarative_shadow_dom. Real applications would install from NPM, but I'd like to do this so the test case is always built at HEAD. Unfortunately this means I need to build the DSD prerender_component() within an external workspace, meaning I'll have to work through similar problems to those I've discovered in Aspect repositories.

Immediate error is that merging resources fails due to a bad file path:

ERROR: /home/doug/.cache/bazel/_bazel_doug/8a09637a7a02e007611e1aa784ebe6a2/external/rules_prerender/packages/rules_prerender/declarative_shadow_dom/BUILD.bazel:35:20: Merging resources (@rules_prerender//packages/rules_prerender/declarative_shadow_dom:declarative_shadow_dom_resources) failed: (Exit 1): resource_packager.sh failed: error executing command bazel-out/host/bin/external/rules_prerender/packages/resource_packager/resource_packager.sh --merge-dir ... (remaining 3 arguments skipped)

Use --sandbox_debug to see verbose messages from the sandbox
Error: ENOENT: no such file or directory, scandir '../rules_prerender/packages/rules_prerender/declarative_shadow_dom/declarative_shadow_dom_resources_entries'
Target //:node_modules/@rules_prerender/declarative_shadow_dom failed to build

Ultimately this is because web_resources() uses file.short_path to get relative paths to all the files referenced. However, this doesn't quite work for external files, because file.short_path is actually ../my_external_wksp/path/to/file.txt, yet external files actually live in external/my_external_wksp/path/to/file.txt. Basically, we can't use file.short_path directly and instead need to do a little more processing. Fortunately in aspect-build/rules_ts#310, Greg pointed out that to_output_relative_path() exists, which pretty much does exactly the behavior I need. Replacing usages of file.short_path with to_output_relative_path(ctx, file) fixes all the relevant issues. Just had to make sure to call it on the files copied to bin, not the original sources.

After landing that fix, I was able to build @rules_prerender/declarative_shadow_dom in an external workspace and then link it into the example workspace. This actually worked out better than expected. link_prerender_component() basically just worked without issue. The only hiccup was that I needed to work around bazel-contrib/bazel-lib#359 much like the rules_prerender package, but that's an easy fix. Otherwise I was able to depend on the DSD component and include a client-side script from it correctly.

I think this is actually in a good enough state to release right now. There's definitely a lot of cleanup and refactoring which can be done, but it's good enough to be usable, even if set up is significantly more complicated than it should be right now. I do need to figure out the release process, given that it's a bit more complicated now. Previously I would just release the rules_prerender NPM package which contained the core runtime, DSD component, and all the Starlark rules. Now we need to release the workspace source on GitHub combined with publishing the rules_prerender and @rules_prerender/declarative_shadow_dom packages (the latter of which doesn't actually exist right now).

The first problem there is stamping the packages, which I've ignored so far. I tried to use stamped_package_json() but found it insufficient because @rules_prerender/declarative_shadow_dom has a peer dependency on rules_prerender which it does not stamp. I filed aspect-build/rules_js#866 to consider updating it to support stamping dependencies, but for now I just made my own version on top of the existing jq support.

Next I'll need to reevaluate the release action to publish both NPM packages simultaneously. Ideally it would also create the GitHub release and include the instructions for how to set up the WORKSPACE.bazel file, similar to how a number of Bazel projects do things. I'll need to see how they generate those instructions. The other problem is that I never got around to generating release notes from the changelog and always created GitHub releases manually. I can still add the changelog afterwards for now, but if tooling will generate the release, it would be cool if it could generate the changelog as well.

dgp1130 added a commit that referenced this issue Feb 13, 2023
Refs #48.

This generates a binary which publishes the given package to NPM. It just merges the given directory with the root `.npmrc` file (which contains authentication information) and calls `npm publish`.

Also made publish binaries for `rules_prerender` and `@rules_prerender/declarative_shadow_dom`.

I tried to add a trap which deletes the staging directory after the binary has run, but I got permission errors and couldn't easily figure out what was going on. Fortunately `mktemp` still creates files under `$TMP`, so they should be cleaned up automatically, just a little less agressively than they could be.
@dgp1130
Copy link
Owner Author

dgp1130 commented Feb 13, 2023

Unfortunately it doesn't seem that publishing NPM packages is directly supported by @aspect_rules_js (see aspect-build/rules_js#155).

In theory I don't think it should be that hard, given that we already have an NPM binary and the GitHub action includes authentication. I think all we need is an sh_binary() with data dependencies on the npm_package() directory, the .npmrc, and the NPM binary, then just does (cd path/to/npm/pkg/ && cp ../../../.npmrc/ . && npm publish).

I developed my own npm_publish() macro which seems to work reasonably well. I did a test release (0.0.16) and successfully published rules_prerender and @rules_prerender/declarative_shadow_dom. I messed up the tar files as assets in the Github archive, but the 0.0.17 release seems to have fix that. I updated the release notes with some general information and confirmed via http_archive() that I can depend on and use @rules_prerender as expected with rules_prerender and @rules_prerender/declarative_shadow_dom from NPM. But the important part here is that @rules_prerender is usable with @aspect_rules_js (with a lot of caveats). 🎉

For now, I generated the release notes by hand, this should be integrated with the release process. I found @aspect_rules_js' implementation here, where they tar their own repository and push it as an assert. We should probably do something similar, which would provide a more consistent prefix in the tar and generate the correct hash up front.

I think this issue has gone on long enough, and we've officially reached the point where I was able to successfully release against @aspect_rules_js. There's definitely still a lot of work and cleanup to do to get to a good place, but I think those can follow up separately. I'll look over the previous comments and any submitted TODOs for actions items and file issues for those, then we can probably close this one.

dgp1130 added a commit that referenced this issue Feb 14, 2023
Refs #48.

I previously updated the styles to reference `span { /* ... */ }`, but forgot to update the HTML.
dgp1130 added a commit that referenced this issue Feb 14, 2023
Refs #48.

Declarative shadow DOM now works and this test passes.
dgp1130 added a commit that referenced this issue Feb 14, 2023
dgp1130 added a commit that referenced this issue Feb 14, 2023
…a custom rule.

Refs #48.

Now that `//tools/internal:script_entry_generator` is compatible with `exec` configuration, it can be used in a `js_run_binary()` which simplifes the implementation a bit.
dgp1130 added a commit that referenced this issue Feb 14, 2023
Refs #48.

The renderer should not depend on `rules_prerender` directly, because that would pull in the JavaScript sources into `.../execroot/rules_prerender/node_modules/rules_prerender/` of the generated binary. The `rules_prerender` dependency should actually be linked against the user's implementation in their own workspace (`//:node_modules/rules_prerender`), not `@rules_prerender//:node_modules/rules_prerender`. However, we still want type checking of `rules_prerender` usage, even if the actual package gets injected via the main function parameter.

`types_only()` accomplishes this by dropping all the JS sources and only propagating TypeScript declarations. This prevents the `rules_prerender` package from `@rules_prerender//:node_modules/rules_prerender` leaking into the renderer binary, so there is only one definition of it which comes from the user's workspace. `import type` also helps restrict that the dependency should _only_ ever be used as type.

Unfortunately, `npm_link_package()` emits a `declarations` field which points to the NPM package's `TreeArtifact`, and that tree contains `*.js` files. That means TypeScript actually _does_ receive `*.js` files and is generally ok with value imports of `rules_prerender`. There's also no easy way of removing `*.js` files from that tree. This is an unfortunate foot-gun, but the most important thing is that the `*.js` files don't persist at runtime, even if the compiler may allow it.

I filed a feature request to `@aspect_rules_ts` with this general idea and also explains more the `npm_link_package()` foot-gun: aspect-build/rules_ts#319.
dgp1130 added a commit that referenced this issue Feb 14, 2023
Refs #48.

This deletes or updates `TODOs` where follow up issues have been filed separate from the `@aspect_rules_js` migration. I generally deleted most of the `TODOs` because I personally feel that this kind of information should be tracked in issues. However I left a couple as notes of exactly where to cleanup when certain tasks are complete.
@dgp1130
Copy link
Owner Author

dgp1130 commented Feb 14, 2023

I resolved a handful of simple TODOs left over from this issue and also looked through a bunch of the above comments for follow-up tasks. I filed issues where they didn't already exist. A mostly-comprehensive list of them includes:

I also filed aspect-build/rules_ts#319 with a types_only() feature request which I found need for. For now I just have my own implementation which is good enough, but it would be cool to upstream.

I think this is enough for one issue. I'll follow up on those for the remaining tasks.

@dgp1130 dgp1130 closed this as completed Feb 14, 2023
@alexeagle
Copy link

Congrats on landing, sorry I never had time to read all the text :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants