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 a13559dcba..583f0c352a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -460,7 +460,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. @@ -492,7 +492,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 24a506495f..afab30b0d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ 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 with the answers a selected asset cache module holds 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..e7ed7b7d6d --- /dev/null +++ b/crates/.prettierrc.json @@ -0,0 +1,13 @@ +{ + "overrides": [ + { + "files": ["*/*/js/{src,test}/**", ".prettierrc.json", "eslint.config.js", "tsconfig.json"], + "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 977a49c909..28ecc6822a 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/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/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 f4e7466b7c..e8af5bb43c 100644 --- a/crates/trusted-server-core/src/publisher.rs +++ b/crates/trusted-server-core/src/publisher.rs @@ -7139,7 +7139,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` 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 @@ -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..25cd8e3f98 100644 --- a/crates/trusted-server-js/build.rs +++ b/crates/trusted-server-js/build.rs @@ -16,19 +16,26 @@ 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", + // 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", @@ -44,12 +51,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 +86,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 +99,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 +153,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..f6136c32b7 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,79 @@ 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 folders = Vec::new(); + for root in roots { + for dir in folders_in(&root)? { + 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 = seen + .into_iter() + .zip(folders) + .filter(|(_, (_, has_entry))| *has_entry) + .map(|((name, _), _)| name) + .collect(); integrations.sort(); let mut ids = Vec::with_capacity(integrations.len() + 1); @@ -54,6 +95,29 @@ pub(crate) fn expected_module_ids(lib_src: &Path) -> Result, String> Ok(ids) } +/// The folders directly inside `dir`, or none when it is not a folder. +/// +/// # Errors +/// +/// Returns a message when `dir` is a folder but cannot be read. +pub(crate) fn folders_in(dir: &Path) -> Result, String> { + if !dir.is_dir() { + 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 +281,59 @@ mod tests { ); } + /// 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 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 = module_ids(&found).expect("should list 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 found = folders(&[ + ("lib/src/integrations/alpha", true), + ("crates/kind/vendor/js/src/alpha", true), + ]); + + let err = module_ids(&found).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 found = folders(&[("crates/kind/vendor/js/src/core", true)]); + + let err = module_ids(&found).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 5cfc36d822..209542f7e1 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,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/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..fd775c5e97 --- /dev/null +++ b/crates/trusted-server-js/lib/test/module-discovery.test.mjs @@ -0,0 +1,116 @@ +// @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}/**', '.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') + ); + + 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..c1a3bdb09e --- /dev/null +++ b/crates/tsconfig.json @@ -0,0 +1,10 @@ +{ + "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", "trusted-server-js/lib/src/core/global.d.ts"] +} diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 5e46500b79..b9417d7669 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -3741,7 +3741,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/error-reference.md b/docs/guide/error-reference.md index 519c06deeb..9c6b1b33f5 100644 --- a/docs/guide/error-reference.md +++ b/docs/guide/error-reference.md @@ -690,7 +690,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/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). 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. 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..717f410f4b --- /dev/null +++ b/docs/superpowers/specs/2026-10-19-module-scripts-in-crates-design.md @@ -0,0 +1,119 @@ +# Design Spec: A module's browser script lives in the crate that supplies it + +**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#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-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. + +## 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. 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. + +### 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 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`. + +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 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. + +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. 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 + +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. | +| 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 + +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.