From e4b42225923c8b71911d1079c72f35e8f89fe10f Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 19 Oct 2026 23:13:33 +0100 Subject: [PATCH 1/8] Add the design for module scripts in their crates A vendor module's Rust lives in crates///, while its browser script, the script's tests and some test data live in the shared trusted-server-js crate. The design moves ten modules' scripts and tests into the nine crates that supply them, makes the build, the tests, the lint and format checks and editors find them there, and moves the Tinybird project into the Tinybird crate. It records what stays in the shared crate and why, being the APS renderer, which the browser core imports, Prebid, whose shim imports an npm package and whose bundle build the CLI runs from the shared crate, and the vendor names still in the browser core. Its review log records round 1. --- ...6-10-19-module-scripts-in-crates-design.md | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md diff --git a/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md b/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md new file mode 100644 index 0000000000..08a95d5c39 --- /dev/null +++ b/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md @@ -0,0 +1,115 @@ +# Design Spec: A module's browser script lives in the crate that supplies it + +**Status:** Proposed, 2026-10-19. Implemented as row 55 of the stack, above [IABTechLab/trusted-server#1412](https://github.com/IABTechLab/trusted-server/pull/1412), which serves asset routes on every adapter, for Tech Lab review. [IABTechLab/trusted-server#1342](https://github.com/IABTechLab/trusted-server/pull/1342) carries every design document of the stack in one place. +**Author:** 51Degrees (contributed), for Tech Lab review +**Related specs:** [integration-provider-seam](https://github.com/IABTechLab/trusted-server/blob/feature/6-integration-seam/docs/superpowers/specs/2026-08-27-integration-provider-seam-design.md), [page-middleware](https://github.com/IABTechLab/trusted-server/blob/feature/6-integration-seam/docs/superpowers/specs/2026-10-12-page-middleware-design.md), [no-modules-crate](https://github.com/IABTechLab/trusted-server/blob/feature/6-integration-seam/docs/superpowers/specs/2026-10-14-no-modules-crate-design.md), and the design documents of the stack in IABTechLab/trusted-server#1342 +**Related PRs:** [IABTechLab/trusted-server#1348](https://github.com/IABTechLab/trusted-server/pull/1348), which moves every vendor module's Rust out of core into `crates///` +**Issues:** none +**Last updated:** 2026-10-19 + +> **Why this spec exists.** Each vendor module's Rust lives in its own crate, `crates///`, but its browser script, the script's tests and some of their test data live in `crates/trusted-server-js/lib/src/integrations//` and `lib/test/integrations//`, a folder that belongs to no vendor. Whoever maintains a module works in two places, and the shared browser crate holds every vendor's code. This change moves ten modules' scripts and tests into the nine crates that supply them, makes the build, the tests, the lint and the format checks, and editors, find them there, and moves the Tinybird project into the Tinybird crate. The 14 built bundles are byte for byte the same as before. + +## 1. Problem + +`build-all.mjs` builds a bundle for every folder in `lib/src/integrations/` that holds an `index.ts`, and `expected_module_ids` in `crates/trusted-server-js/build/bundle_set.rs` lists the same folders, so the build script embeds exactly the bundles the Node build wrote. Vitest, ESLint, Prettier and TypeScript are all configured for `crates/trusted-server-js/lib` and see no file outside it. A module's script reaches the shared browser code by relative paths such as `../../core/log`, and its tests reach the script by paths such as `../../../src/integrations/lockr/script_guard`. + +So a module crate such as `crates/identity/lockr` holds the module's Rust, its settings and its page-change fixtures, while the script that runs in the reader's browser and that script's tests sit in another crate, under a folder shared by every vendor. Three tests in the shared crate also use vendor scripts. The shared DOM insertion dispatcher's test imports five vendors' script guards, the Prebid shim's test imports the GPT and GPT diagnostics scripts, and the IIFE state test builds the Permutive bundle. The GPT script imports the APS renderer. + +The Tinybird module has the same split. Its Rust is in `crates/analytics/tinybird`, and the Tinybird project it sends to, its datasources, pipes, fixtures and tests, is in a `tinybird/` folder at the repository root. + +## 2. Design + +### 2.1 Where a module's script lives + +A crate that supplies a browser module keeps the script in `js/src//`, with `index.ts` as the bundle's entry, and the script's tests in `js/test//`. Test data only that crate's tests use goes in `js/test/fixtures/`. The module's name is its folder's name, as before, so one crate can supply more than one module. + +| Module | Crate | +| -------------------- | -------------------------------- | +| `datadome` | `crates/bot-protection/datadome` | +| `didomi` | `crates/cmp/didomi` | +| `google_tag_manager` | `crates/tag/google-tag-manager` | +| `gpt` | `crates/ad-tag/google` | +| `gpt_diagnostics` | `crates/ad-tag/google` | +| `lockr` | `crates/identity/lockr` | +| `osano` | `crates/cmp/osano` | +| `permutive` | `crates/audience/permutive` | +| `sourcepoint` | `crates/cmp/sourcepoint` | +| `testlight` | `crates/testing/testlight` | + +Git detects each move as a rename, because each moved file keeps most of its content, so `git log --follow` and blame keep each file's history. The GPT tests' helper that locates `gpt_bootstrap.js`, `test/fixtures/paths.ts`, moves into `crates/ad-tag/google/js/test/fixtures/`, because only the GPT tests use it. + +A vendor crate can now hold browser code in three ways, and this change adds only the first. + +- **Module scripts**, in `js/src//` and `js/test//`, which the shared build bundles and which the checks in 2.4 cover. +- **Inline page scripts** in `src/*.js`, such as `crates/ad-tag/google/src/gpt_bootstrap.js` and InMobi's `gpp_stub.js` and `tcf_stub.js`, which the crate's Rust embeds into the page. InMobi's are tested by `node --test` in the last step of the Vitest workflow job, as before. +- **A carried script** directly in `js/`, such as `crates/testing/iabtl/js/probe.js`, which the crate embeds with `include_str!` and hands to the registry as a `CarriedJsModule` with its SHA-256. + +So a folder named `js/` holds either module scripts, in `js/src` and `js/test`, or a carried script directly in `js/`. + +### 2.2 How the build finds them + +`moduleSourceDirs()` in `lib/build-module-options.mjs` lists every module's source folder by name, `lib/src/integrations/` for the modules that stay in the shared crate and `crates///js/src/` for the rest. A name found in two places is an error, because bundles and imports are keyed by name, and so is a module named `core`, which is the shared core's bundle. A link whose target is gone counts as no folder. `build-all.mjs` builds a bundle for each listed folder whose `index.ts` is a file. + +`expected_module_ids` in `build/bundle_set.rs` applies the same rule from the Rust side, given the crates folder as well as `lib/src`, so `build.rs` still embeds exactly the bundles the Node build wrote. Each side has tests of a module found in a crate, a name found twice and a module named `core`, in `build/bundle_set.rs` and `lib/test/module-discovery.test.mjs`. Bundles are still named `tsjs-.js`, so the Rust API, the served URLs and every content hash are unchanged. + +`build.rs` asks Cargo to rerun it when `lib/build-module-options.mjs` or any existing `crates///js/src` folder changes, and `js/test` as well when `TSJS_TEST=1` runs the tests. It names only folders that exist, because Cargo reruns a build script on every build while a watched path is missing. A crate's first `js/src` folder is therefore picked up by the next build that runs for another reason, which `crates/trusted-server-js/README.md` says, with touching `lib/package.json` as the way to force it. + +### 2.3 How scripts and tests import + +`lib/build-module-options.mjs` defines `@tsjs/core`, `@tsjs/shared` and `@tsjs/module/` for the build and for Vitest, Vitest's config adds `@tsjs/test`, and `tsconfig.json` maps the same names for TypeScript. + +- `@tsjs/core/...` and `@tsjs/shared/...` reach the shared browser code from a script in any crate. +- `@tsjs/module//...` reaches another module's code by the module's name rather than by the folder it lives in. The GPT script uses it for the APS renderer, and the dispatcher and Prebid tests use it for the vendor scripts they exercise, so an import of one module's code by another can be found by searching for `@tsjs/module/`. The IIFE state test finds the Permutive bundle's entry through `moduleSourceDirs()` instead. +- `@tsjs/test/...` reaches test data that more than one module's tests share, which is `test/fixtures/aps-renderer-v1.json`, used by the core, APS, GPT and Prebid tests. + +TypeScript's `paths` cannot match a folder by pattern, so `tsconfig.json` lists each crate's `js/src` folder under `@tsjs/module/*`, and a test checks that the list matches what `moduleSourceRoots()` finds, in the same order. A test imports its own module's script by a short relative path within the crate, for example `../../src/lockr/script_guard`. + +A script in a crate cannot import an npm package. Vite and TypeScript look for a package by walking up from the importing file, and this crate's `node_modules` is not above any other crate. None of the moved scripts imports a package, and a script that needs one stays in `lib` for now, which is one reason the Prebid shim, which imports `prebid.js`, has not moved. + +### 2.4 How the checks and editors find them + +Vitest's `test.dir` is the crates folder, with `trusted-server-js/lib/test/**/*.test.{ts,mjs}` and `*/*/js/test/**/*.test.{ts,mjs}` as the test files, and `server.fs.allow` lets it read the crates folder. The type gate, which checks the GPT diagnostics export contract with `expectTypeOf`, covers `*/*/js/test/**/types.test.ts`, because that test moved with the GPT diagnostics script. `lib/tsconfig.json` includes `../../*/*/js`, maps the `@tsjs/...` names, and maps `vitest` into this crate's `node_modules`, which a test in another crate cannot find by walking up from its own folder. + +ESLint's flat config ignores files outside the folder that holds its config, and editors look for the ESLint, Prettier and TypeScript settings by walking up from the file they open. So the crates folder holds three small files. `crates/eslint.config.js` applies `lib/eslint.config.js` to `*/*/js/src` and `*/*/js/test` and ignores everything else under the crates folder. `crates/.prettierrc.json` gives files under those folders `lib/.prettierrc.json`'s options and sets nothing for any other file, and a test checks the options are the same. `crates/tsconfig.json` extends `lib/tsconfig.json` for those folders. + +`npm run lint` lints `lib` as before and then runs ESLint from the crates folder over `*/*/js/{src,test}/**/*.{ts,tsx,js,mjs}`, and `npm run format` checks those folders, each without `--config`, so the command line and an editor apply the same settings. The ESLint config treats `@tsjs/` imports as internal, orders the import groups built-in, external, internal, parent, sibling, index, and gives a crate's `js/test` `.mjs` scripts Node's globals as it gives `lib`'s. The CI jobs still run `npm run lint`, `npm run format`, `npm run build` and `npm test` in `crates/trusted-server-js/lib`, so no workflow changes for the scripts. + +The lint and format checks did not cover inline page scripts or carried scripts before this change, and they still do not. + +### 2.5 The Tinybird project + +`tinybird/` moves to `crates/analytics/tinybird/project/`. Its README's links to the telemetry and configuration guides follow the move, the crate's README points at the project, the telemetry and configuration guides' links follow it, the Markdown format list in `.github/workflows/format.yml` and `AGENTS.md` drops `tinybird/**/*.md`, which `crates/**/*.md` now covers, and the changelog says where the project now is, because an operator runs `tb` from that folder. + +### 2.6 Text that named the old places + +Comments and pages that named a moved script by its old path now name the new one, in `crates/ad-tag/google/src/lib.rs`, `crates/cmp/sourcepoint/src/lib.rs`, `crates/trusted-server-core/src/publisher.rs`, the Didomi test and the GPT guide. The old path also appeared in three comment lines in `crates/ad-tag/google/src/gpt_bootstrap.js`, which the GPT module inlines into each page it changes, so readers receive those three comment lines with the new path, and the module's recorded page-change fixture changes with them. That is the only change a reader receives. + +The `tsjs` guide said there are 12 integration source modules and 13 bundles. There are 13 and 14, because it missed `ec_client_fixed`, and it now says so, with where a vendor module's script lives. `AGENTS.md`, the onboarding guide, the error reference's entry for an invalid bundle set and the changelog name both places a module's script can be. + +## 3. What is not in this change + +- **APS's renderer**, `lib/src/integrations/aps/render.ts`, stays in the shared crate, because the shared browser core imports it in `core/auction.ts` and `core/request.ts` and it is compiled into the `core` bundle. It moves when APS rendering leaves the browser core. +- **Prebid**, its shim, its external bundle build `build-prebid-external.mjs` with its tests, and the Prebid code of the `ts prebid client` command in `crates/trusted-server-cli/src/prebid_bundle.rs`, moves together in a later change. The shim imports `prebid.js`, which a script in a crate cannot (2.3), and the CLI runs the bundle build from `lib` with `lib`'s `node_modules`. +- **The shared browser core still names vendors**, GPT, Prebid, APS, Lockr, DataDome and Google Tag Manager in `lib/src/core` and `lib/src/shared`. Taking them out is a later change, as [#1348](https://github.com/IABTechLab/trusted-server/pull/1348) and the pull requests above it did for the Rust core. +- **`creative` and `ec_client_fixed`** are core's own browser features and stay in the shared crate. +- **Vendor fixtures in the integration test crate** and **vendor pages in the docs site** stay, because the integration tests and the docs site build from their own folders. + +## 4. Acceptance + +- Each of the 14 bundles `build-all.mjs` writes has the same SHA-256 as on row 54. +- `npm run lint` and `npm run format` pass, and a planted unused variable in a crate's script makes `npm run lint` fail. +- Every Vitest file passes, including the discovery tests and the checks of the Prettier options and the `@tsjs/module` list, and the type gate covers the moved contract test, which a planted false `expectTypeOf` assertion makes fail. On Windows the two `build-prebid-external` tests that create symlinks fail, as they do on row 54, because Windows refuses symlinks without the privilege. +- `tsc --noEmit -p tsconfig.json` in `lib` reports no error that it does not report on row 54. +- `cargo test` and `cargo clippy -- -D warnings` pass for `trusted-server-js`, including its discovery tests, and for `trusted-server-ad-tag-google`, whose recorded fixture changed, and `cargo fmt --check` passes. +- The repository's workflows pass on the pull request. + +## Revision record + +| Date | Change | +| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-10-19 | First draft. | +| 2026-10-19 | Fixes from review round 1. TypeScript's `@tsjs/module/*` list and its test, the configuration guide's Tinybird link, the editor settings in the crates folder and their test, the npm package rule, `build.rs` watching `build-module-options.mjs`, discovery refusing `core` and passing over broken links on both sides with JavaScript tests, no unmatched-pattern pass for ESLint, `.mjs` crate tests, the three kinds of browser code a crate can hold, the changelog, `AGENTS.md` and the onboarding guide, and wording throughout. | + +## Review log + +1. **Round 1, 2026-10-19.** A review in a fresh session, against `fixpass11/55-scripts-in-crates` as first committed at `7dc1671fe`, rewritten before it was pushed. Issues found, two of them blocking. TypeScript had no mapping for `@tsjs/module/*`, so the GPT script was no longer type-checked against the APS renderer while this design said TypeScript used the names, and the configuration guide still linked the old Tinybird README. Of the others, editors found no settings for crate scripts, a script in a crate cannot import an npm package and the design did not say so, `build.rs` did not watch `build-module-options.mjs`, the crate lint run passed an empty match, the JavaScript and Rust discovery differed on broken links, an `index.ts` folder and a module named `core`, crate `.mjs` tests were neither run nor given Node's globals, and there was no changelog entry. This revision fixes each of them. From 338b648db2e6de1c0172f47147d71cbaa05bde39 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 19 Oct 2026 23:14:11 +0100 Subject: [PATCH 2/8] Move module browser scripts into their crates Ten modules' browser scripts and their tests move from the shared trusted-server-js crate into the nine crates that supply the modules, as js/src// and js/test//: datadome, didomi, google_tag_manager, gpt and gpt_diagnostics, lockr, osano, permutive, sourcepoint and testlight. The GPT tests' helper that locates gpt_bootstrap.js moves with them. moduleSourceDirs() in build-module-options.mjs and expected_module_ids in build/bundle_set.rs apply one rule, lib/src/integrations/ or crates///js/src/, refuse a name found in two places or the name core, and pass over a broken link, with tests on both sides. build.rs watches build-module-options.mjs and each crate's js/src folder that exists. Bundles keep their tsjs-.js names, so the Rust API, served URLs and content hashes are unchanged, and all 14 built bundles are byte for byte the same as before. Scripts reach the shared browser code as @tsjs/core/... and @tsjs/shared/..., another module's code as @tsjs/module//..., and shared test data as @tsjs/test/.... tsconfig.json lists each crate's js/src folder for @tsjs/module, and a test checks the list. Vitest, ESLint and Prettier cover the crates' js/src and js/test folders, and crates/ holds a tsconfig.json, an ESLint config and a Prettier config so that editors and the npm scripts apply the shared settings there. The type gate covers the moved GPT diagnostics contract test. Comments and guides that named the old paths name the new ones. Three are comment lines in gpt_bootstrap.js, which the GPT module inlines into each page it changes, so readers receive those three lines with the new path, and the recorded page-change fixture changes with them. The tsjs guide now counts 13 source modules and 14 bundles, adding ec_client_fixed. The APS renderer stays in the shared crate because the browser core imports it, and Prebid stays because its shim imports an npm package and the ts prebid client command runs its bundle build from the shared crate. --- AGENTS.md | 2 +- CHANGELOG.md | 1 + crates/.prettierrc.json | 13 ++ .../google/js/src}/gpt/index.ts | 12 +- .../google/js/src}/gpt/script_guard.ts | 4 +- .../google/js/src}/gpt_diagnostics/api.ts | 2 +- .../google/js/src}/gpt_diagnostics/badges.ts | 2 +- .../google/js/src}/gpt_diagnostics/binding.ts | 2 +- .../js/src}/gpt_diagnostics/exhaustive.ts | 0 .../google/js/src}/gpt_diagnostics/index.ts | 4 +- .../js/src}/gpt_diagnostics/observer.ts | 4 +- .../google/js/src}/gpt_diagnostics/overlay.ts | 4 +- .../gpt_diagnostics/presentation_helpers.ts | 2 +- .../gpt_diagnostics/slot_size_observer.ts | 2 +- .../google/js/src}/gpt_diagnostics/store.ts | 2 +- .../google/js}/test/fixtures/paths.ts | 5 +- .../google/js/test}/gpt/ad_init.test.ts | 119 +++++++------ .../google/js/test}/gpt/gpt_bootstrap.test.ts | 7 +- .../google/js/test}/gpt/index.test.ts | 30 ++-- .../gpt/schedule_initial_ad_init.test.ts | 7 +- .../google/js/test}/gpt/script_guard.test.ts | 6 +- .../google/js/test}/gpt/spa_hook.test.ts | 7 +- .../js/test}/gpt_diagnostics/api.test.ts | 7 +- .../js/test}/gpt_diagnostics/badges.test.ts | 9 +- .../js/test}/gpt_diagnostics/binding.test.ts | 7 +- .../js/test}/gpt_diagnostics/index.test.ts | 10 +- .../js/test}/gpt_diagnostics/observer.test.ts | 4 +- .../js/test}/gpt_diagnostics/overlay.test.ts | 14 +- .../slot_size_observer.test.ts | 7 +- .../js/test}/gpt_diagnostics/store.test.ts | 2 +- .../js/test}/gpt_diagnostics/types.test.ts | 2 +- .../src/fixtures/page-change.recorded.html | 6 +- crates/ad-tag/google/src/gpt_bootstrap.js | 6 +- crates/ad-tag/google/src/lib.rs | 2 +- .../permutive/js/src}/permutive/index.ts | 4 +- .../js/src}/permutive/script_guard.ts | 2 +- .../permutive/js/src}/permutive/segments.ts | 2 +- .../js/test}/permutive/segments.test.ts | 2 +- .../datadome/js/src}/datadome/index.ts | 2 +- .../datadome/js/src}/datadome/script_guard.ts | 2 +- .../js/test}/datadome/script_guard.test.ts | 2 +- .../didomi/js/src}/didomi/index.ts | 2 +- .../didomi/js/test}/didomi/index.test.ts | 4 +- .../osano/js/src}/osano/index.ts | 2 +- .../osano/js/test}/osano/index.test.ts | 2 +- .../sourcepoint/js/src}/sourcepoint/index.ts | 2 +- .../js/src}/sourcepoint/script_guard.ts | 2 +- .../js/test}/sourcepoint/index.test.ts | 24 +-- .../js/test}/sourcepoint/script_guard.test.ts | 2 +- crates/cmp/sourcepoint/src/lib.rs | 2 +- crates/eslint.config.js | 11 ++ .../lockr/js/src}/lockr/index.ts | 2 +- .../lockr/js/src}/lockr/script_guard.ts | 2 +- .../lockr/js/test}/lockr/script_guard.test.ts | 6 +- .../js/src}/google_tag_manager/index.ts | 2 +- .../src}/google_tag_manager/script_guard.ts | 6 +- .../google_tag_manager/script_guard.test.ts | 2 +- .../testlight/js/src}/testlight/index.ts | 8 +- crates/trusted-server-core/src/publisher.rs | 2 +- crates/trusted-server-js/README.md | 20 ++- crates/trusted-server-js/build.rs | 48 ++++- crates/trusted-server-js/build/bundle_set.rs | 168 +++++++++++++++--- crates/trusted-server-js/lib/build-all.mjs | 27 ++- .../lib/build-module-options.mjs | 83 +++++++++ crates/trusted-server-js/lib/eslint.config.js | 15 +- crates/trusted-server-js/lib/package.json | 9 +- .../iife-state-artifact-integration.test.mjs | 5 +- .../test/integrations/prebid/index.test.ts | 7 +- .../lib/test/module-discovery.test.mjs | 109 ++++++++++++ .../shared/dom_insertion_dispatcher.test.ts | 13 +- crates/trusted-server-js/lib/tsconfig.json | 22 ++- crates/trusted-server-js/lib/vitest.config.ts | 17 +- crates/tsconfig.json | 7 + docs/guide/error-reference.md | 2 +- docs/guide/integrations/gpt.md | 2 +- docs/guide/onboarding.md | 3 +- docs/guide/tsjs.md | 16 +- 77 files changed, 706 insertions(+), 279 deletions(-) create mode 100644 crates/.prettierrc.json rename crates/{trusted-server-js/lib/src/integrations => ad-tag/google/js/src}/gpt/index.ts (99%) rename crates/{trusted-server-js/lib/src/integrations => ad-tag/google/js/src}/gpt/script_guard.ts (99%) rename crates/{trusted-server-js/lib/src/integrations => ad-tag/google/js/src}/gpt_diagnostics/api.ts (99%) rename crates/{trusted-server-js/lib/src/integrations => ad-tag/google/js/src}/gpt_diagnostics/badges.ts (99%) rename crates/{trusted-server-js/lib/src/integrations => ad-tag/google/js/src}/gpt_diagnostics/binding.ts (99%) rename crates/{trusted-server-js/lib/src/integrations => ad-tag/google/js/src}/gpt_diagnostics/exhaustive.ts (100%) rename crates/{trusted-server-js/lib/src/integrations => ad-tag/google/js/src}/gpt_diagnostics/index.ts (97%) rename crates/{trusted-server-js/lib/src/integrations => ad-tag/google/js/src}/gpt_diagnostics/observer.ts (99%) rename crates/{trusted-server-js/lib/src/integrations => ad-tag/google/js/src}/gpt_diagnostics/overlay.ts (99%) rename crates/{trusted-server-js/lib/src/integrations => ad-tag/google/js/src}/gpt_diagnostics/presentation_helpers.ts (92%) rename crates/{trusted-server-js/lib/src/integrations => ad-tag/google/js/src}/gpt_diagnostics/slot_size_observer.ts (99%) rename crates/{trusted-server-js/lib/src/integrations => ad-tag/google/js/src}/gpt_diagnostics/store.ts (99%) rename crates/{trusted-server-js/lib => ad-tag/google/js}/test/fixtures/paths.ts (83%) rename crates/{trusted-server-js/lib/test/integrations => ad-tag/google/js/test}/gpt/ad_init.test.ts (97%) rename crates/{trusted-server-js/lib/test/integrations => ad-tag/google/js/test}/gpt/gpt_bootstrap.test.ts (99%) rename crates/{trusted-server-js/lib/test/integrations => ad-tag/google/js/test}/gpt/index.test.ts (95%) rename crates/{trusted-server-js/lib/test/integrations => ad-tag/google/js/test}/gpt/schedule_initial_ad_init.test.ts (99%) rename crates/{trusted-server-js/lib/test/integrations => ad-tag/google/js/test}/gpt/script_guard.test.ts (99%) rename crates/{trusted-server-js/lib/test/integrations => ad-tag/google/js/test}/gpt/spa_hook.test.ts (99%) rename crates/{trusted-server-js/lib/test/integrations => ad-tag/google/js/test}/gpt_diagnostics/api.test.ts (98%) rename crates/{trusted-server-js/lib/test/integrations => ad-tag/google/js/test}/gpt_diagnostics/badges.test.ts (97%) rename crates/{trusted-server-js/lib/test/integrations => ad-tag/google/js/test}/gpt_diagnostics/binding.test.ts (97%) rename crates/{trusted-server-js/lib/test/integrations => ad-tag/google/js/test}/gpt_diagnostics/index.test.ts (97%) rename crates/{trusted-server-js/lib/test/integrations => ad-tag/google/js/test}/gpt_diagnostics/observer.test.ts (98%) rename crates/{trusted-server-js/lib/test/integrations => ad-tag/google/js/test}/gpt_diagnostics/overlay.test.ts (98%) rename crates/{trusted-server-js/lib/test/integrations => ad-tag/google/js/test}/gpt_diagnostics/slot_size_observer.test.ts (96%) rename crates/{trusted-server-js/lib/test/integrations => ad-tag/google/js/test}/gpt_diagnostics/store.test.ts (99%) rename crates/{trusted-server-js/lib/test/integrations => ad-tag/google/js/test}/gpt_diagnostics/types.test.ts (99%) rename crates/{trusted-server-js/lib/src/integrations => audience/permutive/js/src}/permutive/index.ts (97%) rename crates/{trusted-server-js/lib/src/integrations => audience/permutive/js/src}/permutive/script_guard.ts (91%) rename crates/{trusted-server-js/lib/src/integrations => audience/permutive/js/src}/permutive/segments.ts (98%) rename crates/{trusted-server-js/lib/test/integrations => audience/permutive/js/test}/permutive/segments.test.ts (97%) rename crates/{trusted-server-js/lib/src/integrations => bot-protection/datadome/js/src}/datadome/index.ts (94%) rename crates/{trusted-server-js/lib/src/integrations => bot-protection/datadome/js/src}/datadome/script_guard.ts (97%) rename crates/{trusted-server-js/lib/test/integrations => bot-protection/datadome/js/test}/datadome/script_guard.test.ts (99%) rename crates/{trusted-server-js/lib/src/integrations => cmp/didomi/js/src}/didomi/index.ts (97%) rename crates/{trusted-server-js/lib/test/integrations => cmp/didomi/js/test}/didomi/index.test.ts (91%) rename crates/{trusted-server-js/lib/src/integrations => cmp/osano/js/src}/osano/index.ts (99%) rename crates/{trusted-server-js/lib/test/integrations => cmp/osano/js/test}/osano/index.test.ts (99%) rename crates/{trusted-server-js/lib/src/integrations => cmp/sourcepoint/js/src}/sourcepoint/index.ts (99%) rename crates/{trusted-server-js/lib/src/integrations => cmp/sourcepoint/js/src}/sourcepoint/script_guard.ts (96%) rename crates/{trusted-server-js/lib/test/integrations => cmp/sourcepoint/js/test}/sourcepoint/index.test.ts (92%) rename crates/{trusted-server-js/lib/test/integrations => cmp/sourcepoint/js/test}/sourcepoint/script_guard.test.ts (98%) create mode 100644 crates/eslint.config.js rename crates/{trusted-server-js/lib/src/integrations => identity/lockr/js/src}/lockr/index.ts (98%) rename crates/{trusted-server-js/lib/src/integrations => identity/lockr/js/src}/lockr/script_guard.ts (96%) rename crates/{trusted-server-js/lib/test/integrations => identity/lockr/js/test}/lockr/script_guard.test.ts (99%) rename crates/{trusted-server-js/lib/src/integrations => tag/google-tag-manager/js/src}/google_tag_manager/index.ts (97%) rename crates/{trusted-server-js/lib/src/integrations => tag/google-tag-manager/js/src}/google_tag_manager/script_guard.ts (96%) rename crates/{trusted-server-js/lib/test/integrations => tag/google-tag-manager/js/test}/google_tag_manager/script_guard.test.ts (99%) rename crates/{trusted-server-js/lib/src/integrations => testing/testlight/js/src}/testlight/index.ts (90%) create mode 100644 crates/trusted-server-js/lib/test/module-discovery.test.mjs create mode 100644 crates/tsconfig.json diff --git a/AGENTS.md b/AGENTS.md index 874c96204b..6d49a7e114 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -450,7 +450,7 @@ IntegrationRegistration::builder(ID) ## JS Build Pipeline -- `build-all.mjs` discovers `src/integrations/*/index.ts` and builds each as a separate IIFE. +- `build-all.mjs` discovers `src/integrations/*/index.ts` and `crates///js/src/*/index.ts`, and builds each as a separate IIFE. - Output: `dist/tsjs-core.js`, `dist/tsjs-{integration}.js` from `npm run build`. `build.rs` passes `--out-dir` to build the same files into a private `OUT_DIR/tsjs-dist`, validates the set, and never reads `dist/`. - `build.rs` auto-generates `tsjs_modules.rs` with `include_str!()` for each discovered file. - `bundle.rs` provides `concatenate_modules(ids)` and `concatenated_hash(ids)` APIs. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f7bcc22ef..cf943aef29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- A vendor module's browser script and its tests live in the crate that supplies the module, under `js/src//` and `js/test//`, where they were under `crates/trusted-server-js/lib/src/integrations/` and `lib/test/integrations/`, for DataDome, Didomi, Google Tag Manager, GPT, GPT diagnostics, Lockr, Osano, Permutive, Sourcepoint and Testlight. The built bundles are unchanged. A script in a crate imports the shared browser code as `@tsjs/core/...` and `@tsjs/shared/...`, and another module's code as `@tsjs/module//...`. - The Axum, Cloudflare and Spin adapters serve `[[proxy.asset_routes]]`, where only the Fastly adapter did, so a `GET` or `HEAD` under a route's prefix is answered from the route's source, with its signing and the answers held for it, and no longer by the publisher's origin. The response skips the Edge Cookie steps, so it neither writes nor expires the Edge Cookie, and takes the route's cache policy after the finalize headers. Those adapters hold the asset body whole, up to 10 MiB on Cloudflare and 8 MiB on Spin. Their HTTP clients refuse Image Optimizer metadata, which the Axum client now does on both of its send paths, so there a route that enables the Image Optimizer answers each request it would optimize with `502` and `no-store, private`, after a signed route has asked its origin for the object with a `HEAD`, and serves its SVG paths and requests that set the debug parameter unoptimized. Each adapter logs a warning naming every such route, once for each instance of the adapter, which on Axum is the process. - **Breaking:** on Spin, a request to a source whose `host_header` names a host other than the one in its `url` answers `502` before its origin is asked, the publisher's `[source.origin]` as well as an asset route's source, because Spin sends the host in `url` as the `Host`. Before, the request reached its origin with the host in `url` in place of `host_header`. A Spin deployment whose publisher source sets such a `host_header` answers every page with `502`, robots.txt included. Before upgrading, check that no source of a Spin deployment sets a `host_header` naming another host, or remove it once its origin answers requests that carry the host in `url` as the `Host`. - The Edge Cookie is written without an identity graph as it is with one, because the graph is one optional store of the data kept with the cookie, such as partner IDs. A Fastly service with no `[ec] ec_store` now writes the cookie a real browser's page navigation creates, and `POST /_ts/api/v1/ec/resolve` answers there as it does with a graph, where neither wrote one. So the identifier auction providers get as `user.id`, where storage and personalized-ad selection are both set, is one value for the life of the cookie, where a server-side module such as `hmac` gave a new value on each page view and a client-side module gave none. Naming an `ec_store` later gives each browser holding a cookie a server-side module wrote without one a new identifier on its first page view, because the identity graph replaces an identifier it has no row for. A client-side module creates nothing at the edge to replace its cookie with, so that cookie keeps its identifier with no row for as long as the browser keeps the cookie and its resolved marker, and each of that browser's page views pays for an extra identity-graph read and a strongly consistent existence check. **Breaking for crates outside core:** `ec_finalize_response` and `handle_ec_resolve` read the bot gate from the context's device signals (`EcContext::set_device_signals`), so a context with none gets no created cookie. diff --git a/crates/.prettierrc.json b/crates/.prettierrc.json new file mode 100644 index 0000000000..69e9a00915 --- /dev/null +++ b/crates/.prettierrc.json @@ -0,0 +1,13 @@ +{ + "overrides": [ + { + "files": "*/*/js/{src,test}/**", + "options": { + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 100 + } + } + ] +} diff --git a/crates/trusted-server-js/lib/src/integrations/gpt/index.ts b/crates/ad-tag/google/js/src/gpt/index.ts similarity index 99% rename from crates/trusted-server-js/lib/src/integrations/gpt/index.ts rename to crates/ad-tag/google/js/src/gpt/index.ts index 1ea47aae6e..348c6b6174 100644 --- a/crates/trusted-server-js/lib/src/integrations/gpt/index.ts +++ b/crates/ad-tag/google/js/src/gpt/index.ts @@ -5,9 +5,9 @@ import { publisherFirstImpressionRetryDelay, releaseTrustedServerFirstImpressionClaim, reservePublisherFirstImpressionFallback, -} from '../../core/first_impression'; -import { log } from '../../core/log'; -import { resolveSlotElementByDivId } from '../../core/slot_element'; +} from '@tsjs/core/first_impression'; +import { log } from '@tsjs/core/log'; +import { resolveSlotElementByDivId } from '@tsjs/core/slot_element'; import type { AuctionSlot, AuctionBidData, @@ -15,8 +15,8 @@ import type { GptDiagnosticsTrustedServerOpportunity, GptSlotHandoff, TsjsApi, -} from '../../core/types'; -import { viewportBandSizes } from '../../core/viewport_sizes'; +} from '@tsjs/core/types'; +import { viewportBandSizes } from '@tsjs/core/viewport_sizes'; import { APS_UNIVERSAL_CREATIVE_RENDERER, APS_UNIVERSAL_CREATIVE_RENDERER_VERSION, @@ -25,7 +25,7 @@ import { consumeApsPrebidRenderer, getApsPrebidRenderer, validateApsRenderer, -} from '../aps/render'; +} from '@tsjs/module/aps/render'; import { installGptGuard } from './script_guard'; diff --git a/crates/trusted-server-js/lib/src/integrations/gpt/script_guard.ts b/crates/ad-tag/google/js/src/gpt/script_guard.ts similarity index 99% rename from crates/trusted-server-js/lib/src/integrations/gpt/script_guard.ts rename to crates/ad-tag/google/js/src/gpt/script_guard.ts index c1bc899457..9710ead02c 100644 --- a/crates/trusted-server-js/lib/src/integrations/gpt/script_guard.ts +++ b/crates/ad-tag/google/js/src/gpt/script_guard.ts @@ -1,9 +1,9 @@ -import { log } from '../../core/log'; +import { log } from '@tsjs/core/log'; import { DEFAULT_DOM_INSERTION_HANDLER_PRIORITY, type DomInsertionCandidate, registerDomInsertionHandler, -} from '../../shared/dom_insertion_dispatcher'; +} from '@tsjs/shared/dom_insertion_dispatcher'; /** * GPT Script Interception Guard diff --git a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/api.ts b/crates/ad-tag/google/js/src/gpt_diagnostics/api.ts similarity index 99% rename from crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/api.ts rename to crates/ad-tag/google/js/src/gpt_diagnostics/api.ts index 475bc7f939..bd36615b5c 100644 --- a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/api.ts +++ b/crates/ad-tag/google/js/src/gpt_diagnostics/api.ts @@ -5,7 +5,7 @@ import type { GptDiagnosticsRecorder, GptDiagnosticsSlotHandle, GptDiagnosticsTrustedServerOpportunity, -} from '../../core/types'; +} from '@tsjs/core/types'; import type { GptDiagnosticsBindingManager } from './binding'; import type { GptDiagnosticsStoreSnapshot } from './store'; diff --git a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/badges.ts b/crates/ad-tag/google/js/src/gpt_diagnostics/badges.ts similarity index 99% rename from crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/badges.ts rename to crates/ad-tag/google/js/src/gpt_diagnostics/badges.ts index 643dc355a3..98ac0a54e6 100644 --- a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/badges.ts +++ b/crates/ad-tag/google/js/src/gpt_diagnostics/badges.ts @@ -1,4 +1,4 @@ -import type { GptDiagnosticsRequestCycle } from '../../core/types'; +import type { GptDiagnosticsRequestCycle } from '@tsjs/core/types'; import type { GptDiagnosticsBindingManager } from './binding'; import { unhandledCase } from './exhaustive'; diff --git a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/binding.ts b/crates/ad-tag/google/js/src/gpt_diagnostics/binding.ts similarity index 99% rename from crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/binding.ts rename to crates/ad-tag/google/js/src/gpt_diagnostics/binding.ts index c6c46a727f..4225ff0517 100644 --- a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/binding.ts +++ b/crates/ad-tag/google/js/src/gpt_diagnostics/binding.ts @@ -1,4 +1,4 @@ -import type { GptDiagnosticsBinding, GptDiagnosticsSlotExport } from '../../core/types'; +import type { GptDiagnosticsBinding, GptDiagnosticsSlotExport } from '@tsjs/core/types'; import { scheduleFrame } from './presentation_helpers'; import type { GptDiagnosticsBindingInput } from './store'; diff --git a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/exhaustive.ts b/crates/ad-tag/google/js/src/gpt_diagnostics/exhaustive.ts similarity index 100% rename from crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/exhaustive.ts rename to crates/ad-tag/google/js/src/gpt_diagnostics/exhaustive.ts diff --git a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/index.ts b/crates/ad-tag/google/js/src/gpt_diagnostics/index.ts similarity index 97% rename from crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/index.ts rename to crates/ad-tag/google/js/src/gpt_diagnostics/index.ts index d7271710ca..c647c40438 100644 --- a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/index.ts +++ b/crates/ad-tag/google/js/src/gpt_diagnostics/index.ts @@ -1,5 +1,5 @@ -import { log } from '../../core/log'; -import type { GptDiagnosticsApi, TsjsApi } from '../../core/types'; +import { log } from '@tsjs/core/log'; +import type { GptDiagnosticsApi, TsjsApi } from '@tsjs/core/types'; import { GptDiagnosticsApiController } from './api'; import { GptDiagnosticsBadgeManager } from './badges'; diff --git a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/observer.ts b/crates/ad-tag/google/js/src/gpt_diagnostics/observer.ts similarity index 99% rename from crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/observer.ts rename to crates/ad-tag/google/js/src/gpt_diagnostics/observer.ts index 7a19ba043f..b9506c8b2e 100644 --- a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/observer.ts +++ b/crates/ad-tag/google/js/src/gpt_diagnostics/observer.ts @@ -1,5 +1,5 @@ -import { log } from '../../core/log'; -import type { GptDiagnosticsAdManagerIdentity, Size, TsjsApi } from '../../core/types'; +import { log } from '@tsjs/core/log'; +import type { GptDiagnosticsAdManagerIdentity, Size, TsjsApi } from '@tsjs/core/types'; import type { GptDiagnosticsSlotLike, GptRenderFacts } from './store'; diff --git a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/overlay.ts b/crates/ad-tag/google/js/src/gpt_diagnostics/overlay.ts similarity index 99% rename from crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/overlay.ts rename to crates/ad-tag/google/js/src/gpt_diagnostics/overlay.ts index 63c0b6fb3e..d2c52fd661 100644 --- a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/overlay.ts +++ b/crates/ad-tag/google/js/src/gpt_diagnostics/overlay.ts @@ -1,5 +1,5 @@ -import { log } from '../../core/log'; -import type { GptDiagnosticsRequestCycle } from '../../core/types'; +import { log } from '@tsjs/core/log'; +import type { GptDiagnosticsRequestCycle } from '@tsjs/core/types'; import type { GptDiagnosticsBindingManager } from './binding'; import { unhandledCase } from './exhaustive'; diff --git a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/presentation_helpers.ts b/crates/ad-tag/google/js/src/gpt_diagnostics/presentation_helpers.ts similarity index 92% rename from crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/presentation_helpers.ts rename to crates/ad-tag/google/js/src/gpt_diagnostics/presentation_helpers.ts index b601833b8f..ce7e9b6197 100644 --- a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/presentation_helpers.ts +++ b/crates/ad-tag/google/js/src/gpt_diagnostics/presentation_helpers.ts @@ -1,4 +1,4 @@ -import type { Size } from '../../core/types'; +import type { Size } from '@tsjs/core/types'; /** Formats CSS sizes consistently across diagnostics presentation surfaces. */ export function formatSizes(sizes: ReadonlyArray): string { diff --git a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/slot_size_observer.ts b/crates/ad-tag/google/js/src/gpt_diagnostics/slot_size_observer.ts similarity index 99% rename from crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/slot_size_observer.ts rename to crates/ad-tag/google/js/src/gpt_diagnostics/slot_size_observer.ts index c3328f992e..71dba0d1dc 100644 --- a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/slot_size_observer.ts +++ b/crates/ad-tag/google/js/src/gpt_diagnostics/slot_size_observer.ts @@ -1,4 +1,4 @@ -import type { Size } from '../../core/types'; +import type { Size } from '@tsjs/core/types'; import type { GptDiagnosticsBindingManager } from './binding'; import { scheduleFrame } from './presentation_helpers'; diff --git a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/store.ts b/crates/ad-tag/google/js/src/gpt_diagnostics/store.ts similarity index 99% rename from crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/store.ts rename to crates/ad-tag/google/js/src/gpt_diagnostics/store.ts index ca3348ae97..7619ae60fd 100644 --- a/crates/trusted-server-js/lib/src/integrations/gpt_diagnostics/store.ts +++ b/crates/ad-tag/google/js/src/gpt_diagnostics/store.ts @@ -15,7 +15,7 @@ import type { GptDiagnosticsSlotHandle, GptDiagnosticsTrustedServerOpportunity, Size, -} from '../../core/types'; +} from '@tsjs/core/types'; export const MAX_DIAGNOSTIC_SLOTS = 64; export const MAX_REQUEST_CYCLES_PER_SLOT = 10; diff --git a/crates/trusted-server-js/lib/test/fixtures/paths.ts b/crates/ad-tag/google/js/test/fixtures/paths.ts similarity index 83% rename from crates/trusted-server-js/lib/test/fixtures/paths.ts rename to crates/ad-tag/google/js/test/fixtures/paths.ts index 90c34dc575..881015afac 100644 --- a/crates/trusted-server-js/lib/test/fixtures/paths.ts +++ b/crates/ad-tag/google/js/test/fixtures/paths.ts @@ -11,7 +11,4 @@ import { resolve } from 'node:path'; * segment doubles. Resolving from this module's own directory also keeps the * path independent of the working directory the suite is launched from. */ -export const GPT_BOOTSTRAP_PATH = resolve( - import.meta.dirname, - '../../../../ad-tag/google/src/gpt_bootstrap.js' -); +export const GPT_BOOTSTRAP_PATH = resolve(import.meta.dirname, '../../../src/gpt_bootstrap.js'); diff --git a/crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts b/crates/ad-tag/google/js/test/gpt/ad_init.test.ts similarity index 97% rename from crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts rename to crates/ad-tag/google/js/test/gpt/ad_init.test.ts index ce8a0d427c..b9bd9c46be 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts +++ b/crates/ad-tag/google/js/test/gpt/ad_init.test.ts @@ -3,17 +3,18 @@ import { readFile } from 'node:fs/promises'; import { describe, it, expect, vi, beforeEach, afterEach, afterAll } from 'vitest'; -import envelope from '../../fixtures/aps-renderer-v1.json'; -import { GPT_BOOTSTRAP_PATH } from '../../fixtures/paths'; +import envelope from '@tsjs/test/fixtures/aps-renderer-v1.json'; import { registerPublisherFirstImpressionAuctions, resolveFirstImpressionElement, -} from '../../../src/core/first_impression'; -import type { AuctionBidData, TsjsApi } from '../../../src/core/types'; +} from '@tsjs/core/first_impression'; +import type { AuctionBidData, TsjsApi } from '@tsjs/core/types'; import { APS_PREBID_CREATIVE_RUNNER_URL, APS_RENDERING_MODE_ATTRIBUTE_NAME, -} from '../../../src/integrations/aps/render'; +} from '@tsjs/module/aps/render'; + +import { GPT_BOOTSTRAP_PATH } from '../fixtures/paths'; let publisherNativeScript: HTMLScriptElement | undefined; @@ -140,7 +141,7 @@ async function installHandoff(implementation: HandoffImplementation): Promise { const ts = (window as TestWindow).tsjs as TsjsApi; registerPublisherFirstImpressionAuctions(ts, ['div-atf-sidebar']); - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); ts.adInit!(); @@ -297,7 +298,7 @@ describe('installTsAdInit', () => { ); const ts = (window as TestWindow).tsjs as TsjsApi; registerPublisherFirstImpressionAuctions(ts, ['div-atf-sidebar']); - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); ts.adInit!(); @@ -383,7 +384,7 @@ describe('installTsAdInit', () => { }; const ts = (window as TestWindow).tsjs as TsjsApi; registerPublisherFirstImpressionAuctions(ts, ['div-atf-sidebar']); - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); ts.adInit!(); expect(defineSlot).not.toHaveBeenCalled(); @@ -424,7 +425,7 @@ describe('installTsAdInit', () => { recordTrustedServerOpportunity ); const ts = (window as TestWindow).tsjs as TsjsApi; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); ts.adInit!(); @@ -447,7 +448,7 @@ describe('installTsAdInit', () => { recordTrustedServerOpportunity ); const ts = (window as TestWindow).tsjs as TsjsApi; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); const lifecycleListener = mockPubads.addEventListener.mock.calls.find( ([registeredEvent]) => registeredEvent === eventName @@ -507,7 +508,7 @@ describe('installTsAdInit', () => { recordTrustedServerOpportunity ); - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -534,7 +535,7 @@ describe('installTsAdInit', () => { recordTrustedServerOpportunity ); - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -571,7 +572,7 @@ describe('installTsAdInit', () => { }, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -597,7 +598,7 @@ describe('installTsAdInit', () => { ); mockPubads.getSlots.mockReturnValue([]); - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -623,7 +624,7 @@ describe('installTsAdInit', () => { }, }); - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); expect(() => (window as TestWindow).tsjs!.adInit!()).not.toThrow(); @@ -636,7 +637,7 @@ describe('installTsAdInit', () => { const recordTrustedServerOpportunity = vi.fn(); const { mockSlot } = configureOpportunityDiagnostics(undefined, recordTrustedServerOpportunity); - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -718,7 +719,7 @@ describe('installTsAdInit', () => { } as unknown as TsjsApi['gptDiagnosticsRecorder'], }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); expect(() => (window as TestWindow).tsjs!.adInit!()).not.toThrow(); @@ -773,7 +774,7 @@ describe('installTsAdInit', () => { const fetchSpy = vi.spyOn(global, 'fetch'); - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -852,7 +853,7 @@ describe('installTsAdInit', () => { bids: {}, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -929,7 +930,7 @@ describe('installTsAdInit', () => { bids: { atf_sidebar_ad: { hb_pb: '1.00' } }, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -1003,7 +1004,7 @@ describe('installTsAdInit', () => { bids: {}, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); const ts = (window as TestWindow).tsjs!; expect(() => ts.adInit!()).not.toThrow(); @@ -1072,7 +1073,7 @@ describe('installTsAdInit', () => { bids: {}, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -1163,7 +1164,7 @@ describe('installTsAdInit', () => { bids: {}, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); ssrDiv.id = hydratedId; @@ -1237,7 +1238,7 @@ describe('installTsAdInit', () => { }, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); const defined = ( (window as TestWindow).googletag as { @@ -1281,7 +1282,7 @@ describe('installTsAdInit', () => { gptSlotHandoffs: { 'div-ts-fallback': handoff }, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); expect(() => @@ -1346,7 +1347,7 @@ describe('installTsAdInit', () => { bids: {}, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -1398,7 +1399,7 @@ describe('installTsAdInit', () => { }, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); expect(() => @@ -1462,7 +1463,7 @@ describe('installTsAdInit', () => { runGptBootstrap(); (window as TestWindow).tsjs!.adInit!(); - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); ssrDiv.id = hydratedId; @@ -1828,7 +1829,7 @@ describe('installTsAdInit', () => { bids: {}, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); pubads.disableInitialLoad(); (window as TestWindow).tsjs!.adInit!(); @@ -1912,7 +1913,7 @@ describe('installTsAdInit', () => { bids: {}, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); pubads.disableInitialLoad(); (window as TestWindow).tsjs!.adInit!(); @@ -1979,7 +1980,7 @@ describe('installTsAdInit', () => { bids: {}, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); // Publisher disables initial load — goes through the wrapper the detector @@ -2183,7 +2184,7 @@ describe('installTsAdInit', () => { bids: {}, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); installTsAdInit(); @@ -2295,7 +2296,7 @@ describe('installTsAdInit', () => { bids: {}, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); expect(getConfigMock).toHaveBeenCalledWith('disableInitialLoad'); @@ -2345,7 +2346,7 @@ describe('installTsAdInit', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -2384,7 +2385,7 @@ describe('installTsAdInit', () => { prevSlotTargetingKeys: { 'div-old-route': ['pos'] }, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -2424,7 +2425,7 @@ describe('installTsAdInit', () => { bids: {}, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -2476,7 +2477,7 @@ describe('installTsAdInit', () => { }, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -2534,7 +2535,7 @@ describe('installTsAdInit', () => { }, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -2608,7 +2609,7 @@ describe('installTsAdInit', () => { }, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -2669,7 +2670,7 @@ describe('installTsAdInit', () => { }, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -2729,7 +2730,7 @@ describe('installTsAdInit', () => { }, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); capturedListener!({ isEmpty: false, slot: mockSlotNoMatch }); @@ -2781,7 +2782,7 @@ describe('installTsAdInit', () => { }, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -2833,7 +2834,7 @@ describe('installTsAdInit', () => { }, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -2880,7 +2881,7 @@ describe('installTsAdInit', () => { }, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -2924,7 +2925,7 @@ describe('installTsAdInit', () => { bids: {}, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); (window as TestWindow).tsjs!.adInit!(); @@ -3085,7 +3086,7 @@ describe('installTsAdInit', () => { }; if (implementation === 'runtime') { - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); } else { runGptBootstrap(); @@ -3163,7 +3164,7 @@ describe('installTsAdInit', () => { const runtimeWarn = vi.spyOn(console, 'warn'); if (implementation === 'runtime') { - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); } else { runGptBootstrap(); @@ -3239,7 +3240,7 @@ describe('installTsAdInit', () => { }; if (implementation === 'runtime') { - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); } else { runGptBootstrap(); @@ -3286,7 +3287,7 @@ describe('installTsAdInit', () => { bids: {}, }; - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); installTsAdInit(); expect(() => (window as TestWindow).tsjs!.adInit!()).not.toThrow(); @@ -3296,7 +3297,7 @@ describe('installTsAdInit', () => { describe('parseCachedBid', () => { async function parseCachedBid(body: string) { - const mod = await import('../../../src/integrations/gpt/index'); + const mod = await import('../../src/gpt/index'); return mod.parseCachedBid(body); } @@ -3430,7 +3431,7 @@ describe('installTsRenderBridge', () => { ); } ); - await import('../../../src/integrations/gpt/index'); + await import('../../src/gpt/index'); addSpy.mockRestore(); currentScriptSpy?.mockRestore(); @@ -4518,7 +4519,7 @@ describe('installTsRenderBridge', () => { ); } ); - await import('../../../src/integrations/gpt/index'); + await import('../../src/gpt/index'); addSpy.mockRestore(); // Restore only addEventListener — fetchStub must stay stubbed expect(bridgeListener, 'bridge listener should be registered').toBeDefined(); @@ -4591,7 +4592,7 @@ describe('installTsRenderBridge', () => { ok: true, text: () => Promise.resolve(JSON.stringify({ adm: '
Creative
' })), } as Response); - const { log } = await import('../../../src/core/log'); + const { log } = await import('@tsjs/core/log'); const debugSpy = vi.spyOn(log, 'debug').mockImplementation(() => { throw new Error('success logging unavailable'); }); @@ -5338,7 +5339,7 @@ describe('installTsRenderBridge', () => { ); } ); - await import('../../../src/integrations/gpt/index'); + await import('../../src/gpt/index'); addSpy.mockRestore(); expect(bridgeListener, 'bridge listener should be registered').toBeDefined(); @@ -5670,7 +5671,7 @@ describe('installTsRenderBridge', () => { }); it('ignores message when adId does not match any TS bid', async () => { - await import('../../../src/integrations/gpt/index'); + await import('../../src/gpt/index'); fetchStub.mockResolvedValue({ ok: true, text: () => Promise.resolve('') } as Response); window.dispatchEvent( @@ -5693,7 +5694,7 @@ describe('installTsRenderBridge', () => { recordTrustedServerCreativeResponse, recordTrustedServerCreativeFailure, } as unknown as TsjsApi['gptDiagnosticsRecorder']; - await import('../../../src/integrations/gpt/index'); + await import('../../src/gpt/index'); fetchStub.mockResolvedValue({ ok: true, text: () => Promise.resolve('') } as Response); const foreignIframe = document.createElement('iframe'); @@ -5749,7 +5750,7 @@ describe('installTsRenderBridge', () => { } as unknown as TsjsApi['gptDiagnosticsRecorder']; const beaconSpy = vi.spyOn(navigator, 'sendBeacon').mockReturnValue(true); - await import('../../../src/integrations/gpt/index'); + await import('../../src/gpt/index'); fetchStub.mockResolvedValue({ ok: true, text: () => Promise.resolve('') } as Response); // Source iframe lives under slot A (div-header). @@ -5777,7 +5778,7 @@ describe('installTsRenderBridge', () => { }); it('ignores non-Prebid messages', async () => { - await import('../../../src/integrations/gpt/index'); + await import('../../src/gpt/index'); window.dispatchEvent( new MessageEvent('message', { data: JSON.stringify({ message: 'Other' }) }) ); diff --git a/crates/trusted-server-js/lib/test/integrations/gpt/gpt_bootstrap.test.ts b/crates/ad-tag/google/js/test/gpt/gpt_bootstrap.test.ts similarity index 99% rename from crates/trusted-server-js/lib/test/integrations/gpt/gpt_bootstrap.test.ts rename to crates/ad-tag/google/js/test/gpt/gpt_bootstrap.test.ts index a5610c005e..1d33440832 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt/gpt_bootstrap.test.ts +++ b/crates/ad-tag/google/js/test/gpt/gpt_bootstrap.test.ts @@ -2,9 +2,10 @@ import { readFileSync } from 'node:fs'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { FIRST_IMPRESSION_LEASE_MS } from '../../../src/core/first_impression'; -import type { FirstImpressionSlotClaim, TsjsApi } from '../../../src/core/types'; -import { GPT_BOOTSTRAP_PATH } from '../../fixtures/paths'; +import { FIRST_IMPRESSION_LEASE_MS } from '@tsjs/core/first_impression'; +import type { FirstImpressionSlotClaim, TsjsApi } from '@tsjs/core/types'; + +import { GPT_BOOTSTRAP_PATH } from '../fixtures/paths'; /** * Executable coverage for the edge-injected `gpt_bootstrap.js` — the diff --git a/crates/trusted-server-js/lib/test/integrations/gpt/index.test.ts b/crates/ad-tag/google/js/test/gpt/index.test.ts similarity index 95% rename from crates/trusted-server-js/lib/test/integrations/gpt/index.test.ts rename to crates/ad-tag/google/js/test/gpt/index.test.ts index 1b6488f739..c76bba832a 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt/index.test.ts +++ b/crates/ad-tag/google/js/test/gpt/index.test.ts @@ -1,13 +1,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { Mock } from 'vitest'; -import type { TsjsApi } from '../../../src/core/types'; +import type { TsjsApi } from '@tsjs/core/types'; // We import installGptShim dynamically so each test can control whether the // GPT enable flag is present before module evaluation. async function importGuardModule() { - return import('../../../src/integrations/gpt/script_guard'); + return import('../../src/gpt/script_guard'); } type GptWindow = Window & { @@ -32,7 +32,7 @@ describe('GPT shim – patchCommandQueue', () => { // Dynamic import to get a fresh reference (the module self-init already // ran at first import, but installGptShim is idempotent via the guard). - const mod = await import('../../../src/integrations/gpt/index'); + const mod = await import('../../src/gpt/index'); installGptShim = mod.installGptShim; }); @@ -175,7 +175,7 @@ describe('GPT – installSlimPrebidLoader', () => { }); it('is a no-op when __tsjs_slim_prebid_url is not set', async () => { - const { installSlimPrebidLoader } = await import('../../../src/integrations/gpt/index'); + const { installSlimPrebidLoader } = await import('../../src/gpt/index'); const addEventListenerSpy = vi.spyOn(window, 'addEventListener'); installSlimPrebidLoader(); expect(addEventListenerSpy).not.toHaveBeenCalledWith('load', expect.any(Function)); @@ -184,7 +184,7 @@ describe('GPT – installSlimPrebidLoader', () => { it('appends a deferred script tag when __tsjs_slim_prebid_url is set and load fires', async () => { (window as SlimWindow).__tsjs_slim_prebid_url = 'https://cdn.example.com/slim-prebid.js'; - const { installSlimPrebidLoader } = await import('../../../src/integrations/gpt/index'); + const { installSlimPrebidLoader } = await import('../../src/gpt/index'); installSlimPrebidLoader(); @@ -205,7 +205,7 @@ describe('GPT – installSlimPrebidLoader', () => { vi.resetModules(); (window as SlimWindow).__tsjs_slim_prebid_url = 'https://cdn.example.com/slim-prebid-init.js'; - await import('../../../src/integrations/gpt/index'); + await import('../../src/gpt/index'); window.dispatchEvent(new Event('load')); const scripts = Array.from(document.querySelectorAll('script[defer]')); @@ -264,7 +264,7 @@ describe('GPT – installTsAdInit', () => { }); it('clears stale TS-managed targeting before applying a new route to a reused GPT slot', async () => { - const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + const { installTsAdInit } = await import('../../src/gpt/index'); const slotTargeting = new Map([ ['hb_pb', ['1.20']], ['hb_bidder', ['kargo']], @@ -375,7 +375,7 @@ describe('GPT shim – runtime gating', () => { it('installs the shim when activation function is called (simulates server inline script)', async () => { const guard = await importGuardModule(); - const { installGptShim } = await import('../../../src/integrations/gpt/index'); + const { installGptShim } = await import('../../src/gpt/index'); // Simulate what the server-injected inline script does: // set the flag then call the activation function. @@ -388,7 +388,7 @@ describe('GPT shim – runtime gating', () => { it('registers __tsjs_installGptShim on window after import', async () => { vi.resetModules(); - await import('../../../src/integrations/gpt/index'); + await import('../../src/gpt/index'); expect(typeof (window as GatedWindow).__tsjs_installGptShim).toBe('function'); }); @@ -398,7 +398,7 @@ describe('GPT shim – runtime gating', () => { win.__tsjs_gpt_enabled = true; const guard = await importGuardModule(); - await import('../../../src/integrations/gpt/index'); + await import('../../src/gpt/index'); expect(guard.isGuardInstalled()).toBe(true); expect(win.googletag).toBeDefined(); @@ -411,7 +411,7 @@ describe('GPT shim – runtime gating', () => { const guard = await importGuardModule(); // Import a fresh copy — the module should register the activation // function on `window` but NOT call `installGptShim()` on its own. - await import('../../../src/integrations/gpt/index'); + await import('../../src/gpt/index'); // Assert immediately — the guard must not be installed because the // module only registers `__tsjs_installGptShim`, it does not auto-init. @@ -473,7 +473,7 @@ describe('GPT GAM attribution bundle fallback', () => { } async function importFreshGptBundle(): Promise { - await import('../../../src/integrations/gpt/index'); + await import('../../src/gpt/index'); } beforeEach(async () => { @@ -643,7 +643,7 @@ describe('GPT GAM attribution bundle fallback', () => { describe('GPT debug ADM iframe hardening', () => { it('sandbox token list omits allow-same-origin', async () => { - const mod = await import('../../../src/integrations/gpt/index'); + const mod = await import('../../src/gpt/index'); expect(mod.ADM_IFRAME_SANDBOX).toContain('allow-scripts'); // allow-scripts + allow-same-origin on srcdoc content removes the @@ -652,7 +652,7 @@ describe('GPT debug ADM iframe hardening', () => { }); it('safeAdmIframeSrc accepts http(s), relative, and protocol-relative URLs', async () => { - const { safeAdmIframeSrc } = await import('../../../src/integrations/gpt/index'); + const { safeAdmIframeSrc } = await import('../../src/gpt/index'); expect(safeAdmIframeSrc('https://ads.example.com/creative')).toBe( 'https://ads.example.com/creative' @@ -665,7 +665,7 @@ describe('GPT debug ADM iframe hardening', () => { }); it('safeAdmIframeSrc rejects script-executing and opaque schemes', async () => { - const { safeAdmIframeSrc } = await import('../../../src/integrations/gpt/index'); + const { safeAdmIframeSrc } = await import('../../src/gpt/index'); expect(safeAdmIframeSrc('javascript:alert(1)')).toBeUndefined(); expect(safeAdmIframeSrc('data:text/html,')).toBeUndefined(); diff --git a/crates/trusted-server-js/lib/test/integrations/gpt/schedule_initial_ad_init.test.ts b/crates/ad-tag/google/js/test/gpt/schedule_initial_ad_init.test.ts similarity index 99% rename from crates/trusted-server-js/lib/test/integrations/gpt/schedule_initial_ad_init.test.ts rename to crates/ad-tag/google/js/test/gpt/schedule_initial_ad_init.test.ts index 6ca6105477..d9dd5f5f4a 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt/schedule_initial_ad_init.test.ts +++ b/crates/ad-tag/google/js/test/gpt/schedule_initial_ad_init.test.ts @@ -2,8 +2,9 @@ import { readFileSync } from 'node:fs'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import type { TsjsApi } from '../../../src/core/types'; -import { GPT_BOOTSTRAP_PATH } from '../../fixtures/paths'; +import type { TsjsApi } from '@tsjs/core/types'; + +import { GPT_BOOTSTRAP_PATH } from '../fixtures/paths'; type TestWindow = Window & { googletag?: unknown; @@ -46,7 +47,7 @@ describe('scheduleInitialAdInit', () => { } async function importGptModule() { - return import('../../../src/integrations/gpt/index'); + return import('../../src/gpt/index'); } beforeEach(() => { diff --git a/crates/trusted-server-js/lib/test/integrations/gpt/script_guard.test.ts b/crates/ad-tag/google/js/test/gpt/script_guard.test.ts similarity index 99% rename from crates/trusted-server-js/lib/test/integrations/gpt/script_guard.test.ts rename to crates/ad-tag/google/js/test/gpt/script_guard.test.ts index 9a02874420..57f0ec7ebd 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt/script_guard.test.ts +++ b/crates/ad-tag/google/js/test/gpt/script_guard.test.ts @@ -1,10 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { - installGptGuard, - isGuardInstalled, - resetGuardState, -} from '../../../src/integrations/gpt/script_guard'; +import { installGptGuard, isGuardInstalled, resetGuardState } from '../../src/gpt/script_guard'; describe('GPT script guard', () => { let originalDocumentWrite: typeof document.write; diff --git a/crates/trusted-server-js/lib/test/integrations/gpt/spa_hook.test.ts b/crates/ad-tag/google/js/test/gpt/spa_hook.test.ts similarity index 99% rename from crates/trusted-server-js/lib/test/integrations/gpt/spa_hook.test.ts rename to crates/ad-tag/google/js/test/gpt/spa_hook.test.ts index 12b26e3469..f7e19d1c2d 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt/spa_hook.test.ts +++ b/crates/ad-tag/google/js/test/gpt/spa_hook.test.ts @@ -2,8 +2,9 @@ import { readFileSync } from 'node:fs'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import type { TsjsApi } from '../../../src/core/types'; -import { GPT_BOOTSTRAP_PATH } from '../../fixtures/paths'; +import type { TsjsApi } from '@tsjs/core/types'; + +import { GPT_BOOTSTRAP_PATH } from '../fixtures/paths'; type TestWindow = Window & { googletag?: unknown; @@ -14,7 +15,7 @@ const originalPushState = history.pushState.bind(history); const originalReplaceState = history.replaceState.bind(history); async function importGptModule() { - return import('../../../src/integrations/gpt/index'); + return import('../../src/gpt/index'); } /** Flush the microtask/timer queue so onNavigate's awaits settle. */ diff --git a/crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/api.test.ts b/crates/ad-tag/google/js/test/gpt_diagnostics/api.test.ts similarity index 98% rename from crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/api.test.ts rename to crates/ad-tag/google/js/test/gpt_diagnostics/api.test.ts index abbdeca692..a980abf004 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/api.test.ts +++ b/crates/ad-tag/google/js/test/gpt_diagnostics/api.test.ts @@ -1,8 +1,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { GptDiagnosticsBinding } from '../../../src/core/types'; -import { GptDiagnosticsApiController } from '../../../src/integrations/gpt_diagnostics/api'; -import { GptDiagnosticsStore } from '../../../src/integrations/gpt_diagnostics/store'; +import type { GptDiagnosticsBinding } from '@tsjs/core/types'; + +import { GptDiagnosticsApiController } from '../../src/gpt_diagnostics/api'; +import { GptDiagnosticsStore } from '../../src/gpt_diagnostics/store'; class FakeBindings { private readonly listeners = new Set<() => void>(); diff --git a/crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/badges.test.ts b/crates/ad-tag/google/js/test/gpt_diagnostics/badges.test.ts similarity index 97% rename from crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/badges.test.ts rename to crates/ad-tag/google/js/test/gpt_diagnostics/badges.test.ts index 329c4d2990..7b6f233841 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/badges.test.ts +++ b/crates/ad-tag/google/js/test/gpt_diagnostics/badges.test.ts @@ -1,12 +1,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { GptDiagnosticsBinding } from '../../../src/core/types'; -import type { GptDiagnosticsBindingView } from '../../../src/integrations/gpt_diagnostics/binding'; +import type { GptDiagnosticsBinding } from '@tsjs/core/types'; + +import type { GptDiagnosticsBindingView } from '../../src/gpt_diagnostics/binding'; import { GptDiagnosticsBadgeManager, gptDiagnosticsBadgeTextForTest, -} from '../../../src/integrations/gpt_diagnostics/badges'; -import { GptDiagnosticsStore } from '../../../src/integrations/gpt_diagnostics/store'; +} from '../../src/gpt_diagnostics/badges'; +import { GptDiagnosticsStore } from '../../src/gpt_diagnostics/store'; class FakeBindings { private readonly listeners = new Set<() => void>(); diff --git a/crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/binding.test.ts b/crates/ad-tag/google/js/test/gpt_diagnostics/binding.test.ts similarity index 97% rename from crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/binding.test.ts rename to crates/ad-tag/google/js/test/gpt_diagnostics/binding.test.ts index 5a1773a59b..50ff42f1f9 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/binding.test.ts +++ b/crates/ad-tag/google/js/test/gpt_diagnostics/binding.test.ts @@ -1,10 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { GptDiagnosticsBindingManager } from '../../../src/integrations/gpt_diagnostics/binding'; -import { - GptDiagnosticsStore, - type GptDiagnosticsSlotLike, -} from '../../../src/integrations/gpt_diagnostics/store'; +import { GptDiagnosticsBindingManager } from '../../src/gpt_diagnostics/binding'; +import { GptDiagnosticsStore, type GptDiagnosticsSlotLike } from '../../src/gpt_diagnostics/store'; const managers: GptDiagnosticsBindingManager[] = []; diff --git a/crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/index.test.ts b/crates/ad-tag/google/js/test/gpt_diagnostics/index.test.ts similarity index 97% rename from crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/index.test.ts rename to crates/ad-tag/google/js/test/gpt_diagnostics/index.test.ts index fa50d63662..2c7f1891fc 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/index.test.ts +++ b/crates/ad-tag/google/js/test/gpt_diagnostics/index.test.ts @@ -1,11 +1,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { TsjsApi } from '../../../src/core/types'; -import { - installGptDiagnosticsRuntime, - isGptDiagnosticsActive, -} from '../../../src/integrations/gpt_diagnostics'; -import { GPT_DIAGNOSTICS_HOST_ID } from '../../../src/integrations/gpt_diagnostics/overlay'; +import type { TsjsApi } from '@tsjs/core/types'; + +import { installGptDiagnosticsRuntime, isGptDiagnosticsActive } from '../../src/gpt_diagnostics'; +import { GPT_DIAGNOSTICS_HOST_ID } from '../../src/gpt_diagnostics/overlay'; interface FakeSlot { getSlotElementId(): string; diff --git a/crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/observer.test.ts b/crates/ad-tag/google/js/test/gpt_diagnostics/observer.test.ts similarity index 98% rename from crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/observer.test.ts rename to crates/ad-tag/google/js/test/gpt_diagnostics/observer.test.ts index 47d82697b4..b54a8bbb8d 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/observer.test.ts +++ b/crates/ad-tag/google/js/test/gpt_diagnostics/observer.test.ts @@ -4,8 +4,8 @@ import { GptDiagnosticsObserver, type GptDiagnosticsObserverStore, type GptObserverWindow, -} from '../../../src/integrations/gpt_diagnostics/observer'; -import type { GptDiagnosticsSlotLike } from '../../../src/integrations/gpt_diagnostics/store'; +} from '../../src/gpt_diagnostics/observer'; +import type { GptDiagnosticsSlotLike } from '../../src/gpt_diagnostics/store'; const EVENT_NAMES = [ 'slotRequested', diff --git a/crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/overlay.test.ts b/crates/ad-tag/google/js/test/gpt_diagnostics/overlay.test.ts similarity index 98% rename from crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/overlay.test.ts rename to crates/ad-tag/google/js/test/gpt_diagnostics/overlay.test.ts index 41cad667a5..542c8394f0 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/overlay.test.ts +++ b/crates/ad-tag/google/js/test/gpt_diagnostics/overlay.test.ts @@ -1,16 +1,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { log } from '../../../src/core/log'; -import type { GptDiagnosticsBinding } from '../../../src/core/types'; -import type { GptDiagnosticsBindingView } from '../../../src/integrations/gpt_diagnostics/binding'; -import { - GPT_DIAGNOSTICS_HOST_ID, - GptDiagnosticsOverlay, -} from '../../../src/integrations/gpt_diagnostics/overlay'; +import { log } from '@tsjs/core/log'; +import type { GptDiagnosticsBinding } from '@tsjs/core/types'; + +import type { GptDiagnosticsBindingView } from '../../src/gpt_diagnostics/binding'; +import { GPT_DIAGNOSTICS_HOST_ID, GptDiagnosticsOverlay } from '../../src/gpt_diagnostics/overlay'; import { GptDiagnosticsStore, TRUSTED_SERVER_ATTRIBUTION_WINDOW_MS, -} from '../../../src/integrations/gpt_diagnostics/store'; +} from '../../src/gpt_diagnostics/store'; class FakeBindings { private readonly listeners = new Set<() => void>(); diff --git a/crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/slot_size_observer.test.ts b/crates/ad-tag/google/js/test/gpt_diagnostics/slot_size_observer.test.ts similarity index 96% rename from crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/slot_size_observer.test.ts rename to crates/ad-tag/google/js/test/gpt_diagnostics/slot_size_observer.test.ts index 3709221ab6..8ad4dafac8 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/slot_size_observer.test.ts +++ b/crates/ad-tag/google/js/test/gpt_diagnostics/slot_size_observer.test.ts @@ -1,8 +1,9 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import type { GptDiagnosticsRequestCycle } from '../../../src/core/types'; -import { GptDiagnosticsSlotSizeObserver } from '../../../src/integrations/gpt_diagnostics/slot_size_observer'; -import type { GptDiagnosticsStoreSnapshot } from '../../../src/integrations/gpt_diagnostics/store'; +import type { GptDiagnosticsRequestCycle } from '@tsjs/core/types'; + +import { GptDiagnosticsSlotSizeObserver } from '../../src/gpt_diagnostics/slot_size_observer'; +import type { GptDiagnosticsStoreSnapshot } from '../../src/gpt_diagnostics/store'; type SlotSizeTestWindow = Window & { HTMLElement: typeof HTMLElement; diff --git a/crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/store.test.ts b/crates/ad-tag/google/js/test/gpt_diagnostics/store.test.ts similarity index 99% rename from crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/store.test.ts rename to crates/ad-tag/google/js/test/gpt_diagnostics/store.test.ts index 52aef6a7f5..f8f92540ed 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/store.test.ts +++ b/crates/ad-tag/google/js/test/gpt_diagnostics/store.test.ts @@ -13,7 +13,7 @@ import { REQUEST_PATH_ATTRIBUTION_WINDOW_MS, TRUSTED_SERVER_ATTRIBUTION_WINDOW_MS, type GptDiagnosticsSlotLike, -} from '../../../src/integrations/gpt_diagnostics/store'; +} from '../../src/gpt_diagnostics/store'; function fakeSlot(elementId: string, adUnitPath = `/example/${elementId}`): GptDiagnosticsSlotLike { return { diff --git a/crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/types.test.ts b/crates/ad-tag/google/js/test/gpt_diagnostics/types.test.ts similarity index 99% rename from crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/types.test.ts rename to crates/ad-tag/google/js/test/gpt_diagnostics/types.test.ts index f4f4c74864..1d84463adc 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt_diagnostics/types.test.ts +++ b/crates/ad-tag/google/js/test/gpt_diagnostics/types.test.ts @@ -13,7 +13,7 @@ import type { GptDiagnosticsSlotExport, Size, TsjsApi, -} from '../../../src/core/types'; +} from '@tsjs/core/types'; describe('GPT diagnostics public types', () => { it('keeps the documented read-only API source-compatible', () => { diff --git a/crates/ad-tag/google/src/fixtures/page-change.recorded.html b/crates/ad-tag/google/src/fixtures/page-change.recorded.html index 0cb4951f41..0d1a04ed29 100644 --- a/crates/ad-tag/google/src/fixtures/page-change.recorded.html +++ b/crates/ad-tag/google/src/fixtures/page-change.recorded.html @@ -5,7 +5,7 @@ // This is the minimal `window.tsjs.adInit` that runs on first page load // before the TSJS bundle has had a chance to install its richer // idempotent implementation. The bundle in -// crates/trusted-server-js/lib/src/integrations/gpt/index.ts overwrites `tsjs.adInit` +// crates/ad-tag/google/js/src/gpt/index.ts overwrites `tsjs.adInit` // once it loads. // // Contract with the bundle: @@ -315,7 +315,7 @@ // Minimal fallback for tsjs.scheduleInitialAdInit, mirroring the bundle's // hydration-safe scheduler in - // crates/trusted-server-js/lib/src/integrations/gpt/index.ts: the + // crates/ad-tag/google/js/src/gpt/index.ts: the // bids script hands the SSR bids payload to this scheduler, which applies // it and runs adInit only while the page is still on navigation // generation 0 (the SSR document), after window load plus a double @@ -971,7 +971,7 @@ // server-side targeting to GAM. If slim-Prebid has already wrapped // refresh(), it must pass this call straight through — not clear the // targeting and run a duplicate client-side auction. Mirrors the - // bundle's adInit() in crates/trusted-server-js/lib/src/integrations/gpt/index.ts. + // bundle's adInit() in crates/ad-tag/google/js/src/gpt/index.ts. ts.adInitRefreshInProgress = true; try { runHandoffInternal(function () { diff --git a/crates/ad-tag/google/src/gpt_bootstrap.js b/crates/ad-tag/google/src/gpt_bootstrap.js index 1ec0ed21de..cabf927504 100644 --- a/crates/ad-tag/google/src/gpt_bootstrap.js +++ b/crates/ad-tag/google/src/gpt_bootstrap.js @@ -3,7 +3,7 @@ // This is the minimal `window.tsjs.adInit` that runs on first page load // before the TSJS bundle has had a chance to install its richer // idempotent implementation. The bundle in -// crates/trusted-server-js/lib/src/integrations/gpt/index.ts overwrites `tsjs.adInit` +// crates/ad-tag/google/js/src/gpt/index.ts overwrites `tsjs.adInit` // once it loads. // // Contract with the bundle: @@ -313,7 +313,7 @@ // Minimal fallback for tsjs.scheduleInitialAdInit, mirroring the bundle's // hydration-safe scheduler in - // crates/trusted-server-js/lib/src/integrations/gpt/index.ts: the + // crates/ad-tag/google/js/src/gpt/index.ts: the // bids script hands the SSR bids payload to this scheduler, which applies // it and runs adInit only while the page is still on navigation // generation 0 (the SSR document), after window load plus a double @@ -969,7 +969,7 @@ // server-side targeting to GAM. If slim-Prebid has already wrapped // refresh(), it must pass this call straight through — not clear the // targeting and run a duplicate client-side auction. Mirrors the - // bundle's adInit() in crates/trusted-server-js/lib/src/integrations/gpt/index.ts. + // bundle's adInit() in crates/ad-tag/google/js/src/gpt/index.ts. ts.adInitRefreshInProgress = true; try { runHandoffInternal(function () { diff --git a/crates/ad-tag/google/src/lib.rs b/crates/ad-tag/google/src/lib.rs index 1cbcccc0e3..c3dd810e0d 100644 --- a/crates/ad-tag/google/src/lib.rs +++ b/crates/ad-tag/google/src/lib.rs @@ -741,7 +741,7 @@ impl GptIntegration { /// script at `` can call it before the TSJS bundle has loaded. /// /// The bundle's idempotent implementation in -/// `crates/trusted-server-js/lib/src/integrations/gpt/index.ts` later overwrites this stub. +/// `crates/ad-tag/google/js/src/gpt/index.ts` later overwrites this stub. /// Both implementations guard the one-time-per-page setup with /// `window.tsjs.servicesEnabled` so neither double-enables services if the /// publisher's own init code also calls `googletag.enableServices()`. diff --git a/crates/trusted-server-js/lib/src/integrations/permutive/index.ts b/crates/audience/permutive/js/src/permutive/index.ts similarity index 97% rename from crates/trusted-server-js/lib/src/integrations/permutive/index.ts rename to crates/audience/permutive/js/src/permutive/index.ts index 60eb5134a7..c5c05ea68b 100644 --- a/crates/trusted-server-js/lib/src/integrations/permutive/index.ts +++ b/crates/audience/permutive/js/src/permutive/index.ts @@ -1,5 +1,5 @@ -import { log } from '../../core/log'; -import { registerContextProvider } from '../../core/context'; +import { log } from '@tsjs/core/log'; +import { registerContextProvider } from '@tsjs/core/context'; import { installPermutiveGuard } from './script_guard'; import { getPermutiveSegments } from './segments'; diff --git a/crates/trusted-server-js/lib/src/integrations/permutive/script_guard.ts b/crates/audience/permutive/js/src/permutive/script_guard.ts similarity index 91% rename from crates/trusted-server-js/lib/src/integrations/permutive/script_guard.ts rename to crates/audience/permutive/js/src/permutive/script_guard.ts index 31891db9bb..085942767b 100644 --- a/crates/trusted-server-js/lib/src/integrations/permutive/script_guard.ts +++ b/crates/audience/permutive/js/src/permutive/script_guard.ts @@ -1,4 +1,4 @@ -import { createScriptGuard } from '../../shared/script_guard'; +import { createScriptGuard } from '@tsjs/shared/script_guard'; /** * Check if a URL is a Permutive SDK URL. diff --git a/crates/trusted-server-js/lib/src/integrations/permutive/segments.ts b/crates/audience/permutive/js/src/permutive/segments.ts similarity index 98% rename from crates/trusted-server-js/lib/src/integrations/permutive/segments.ts rename to crates/audience/permutive/js/src/permutive/segments.ts index 0120f01659..aafdc12080 100644 --- a/crates/trusted-server-js/lib/src/integrations/permutive/segments.ts +++ b/crates/audience/permutive/js/src/permutive/segments.ts @@ -1,7 +1,7 @@ // Permutive segment extraction from localStorage. // This logic is owned by the Permutive integration, keeping core free of // integration-specific data-reading code. -import { log } from '../../core/log'; +import { log } from '@tsjs/core/log'; /** Upper bound on the number of segments we forward to avoid oversized URLs. */ const MAX_SEGMENTS = 100; diff --git a/crates/trusted-server-js/lib/test/integrations/permutive/segments.test.ts b/crates/audience/permutive/js/test/permutive/segments.test.ts similarity index 97% rename from crates/trusted-server-js/lib/test/integrations/permutive/segments.test.ts rename to crates/audience/permutive/js/test/permutive/segments.test.ts index 030581751e..1934864cd6 100644 --- a/crates/trusted-server-js/lib/test/integrations/permutive/segments.test.ts +++ b/crates/audience/permutive/js/test/permutive/segments.test.ts @@ -6,7 +6,7 @@ describe('getPermutiveSegments', () => { beforeEach(async () => { await vi.resetModules(); localStorage.clear(); - const mod = await import('../../../src/integrations/permutive/segments'); + const mod = await import('../../src/permutive/segments'); getPermutiveSegments = mod.getPermutiveSegments; }); diff --git a/crates/trusted-server-js/lib/src/integrations/datadome/index.ts b/crates/bot-protection/datadome/js/src/datadome/index.ts similarity index 94% rename from crates/trusted-server-js/lib/src/integrations/datadome/index.ts rename to crates/bot-protection/datadome/js/src/datadome/index.ts index b7dacdebcd..a1acca3fa3 100644 --- a/crates/trusted-server-js/lib/src/integrations/datadome/index.ts +++ b/crates/bot-protection/datadome/js/src/datadome/index.ts @@ -1,4 +1,4 @@ -import { log } from '../../core/log'; +import { log } from '@tsjs/core/log'; import { installDataDomeGuard } from './script_guard'; diff --git a/crates/trusted-server-js/lib/src/integrations/datadome/script_guard.ts b/crates/bot-protection/datadome/js/src/datadome/script_guard.ts similarity index 97% rename from crates/trusted-server-js/lib/src/integrations/datadome/script_guard.ts rename to crates/bot-protection/datadome/js/src/datadome/script_guard.ts index 2606571262..00a7c4ea56 100644 --- a/crates/trusted-server-js/lib/src/integrations/datadome/script_guard.ts +++ b/crates/bot-protection/datadome/js/src/datadome/script_guard.ts @@ -1,4 +1,4 @@ -import { createScriptGuard } from '../../shared/script_guard'; +import { createScriptGuard } from '@tsjs/shared/script_guard'; /** * DataDome SDK Script Interception Guard diff --git a/crates/trusted-server-js/lib/test/integrations/datadome/script_guard.test.ts b/crates/bot-protection/datadome/js/test/datadome/script_guard.test.ts similarity index 99% rename from crates/trusted-server-js/lib/test/integrations/datadome/script_guard.test.ts rename to crates/bot-protection/datadome/js/test/datadome/script_guard.test.ts index 70fe191fd3..5b8277d476 100644 --- a/crates/trusted-server-js/lib/test/integrations/datadome/script_guard.test.ts +++ b/crates/bot-protection/datadome/js/test/datadome/script_guard.test.ts @@ -7,7 +7,7 @@ import { isDataDomeSdkUrl, extractDataDomePath, rewriteDataDomeUrl, -} from '../../../src/integrations/datadome/script_guard'; +} from '../../src/datadome/script_guard'; describe('DataDome SDK Script Interception Guard', () => { let originalAppendChild: typeof Element.prototype.appendChild; diff --git a/crates/trusted-server-js/lib/src/integrations/didomi/index.ts b/crates/cmp/didomi/js/src/didomi/index.ts similarity index 97% rename from crates/trusted-server-js/lib/src/integrations/didomi/index.ts rename to crates/cmp/didomi/js/src/didomi/index.ts index 3595b2f9a1..1a159e6091 100644 --- a/crates/trusted-server-js/lib/src/integrations/didomi/index.ts +++ b/crates/cmp/didomi/js/src/didomi/index.ts @@ -1,4 +1,4 @@ -import { log } from '../../core/log'; +import { log } from '@tsjs/core/log'; const DEFAULT_CONSENT_PROXY_PATH = '/integrations/didomi/consent/'; diff --git a/crates/trusted-server-js/lib/test/integrations/didomi/index.test.ts b/crates/cmp/didomi/js/test/didomi/index.test.ts similarity index 91% rename from crates/trusted-server-js/lib/test/integrations/didomi/index.test.ts rename to crates/cmp/didomi/js/test/didomi/index.test.ts index 5ca9d75987..36689bc4f9 100644 --- a/crates/trusted-server-js/lib/test/integrations/didomi/index.test.ts +++ b/crates/cmp/didomi/js/test/didomi/index.test.ts @@ -1,10 +1,10 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { installDidomiSdkProxy } from '../../../src/integrations/didomi'; +import { installDidomiSdkProxy } from '../../src/didomi'; const ORIGINAL_WINDOW = global.window; -// Mirrors the non-exported DidomiConfig shape in src/integrations/didomi. +// Mirrors the non-exported DidomiConfig shape in js/src/didomi. type TestDidomiConfig = { sdkPath?: string; [key: string]: unknown; diff --git a/crates/trusted-server-js/lib/src/integrations/osano/index.ts b/crates/cmp/osano/js/src/osano/index.ts similarity index 99% rename from crates/trusted-server-js/lib/src/integrations/osano/index.ts rename to crates/cmp/osano/js/src/osano/index.ts index 8bd3daae15..46f4064f01 100644 --- a/crates/trusted-server-js/lib/src/integrations/osano/index.ts +++ b/crates/cmp/osano/js/src/osano/index.ts @@ -1,4 +1,4 @@ -import { log } from '../../core/log'; +import { log } from '@tsjs/core/log'; const MARKER_COOKIE_NAME = '_ts_consent_src'; const MARKER_COOKIE_VALUE = 'osano'; diff --git a/crates/trusted-server-js/lib/test/integrations/osano/index.test.ts b/crates/cmp/osano/js/test/osano/index.test.ts similarity index 99% rename from crates/trusted-server-js/lib/test/integrations/osano/index.test.ts rename to crates/cmp/osano/js/test/osano/index.test.ts index 891fd55408..e452cb78e2 100644 --- a/crates/trusted-server-js/lib/test/integrations/osano/index.test.ts +++ b/crates/cmp/osano/js/test/osano/index.test.ts @@ -4,7 +4,7 @@ import { initializeOsanoConsentMirror, mirrorOsanoConsent, resetOsanoConsentMirrorForTest, -} from '../../../src/integrations/osano'; +} from '../../src/osano'; type TestWindow = Window & { Osano?: { diff --git a/crates/trusted-server-js/lib/src/integrations/sourcepoint/index.ts b/crates/cmp/sourcepoint/js/src/sourcepoint/index.ts similarity index 99% rename from crates/trusted-server-js/lib/src/integrations/sourcepoint/index.ts rename to crates/cmp/sourcepoint/js/src/sourcepoint/index.ts index f323468bbd..301163acec 100644 --- a/crates/trusted-server-js/lib/src/integrations/sourcepoint/index.ts +++ b/crates/cmp/sourcepoint/js/src/sourcepoint/index.ts @@ -1,4 +1,4 @@ -import { log } from '../../core/log'; +import { log } from '@tsjs/core/log'; import { installSourcepointGuard } from './script_guard'; diff --git a/crates/trusted-server-js/lib/src/integrations/sourcepoint/script_guard.ts b/crates/cmp/sourcepoint/js/src/sourcepoint/script_guard.ts similarity index 96% rename from crates/trusted-server-js/lib/src/integrations/sourcepoint/script_guard.ts rename to crates/cmp/sourcepoint/js/src/sourcepoint/script_guard.ts index dd4624aa77..d33a4867f2 100644 --- a/crates/trusted-server-js/lib/src/integrations/sourcepoint/script_guard.ts +++ b/crates/cmp/sourcepoint/js/src/sourcepoint/script_guard.ts @@ -1,4 +1,4 @@ -import { createScriptGuard } from '../../shared/script_guard'; +import { createScriptGuard } from '@tsjs/shared/script_guard'; const SOURCEPOINT_CDN_HOST = 'cdn.privacy-mgmt.com'; diff --git a/crates/trusted-server-js/lib/test/integrations/sourcepoint/index.test.ts b/crates/cmp/sourcepoint/js/test/sourcepoint/index.test.ts similarity index 92% rename from crates/trusted-server-js/lib/test/integrations/sourcepoint/index.test.ts rename to crates/cmp/sourcepoint/js/test/sourcepoint/index.test.ts index fc29e14c22..4cdf76ccf5 100644 --- a/crates/trusted-server-js/lib/test/integrations/sourcepoint/index.test.ts +++ b/crates/cmp/sourcepoint/js/test/sourcepoint/index.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { mirrorSourcepointConsent } from '../../../src/integrations/sourcepoint'; +import { mirrorSourcepointConsent } from '../../src/sourcepoint'; type SourcepointWindow = Window & { __tsjs_sourcepoint?: { @@ -16,12 +16,12 @@ describe('Sourcepoint integration initialization', () => { win = window as SourcepointWindow; delete win.__tsjs_sourcepoint; - const guard = await import('../../../src/integrations/sourcepoint/script_guard'); + const guard = await import('../../src/sourcepoint/script_guard'); guard.resetGuardState(); }); afterEach(async () => { - const guard = await import('../../../src/integrations/sourcepoint/script_guard'); + const guard = await import('../../src/sourcepoint/script_guard'); guard.resetGuardState(); delete win.__tsjs_sourcepoint; delete win.__tsjs_installSourcepointGuard; @@ -31,8 +31,8 @@ describe('Sourcepoint integration initialization', () => { vi.resetModules(); win.__tsjs_sourcepoint = { rewriteSdk: true }; - const guard = await import('../../../src/integrations/sourcepoint/script_guard'); - await import('../../../src/integrations/sourcepoint/index'); + const guard = await import('../../src/sourcepoint/script_guard'); + await import('../../src/sourcepoint/index'); expect(guard.isGuardInstalled()).toBe(true); }); @@ -41,8 +41,8 @@ describe('Sourcepoint integration initialization', () => { vi.resetModules(); win.__tsjs_sourcepoint = { rewriteSdk: false }; - const guard = await import('../../../src/integrations/sourcepoint/script_guard'); - await import('../../../src/integrations/sourcepoint/index'); + const guard = await import('../../src/sourcepoint/script_guard'); + await import('../../src/sourcepoint/index'); expect(guard.isGuardInstalled()).toBe(false); }); @@ -50,8 +50,8 @@ describe('Sourcepoint integration initialization', () => { it('defaults to installing the guard when rewriteSdk is missing for backward compatibility', async () => { vi.resetModules(); - const guard = await import('../../../src/integrations/sourcepoint/script_guard'); - await import('../../../src/integrations/sourcepoint/index'); + const guard = await import('../../src/sourcepoint/script_guard'); + await import('../../src/sourcepoint/index'); expect(guard.isGuardInstalled()).toBe(true); }); @@ -309,7 +309,7 @@ describe('integrations/sourcepoint', () => { localStorage.clear(); clearAllCookies(); - await import('../../../src/integrations/sourcepoint'); + await import('../../src/sourcepoint'); localStorage.setItem( '_sp_user_consent_12345', @@ -328,7 +328,7 @@ describe('integrations/sourcepoint', () => { clearAllCookies(); Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true }); - const sourcepoint = await import('../../../src/integrations/sourcepoint'); + const sourcepoint = await import('../../src/sourcepoint'); localStorage.setItem( '_sp_user_consent_12345', @@ -354,7 +354,7 @@ describe('integrations/sourcepoint', () => { clearAllCookies(); Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true }); - await import('../../../src/integrations/sourcepoint'); + await import('../../src/sourcepoint'); localStorage.setItem( '_sp_user_consent_12345', diff --git a/crates/trusted-server-js/lib/test/integrations/sourcepoint/script_guard.test.ts b/crates/cmp/sourcepoint/js/test/sourcepoint/script_guard.test.ts similarity index 98% rename from crates/trusted-server-js/lib/test/integrations/sourcepoint/script_guard.test.ts rename to crates/cmp/sourcepoint/js/test/sourcepoint/script_guard.test.ts index f68bc07450..4fd788237a 100644 --- a/crates/trusted-server-js/lib/test/integrations/sourcepoint/script_guard.test.ts +++ b/crates/cmp/sourcepoint/js/test/sourcepoint/script_guard.test.ts @@ -6,7 +6,7 @@ import { isSourcepointUrl, resetGuardState, rewriteSourcepointUrl, -} from '../../../src/integrations/sourcepoint/script_guard'; +} from '../../src/sourcepoint/script_guard'; describe('Sourcepoint SDK Script Interception Guard', () => { let originalAppendChild: typeof Element.prototype.appendChild; diff --git a/crates/cmp/sourcepoint/src/lib.rs b/crates/cmp/sourcepoint/src/lib.rs index 9230a35863..133fe5c049 100644 --- a/crates/cmp/sourcepoint/src/lib.rs +++ b/crates/cmp/sourcepoint/src/lib.rs @@ -763,7 +763,7 @@ fn parse_sourcepoint_url(url: &str) -> Option { } // Keep in sync with JS normalization in: - // crates/trusted-server-js/lib/src/integrations/sourcepoint/script_guard.ts + // crates/cmp/sourcepoint/js/src/sourcepoint/script_guard.ts // (protocol-relative + bare-domain handling + host-validation behavior). let normalized = if trimmed.starts_with("//") { format!("https:{trimmed}") diff --git a/crates/eslint.config.js b/crates/eslint.config.js new file mode 100644 index 0000000000..e0ead2129c --- /dev/null +++ b/crates/eslint.config.js @@ -0,0 +1,11 @@ +// Editors look for an ESLint config above the file they lint. This one applies +// the shared browser code's rules to module scripts kept in a crate's +// js/src and js/test folders, and to nothing else under crates. +import shared from './trusted-server-js/lib/eslint.config.js'; + +export default [ + { + ignores: ['**/*', '!*/', '!*/*/', '!*/*/js/', '!*/*/js/src/**', '!*/*/js/test/**'], + }, + ...shared, +]; diff --git a/crates/trusted-server-js/lib/src/integrations/lockr/index.ts b/crates/identity/lockr/js/src/lockr/index.ts similarity index 98% rename from crates/trusted-server-js/lib/src/integrations/lockr/index.ts rename to crates/identity/lockr/js/src/lockr/index.ts index e7b98e2cf1..6de6341e70 100644 --- a/crates/trusted-server-js/lib/src/integrations/lockr/index.ts +++ b/crates/identity/lockr/js/src/lockr/index.ts @@ -1,4 +1,4 @@ -import { log } from '../../core/log'; +import { log } from '@tsjs/core/log'; import { installLockrGuard } from './script_guard'; diff --git a/crates/trusted-server-js/lib/src/integrations/lockr/script_guard.ts b/crates/identity/lockr/js/src/lockr/script_guard.ts similarity index 96% rename from crates/trusted-server-js/lib/src/integrations/lockr/script_guard.ts rename to crates/identity/lockr/js/src/lockr/script_guard.ts index 77a6363677..14ef4c3079 100644 --- a/crates/trusted-server-js/lib/src/integrations/lockr/script_guard.ts +++ b/crates/identity/lockr/js/src/lockr/script_guard.ts @@ -1,4 +1,4 @@ -import { createScriptGuard } from '../../shared/script_guard'; +import { createScriptGuard } from '@tsjs/shared/script_guard'; /** * Lockr SDK Script Interception Guard diff --git a/crates/trusted-server-js/lib/test/integrations/lockr/script_guard.test.ts b/crates/identity/lockr/js/test/lockr/script_guard.test.ts similarity index 99% rename from crates/trusted-server-js/lib/test/integrations/lockr/script_guard.test.ts rename to crates/identity/lockr/js/test/lockr/script_guard.test.ts index 2f53c06b27..5b63401e75 100644 --- a/crates/trusted-server-js/lib/test/integrations/lockr/script_guard.test.ts +++ b/crates/identity/lockr/js/test/lockr/script_guard.test.ts @@ -1,10 +1,6 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { - installLockrGuard, - isGuardInstalled, - resetGuardState, -} from '../../../src/integrations/lockr/script_guard'; +import { installLockrGuard, isGuardInstalled, resetGuardState } from '../../src/lockr/script_guard'; describe('Lockr SDK Script Interception Guard', () => { let originalAppendChild: typeof Element.prototype.appendChild; diff --git a/crates/trusted-server-js/lib/src/integrations/google_tag_manager/index.ts b/crates/tag/google-tag-manager/js/src/google_tag_manager/index.ts similarity index 97% rename from crates/trusted-server-js/lib/src/integrations/google_tag_manager/index.ts rename to crates/tag/google-tag-manager/js/src/google_tag_manager/index.ts index ca73f24829..0d25993173 100644 --- a/crates/trusted-server-js/lib/src/integrations/google_tag_manager/index.ts +++ b/crates/tag/google-tag-manager/js/src/google_tag_manager/index.ts @@ -1,4 +1,4 @@ -import { log } from '../../core/log'; +import { log } from '@tsjs/core/log'; import { installGtmBeaconGuard } from './script_guard'; import { installGtmGuard } from './script_guard'; diff --git a/crates/trusted-server-js/lib/src/integrations/google_tag_manager/script_guard.ts b/crates/tag/google-tag-manager/js/src/google_tag_manager/script_guard.ts similarity index 96% rename from crates/trusted-server-js/lib/src/integrations/google_tag_manager/script_guard.ts rename to crates/tag/google-tag-manager/js/src/google_tag_manager/script_guard.ts index 874748bec8..ba3476db5d 100644 --- a/crates/trusted-server-js/lib/src/integrations/google_tag_manager/script_guard.ts +++ b/crates/tag/google-tag-manager/js/src/google_tag_manager/script_guard.ts @@ -1,6 +1,6 @@ -import { log } from '../../core/log'; -import { createBeaconGuard } from '../../shared/beacon_guard'; -import { createScriptGuard } from '../../shared/script_guard'; +import { log } from '@tsjs/core/log'; +import { createBeaconGuard } from '@tsjs/shared/beacon_guard'; +import { createScriptGuard } from '@tsjs/shared/script_guard'; /** * Google Tag Manager Script Interception Guard diff --git a/crates/trusted-server-js/lib/test/integrations/google_tag_manager/script_guard.test.ts b/crates/tag/google-tag-manager/js/test/google_tag_manager/script_guard.test.ts similarity index 99% rename from crates/trusted-server-js/lib/test/integrations/google_tag_manager/script_guard.test.ts rename to crates/tag/google-tag-manager/js/test/google_tag_manager/script_guard.test.ts index 971d396f49..ce22e26fa9 100644 --- a/crates/trusted-server-js/lib/test/integrations/google_tag_manager/script_guard.test.ts +++ b/crates/tag/google-tag-manager/js/test/google_tag_manager/script_guard.test.ts @@ -10,7 +10,7 @@ import { installGtmBeaconGuard, isBeaconGuardInstalled, resetBeaconGuardState, -} from '../../../src/integrations/google_tag_manager/script_guard'; +} from '../../src/google_tag_manager/script_guard'; describe('GTM Script Interception Guard', () => { let originalAppendChild: typeof Element.prototype.appendChild; diff --git a/crates/trusted-server-js/lib/src/integrations/testlight/index.ts b/crates/testing/testlight/js/src/testlight/index.ts similarity index 90% rename from crates/trusted-server-js/lib/src/integrations/testlight/index.ts rename to crates/testing/testlight/js/src/testlight/index.ts index 3db00ce052..beaa090c49 100644 --- a/crates/trusted-server-js/lib/src/integrations/testlight/index.ts +++ b/crates/testing/testlight/js/src/testlight/index.ts @@ -1,7 +1,7 @@ -import type { TsjsApi } from '../../core/types'; -import { installQueue } from '../../core/queue'; -import { log } from '../../core/log'; -import { resolvePrebidWindow, PrebidWindow } from '../../shared/globals'; +import type { TsjsApi } from '@tsjs/core/types'; +import { installQueue } from '@tsjs/core/queue'; +import { log } from '@tsjs/core/log'; +import { resolvePrebidWindow, PrebidWindow } from '@tsjs/shared/globals'; type TestlightCallback = () => void; diff --git a/crates/trusted-server-core/src/publisher.rs b/crates/trusted-server-core/src/publisher.rs index 26228d189f..8ebe52a4a6 100644 --- a/crates/trusted-server-core/src/publisher.rs +++ b/crates/trusted-server-core/src/publisher.rs @@ -7140,7 +7140,7 @@ pub(crate) fn build_bids_script(bid_map: &serde_json::Map/`, with its tests under `js/test//`, apart from +Prebid's shim and the APS renderer, which are still under +`lib/src/integrations`. A script in a crate reaches the shared code as +`@tsjs/core/...` and `@tsjs/shared/...`, another module's code as +`@tsjs/module//...`, and its own files by relative paths. It cannot +import an npm package, because packages resolve only from this crate's +`node_modules`. `tsconfig.json` lists each crate's `js/src` folder for +`@tsjs/module`, and a test checks the list. The `crates` folder holds a +`tsconfig.json`, an ESLint config and a Prettier config that apply this +crate's settings to crate scripts in an editor. Cargo reruns the build when a +watched script folder changes, and a crate's first `js/src` folder is watched +from the next build that runs for another reason, so touch `lib/package.json` +after adding one. Cargo builds the bundles into a private directory under its `OUT_DIR` and fails if any expected bundle is missing or empty; the `dist` directory is written only by a manual `npm run build` (for browser tests) and is never embedded unless you pass it @@ -23,6 +37,6 @@ npm run build ``` Rust target checks include this crate through `cargo test-fastly`. See -[Trusted Server JavaScript](../../docs/guide/tsjs.md) for the 12 integration -entries, 13 emitted bundles, and immediate, deferred, and standalone loading +[Trusted Server JavaScript](../../docs/guide/tsjs.md) for the 13 integration +entries, 14 emitted bundles, and immediate, deferred, and standalone loading modes. diff --git a/crates/trusted-server-js/build.rs b/crates/trusted-server-js/build.rs index a9148c8d93..ff82bd5bc5 100644 --- a/crates/trusted-server-js/build.rs +++ b/crates/trusted-server-js/build.rs @@ -16,19 +16,23 @@ use std::process::{Command, ExitStatus}; use build_print::info; use sha2::{Digest as _, Sha256}; -use crate::bundle_set::{bundle_file_name, check_bundle_set, expected_module_ids, scan_bundle_dir}; +use crate::bundle_set::{ + bundle_file_name, check_bundle_set, expected_module_ids, folders_in, scan_bundle_dir, +}; const PREBUILT_DIR_VAR: &str = "TSJS_PREBUILT_DIR"; const SKIP_BUILD_VAR: &str = "TSJS_SKIP_BUILD"; const TEST_VAR: &str = "TSJS_TEST"; fn main() { - // Cargo scans directories recursively, so these cover every TS source. + // Cargo scans directories recursively, so these cover the shared TS + // sources, and module scripts kept in other crates are watched below. // Every path must exist: a missing entry makes Cargo rerun this script on // every build, the opposite of the intent. for path in [ "lib/src", "lib/build-all.mjs", + "lib/build-module-options.mjs", "lib/build-prebid-external.mjs", "lib/package.json", "lib/package-lock.json", @@ -44,12 +48,24 @@ fn main() { let crate_dir = PathBuf::from( env::var("CARGO_MANIFEST_DIR").expect("should set CARGO_MANIFEST_DIR for build script"), ); + let crates_dir = crate_dir + .parent() + .expect("should have the crates folder above this crate") + .to_path_buf(); let out_dir = PathBuf::from(env::var("OUT_DIR").expect("should set OUT_DIR for build script")); let ts_dir = crate_dir.join("lib"); // Private to this build script run: concurrent builds never share it. let bundle_dir = out_dir.join("tsjs-dist"); - let expected = expected_module_ids(&ts_dir.join("src")) + // A module whose script lives in the crate that supplies it keeps the + // script in that crate's `js/src`, so each such folder is watched too. + // Only folders that exist are named, because Cargo reruns this script on + // every build while a watched path is missing. + for scripts in crate_script_folders(&crates_dir, "src") { + println!("cargo:rerun-if-changed={}", scripts.display()); + } + + let expected = expected_module_ids(&ts_dir.join("src"), &crates_dir) .unwrap_or_else(|err| panic!("tsjs: failed to discover modules: {err}")); if let Some(prebuilt_dir) = env::var_os(PREBUILT_DIR_VAR).map(PathBuf::from) { @@ -67,7 +83,7 @@ fn main() { validate_bundle_dir(&expected, &prebuilt_dir); copy_prebuilt_bundles(&expected, &prebuilt_dir, &bundle_dir); } else { - build_bundles(&ts_dir, &bundle_dir); + build_bundles(&ts_dir, &crates_dir, &bundle_dir); } validate_bundle_dir(&expected, &bundle_dir); @@ -80,7 +96,26 @@ fn main() { write_module_table(&expected, &bundle_dir, &out_dir); } -fn build_bundles(ts_dir: &Path, bundle_dir: &Path) { +/// Each `crates///js/` folder that exists, `kind` being +/// `src` for module scripts or `test` for their tests. +fn crate_script_folders(crates_dir: &Path, kind: &str) -> Vec { + let crates = folders_in(crates_dir) + .and_then(|types| { + types + .iter() + .map(|type_dir| folders_in(type_dir)) + .collect::, _>>() + }) + .unwrap_or_else(|err| panic!("tsjs: failed to list crates: {err}")); + crates + .into_iter() + .flatten() + .map(|crate_dir| crate_dir.join("js").join(kind)) + .filter(|folder| folder.is_dir()) + .collect() +} + +fn build_bundles(ts_dir: &Path, crates_dir: &Path, bundle_dir: &Path) { let how_to_prebuild = format!( "To embed prebuilt bundles instead, run `npm run build` in {} and set \ {PREBUILT_DIR_VAR} to the directory holding the tsjs-*.js files (by default {}).", @@ -115,6 +150,9 @@ fn build_bundles(ts_dir: &Path, bundle_dir: &Path) { for path in ["test", "vitest.config.ts", "build-prebid-external.mjs"] { println!("cargo:rerun-if-changed={}", ts_dir.join(path).display()); } + for tests in crate_script_folders(crates_dir, "test") { + println!("cargo:rerun-if-changed={}", tests.display()); + } let status = Command::new(&npm) .args(["run", "test", "--", "--run"]) .current_dir(ts_dir) diff --git a/crates/trusted-server-js/build/bundle_set.rs b/crates/trusted-server-js/build/bundle_set.rs index ec9b52ab34..587e97333d 100644 --- a/crates/trusted-server-js/build/bundle_set.rs +++ b/crates/trusted-server-js/build/bundle_set.rs @@ -4,7 +4,7 @@ //! depends only on `std`. use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; /// Module ID of the core bundle, which is always expected and ordered first. pub(crate) const CORE_MODULE_ID: &str = "core"; @@ -14,38 +14,59 @@ pub(crate) fn bundle_file_name(id: &str) -> String { format!("tsjs-{id}.js") } -/// Return the module IDs `build-all.mjs` builds from `lib_src`. +/// Return the module IDs `build-all.mjs` builds. /// -/// Mirrors the discovery in `build-all.mjs`: `core` plus every -/// `integrations//index.ts`. `core` comes first, integrations follow in -/// alphabetical order. +/// Finds module folders by the rule of `moduleSourceDirs` in +/// `build-module-options.mjs`, `integrations/` under `lib_src` and +/// `//js/src/` under `crates_dir`, and keeps those with an +/// `index.ts` file, as `build-all.mjs` does. `core` comes first, the rest +/// follow in alphabetical order. /// /// # Errors /// -/// Returns a message when the integrations directory cannot be read. -pub(crate) fn expected_module_ids(lib_src: &Path) -> Result, String> { - let integrations_dir = lib_src.join("integrations"); - let mut integrations = Vec::new(); - if integrations_dir.exists() { - let entries = fs::read_dir(&integrations_dir).map_err(|err| { - format!( - "failed to read integrations directory {}: {err}", - integrations_dir.display() - ) - })?; - for entry in entries { - let entry = entry.map_err(|err| { - format!( - "failed to read an entry in {}: {err}", - integrations_dir.display() - ) - })?; - let path = entry.path(); - if path.is_dir() && path.join("index.ts").is_file() { - integrations.push(entry.file_name().to_string_lossy().into_owned()); +/// Returns a message when a directory cannot be read, when two folders supply +/// a module of the same name, or when a folder is named `core`, which is the +/// shared core's bundle. +pub(crate) fn expected_module_ids( + lib_src: &Path, + crates_dir: &Path, +) -> Result, String> { + let mut roots = vec![lib_src.join("integrations")]; + for type_dir in folders_in(crates_dir)? { + for crate_dir in folders_in(&type_dir)? { + roots.push(crate_dir.join("js").join("src")); + } + } + + let mut modules: Vec<(String, PathBuf)> = Vec::new(); + for root in roots { + for dir in folders_in(&root)? { + let name = dir + .file_name() + .map(|name| name.to_string_lossy().into_owned()) + .unwrap_or_default(); + if name == CORE_MODULE_ID { + return Err(format!( + "module name {CORE_MODULE_ID}, used by {}, is the shared core's bundle", + dir.display() + )); } + if let Some((_, earlier)) = modules.iter().find(|(known, _)| *known == name) { + return Err(format!( + "module {name} is in both {} and {}", + earlier.display(), + dir.display() + )); + } + modules.push((name, dir)); } } + + let mut integrations: Vec = modules + .into_iter() + .filter(|(_, dir)| dir.join("index.ts").is_file()) + .map(|(name, _)| name) + .collect(); integrations.sort(); let mut ids = Vec::with_capacity(integrations.len() + 1); @@ -54,6 +75,29 @@ pub(crate) fn expected_module_ids(lib_src: &Path) -> Result, String> Ok(ids) } +/// The folders directly inside `dir`, or none when it does not exist. +/// +/// # Errors +/// +/// Returns a message when `dir` exists but cannot be read. +pub(crate) fn folders_in(dir: &Path) -> Result, String> { + if !dir.exists() { + return Ok(Vec::new()); + } + let entries = fs::read_dir(dir) + .map_err(|err| format!("failed to read directory {}: {err}", dir.display()))?; + let mut folders = Vec::new(); + for entry in entries { + let entry = + entry.map_err(|err| format!("failed to read an entry in {}: {err}", dir.display()))?; + if entry.path().is_dir() { + folders.push(entry.path()); + } + } + folders.sort(); + Ok(folders) +} + /// List the `tsjs-.js` bundles in `dir` as `(id, byte length)` pairs. /// /// # Errors @@ -217,6 +261,78 @@ mod tests { ); } + /// A fresh folder for one test, removed when the guard drops. + struct TempTree(PathBuf); + + impl TempTree { + fn new(name: &str) -> Self { + let root = + std::env::temp_dir().join(format!("tsjs-bundle-set-{}-{name}", std::process::id())); + let _ = fs::remove_dir_all(&root); + fs::create_dir_all(&root).expect("should create the test folder"); + Self(root) + } + + fn file(&self, relative: &str) { + let path = self.0.join(relative); + fs::create_dir_all(path.parent().expect("should have a parent folder")) + .expect("should create the file's folder"); + fs::write(&path, "").expect("should write the file"); + } + } + + impl Drop for TempTree { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } + } + + #[test] + fn finds_modules_here_and_in_the_crate_that_supplies_them() { + let tree = TempTree::new("finds"); + tree.file("lib/src/integrations/zeta/index.ts"); + tree.file("crates/kind/vendor/js/src/alpha/index.ts"); + tree.file("crates/kind/vendor/js/src/helpers/shared.ts"); + + let ids = expected_module_ids(&tree.0.join("lib/src"), &tree.0.join("crates")) + .expect("should discover the modules"); + + assert_eq!( + ids, + ["core", "alpha", "zeta"], + "should list core first, then every folder with an index.ts in name order" + ); + } + + #[test] + fn rejects_a_module_name_two_folders_supply() { + let tree = TempTree::new("twice"); + tree.file("lib/src/integrations/alpha/index.ts"); + tree.file("crates/kind/vendor/js/src/alpha/index.ts"); + + let err = expected_module_ids(&tree.0.join("lib/src"), &tree.0.join("crates")) + .expect_err("should refuse a module name found twice"); + + assert!( + err.contains("module alpha is in both"), + "should name the module and both folders: {err}" + ); + } + + #[test] + fn rejects_a_module_named_core() { + let tree = TempTree::new("core"); + tree.file("crates/kind/vendor/js/src/core/index.ts"); + + let err = expected_module_ids(&tree.0.join("lib/src"), &tree.0.join("crates")) + .expect_err("should refuse a module named core"); + + assert!( + err.contains("module name core"), + "should name the reserved name: {err}" + ); + } + #[test] fn bundle_file_name_matches_build_all_output() { assert_eq!( diff --git a/crates/trusted-server-js/lib/build-all.mjs b/crates/trusted-server-js/lib/build-all.mjs index 2a48d7071b..557b42c21a 100644 --- a/crates/trusted-server-js/lib/build-all.mjs +++ b/crates/trusted-server-js/lib/build-all.mjs @@ -23,12 +23,11 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { build } from 'vite'; -import { moduleBuildOptions } from './build-module-options.mjs'; +import { moduleBuildOptions, moduleEntry, moduleSourceDirs } from './build-module-options.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const srcDir = path.resolve(__dirname, 'src'); const distDir = resolveOutDir(process.argv.slice(2)); -const integrationsDir = path.join(srcDir, 'integrations'); /** Resolve `--out-dir ` (relative to the cwd), defaulting to ../dist. */ function resolveOutDir(args) { @@ -52,18 +51,14 @@ for (const name of fs.readdirSync(distDir)) { } } -// Discover integration modules: directories in src/integrations/ with index.ts -const integrationModules = fs.existsSync(integrationsDir) - ? fs - .readdirSync(integrationsDir) - .filter((name) => { - const fullPath = path.join(integrationsDir, name); - return ( - fs.statSync(fullPath).isDirectory() && fs.existsSync(path.join(fullPath, 'index.ts')) - ); - }) - .sort() - : []; +// Discover modules: every module folder with an index.ts, here or in the +// crate that supplies it (see moduleSourceDirs). +const moduleEntries = new Map( + [...moduleSourceDirs()] + .map(([name, dir]) => [name, moduleEntry(dir)]) + .filter(([, entryPath]) => entryPath !== null) +); +const integrationModules = [...moduleEntries.keys()].sort(); console.log('[build-all] Discovered integrations:', integrationModules); @@ -80,9 +75,7 @@ async function buildModule(name, entryPath) { // Build core first (synchronously), then all integrations in parallel await buildModule('core', path.join(srcDir, 'core', 'index.ts')); -await Promise.all( - integrationModules.map((name) => buildModule(name, path.join(integrationsDir, name, 'index.ts'))) -); +await Promise.all(integrationModules.map((name) => buildModule(name, moduleEntries.get(name)))); // List all built files const builtFiles = fs diff --git a/crates/trusted-server-js/lib/build-module-options.mjs b/crates/trusted-server-js/lib/build-module-options.mjs index 64b78a3fb7..bb2ad40c6a 100644 --- a/crates/trusted-server-js/lib/build-module-options.mjs +++ b/crates/trusted-server-js/lib/build-module-options.mjs @@ -1,13 +1,96 @@ +import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const libDir = path.dirname(fileURLToPath(import.meta.url)); +const cratesDir = path.resolve(libDir, '..', '..'); + +/** + * The names a module's script uses for the shared browser code, so a script + * kept in its own crate's `js/` folder imports it without a path into this + * crate. + */ +export const sharedCodeAliases = { + '@tsjs/core': path.join(libDir, 'src', 'core'), + '@tsjs/shared': path.join(libDir, 'src', 'shared'), +}; + +/** Whether `full` is a folder, false for anything missing, a broken link included. */ +function isFolder(full) { + return fs.statSync(full, { throwIfNoEntry: false })?.isDirectory() === true; +} + +/** The folders directly inside `dir`, in name order, or none when it is not a folder. */ +function foldersIn(dir) { + if (!isFolder(dir)) { + return []; + } + return fs + .readdirSync(dir) + .sort() + .map((name) => path.join(dir, name)) + .filter(isFolder); +} + +/** + * The folders that hold modules' sources: this crate's `src/integrations`, then + * `//js/src` for each crate under `cratesDir` that has one. + */ +export function moduleSourceRoots({ lib = libDir, crates = cratesDir } = {}) { + const crateScriptRoots = foldersIn(crates) + .flatMap((typeDir) => foldersIn(typeDir)) + .map((crateDir) => path.join(crateDir, 'js', 'src')) + .filter(isFolder); + return [path.join(lib, 'src', 'integrations'), ...crateScriptRoots]; +} + +/** + * Every module's source folder by module name: `src/integrations/` for + * this crate's own modules, and `crates///js/src/` for a + * module whose script lives in the crate that supplies it. A name found twice + * is an error, because bundles and imports are keyed by name, and so is the + * name `core`, which is the shared core's bundle. + */ +export function moduleSourceDirs({ lib = libDir, crates = cratesDir } = {}) { + const dirs = new Map(); + for (const root of moduleSourceRoots({ lib, crates })) { + for (const dir of foldersIn(root)) { + const name = path.basename(dir); + if (name === 'core') { + throw new Error(`module name core, used by ${dir}, is the shared core's bundle`); + } + const earlier = dirs.get(name); + if (earlier) { + throw new Error(`module ${name} is in both ${earlier} and ${dir}`); + } + dirs.set(name, dir); + } + } + return dirs; +} + +/** The entry of the module whose sources are in `dir`, or none when it has no `index.ts`. */ +export function moduleEntry(dir) { + const entryPath = path.join(dir, 'index.ts'); + return fs.statSync(entryPath, { throwIfNoEntry: false })?.isFile() === true ? entryPath : null; +} + +/** + * `@tsjs/module/` for each module, so a script or a test that needs + * another module's code names the module rather than the folder it lives in. + */ +export function moduleAliases() { + return Object.fromEntries( + [...moduleSourceDirs()].map(([name, dir]) => [`@tsjs/module/${name}`, dir]) + ); +} /** Production Vite options for one independently bundled tsjs IIFE. */ export function moduleBuildOptions({ name, entryPath, outDir }) { return { configFile: false, root: libDir, + resolve: { alias: { ...sharedCodeAliases, ...moduleAliases() } }, build: { emptyOutDir: false, outDir, diff --git a/crates/trusted-server-js/lib/eslint.config.js b/crates/trusted-server-js/lib/eslint.config.js index 2720ba3a04..f00c2bc0ee 100644 --- a/crates/trusted-server-js/lib/eslint.config.js +++ b/crates/trusted-server-js/lib/eslint.config.js @@ -31,10 +31,19 @@ export default [ unicorn, '@typescript-eslint': tseslint.plugin, }, + // `@tsjs/...` names the shared browser code and the modules, wherever a + // module's script lives, so those imports order as this project's own. + settings: { 'import/internal-regex': '^@tsjs/' }, rules: { 'unicorn/prevent-abbreviations': 'off', 'unicorn/filename-case': 'off', - 'import/order': ['error', { 'newlines-between': 'always' }], + 'import/order': [ + 'error', + { + groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], + 'newlines-between': 'always', + }, + ], }, }, // Honor the `_`-prefix convention for intentionally unused bindings in every @@ -52,7 +61,9 @@ export default [ // so CommonJS-only names (__dirname, require, module) still fail no-undef // in these ES modules { - files: ['*.mjs', 'test/**/*.mjs'], + // In the crate run ESLint's base is the crates folder, so a crate's + // `js/test` scripts are named from there. + files: ['*.mjs', 'test/**/*.mjs', '*/*/js/test/**/*.mjs'], languageOptions: { globals: globals.nodeBuiltin, }, diff --git a/crates/trusted-server-js/lib/package.json b/crates/trusted-server-js/lib/package.json index 427fef1e1f..664d33cdb5 100644 --- a/crates/trusted-server-js/lib/package.json +++ b/crates/trusted-server-js/lib/package.json @@ -10,10 +10,11 @@ "dev": "vite build --watch", "test": "vitest run", "test:watch": "vitest", - "lint": "eslint . --max-warnings=0", - "lint:fix": "eslint --fix . --max-warnings=0", - "format": "prettier --check \"**/*.{ts,tsx,js,json,css,md}\"", - "format:write": "prettier --write \"**/*.{ts,tsx,js,json,css,md}\"" + "lint": "eslint . --max-warnings=0 && npm run lint:crates", + "lint:crates": "cd ../.. && eslint --max-warnings=0 \"*/*/js/{src,test}/**/*.{ts,tsx,js,mjs}\"", + "lint:fix": "eslint --fix . --max-warnings=0 && cd ../.. && eslint --fix --max-warnings=0 \"*/*/js/{src,test}/**/*.{ts,tsx,js,mjs}\"", + "format": "prettier --check \"**/*.{ts,tsx,js,json,css,md}\" && prettier --check \"../../*/*/js/{src,test}/**/*.{ts,tsx,js,json,css,md}\"", + "format:write": "prettier --write \"**/*.{ts,tsx,js,json,css,md}\" && prettier --write \"../../*/*/js/{src,test}/**/*.{ts,tsx,js,json,css,md}\"" }, "dependencies": { "prebid.js": "^10.26.0" diff --git a/crates/trusted-server-js/lib/test/iife-state-artifact-integration.test.mjs b/crates/trusted-server-js/lib/test/iife-state-artifact-integration.test.mjs index 127b122def..021b2a24e2 100644 --- a/crates/trusted-server-js/lib/test/iife-state-artifact-integration.test.mjs +++ b/crates/trusted-server-js/lib/test/iife-state-artifact-integration.test.mjs @@ -11,7 +11,7 @@ import { JSDOM } from 'jsdom'; import { build } from 'vite'; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; -import { moduleBuildOptions } from '../build-module-options.mjs'; +import { moduleBuildOptions, moduleSourceDirs } from '../build-module-options.mjs'; const libDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const bundles = new Map(); @@ -20,11 +20,12 @@ let dom; beforeAll(async () => { outputDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'trusted-server-iife-state-')); + const moduleDirs = moduleSourceDirs(); for (const name of ['core', 'creative', 'permutive']) { const entryPath = name === 'core' ? path.join(libDir, 'src', 'core', 'index.ts') - : path.join(libDir, 'src', 'integrations', name, 'index.ts'); + : path.join(moduleDirs.get(name), 'index.ts'); await build(moduleBuildOptions({ name, entryPath, outDir: outputDirectory })); bundles.set(name, fs.readFileSync(path.join(outputDirectory, `tsjs-${name}.js`), 'utf8')); } diff --git a/crates/trusted-server-js/lib/test/integrations/prebid/index.test.ts b/crates/trusted-server-js/lib/test/integrations/prebid/index.test.ts index 55c4a4e1ab..738a55c683 100644 --- a/crates/trusted-server-js/lib/test/integrations/prebid/index.test.ts +++ b/crates/trusted-server-js/lib/test/integrations/prebid/index.test.ts @@ -340,6 +340,10 @@ const { }; }); +import { GptDiagnosticsStore } from '@tsjs/module/gpt_diagnostics/store'; +import { GptDiagnosticsObserver } from '@tsjs/module/gpt_diagnostics/observer'; +import { installTsAdInit } from '@tsjs/module/gpt/index'; + import { collectBidders, getInjectedConfig, @@ -347,7 +351,6 @@ import { installPrebidNpm, installRefreshHandler, } from '../../../src/integrations/prebid/index'; -import { installTsAdInit } from '../../../src/integrations/gpt/index'; import type { AuctionBid } from '../../../src/core/auction'; import { claimFirstImpressionForTrustedServer, @@ -361,8 +364,6 @@ import { } from '../../../src/core/first_impression'; import { log } from '../../../src/core/log'; import type { TsjsApi } from '../../../src/core/types'; -import { GptDiagnosticsObserver } from '../../../src/integrations/gpt_diagnostics/observer'; -import { GptDiagnosticsStore } from '../../../src/integrations/gpt_diagnostics/store'; import envelope from '../../fixtures/aps-renderer-v1.json'; // installPrebidNpm is a per-page no-op once the sentinel is set (the module diff --git a/crates/trusted-server-js/lib/test/module-discovery.test.mjs b/crates/trusted-server-js/lib/test/module-discovery.test.mjs new file mode 100644 index 0000000000..8d74a6a3ee --- /dev/null +++ b/crates/trusted-server-js/lib/test/module-discovery.test.mjs @@ -0,0 +1,109 @@ +// @vitest-environment node + +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { afterEach, describe, expect, it } from 'vitest'; + +import { moduleEntry, moduleSourceDirs, moduleSourceRoots } from '../build-module-options.mjs'; + +const libDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const trees = []; + +/** A scratch tree with a lib and a crates folder holding the given files. */ +function tree(files) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'tsjs-module-discovery-')); + trees.push(root); + for (const file of files) { + const full = path.join(root, file); + fs.mkdirSync(path.dirname(full), { recursive: true }); + fs.writeFileSync(full, ''); + } + return { root, lib: path.join(root, 'lib'), crates: path.join(root, 'crates') }; +} + +afterEach(() => { + for (const root of trees.splice(0)) { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +describe('moduleSourceDirs', () => { + it('finds modules here and in the crate that supplies them, and nothing else', () => { + const { lib, crates } = tree([ + 'lib/src/integrations/zeta/index.ts', + 'crates/kind/vendor/js/src/alpha/index.ts', + 'crates/kind/vendor/js/src/helpers/shared.ts', + 'crates/kind/carried/js/probe.js', + 'crates/kind/plain/src/lib.rs', + ]); + + const dirs = moduleSourceDirs({ lib, crates }); + + expect([...dirs.keys()]).toEqual(['zeta', 'alpha', 'helpers']); + expect(moduleEntry(dirs.get('alpha'))).toBe(path.join(dirs.get('alpha'), 'index.ts')); + expect(moduleEntry(dirs.get('helpers'))).toBeNull(); + }); + + it('refuses a module name two folders supply', () => { + const { lib, crates } = tree([ + 'lib/src/integrations/alpha/index.ts', + 'crates/kind/vendor/js/src/alpha/index.ts', + ]); + + expect(() => moduleSourceDirs({ lib, crates })).toThrow(/module alpha is in both/); + }); + + it('refuses a module named core, the shared core bundle', () => { + const { lib, crates } = tree(['crates/kind/vendor/js/src/core/index.ts']); + + expect(() => moduleSourceDirs({ lib, crates })).toThrow(/module name core/); + }); + + it('passes over a link whose target is gone', () => { + const { root, lib, crates } = tree(['crates/kind/vendor/js/src/alpha/index.ts']); + const gone = path.join(root, 'gone'); + fs.mkdirSync(gone); + fs.symlinkSync(gone, path.join(crates, 'kind', 'dangling'), 'junction'); + fs.rmSync(gone, { recursive: true }); + + expect([...moduleSourceDirs({ lib, crates }).keys()]).toEqual(['alpha']); + }); + + it('takes a folder named index.ts for no entry', () => { + const { crates } = tree(['crates/kind/vendor/js/src/alpha/index.ts/placeholder']); + + expect(moduleEntry(path.join(crates, 'kind', 'vendor', 'js', 'src', 'alpha'))).toBeNull(); + }); +}); + +describe('the settings editors find for crate scripts', () => { + it('format crate scripts with the same Prettier options as this crate', () => { + const own = JSON.parse(fs.readFileSync(path.join(libDir, '.prettierrc.json'), 'utf8')); + const crates = JSON.parse( + fs.readFileSync(path.join(libDir, '..', '..', '.prettierrc.json'), 'utf8') + ); + + expect(crates.overrides).toEqual([{ files: '*/*/js/{src,test}/**', options: own }]); + }); + + it('extend this crate TypeScript settings', () => { + const crates = JSON.parse(fs.readFileSync(path.join(libDir, '..', '..', 'tsconfig.json'), 'utf8')); + + expect(crates.extends).toBe('./trusted-server-js/lib/tsconfig.json'); + }); +}); + +describe('tsconfig.json', () => { + it('lists every folder of module sources for @tsjs/module, in discovery order', () => { + const tsconfig = JSON.parse(fs.readFileSync(path.join(libDir, 'tsconfig.json'), 'utf8')); + const expected = moduleSourceRoots().map((root) => { + const relative = path.relative(libDir, root).split(path.sep).join('/'); + return `${relative.startsWith('.') ? relative : `./${relative}`}/*`; + }); + + expect(tsconfig.compilerOptions.paths['@tsjs/module/*']).toEqual(expected); + }); +}); diff --git a/crates/trusted-server-js/lib/test/shared/dom_insertion_dispatcher.test.ts b/crates/trusted-server-js/lib/test/shared/dom_insertion_dispatcher.test.ts index 84cf21ae3f..e3db513851 100644 --- a/crates/trusted-server-js/lib/test/shared/dom_insertion_dispatcher.test.ts +++ b/crates/trusted-server-js/lib/test/shared/dom_insertion_dispatcher.test.ts @@ -3,28 +3,29 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { installDataDomeGuard, resetGuardState as resetDataDomeGuardState, -} from '../../src/integrations/datadome/script_guard'; +} from '@tsjs/module/datadome/script_guard'; import { installGptGuard, resetGuardState as resetGptGuardState, -} from '../../src/integrations/gpt/script_guard'; +} from '@tsjs/module/gpt/script_guard'; import { installGtmGuard, resetGuardState as resetGtmGuardState, -} from '../../src/integrations/google_tag_manager/script_guard'; +} from '@tsjs/module/google_tag_manager/script_guard'; import { installLockrGuard, resetGuardState as resetLockrGuardState, -} from '../../src/integrations/lockr/script_guard'; +} from '@tsjs/module/lockr/script_guard'; import { installPermutiveGuard, resetGuardState as resetPermutiveGuardState, -} from '../../src/integrations/permutive/script_guard'; +} from '@tsjs/module/permutive/script_guard'; + +import { createScriptGuard } from '../../src/shared/script_guard'; import { registerDomInsertionHandler, resetDomInsertionDispatcherForTests, } from '../../src/shared/dom_insertion_dispatcher'; -import { createScriptGuard } from '../../src/shared/script_guard'; function resetAllScriptGuards(): void { resetDataDomeGuardState(); diff --git a/crates/trusted-server-js/lib/tsconfig.json b/crates/trusted-server-js/lib/tsconfig.json index b17377a148..8b8d31b28f 100644 --- a/crates/trusted-server-js/lib/tsconfig.json +++ b/crates/trusted-server-js/lib/tsconfig.json @@ -10,7 +10,25 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, - "types": ["vitest/globals", "node"] + "types": ["vitest/globals", "node"], + "paths": { + "@tsjs/core/*": ["./src/core/*"], + "@tsjs/shared/*": ["./src/shared/*"], + "@tsjs/test/*": ["./test/*"], + "@tsjs/module/*": [ + "./src/integrations/*", + "../../ad-tag/google/js/src/*", + "../../audience/permutive/js/src/*", + "../../bot-protection/datadome/js/src/*", + "../../cmp/didomi/js/src/*", + "../../cmp/osano/js/src/*", + "../../cmp/sourcepoint/js/src/*", + "../../identity/lockr/js/src/*", + "../../tag/google-tag-manager/js/src/*", + "../../testing/testlight/js/src/*" + ], + "vitest": ["./node_modules/vitest"] + } }, - "include": ["src", "test"] + "include": ["src", "test", "../../*/*/js"] } diff --git a/crates/trusted-server-js/lib/vitest.config.ts b/crates/trusted-server-js/lib/vitest.config.ts index acb591cdc5..fd0f69b5a1 100644 --- a/crates/trusted-server-js/lib/vitest.config.ts +++ b/crates/trusted-server-js/lib/vitest.config.ts @@ -2,9 +2,20 @@ import path from 'node:path'; import { defineConfig } from 'vitest/config'; +import { moduleAliases, sharedCodeAliases } from './build-module-options.mjs'; + +const cratesDir = path.resolve(__dirname, '..', '..'); + export default defineConfig({ + // Modules keep their scripts and tests in their own crates, outside this + // folder, so the test server may read the whole crates folder. + server: { fs: { allow: [cratesDir] } }, resolve: { alias: { + ...sharedCodeAliases, + ...moduleAliases(), + // Test data more than one module's tests share. + '@tsjs/test': path.resolve(__dirname, 'test'), // prebid.js doesn't expose src/adapterManager.js via its package // "exports" map, but we need it for client-side bidder validation. // Map the specifier to the actual dist file. @@ -19,6 +30,10 @@ export default defineConfig({ }, }, test: { + // A module's tests live in its own crate, in `js/test/`, beside the + // script they test, so the tests are found from the crates folder. + dir: cratesDir, + include: ['trusted-server-js/lib/test/**/*.test.{ts,mjs}', '*/*/js/test/**/*.test.{ts,mjs}'], environment: 'jsdom', globals: true, // Run tests in the main thread to avoid spawning @@ -34,7 +49,7 @@ export default defineConfig({ // pre-existing errors elsewhere. typecheck: { enabled: true, - include: ['test/**/types.test.ts'], + include: ['trusted-server-js/lib/test/**/types.test.ts', '*/*/js/test/**/types.test.ts'], // Errors reported from files outside `include` are pre-existing and // unrelated; only the type assertions in the included files gate here. ignoreSourceErrors: true, diff --git a/crates/tsconfig.json b/crates/tsconfig.json new file mode 100644 index 0000000000..b3a0485556 --- /dev/null +++ b/crates/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "./trusted-server-js/lib/tsconfig.json", + "compilerOptions": { + "typeRoots": ["./trusted-server-js/lib/node_modules/@types", "./trusted-server-js/lib/node_modules"] + }, + "include": ["*/*/js/src", "*/*/js/test"] +} diff --git a/docs/guide/error-reference.md b/docs/guide/error-reference.md index 0487a22772..ba126586d0 100644 --- a/docs/guide/error-reference.md +++ b/docs/guide/error-reference.md @@ -658,7 +658,7 @@ writes. | `tsjs: TSJS_SKIP_BUILD is no longer supported` | `TSJS_SKIP_BUILD=1` no longer skips the build. Unset it and use `TSJS_PREBUILT_DIR` instead. | | `tsjs: node_modules is out of date with package-lock.json` | Dependencies changed, for example after switching branches. Run `npm ci` in `crates/trusted-server-js/lib`. The check compares file times, so a checkout that rewrites an unchanged lockfile also triggers it; `npm ci` clears it. | | `tsjs: npm ci failed` | `node_modules` was missing and the automatic install failed. Run `npm ci` in `crates/trusted-server-js/lib` to see the error. | -| `tsjs: invalid bundle set ... missing bundles` / `empty bundles` | The Node build or the `TSJS_PREBUILT_DIR` directory did not produce one `tsjs-.js` per `core` and `lib/src/integrations//index.ts`. | +| `tsjs: invalid bundle set ... missing bundles` / `empty bundles` | The Node build or the `TSJS_PREBUILT_DIR` directory did not produce one `tsjs-.js` per `core`, `lib/src/integrations//index.ts` and `crates///js/src//index.ts`. | | `tsjs: invalid bundle set ... unexpected bundles` | `TSJS_PREBUILT_DIR` holds bundles from a different source tree. Rebuild it with `npm run build`. | Set `TSJS_TEST=1` to run the TypeScript tests before the build; a failing test diff --git a/docs/guide/integrations/gpt.md b/docs/guide/integrations/gpt.md index a493f8cd81..d4d1eccff9 100644 --- a/docs/guide/integrations/gpt.md +++ b/docs/guide/integrations/gpt.md @@ -252,7 +252,7 @@ treated as invalid for experiment reporting. ## Implementation - **Rust**: [crates/ad-tag/google/src/lib.rs](https://github.com/IABTechLab/trusted-server/blob/main/crates/ad-tag/google/src/lib.rs) -- **TypeScript**: [crates/trusted-server-js/lib/src/integrations/gpt/](https://github.com/IABTechLab/trusted-server/blob/main/crates/trusted-server-js/lib/src/integrations/gpt/) +- **TypeScript**: [crates/ad-tag/google/js/src/gpt/](https://github.com/IABTechLab/trusted-server/blob/main/crates/ad-tag/google/js/src/gpt/) ## Next Steps diff --git a/docs/guide/onboarding.md b/docs/guide/onboarding.md index ba82dc68e2..46925e0b69 100644 --- a/docs/guide/onboarding.md +++ b/docs/guide/onboarding.md @@ -85,7 +85,8 @@ See [Architecture](/guide/architecture) for component boundaries and | `crates/trusted-server-core/src/consent/` | Consent extraction, decoding, and enforcement | | `crates/trusted-server-core/src/integrations/` | Integration registry and the builder a module registers with | | `crates///` | One module each: a page integration or an auction implementation | -| `crates/trusted-server-js/lib/src/` | Browser-side TypeScript and integration modules | +| `crates/trusted-server-js/lib/src/` | Shared browser-side TypeScript, and the modules kept here | +| `crates///js/` | A vendor module's browser script and its tests | | `crates/trusted-server-adapter-*` | Fastly, Axum, Cloudflare, and Spin runtime entry points | | `crates/trusted-server-cli/` | Native `ts` operator CLI | diff --git a/docs/guide/tsjs.md b/docs/guide/tsjs.md index 0c1018f1df..420f0f9584 100644 --- a/docs/guide/tsjs.md +++ b/docs/guide/tsjs.md @@ -2,16 +2,20 @@ `tsjs` is the browser runtime produced by `crates/trusted-server-js/lib`. It supplies shared registry/render behavior -and integration modules selected by the server-side registry. +and integration modules selected by the server-side registry. A vendor +module's script lives in the crate that supplies the module, under +`js/src//`, with its tests under `js/test//`, and the build +finds it there. Prebid's shim and the APS renderer are still under +`lib/src/integrations/`. ## Module and bundle inventory -There are 12 integration source modules: `creative`, `datadome`, `didomi`, -`google_tag_manager`, `gpt`, `gpt_diagnostics`, `lockr`, `osano`, -`permutive`, `prebid`, `sourcepoint`, and `testlight`. The build emits -13 named bundles because it also emits `core`. +There are 13 integration source modules: `creative`, `datadome`, `didomi`, +`ec_client_fixed`, `google_tag_manager`, `gpt`, `gpt_diagnostics`, `lockr`, +`osano`, `permutive`, `prebid`, `sourcepoint`, and `testlight`. The build +emits 14 named bundles because it also emits `core`. -Those counts do not mean a page downloads 13 scripts. The server computes one +Those counts do not mean a page downloads 14 scripts. The server computes one content-addressed unified bundle from `core` plus the immediate modules of the integrations the settings select. Its URL includes the exact module-set hash, so different selections do not share incorrect browser or edge-cache bytes. From b89557183a4c8846ae5447a6a7af511c076d774d Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 19 Oct 2026 23:14:23 +0100 Subject: [PATCH 3/8] Move the Tinybird project into its crate The Tinybird project the analytics module sends to, its datasources, pipes, fixtures and tests, moves from tinybird/ at the repository root to crates/analytics/tinybird/project/, beside the module's Rust. Its README's links to the telemetry and configuration guides follow the move, the crate's README points at the project, the telemetry and configuration guides' links follow it, the Markdown format list in format.yml and AGENTS.md drops tinybird/**/*.md, which crates/**/*.md covers, and the changelog gives the new folder, because an operator runs tb from it. --- .github/workflows/format.yml | 2 +- AGENTS.md | 2 +- CHANGELOG.md | 1 + crates/analytics/tinybird/README.md | 3 +++ {tinybird => crates/analytics/tinybird/project}/README.md | 4 ++-- .../tinybird/project}/datasources/access_logs_raw.datasource | 0 .../project}/datasources/auction_bid_stats_rollup.datasource | 0 .../project}/datasources/auction_events_raw.datasource | 0 .../project}/datasources/auction_overview_rollup.datasource | 0 .../datasources/auction_provider_stats_rollup.datasource | 0 .../tinybird/project}/fixtures/auction_events_raw.ndjson | 0 .../tinybird/project}/pipes/auction_bid_stats_mv.pipe | 0 .../tinybird/project}/pipes/auction_overview_mv.pipe | 0 .../tinybird/project}/pipes/auction_provider_stats_mv.pipe | 0 .../analytics/tinybird/project}/pipes/auction_summary.pipe | 0 .../tinybird/project}/pipes/ingestion_freshness.pipe | 0 .../analytics/tinybird/project}/pipes/provider_health.pipe | 0 .../analytics/tinybird/project}/pipes/provider_latency.pipe | 0 .../analytics/tinybird/project}/pipes/quarantine_counts.pipe | 0 .../analytics/tinybird/project}/pipes/seat_yield.pipe | 0 .../analytics/tinybird/project}/tests/auction_summary.yaml | 0 .../analytics/tinybird/project}/tests/provider_health.yaml | 0 .../analytics/tinybird/project}/tests/seat_yield.yaml | 0 docs/guide/configuration.md | 2 +- docs/guide/telemetry.md | 2 +- 25 files changed, 10 insertions(+), 6 deletions(-) rename {tinybird => crates/analytics/tinybird/project}/README.md (96%) rename {tinybird => crates/analytics/tinybird/project}/datasources/access_logs_raw.datasource (100%) rename {tinybird => crates/analytics/tinybird/project}/datasources/auction_bid_stats_rollup.datasource (100%) rename {tinybird => crates/analytics/tinybird/project}/datasources/auction_events_raw.datasource (100%) rename {tinybird => crates/analytics/tinybird/project}/datasources/auction_overview_rollup.datasource (100%) rename {tinybird => crates/analytics/tinybird/project}/datasources/auction_provider_stats_rollup.datasource (100%) rename {tinybird => crates/analytics/tinybird/project}/fixtures/auction_events_raw.ndjson (100%) rename {tinybird => crates/analytics/tinybird/project}/pipes/auction_bid_stats_mv.pipe (100%) rename {tinybird => crates/analytics/tinybird/project}/pipes/auction_overview_mv.pipe (100%) rename {tinybird => crates/analytics/tinybird/project}/pipes/auction_provider_stats_mv.pipe (100%) rename {tinybird => crates/analytics/tinybird/project}/pipes/auction_summary.pipe (100%) rename {tinybird => crates/analytics/tinybird/project}/pipes/ingestion_freshness.pipe (100%) rename {tinybird => crates/analytics/tinybird/project}/pipes/provider_health.pipe (100%) rename {tinybird => crates/analytics/tinybird/project}/pipes/provider_latency.pipe (100%) rename {tinybird => crates/analytics/tinybird/project}/pipes/quarantine_counts.pipe (100%) rename {tinybird => crates/analytics/tinybird/project}/pipes/seat_yield.pipe (100%) rename {tinybird => crates/analytics/tinybird/project}/tests/auction_summary.yaml (100%) rename {tinybird => crates/analytics/tinybird/project}/tests/provider_health.yaml (100%) rename {tinybird => crates/analytics/tinybird/project}/tests/seat_yield.yaml (100%) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index a8afcdf05d..e04d477978 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -180,7 +180,7 @@ jobs: working-directory: . run: >- docs/node_modules/.bin/prettier --config docs/.prettierrc --check - "*.md" ".claude/**/*.md" ".github/**/*.md" "crates/**/*.md" "scripts/**/*.md" "tinybird/**/*.md" + "*.md" ".claude/**/*.md" ".github/**/*.md" "crates/**/*.md" "scripts/**/*.md" - name: Build with VitePress (fails on dead links) run: npm run build diff --git a/AGENTS.md b/AGENTS.md index 6d49a7e114..1cb0896acd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -482,7 +482,7 @@ Every PR must pass: 5. JS build and test (`cd crates/trusted-server-js/lib && npx vitest run`) 6. JS format (`cd crates/trusted-server-js/lib && npm run format`) 7. Docs format (`cd docs && npm run format`) -8. Markdown format outside `docs/` (requires `cd docs && npm ci` first): `docs/node_modules/.bin/prettier --config docs/.prettierrc --check "*.md" ".claude/**/*.md" ".github/**/*.md" "crates/**/*.md" "scripts/**/*.md" "tinybird/**/*.md"`; fix with `--write` in place of `--check` +8. Markdown format outside `docs/` (requires `cd docs && npm ci` first): `docs/node_modules/.bin/prettier --config docs/.prettierrc --check "*.md" ".claude/**/*.md" ".github/**/*.md" "crates/**/*.md" "scripts/**/*.md"`; fix with `--write` in place of `--check` 9. Native core build-digest test and lint (`cargo test -p trusted-server-core --test template_build_digest` and `cargo clippy -p trusted-server-core --test template_build_digest -- -D warnings`) 10. Wasm clock lint: `scripts/lint-wasm-clock.sh` (crates built into the Cloudflare Worker must use `web_time`, not the `std::time::{Instant,SystemTime}` `now`/`elapsed` methods, which panic there) 11. Documentation proposal scripts: `shellcheck scripts/docs-proposal/*.sh && node --test scripts/docs-proposal/hunks.test.mjs && scripts/docs-proposal/test.sh` diff --git a/CHANGELOG.md b/CHANGELOG.md index cf943aef29..37560cd725 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- The Tinybird project, its datasources, pipes, fixtures and tests, moves from `tinybird/` to `crates/analytics/tinybird/project/`, beside the analytics module's Rust, so run `tb` from the new folder. - A vendor module's browser script and its tests live in the crate that supplies the module, under `js/src//` and `js/test//`, where they were under `crates/trusted-server-js/lib/src/integrations/` and `lib/test/integrations/`, for DataDome, Didomi, Google Tag Manager, GPT, GPT diagnostics, Lockr, Osano, Permutive, Sourcepoint and Testlight. The built bundles are unchanged. A script in a crate imports the shared browser code as `@tsjs/core/...` and `@tsjs/shared/...`, and another module's code as `@tsjs/module//...`. - The Axum, Cloudflare and Spin adapters serve `[[proxy.asset_routes]]`, where only the Fastly adapter did, so a `GET` or `HEAD` under a route's prefix is answered from the route's source, with its signing and the answers held for it, and no longer by the publisher's origin. The response skips the Edge Cookie steps, so it neither writes nor expires the Edge Cookie, and takes the route's cache policy after the finalize headers. Those adapters hold the asset body whole, up to 10 MiB on Cloudflare and 8 MiB on Spin. Their HTTP clients refuse Image Optimizer metadata, which the Axum client now does on both of its send paths, so there a route that enables the Image Optimizer answers each request it would optimize with `502` and `no-store, private`, after a signed route has asked its origin for the object with a `HEAD`, and serves its SVG paths and requests that set the debug parameter unoptimized. Each adapter logs a warning naming every such route, once for each instance of the adapter, which on Axum is the process. - **Breaking:** on Spin, a request to a source whose `host_header` names a host other than the one in its `url` answers `502` before its origin is asked, the publisher's `[source.origin]` as well as an asset route's source, because Spin sends the host in `url` as the `Host`. Before, the request reached its origin with the host in `url` in place of `host_header`. A Spin deployment whose publisher source sets such a `host_header` answers every page with `502`, robots.txt included. Before upgrading, check that no source of a Spin deployment sets a `host_header` naming another host, or remove it once its origin answers requests that carry the host in `url` as the `Host`. diff --git a/crates/analytics/tinybird/README.md b/crates/analytics/tinybird/README.md index d3d91f98cf..fe551f64ae 100644 --- a/crates/analytics/tinybird/README.md +++ b/crates/analytics/tinybird/README.md @@ -23,6 +23,9 @@ modules it links. The settings, what each one accepts and what is refused are in the configuration guide under "Analytics Configuration", and the data path in the telemetry guide. +The Tinybird project the module sends to, its datasources, pipes, fixtures +and tests, is in [`project/`](project/README.md). + Build and test from the repository root: ```bash diff --git a/tinybird/README.md b/crates/analytics/tinybird/project/README.md similarity index 96% rename from tinybird/README.md rename to crates/analytics/tinybird/project/README.md index 57571e3172..8013f9100f 100644 --- a/tinybird/README.md +++ b/crates/analytics/tinybird/project/README.md @@ -24,8 +24,8 @@ regional Events API host configured by `analytics.tinybird.api_host`, when `[analytics] module = "tinybird"` selects the module. For runtime behavior, limits, privacy properties, and the Fastly-only emission -boundary, see [Auction Telemetry](../docs/guide/telemetry.md). For exact -configuration fields, see [Configuration](../docs/guide/configuration.md). +boundary, see [Auction Telemetry](../../../../docs/guide/telemetry.md). For exact +configuration fields, see [Configuration](../../../../docs/guide/configuration.md). ## Deploy ordering diff --git a/tinybird/datasources/access_logs_raw.datasource b/crates/analytics/tinybird/project/datasources/access_logs_raw.datasource similarity index 100% rename from tinybird/datasources/access_logs_raw.datasource rename to crates/analytics/tinybird/project/datasources/access_logs_raw.datasource diff --git a/tinybird/datasources/auction_bid_stats_rollup.datasource b/crates/analytics/tinybird/project/datasources/auction_bid_stats_rollup.datasource similarity index 100% rename from tinybird/datasources/auction_bid_stats_rollup.datasource rename to crates/analytics/tinybird/project/datasources/auction_bid_stats_rollup.datasource diff --git a/tinybird/datasources/auction_events_raw.datasource b/crates/analytics/tinybird/project/datasources/auction_events_raw.datasource similarity index 100% rename from tinybird/datasources/auction_events_raw.datasource rename to crates/analytics/tinybird/project/datasources/auction_events_raw.datasource diff --git a/tinybird/datasources/auction_overview_rollup.datasource b/crates/analytics/tinybird/project/datasources/auction_overview_rollup.datasource similarity index 100% rename from tinybird/datasources/auction_overview_rollup.datasource rename to crates/analytics/tinybird/project/datasources/auction_overview_rollup.datasource diff --git a/tinybird/datasources/auction_provider_stats_rollup.datasource b/crates/analytics/tinybird/project/datasources/auction_provider_stats_rollup.datasource similarity index 100% rename from tinybird/datasources/auction_provider_stats_rollup.datasource rename to crates/analytics/tinybird/project/datasources/auction_provider_stats_rollup.datasource diff --git a/tinybird/fixtures/auction_events_raw.ndjson b/crates/analytics/tinybird/project/fixtures/auction_events_raw.ndjson similarity index 100% rename from tinybird/fixtures/auction_events_raw.ndjson rename to crates/analytics/tinybird/project/fixtures/auction_events_raw.ndjson diff --git a/tinybird/pipes/auction_bid_stats_mv.pipe b/crates/analytics/tinybird/project/pipes/auction_bid_stats_mv.pipe similarity index 100% rename from tinybird/pipes/auction_bid_stats_mv.pipe rename to crates/analytics/tinybird/project/pipes/auction_bid_stats_mv.pipe diff --git a/tinybird/pipes/auction_overview_mv.pipe b/crates/analytics/tinybird/project/pipes/auction_overview_mv.pipe similarity index 100% rename from tinybird/pipes/auction_overview_mv.pipe rename to crates/analytics/tinybird/project/pipes/auction_overview_mv.pipe diff --git a/tinybird/pipes/auction_provider_stats_mv.pipe b/crates/analytics/tinybird/project/pipes/auction_provider_stats_mv.pipe similarity index 100% rename from tinybird/pipes/auction_provider_stats_mv.pipe rename to crates/analytics/tinybird/project/pipes/auction_provider_stats_mv.pipe diff --git a/tinybird/pipes/auction_summary.pipe b/crates/analytics/tinybird/project/pipes/auction_summary.pipe similarity index 100% rename from tinybird/pipes/auction_summary.pipe rename to crates/analytics/tinybird/project/pipes/auction_summary.pipe diff --git a/tinybird/pipes/ingestion_freshness.pipe b/crates/analytics/tinybird/project/pipes/ingestion_freshness.pipe similarity index 100% rename from tinybird/pipes/ingestion_freshness.pipe rename to crates/analytics/tinybird/project/pipes/ingestion_freshness.pipe diff --git a/tinybird/pipes/provider_health.pipe b/crates/analytics/tinybird/project/pipes/provider_health.pipe similarity index 100% rename from tinybird/pipes/provider_health.pipe rename to crates/analytics/tinybird/project/pipes/provider_health.pipe diff --git a/tinybird/pipes/provider_latency.pipe b/crates/analytics/tinybird/project/pipes/provider_latency.pipe similarity index 100% rename from tinybird/pipes/provider_latency.pipe rename to crates/analytics/tinybird/project/pipes/provider_latency.pipe diff --git a/tinybird/pipes/quarantine_counts.pipe b/crates/analytics/tinybird/project/pipes/quarantine_counts.pipe similarity index 100% rename from tinybird/pipes/quarantine_counts.pipe rename to crates/analytics/tinybird/project/pipes/quarantine_counts.pipe diff --git a/tinybird/pipes/seat_yield.pipe b/crates/analytics/tinybird/project/pipes/seat_yield.pipe similarity index 100% rename from tinybird/pipes/seat_yield.pipe rename to crates/analytics/tinybird/project/pipes/seat_yield.pipe diff --git a/tinybird/tests/auction_summary.yaml b/crates/analytics/tinybird/project/tests/auction_summary.yaml similarity index 100% rename from tinybird/tests/auction_summary.yaml rename to crates/analytics/tinybird/project/tests/auction_summary.yaml diff --git a/tinybird/tests/provider_health.yaml b/crates/analytics/tinybird/project/tests/provider_health.yaml similarity index 100% rename from tinybird/tests/provider_health.yaml rename to crates/analytics/tinybird/project/tests/provider_health.yaml diff --git a/tinybird/tests/seat_yield.yaml b/crates/analytics/tinybird/project/tests/seat_yield.yaml similarity index 100% rename from tinybird/tests/seat_yield.yaml rename to crates/analytics/tinybird/project/tests/seat_yield.yaml diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 332ff8518d..4936c80fd6 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -3674,7 +3674,7 @@ saying nothing about this setting. 5. Watch the `origin_cache_shareable` breakdown in publisher summary telemetry. Its denominator is matching-slot candidates, including skipped auctions; it does not measure every publisher origin fetch or a site-wide admission rate. - See the [telemetry population and query](https://github.com/IABTechLab/trusted-server/blob/main/tinybird/README.md#the-denominator-is-matching-slot-candidates-not-all-requests). + See the [telemetry population and query](https://github.com/IABTechLab/trusted-server/blob/main/crates/analytics/tinybird/project/README.md#the-denominator-is-matching-slot-candidates-not-all-requests). The predicate estimates eligibility in that population before or after enablement, not actual cache hits. 6. Confirm the origin's own hit rate and page correctness before widening to more diff --git a/docs/guide/telemetry.md b/docs/guide/telemetry.md index 04d8c251a8..23151fb583 100644 --- a/docs/guide/telemetry.md +++ b/docs/guide/telemetry.md @@ -69,4 +69,4 @@ enable access telemetry. The exact settings and secret-handling dispositions are in [Configuration](/guide/configuration#analytics-configuration). The tracked -Tinybird project is described in [`tinybird/README.md`](https://github.com/IABTechLab/trusted-server/blob/main/tinybird/README.md). +Tinybird project is described in [`crates/analytics/tinybird/project/README.md`](https://github.com/IABTechLab/trusted-server/blob/main/crates/analytics/tinybird/project/README.md). From 0e0d3aa72b41eecfe568a484d908eac6d90a208a Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 19 Oct 2026 23:48:51 +0100 Subject: [PATCH 4/8] Apply the second review's corrections Rust discovery passes over a module root that is a file, as the JavaScript side does. crates/tsconfig.json takes in the shared core's global.d.ts, so an editor checks a crate script as lib's TypeScript run does, and build.rs watches it, because Vite reads it when it compiles a crate script. The format check covers .mjs files and the configuration files in crates/, which crates/.prettierrc.json formats with lib's options. The design and the README say how ESLint finds the crates config, how modules kept in lib import each other, and that a Tinybird change starts the documentation proposal workflow, and the design's review log records round 2. --- crates/.prettierrc.json | 2 +- crates/trusted-server-js/README.md | 12 ++++++------ crates/trusted-server-js/build.rs | 3 +++ crates/trusted-server-js/build/bundle_set.rs | 6 +++--- crates/trusted-server-js/lib/package.json | 4 ++-- .../lib/test/module-discovery.test.mjs | 11 +++++++++-- crates/tsconfig.json | 7 +++++-- .../2026-10-19-module-scripts-in-crates-design.md | 8 +++++--- 8 files changed, 34 insertions(+), 19 deletions(-) diff --git a/crates/.prettierrc.json b/crates/.prettierrc.json index 69e9a00915..e7ed7b7d6d 100644 --- a/crates/.prettierrc.json +++ b/crates/.prettierrc.json @@ -1,7 +1,7 @@ { "overrides": [ { - "files": "*/*/js/{src,test}/**", + "files": ["*/*/js/{src,test}/**", ".prettierrc.json", "eslint.config.js", "tsconfig.json"], "options": { "semi": true, "singleQuote": true, diff --git a/crates/trusted-server-js/README.md b/crates/trusted-server-js/README.md index db9623241c..2c6ad84451 100644 --- a/crates/trusted-server-js/README.md +++ b/crates/trusted-server-js/README.md @@ -15,12 +15,12 @@ Prebid's shim and the APS renderer, which are still under import an npm package, because packages resolve only from this crate's `node_modules`. `tsconfig.json` lists each crate's `js/src` folder for `@tsjs/module`, and a test checks the list. The `crates` folder holds a -`tsconfig.json`, an ESLint config and a Prettier config that apply this -crate's settings to crate scripts in an editor. Cargo reruns the build when a -watched script folder changes, and a crate's first `js/src` folder is watched -from the next build that runs for another reason, so touch `lib/package.json` -after adding one. Cargo builds the -bundles into a private directory under its `OUT_DIR` and fails if any expected +`tsconfig.json` and a Prettier config that editors find for crate scripts, +and an ESLint config that `npm run lint` uses from the crates folder. Cargo +reruns the build when a watched script folder changes, and a crate's first +`js/src` folder is watched from the next build that runs for another reason, +so touch `lib/package.json` after adding one. Cargo builds the bundles into a +private directory under its `OUT_DIR` and fails if any expected bundle is missing or empty; the `dist` directory is written only by a manual `npm run build` (for browser tests) and is never embedded unless you pass it explicitly through `TSJS_PREBUILT_DIR`. The external Prebid.js artifact is diff --git a/crates/trusted-server-js/build.rs b/crates/trusted-server-js/build.rs index ff82bd5bc5..25cd8e3f98 100644 --- a/crates/trusted-server-js/build.rs +++ b/crates/trusted-server-js/build.rs @@ -33,6 +33,9 @@ fn main() { "lib/src", "lib/build-all.mjs", "lib/build-module-options.mjs", + // Vite reads the nearest tsconfig.json when it compiles a script, and + // for a module script in another crate that is this one. + "../tsconfig.json", "lib/build-prebid-external.mjs", "lib/package.json", "lib/package-lock.json", diff --git a/crates/trusted-server-js/build/bundle_set.rs b/crates/trusted-server-js/build/bundle_set.rs index 587e97333d..ffa0e2ae0d 100644 --- a/crates/trusted-server-js/build/bundle_set.rs +++ b/crates/trusted-server-js/build/bundle_set.rs @@ -75,13 +75,13 @@ pub(crate) fn expected_module_ids( Ok(ids) } -/// The folders directly inside `dir`, or none when it does not exist. +/// The folders directly inside `dir`, or none when it is not a folder. /// /// # Errors /// -/// Returns a message when `dir` exists but cannot be read. +/// Returns a message when `dir` is a folder but cannot be read. pub(crate) fn folders_in(dir: &Path) -> Result, String> { - if !dir.exists() { + if !dir.is_dir() { return Ok(Vec::new()); } let entries = fs::read_dir(dir) diff --git a/crates/trusted-server-js/lib/package.json b/crates/trusted-server-js/lib/package.json index 664d33cdb5..8d9c9ce202 100644 --- a/crates/trusted-server-js/lib/package.json +++ b/crates/trusted-server-js/lib/package.json @@ -13,8 +13,8 @@ "lint": "eslint . --max-warnings=0 && npm run lint:crates", "lint:crates": "cd ../.. && eslint --max-warnings=0 \"*/*/js/{src,test}/**/*.{ts,tsx,js,mjs}\"", "lint:fix": "eslint --fix . --max-warnings=0 && cd ../.. && eslint --fix --max-warnings=0 \"*/*/js/{src,test}/**/*.{ts,tsx,js,mjs}\"", - "format": "prettier --check \"**/*.{ts,tsx,js,json,css,md}\" && prettier --check \"../../*/*/js/{src,test}/**/*.{ts,tsx,js,json,css,md}\"", - "format:write": "prettier --write \"**/*.{ts,tsx,js,json,css,md}\" && prettier --write \"../../*/*/js/{src,test}/**/*.{ts,tsx,js,json,css,md}\"" + "format": "prettier --check \"**/*.{ts,tsx,js,mjs,json,css,md}\" && prettier --check \"../../*/*/js/{src,test}/**/*.{ts,tsx,js,mjs,json,css,md}\" \"../../{.prettierrc.json,eslint.config.js,tsconfig.json}\"", + "format:write": "prettier --write \"**/*.{ts,tsx,js,mjs,json,css,md}\" && prettier --write \"../../*/*/js/{src,test}/**/*.{ts,tsx,js,mjs,json,css,md}\" \"../../{.prettierrc.json,eslint.config.js,tsconfig.json}\"" }, "dependencies": { "prebid.js": "^10.26.0" diff --git a/crates/trusted-server-js/lib/test/module-discovery.test.mjs b/crates/trusted-server-js/lib/test/module-discovery.test.mjs index 8d74a6a3ee..fd775c5e97 100644 --- a/crates/trusted-server-js/lib/test/module-discovery.test.mjs +++ b/crates/trusted-server-js/lib/test/module-discovery.test.mjs @@ -86,11 +86,18 @@ describe('the settings editors find for crate scripts', () => { fs.readFileSync(path.join(libDir, '..', '..', '.prettierrc.json'), 'utf8') ); - expect(crates.overrides).toEqual([{ files: '*/*/js/{src,test}/**', options: own }]); + expect(crates.overrides).toEqual([ + { + files: ['*/*/js/{src,test}/**', '.prettierrc.json', 'eslint.config.js', 'tsconfig.json'], + options: own, + }, + ]); }); it('extend this crate TypeScript settings', () => { - const crates = JSON.parse(fs.readFileSync(path.join(libDir, '..', '..', 'tsconfig.json'), 'utf8')); + const crates = JSON.parse( + fs.readFileSync(path.join(libDir, '..', '..', 'tsconfig.json'), 'utf8') + ); expect(crates.extends).toBe('./trusted-server-js/lib/tsconfig.json'); }); diff --git a/crates/tsconfig.json b/crates/tsconfig.json index b3a0485556..c1a3bdb09e 100644 --- a/crates/tsconfig.json +++ b/crates/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "./trusted-server-js/lib/tsconfig.json", "compilerOptions": { - "typeRoots": ["./trusted-server-js/lib/node_modules/@types", "./trusted-server-js/lib/node_modules"] + "typeRoots": [ + "./trusted-server-js/lib/node_modules/@types", + "./trusted-server-js/lib/node_modules" + ] }, - "include": ["*/*/js/src", "*/*/js/test"] + "include": ["*/*/js/src", "*/*/js/test", "trusted-server-js/lib/src/core/global.d.ts"] } diff --git a/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md b/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md index 08a95d5c39..5a47e73cbf 100644 --- a/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md +++ b/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md @@ -59,7 +59,7 @@ So a folder named `js/` holds either module scripts, in `js/src` and `js/test`, `lib/build-module-options.mjs` defines `@tsjs/core`, `@tsjs/shared` and `@tsjs/module/` for the build and for Vitest, Vitest's config adds `@tsjs/test`, and `tsconfig.json` maps the same names for TypeScript. - `@tsjs/core/...` and `@tsjs/shared/...` reach the shared browser code from a script in any crate. -- `@tsjs/module//...` reaches another module's code by the module's name rather than by the folder it lives in. The GPT script uses it for the APS renderer, and the dispatcher and Prebid tests use it for the vendor scripts they exercise, so an import of one module's code by another can be found by searching for `@tsjs/module/`. The IIFE state test finds the Permutive bundle's entry through `moduleSourceDirs()` instead. +- `@tsjs/module//...` reaches another module's code by the module's name rather than by the folder it lives in. The GPT script uses it for the APS renderer, and the dispatcher and Prebid tests use it for the vendor scripts they exercise, so an import of a crate script's code by another module can be found by searching for `@tsjs/module/`. Modules kept in `lib` still import each other by relative path, as the Prebid shim imports the APS renderer, and the IIFE state test finds the Permutive bundle's entry through `moduleSourceDirs()`. - `@tsjs/test/...` reaches test data that more than one module's tests share, which is `test/fixtures/aps-renderer-v1.json`, used by the core, APS, GPT and Prebid tests. TypeScript's `paths` cannot match a folder by pattern, so `tsconfig.json` lists each crate's `js/src` folder under `@tsjs/module/*`, and a test checks that the list matches what `moduleSourceRoots()` finds, in the same order. A test imports its own module's script by a short relative path within the crate, for example `../../src/lockr/script_guard`. @@ -70,7 +70,7 @@ A script in a crate cannot import an npm package. Vite and TypeScript look for a Vitest's `test.dir` is the crates folder, with `trusted-server-js/lib/test/**/*.test.{ts,mjs}` and `*/*/js/test/**/*.test.{ts,mjs}` as the test files, and `server.fs.allow` lets it read the crates folder. The type gate, which checks the GPT diagnostics export contract with `expectTypeOf`, covers `*/*/js/test/**/types.test.ts`, because that test moved with the GPT diagnostics script. `lib/tsconfig.json` includes `../../*/*/js`, maps the `@tsjs/...` names, and maps `vitest` into this crate's `node_modules`, which a test in another crate cannot find by walking up from its own folder. -ESLint's flat config ignores files outside the folder that holds its config, and editors look for the ESLint, Prettier and TypeScript settings by walking up from the file they open. So the crates folder holds three small files. `crates/eslint.config.js` applies `lib/eslint.config.js` to `*/*/js/src` and `*/*/js/test` and ignores everything else under the crates folder. `crates/.prettierrc.json` gives files under those folders `lib/.prettierrc.json`'s options and sets nothing for any other file, and a test checks the options are the same. `crates/tsconfig.json` extends `lib/tsconfig.json` for those folders. +ESLint's flat config ignores files outside the folder it runs from, and Prettier and TypeScript look for their settings by walking up from the file they read, so the crates folder holds three small files. `crates/eslint.config.js` applies `lib/eslint.config.js` to `*/*/js/src` and `*/*/js/test` and ignores everything else under the crates folder. ESLint 9 finds it when it runs from the crates folder, as `npm run lint` runs it, and an editor uses it only when it runs ESLint from there with `lib`'s copy of ESLint. `crates/.prettierrc.json` gives files under those folders, and the three files themselves, `lib/.prettierrc.json`'s options and sets nothing for any other file, and a test checks the options are the same. `crates/tsconfig.json` extends `lib/tsconfig.json` for those folders and the shared core's `global.d.ts`, so an editor checks a crate script as `lib`'s TypeScript run does. Vite also reads it when it compiles a crate script, so `build.rs` watches it. `npm run lint` lints `lib` as before and then runs ESLint from the crates folder over `*/*/js/{src,test}/**/*.{ts,tsx,js,mjs}`, and `npm run format` checks those folders, each without `--config`, so the command line and an editor apply the same settings. The ESLint config treats `@tsjs/` imports as internal, orders the import groups built-in, external, internal, parent, sibling, index, and gives a crate's `js/test` `.mjs` scripts Node's globals as it gives `lib`'s. The CI jobs still run `npm run lint`, `npm run format`, `npm run build` and `npm test` in `crates/trusted-server-js/lib`, so no workflow changes for the scripts. @@ -78,7 +78,7 @@ The lint and format checks did not cover inline page scripts or carried scripts ### 2.5 The Tinybird project -`tinybird/` moves to `crates/analytics/tinybird/project/`. Its README's links to the telemetry and configuration guides follow the move, the crate's README points at the project, the telemetry and configuration guides' links follow it, the Markdown format list in `.github/workflows/format.yml` and `AGENTS.md` drops `tinybird/**/*.md`, which `crates/**/*.md` now covers, and the changelog says where the project now is, because an operator runs `tb` from that folder. +`tinybird/` moves to `crates/analytics/tinybird/project/`. Its README's links to the telemetry and configuration guides follow the move, the crate's README points at the project, the telemetry and configuration guides' links follow it, the Markdown format list in `.github/workflows/format.yml` and `AGENTS.md` drops `tinybird/**/*.md`, which `crates/**/*.md` now covers, and the changelog says where the project now is, because an operator runs `tb` from that folder. Because the project is now under `crates/`, a merge that changes only the project starts the documentation proposal workflow, `.github/workflows/docs-proposal.yml`, which runs on changes under `crates/**`. ### 2.6 Text that named the old places @@ -109,7 +109,9 @@ The `tsjs` guide said there are 12 integration source modules and 13 bundles. Th | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 2026-10-19 | First draft. | | 2026-10-19 | Fixes from review round 1. TypeScript's `@tsjs/module/*` list and its test, the configuration guide's Tinybird link, the editor settings in the crates folder and their test, the npm package rule, `build.rs` watching `build-module-options.mjs`, discovery refusing `core` and passing over broken links on both sides with JavaScript tests, no unmatched-pattern pass for ESLint, `.mjs` crate tests, the three kinds of browser code a crate can hold, the changelog, `AGENTS.md` and the onboarding guide, and wording throughout. | +| 2026-10-19 | Corrections from review round 2. The `@tsjs/module` search claim, `crates/tsconfig.json` taking in the shared `global.d.ts`, how ESLint finds `crates/eslint.config.js`, `build.rs` watching `crates/tsconfig.json`, the documentation proposal workflow a Tinybird change starts, and Rust discovery passing over a module root that is a file. | ## Review log 1. **Round 1, 2026-10-19.** A review in a fresh session, against `fixpass11/55-scripts-in-crates` as first committed at `7dc1671fe`, rewritten before it was pushed. Issues found, two of them blocking. TypeScript had no mapping for `@tsjs/module/*`, so the GPT script was no longer type-checked against the APS renderer while this design said TypeScript used the names, and the configuration guide still linked the old Tinybird README. Of the others, editors found no settings for crate scripts, a script in a crate cannot import an npm package and the design did not say so, `build.rs` did not watch `build-module-options.mjs`, the crate lint run passed an empty match, the JavaScript and Rust discovery differed on broken links, an `index.ts` folder and a module named `core`, crate `.mjs` tests were neither run nor given Node's globals, and there was no changelog entry. This revision fixes each of them. +2. **Round 2, 2026-10-19.** A lighter review in a fresh session, against this page at `b89557183`, of the round 1 fixes and the new text. No blocking issues. One round 1 item was only partly fixed, the claim that every import of one module by another uses `@tsjs/module/`, which the Prebid shim's relative import of the APS renderer contradicted. It also found that `crates/tsconfig.json` left out the shared `global.d.ts`, that ESLint 9 finds its config from the folder it runs in rather than from the file, that the build reads `crates/tsconfig.json` without `build.rs` watching it, that Rust discovery failed on a module root that is a file where JavaScript passed over it, that the new configuration files and `.mjs` test were outside the format check, and that a Tinybird change now starts the documentation proposal workflow. This revision fixes each of them, and the design joins the specs pull request when the row is pushed. From b08a8ff68917f5b3b4abe550cde803d16138f2f7 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Tue, 20 Oct 2026 07:18:03 +0100 Subject: [PATCH 5/8] Name the design's pull request The module scripts design names IABTechLab/trusted-server#1423, the draft pull request that carries it, in its status and related pull requests, as the stack's other designs do. --- .../specs/2026-10-19-module-scripts-in-crates-design.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md b/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md index 5a47e73cbf..dfa0793cc8 100644 --- a/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md +++ b/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md @@ -1,11 +1,11 @@ # Design Spec: A module's browser script lives in the crate that supplies it -**Status:** Proposed, 2026-10-19. Implemented as row 55 of the stack, above [IABTechLab/trusted-server#1412](https://github.com/IABTechLab/trusted-server/pull/1412), which serves asset routes on every adapter, for Tech Lab review. [IABTechLab/trusted-server#1342](https://github.com/IABTechLab/trusted-server/pull/1342) carries every design document of the stack in one place. +**Status:** Proposed, 2026-10-19. Implemented as the draft pull request [IABTechLab/trusted-server#1423](https://github.com/IABTechLab/trusted-server/pull/1423), row 55 of the stack, above [IABTechLab/trusted-server#1412](https://github.com/IABTechLab/trusted-server/pull/1412), which serves asset routes on every adapter, for Tech Lab review. [IABTechLab/trusted-server#1342](https://github.com/IABTechLab/trusted-server/pull/1342) carries every design document of the stack in one place. **Author:** 51Degrees (contributed), for Tech Lab review **Related specs:** [integration-provider-seam](https://github.com/IABTechLab/trusted-server/blob/feature/6-integration-seam/docs/superpowers/specs/2026-08-27-integration-provider-seam-design.md), [page-middleware](https://github.com/IABTechLab/trusted-server/blob/feature/6-integration-seam/docs/superpowers/specs/2026-10-12-page-middleware-design.md), [no-modules-crate](https://github.com/IABTechLab/trusted-server/blob/feature/6-integration-seam/docs/superpowers/specs/2026-10-14-no-modules-crate-design.md), and the design documents of the stack in IABTechLab/trusted-server#1342 -**Related PRs:** [IABTechLab/trusted-server#1348](https://github.com/IABTechLab/trusted-server/pull/1348), which moves every vendor module's Rust out of core into `crates///` +**Related PRs:** [IABTechLab/trusted-server#1423](https://github.com/IABTechLab/trusted-server/pull/1423). [IABTechLab/trusted-server#1348](https://github.com/IABTechLab/trusted-server/pull/1348), which moves every vendor module's Rust out of core into `crates///` **Issues:** none -**Last updated:** 2026-10-19 +**Last updated:** 2026-10-20 > **Why this spec exists.** Each vendor module's Rust lives in its own crate, `crates///`, but its browser script, the script's tests and some of their test data live in `crates/trusted-server-js/lib/src/integrations//` and `lib/test/integrations//`, a folder that belongs to no vendor. Whoever maintains a module works in two places, and the shared browser crate holds every vendor's code. This change moves ten modules' scripts and tests into the nine crates that supply them, makes the build, the tests, the lint and the format checks, and editors, find them there, and moves the Tinybird project into the Tinybird crate. The 14 built bundles are byte for byte the same as before. @@ -110,6 +110,7 @@ The `tsjs` guide said there are 12 integration source modules and 13 bundles. Th | 2026-10-19 | First draft. | | 2026-10-19 | Fixes from review round 1. TypeScript's `@tsjs/module/*` list and its test, the configuration guide's Tinybird link, the editor settings in the crates folder and their test, the npm package rule, `build.rs` watching `build-module-options.mjs`, discovery refusing `core` and passing over broken links on both sides with JavaScript tests, no unmatched-pattern pass for ESLint, `.mjs` crate tests, the three kinds of browser code a crate can hold, the changelog, `AGENTS.md` and the onboarding guide, and wording throughout. | | 2026-10-19 | Corrections from review round 2. The `@tsjs/module` search claim, `crates/tsconfig.json` taking in the shared `global.d.ts`, how ESLint finds `crates/eslint.config.js`, `build.rs` watching `crates/tsconfig.json`, the documentation proposal workflow a Tinybird change starts, and Rust discovery passing over a module root that is a file. | +| 2026-10-20 | Names its pull request, IABTechLab/trusted-server#1423. | ## Review log From 5be114773345a03e207c1da2d3b163a23510985f Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Tue, 20 Oct 2026 07:45:28 +0100 Subject: [PATCH 6/8] Test module discovery without a file system The crate's tests run as WebAssembly under Viceroy in CI, where std::env::temp_dir panics, so the three discovery tests that built folders in a scratch tree aborted the cargo test job. The folder walk now hands what it found to module_ids, which applies the naming rules and is tested with in-memory folders. The JavaScript tests still walk a scratch tree. The design says why. --- crates/trusted-server-js/build/bundle_set.rs | 125 +++++++++--------- ...6-10-19-module-scripts-in-crates-design.md | 3 +- 2 files changed, 65 insertions(+), 63 deletions(-) diff --git a/crates/trusted-server-js/build/bundle_set.rs b/crates/trusted-server-js/build/bundle_set.rs index ffa0e2ae0d..f6136c32b7 100644 --- a/crates/trusted-server-js/build/bundle_set.rs +++ b/crates/trusted-server-js/build/bundle_set.rs @@ -38,34 +38,54 @@ pub(crate) fn expected_module_ids( } } - let mut modules: Vec<(String, PathBuf)> = Vec::new(); + let mut folders = Vec::new(); for root in roots { for dir in folders_in(&root)? { - let name = dir - .file_name() - .map(|name| name.to_string_lossy().into_owned()) - .unwrap_or_default(); - if name == CORE_MODULE_ID { - return Err(format!( - "module name {CORE_MODULE_ID}, used by {}, is the shared core's bundle", - dir.display() - )); - } - if let Some((_, earlier)) = modules.iter().find(|(known, _)| *known == name) { - return Err(format!( - "module {name} is in both {} and {}", - earlier.display(), - dir.display() - )); - } - modules.push((name, dir)); + let has_entry = dir.join("index.ts").is_file(); + folders.push((dir, has_entry)); + } + } + module_ids(&folders) +} + +/// Return the module IDs for module folders in the order they were found, +/// each with whether it holds an `index.ts` file. +/// +/// Kept apart from the folder walk so its rules are tested without a file +/// system, which the WebAssembly test run does not have. +/// +/// # Errors +/// +/// Returns a message when two folders supply a module of the same name, or +/// when a folder is named `core`, which is the shared core's bundle. +pub(crate) fn module_ids(folders: &[(PathBuf, bool)]) -> Result, String> { + let mut seen: Vec<(String, &Path)> = Vec::new(); + for (dir, _) in folders { + let name = dir + .file_name() + .map(|name| name.to_string_lossy().into_owned()) + .unwrap_or_default(); + if name == CORE_MODULE_ID { + return Err(format!( + "module name {CORE_MODULE_ID}, used by {}, is the shared core's bundle", + dir.display() + )); + } + if let Some((_, earlier)) = seen.iter().find(|(known, _)| *known == name) { + return Err(format!( + "module {name} is in both {} and {}", + earlier.display(), + dir.display() + )); } + seen.push((name, dir.as_path())); } - let mut integrations: Vec = modules + let mut integrations: Vec = seen .into_iter() - .filter(|(_, dir)| dir.join("index.ts").is_file()) - .map(|(name, _)| name) + .zip(folders) + .filter(|(_, (_, has_entry))| *has_entry) + .map(|((name, _), _)| name) .collect(); integrations.sort(); @@ -261,41 +281,24 @@ mod tests { ); } - /// A fresh folder for one test, removed when the guard drops. - struct TempTree(PathBuf); - - impl TempTree { - fn new(name: &str) -> Self { - let root = - std::env::temp_dir().join(format!("tsjs-bundle-set-{}-{name}", std::process::id())); - let _ = fs::remove_dir_all(&root); - fs::create_dir_all(&root).expect("should create the test folder"); - Self(root) - } - - fn file(&self, relative: &str) { - let path = self.0.join(relative); - fs::create_dir_all(path.parent().expect("should have a parent folder")) - .expect("should create the file's folder"); - fs::write(&path, "").expect("should write the file"); - } - } - - impl Drop for TempTree { - fn drop(&mut self) { - let _ = fs::remove_dir_all(&self.0); - } + /// Module folders as the folder walk finds them, each with whether it + /// holds an `index.ts` file. + fn folders(entries: &[(&str, bool)]) -> Vec<(PathBuf, bool)> { + entries + .iter() + .map(|(dir, has_entry)| (PathBuf::from(dir), *has_entry)) + .collect() } #[test] - fn finds_modules_here_and_in_the_crate_that_supplies_them() { - let tree = TempTree::new("finds"); - tree.file("lib/src/integrations/zeta/index.ts"); - tree.file("crates/kind/vendor/js/src/alpha/index.ts"); - tree.file("crates/kind/vendor/js/src/helpers/shared.ts"); + fn lists_core_then_each_folder_with_an_entry_in_name_order() { + let found = folders(&[ + ("lib/src/integrations/zeta", true), + ("crates/kind/vendor/js/src/alpha", true), + ("crates/kind/vendor/js/src/helpers", false), + ]); - let ids = expected_module_ids(&tree.0.join("lib/src"), &tree.0.join("crates")) - .expect("should discover the modules"); + let ids = module_ids(&found).expect("should list the modules"); assert_eq!( ids, @@ -306,12 +309,12 @@ mod tests { #[test] fn rejects_a_module_name_two_folders_supply() { - let tree = TempTree::new("twice"); - tree.file("lib/src/integrations/alpha/index.ts"); - tree.file("crates/kind/vendor/js/src/alpha/index.ts"); + let found = folders(&[ + ("lib/src/integrations/alpha", true), + ("crates/kind/vendor/js/src/alpha", true), + ]); - let err = expected_module_ids(&tree.0.join("lib/src"), &tree.0.join("crates")) - .expect_err("should refuse a module name found twice"); + let err = module_ids(&found).expect_err("should refuse a module name found twice"); assert!( err.contains("module alpha is in both"), @@ -321,11 +324,9 @@ mod tests { #[test] fn rejects_a_module_named_core() { - let tree = TempTree::new("core"); - tree.file("crates/kind/vendor/js/src/core/index.ts"); + let found = folders(&[("crates/kind/vendor/js/src/core", true)]); - let err = expected_module_ids(&tree.0.join("lib/src"), &tree.0.join("crates")) - .expect_err("should refuse a module named core"); + let err = module_ids(&found).expect_err("should refuse a module named core"); assert!( err.contains("module name core"), diff --git a/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md b/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md index dfa0793cc8..c9bbd2d270 100644 --- a/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md +++ b/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md @@ -50,7 +50,7 @@ So a folder named `js/` holds either module scripts, in `js/src` and `js/test`, `moduleSourceDirs()` in `lib/build-module-options.mjs` lists every module's source folder by name, `lib/src/integrations/` for the modules that stay in the shared crate and `crates///js/src/` for the rest. A name found in two places is an error, because bundles and imports are keyed by name, and so is a module named `core`, which is the shared core's bundle. A link whose target is gone counts as no folder. `build-all.mjs` builds a bundle for each listed folder whose `index.ts` is a file. -`expected_module_ids` in `build/bundle_set.rs` applies the same rule from the Rust side, given the crates folder as well as `lib/src`, so `build.rs` still embeds exactly the bundles the Node build wrote. Each side has tests of a module found in a crate, a name found twice and a module named `core`, in `build/bundle_set.rs` and `lib/test/module-discovery.test.mjs`. Bundles are still named `tsjs-.js`, so the Rust API, the served URLs and every content hash are unchanged. +`expected_module_ids` in `build/bundle_set.rs` applies the same rule from the Rust side, given the crates folder as well as `lib/src`, so `build.rs` still embeds exactly the bundles the Node build wrote. Both sides have tests of the module list, a name found twice and a module named `core`, in `build/bundle_set.rs` and `lib/test/module-discovery.test.mjs`, and the JavaScript tests also walk folders in a scratch tree. The Rust tests check `module_ids`, which takes the folders the walk found, because the crate's tests also run as WebAssembly under Viceroy, where there is no file system. Bundles are still named `tsjs-.js`, so the Rust API, the served URLs and every content hash are unchanged. `build.rs` asks Cargo to rerun it when `lib/build-module-options.mjs` or any existing `crates///js/src` folder changes, and `js/test` as well when `TSJS_TEST=1` runs the tests. It names only folders that exist, because Cargo reruns a build script on every build while a watched path is missing. A crate's first `js/src` folder is therefore picked up by the next build that runs for another reason, which `crates/trusted-server-js/README.md` says, with touching `lib/package.json` as the way to force it. @@ -111,6 +111,7 @@ The `tsjs` guide said there are 12 integration source modules and 13 bundles. Th | 2026-10-19 | Fixes from review round 1. TypeScript's `@tsjs/module/*` list and its test, the configuration guide's Tinybird link, the editor settings in the crates folder and their test, the npm package rule, `build.rs` watching `build-module-options.mjs`, discovery refusing `core` and passing over broken links on both sides with JavaScript tests, no unmatched-pattern pass for ESLint, `.mjs` crate tests, the three kinds of browser code a crate can hold, the changelog, `AGENTS.md` and the onboarding guide, and wording throughout. | | 2026-10-19 | Corrections from review round 2. The `@tsjs/module` search claim, `crates/tsconfig.json` taking in the shared `global.d.ts`, how ESLint finds `crates/eslint.config.js`, `build.rs` watching `crates/tsconfig.json`, the documentation proposal workflow a Tinybird change starts, and Rust discovery passing over a module root that is a file. | | 2026-10-20 | Names its pull request, IABTechLab/trusted-server#1423. | +| 2026-10-20 | The Rust discovery tests check `module_ids` without a file system, because the WebAssembly test run under Viceroy has none. | ## Review log From 63fa9db61114674154238b1372d0d68d4ad2913c Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Wed, 28 Oct 2026 11:30:45 +0000 Subject: [PATCH 7/8] The scripts design's review log cites no unpublished commit --- .../specs/2026-10-19-module-scripts-in-crates-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md b/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md index c9bbd2d270..5fe1e9e716 100644 --- a/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md +++ b/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md @@ -115,5 +115,5 @@ The `tsjs` guide said there are 12 integration source modules and 13 bundles. Th ## Review log -1. **Round 1, 2026-10-19.** A review in a fresh session, against `fixpass11/55-scripts-in-crates` as first committed at `7dc1671fe`, rewritten before it was pushed. Issues found, two of them blocking. TypeScript had no mapping for `@tsjs/module/*`, so the GPT script was no longer type-checked against the APS renderer while this design said TypeScript used the names, and the configuration guide still linked the old Tinybird README. Of the others, editors found no settings for crate scripts, a script in a crate cannot import an npm package and the design did not say so, `build.rs` did not watch `build-module-options.mjs`, the crate lint run passed an empty match, the JavaScript and Rust discovery differed on broken links, an `index.ts` folder and a module named `core`, crate `.mjs` tests were neither run nor given Node's globals, and there was no changelog entry. This revision fixes each of them. +1. **Round 1, 2026-10-19.** A review in a fresh session, against `fixpass11/55-scripts-in-crates` as first committed, before it was rewritten and pushed. Issues found, two of them blocking. TypeScript had no mapping for `@tsjs/module/*`, so the GPT script was no longer type-checked against the APS renderer while this design said TypeScript used the names, and the configuration guide still linked the old Tinybird README. Of the others, editors found no settings for crate scripts, a script in a crate cannot import an npm package and the design did not say so, `build.rs` did not watch `build-module-options.mjs`, the crate lint run passed an empty match, the JavaScript and Rust discovery differed on broken links, an `index.ts` folder and a module named `core`, crate `.mjs` tests were neither run nor given Node's globals, and there was no changelog entry. This revision fixes each of them. 2. **Round 2, 2026-10-19.** A lighter review in a fresh session, against this page at `b89557183`, of the round 1 fixes and the new text. No blocking issues. One round 1 item was only partly fixed, the claim that every import of one module by another uses `@tsjs/module/`, which the Prebid shim's relative import of the APS renderer contradicted. It also found that `crates/tsconfig.json` left out the shared `global.d.ts`, that ESLint 9 finds its config from the folder it runs in rather than from the file, that the build reads `crates/tsconfig.json` without `build.rs` watching it, that Rust discovery failed on a module root that is a file where JavaScript passed over it, that the new configuration files and `.mjs` test were outside the format check, and that a Tinybird change now starts the documentation proposal workflow. This revision fixes each of them, and the design joins the specs pull request when the row is pushed. From 6e9d0f3b33fa1e15026ae961d42f4e28c6471257 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Wed, 28 Oct 2026 11:32:14 +0000 Subject: [PATCH 8/8] Row 55's design copy names no local branch --- .../specs/2026-10-19-module-scripts-in-crates-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md b/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md index 5fe1e9e716..717f410f4b 100644 --- a/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md +++ b/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md @@ -115,5 +115,5 @@ The `tsjs` guide said there are 12 integration source modules and 13 bundles. Th ## Review log -1. **Round 1, 2026-10-19.** A review in a fresh session, against `fixpass11/55-scripts-in-crates` as first committed, before it was rewritten and pushed. Issues found, two of them blocking. TypeScript had no mapping for `@tsjs/module/*`, so the GPT script was no longer type-checked against the APS renderer while this design said TypeScript used the names, and the configuration guide still linked the old Tinybird README. Of the others, editors found no settings for crate scripts, a script in a crate cannot import an npm package and the design did not say so, `build.rs` did not watch `build-module-options.mjs`, the crate lint run passed an empty match, the JavaScript and Rust discovery differed on broken links, an `index.ts` folder and a module named `core`, crate `.mjs` tests were neither run nor given Node's globals, and there was no changelog entry. This revision fixes each of them. +1. **Round 1, 2026-10-19.** A review in a fresh session, against row 55's branch as first committed, before it was rewritten and pushed. Issues found, two of them blocking. TypeScript had no mapping for `@tsjs/module/*`, so the GPT script was no longer type-checked against the APS renderer while this design said TypeScript used the names, and the configuration guide still linked the old Tinybird README. Of the others, editors found no settings for crate scripts, a script in a crate cannot import an npm package and the design did not say so, `build.rs` did not watch `build-module-options.mjs`, the crate lint run passed an empty match, the JavaScript and Rust discovery differed on broken links, an `index.ts` folder and a module named `core`, crate `.mjs` tests were neither run nor given Node's globals, and there was no changelog entry. This revision fixes each of them. 2. **Round 2, 2026-10-19.** A lighter review in a fresh session, against this page at `b89557183`, of the round 1 fixes and the new text. No blocking issues. One round 1 item was only partly fixed, the claim that every import of one module by another uses `@tsjs/module/`, which the Prebid shim's relative import of the APS renderer contradicted. It also found that `crates/tsconfig.json` left out the shared `global.d.ts`, that ESLint 9 finds its config from the folder it runs in rather than from the file, that the build reads `crates/tsconfig.json` without `build.rs` watching it, that Rust discovery failed on a module root that is a file where JavaScript passed over it, that the new configuration files and `.mjs` test were outside the format check, and that a Tinybird change now starts the documentation proposal workflow. This revision fixes each of them, and the design joins the specs pull request when the row is pushed.