From 7b114a8e503c8189efffddf1ca0ffc28263143fe Mon Sep 17 00:00:00 2001 From: Erin Sparling Date: Fri, 6 Feb 2026 21:55:07 +0000 Subject: [PATCH 1/3] feat: complete ESM core implementation with 100% IIFE compatibility - Implemented .ready() promise and Passive-by-Default ESM logic. - Preserved Active-by-Default IIFE logic using win.SWG array augmentation. - Added comprehensive integration tests for ESM entry points. - Fixed GAA/SwG global object conflict in callSwg. - Reorganized ESM documentation. --- build_binaries.sh | 43 ++++-- docs/builds.md | 107 +++++++++++++++ docs/esm/esm-implementation.md | 116 ++++++++++++++++ docs/esm/esm-pr-overview.md | 64 +++++++++ docs/esm/iife-to-esm-init-pattern.md | 65 +++++++++ package-lock.json | 143 ++++++++++++++------ src/api/basic-subscriptions.ts | 5 + src/api/subscriptions.ts | 5 + src/basic-main.ts | 4 +- src/constants.ts | 1 + src/gaa-main.ts | 8 ++ src/main.ts | 4 +- src/runtime/basic-runtime-test.js | 2 + src/runtime/basic-runtime.ts | 47 +++++-- src/runtime/entry-point-exports-test.js | 42 ++++++ src/runtime/esm-basic-integration-test.js | 74 ++++++++++ src/runtime/esm-integration-test.js | 75 ++++++++++ src/runtime/extended-access/gaa-metering.ts | 7 + src/runtime/extended-access/utils.ts | 8 +- src/runtime/runtime-test.js | 2 + src/runtime/runtime.ts | 59 ++++++-- vite.config.js | 20 ++- 22 files changed, 825 insertions(+), 76 deletions(-) create mode 100644 docs/builds.md create mode 100644 docs/esm/esm-implementation.md create mode 100644 docs/esm/esm-pr-overview.md create mode 100644 docs/esm/iife-to-esm-init-pattern.md create mode 100644 src/runtime/entry-point-exports-test.js create mode 100644 src/runtime/esm-basic-integration-test.js create mode 100644 src/runtime/esm-integration-test.js diff --git a/build_binaries.sh b/build_binaries.sh index 410659c649..c5379dcaa6 100755 --- a/build_binaries.sh +++ b/build_binaries.sh @@ -21,20 +21,36 @@ function build_template_binary() { if [[ $target != "classic" ]]; then filename="$filename-$target" fi - filename="$filename.template.js" + local -r template_filename="$filename.template" + # Build IIFE npx vite build -- \ "--assets=https://news.google.com/swg/js/v1" \ "--experiments=$experiments" \ "--frontend=https://FRONTEND.com" \ "--frontendCache=nocache" \ - "--minifiedBasicName=$filename" \ - "--minifiedGaaName=$filename" \ - "--minifiedName=$filename" \ + "--minifiedBasicName=$template_filename.js" \ + "--minifiedGaaName=$template_filename.js" \ + "--minifiedName=$template_filename.js" \ "--payEnvironment=___PAY_ENVIRONMENT___" \ "--playEnvironment=___PLAY_ENVIRONMENT___" \ "--swgVersion=$SWG_VERSION" \ "--target=$target" + + # Build ESM + npx vite build -- \ + "--assets=https://news.google.com/swg/js/v1" \ + "--experiments=$experiments" \ + "--frontend=https://FRONTEND.com" \ + "--frontendCache=nocache" \ + "--minifiedBasicName=$template_filename.js" \ + "--minifiedGaaName=$template_filename.js" \ + "--minifiedName=$template_filename.js" \ + "--payEnvironment=___PAY_ENVIRONMENT___" \ + "--playEnvironment=___PLAY_ENVIRONMENT___" \ + "--swgVersion=$SWG_VERSION" \ + "--target=$target" \ + "--esm" } build_template_binary basic $EXPERIMENTS & build_template_binary classic $EXPERIMENTS & @@ -50,15 +66,17 @@ function create_binaries_for_environment() { shift 4 for variant in "" "-basic" "-gaa"; do - # Copy files. - cp dist/swg$variant.template.js dist/swg$variant$target.js - cp dist/swg$variant.template.js.map dist/swg$variant$target.js.map + for ext in "js" "mjs"; do + # Copy files. + cp dist/swg$variant.template.$ext dist/swg$variant$target.$ext + cp dist/swg$variant.template.$ext.map dist/swg$variant$target.$ext.map - # Replace values. - sed -i "s|https://FRONTEND.com|$frontend|g" dist/swg$variant$target* - sed -i "s|___PAY_ENVIRONMENT___|$pay_environment|g" dist/swg$variant$target* - sed -i "s|___PLAY_ENVIRONMENT___|$play_environment|g" dist/swg$variant$target* - sed -i "s|swg$variant.template.js.map|swg$variant$target.js.map|g" dist/swg$variant$target* + # Replace values. + sed -i "s|https://FRONTEND.com|$frontend|g" dist/swg$variant$target.$ext* + sed -i "s|___PAY_ENVIRONMENT___|$pay_environment|g" dist/swg$variant$target.$ext* + sed -i "s|___PLAY_ENVIRONMENT___|$play_environment|g" dist/swg$variant$target.$ext* + sed -i "s|swg$variant.template.$ext.map|swg$variant$target.$ext.map|g" dist/swg$variant$target.$ext* + done done } create_binaries_for_environment \ @@ -80,3 +98,4 @@ wait # Remove template binaries. rm dist/*template.js* +rm dist/*template.mjs* diff --git a/docs/builds.md b/docs/builds.md new file mode 100644 index 0000000000..4af5b7294f --- /dev/null +++ b/docs/builds.md @@ -0,0 +1,107 @@ +# Build Process + +This document outlines how the various binaries are produced in this project, specifically how source files like `main.ts` are transformed into distributable files like `swg.js`. + +## Orchestration + +The project uses two primary build systems: +1. **Gulp (Browserify):** Used primarily for local development and testing. It supports `gulp watch` for rapid iteration and serves files via the local dev server. +2. **Vite (Rollup):** Used for production builds and official releases. It is optimized for minification and environment-specific deployments. + +The entry point for official builds is the `build_binaries.sh` script. + +## The `build_binaries.sh` Script + +The `build_binaries.sh` script orchestrates the production of environment-specific binaries. + +### 1. Building Templates +The script first builds "template" binaries for each major product variant using Vite. + +| Target | Entry Point | Template Output | +| :--- | :--- | :--- | +| `classic` | `src/main.ts` | `dist/swg.template.js` | +| `basic` | `src/basic-main.ts` | `dist/swg-basic.template.js` | +| `gaa` | `src/gaa-main.ts` | `dist/swg-gaa.template.js` | + +This is done via the `build_template_binary` function which calls: +```bash +npx vite build -- --target=[target] --minifiedName=[filename] ... +``` + +### 2. Environment Substitution +Once the templates are built, the script creates specific versions for different environments (Production, Autopush, Qual) by performing string replacements using `sed`. + +For each variant (Classic, Basic, GAA), it produces: +- **Production:** `swg.js`, `swg-basic.js`, `swg-gaa.js` +- **Autopush:** `swg-autopush.js`, `swg-basic-autopush.js`, `swg-gaa-autopush.js` +- **Qual:** `swg-qual.js`, `swg-basic-qual.js`, `swg-gaa-qual.js` + +The replacements include: +- `https://FRONTEND.com` -> The environment's frontend URL. +- `___PAY_ENVIRONMENT___` -> `PRODUCTION` or `SANDBOX`. +- `___PLAY_ENVIRONMENT___` -> `PROD`, `AUTOPUSH`, or `STAGING`. + +## Constants Injection + +During the build process, several constants in `src/constants.ts` are overwritten with values passed via CLI arguments. This allows the same source code to be built for different environments. + +The values are resolved in `build-system/tasks/compile-config.js` and injected: +- **In Vite:** Via `@rollup/plugin-replace`. +- **In Gulp:** Via a custom Babel transform (`build-system/transform-define-constants.js`). + +Commonly injected constants include: +- `FRONTEND`: The URL of the SwG server. +- `PAY_ENVIRONMENT`: The environment for Google Pay (PRODUCTION/SANDBOX). +- `INTERNAL_RUNTIME_VERSION`: The version of the library (often the git commit hash). + +## Development Build (Gulp) + +For local development, running `npm run build` or `gulp build` uses the Gulp-based build system. + +- **Configuration:** `build-system/tasks/compile.js` +- **Tooling:** Browserify + tsify + Babel. +- **Output:** Files are typically placed in `dist/` with names like `subscriptions.js` (equivalent to `swg.js`). + +## How to Add a New Build + +To add a new binary/build target to the project: + +1. **Create an Entry Point:** + Add a new `.ts` file in `src/` (e.g., `src/new-feature-main.ts`). + +2. **Update Vite Configuration:** + Add the new target to the `builds` object in `vite.config.js`: + ```javascript + const builds = { + // ... existing builds + 'new-feature': { + output: args.minifiedNewFeatureName || 'new-feature-subscriptions.js', + input: './src/new-feature-main.ts', + }, + }; + ``` + +3. **Update Gulp Configuration (Optional):** + If you want the new target to be available via Gulp tasks (e.g., for local development with `gulp watch` or testing with the local demo server), update `build-system/tasks/compile.js`: + ```javascript + const scriptCompilations = { + // ... + 'new-feature': () => compileScript('./src/', 'new-feature-main.ts', './dist', { + toName: 'new-feature-subscriptions.max.js', + minifiedName: args.minifiedNewFeatureName || 'new-feature-subscriptions.js', + wrapper: '(function(){<%= contents %>})();', + ...options + }), + }; + ``` + *Note: You might skip this if the build is a production-only artifact and you don't need hot-reloading or local demo support for it.* + +4. **Update `build_binaries.sh`:** + Add the new target to the template build section: + ```bash + build_template_binary new-feature $EXPERIMENTS & + ``` + The `create_binaries_for_environment` function will automatically pick up the new `-new-feature` variant if you add it to the loop: + ```bash + for variant in "" "-basic" "-gaa" "-new-feature"; do + ``` diff --git a/docs/esm/esm-implementation.md b/docs/esm/esm-implementation.md new file mode 100644 index 0000000000..6a3f849db8 --- /dev/null +++ b/docs/esm/esm-implementation.md @@ -0,0 +1,116 @@ +# Implementation Plan: ES Module Support (Proposal B) + +This document outlines the steps to implement ES Module (ESM) support for the SwG library. The goal is to allow modern developers to use `import { subscriptions } from 'swg.js'` while ensuring 100% backwards compatibility with the existing `(self.SWG = self.SWG || []).push(...)` pattern. + +## 1. Objectives +- Enable `import` syntax for `swg.js`, `swg-basic.js`, and `swg-gaa.js`. +- Maintain full functionality of the legacy async snippet pattern. +- Ensure the library only initializes once, even if imported multiple times or mixed with legacy snippets. +- Achieve 100% test coverage for new initialization paths. + +## 2. Core Library Changes + +### 2.1. Update Runtime Initialization (`src/runtime/runtime.ts`) +The `installRuntime` function must be updated to return the public API. It must also be idempotent and return the existing instance if already installed. + +- **Storage:** Create a module-level variable `let publicRuntimeInstance: SubscriptionsInterface | null = null;`. +- **Logic:** + - If `publicRuntimeInstance` exists, return it immediately. + - If it doesn't exist but `win[RUNTIME_PROP]` is already an object (not an array), attempt to retrieve or recreate the public API (or better, ensure `publicRuntimeInstance` is always set when `installRuntime` runs). +- **Return Type:** Change from `void` to `SubscriptionsInterface`. +- **Parameter:** Add an optional `options` object: `installRuntime(win: Window, options?: {autoStart?: boolean})`. + +Repeat similar changes for `installBasicRuntime` in `src/runtime/basic-runtime.ts`. + +### 2.2. Update Entry Points (`src/main.ts`, `src/basic-main.ts`, `src/gaa-main.ts`) +Modify entry points to export the initialized instances. + +**Example (`src/main.ts`):** +The entry point will now behave differently depending on how it's bundled. For ESM, we suppress auto-start. + +```typescript +export const subscriptions = installRuntime(self, { + autoStart: /* logic to detect if we are in an ESM build */ +}); +``` + +**Example (`src/gaa-main.ts`):** +```typescript +export { + GaaGoogle3pSignInButton, + GaaGoogleSignInButton, + GaaMetering, + GaaMeteringRegwall, + GaaSignInWithGoogleButton, +} from './runtime/extended-access'; +``` + +## 3. Build Configuration Changes (`vite.config.js`) + +The Vite configuration needs to be updated to output both IIFE (for legacy ` + +``` + +**Why this still works:** +1. The IIFE build still executes `installRuntime(self)` with `autoStart: true` immediately upon loading. +2. `installRuntime` still processes the `win.SWG` array and replaces it with a `.push` proxy. +3. The ESM build, when loaded via `import`, *also* executes the side effect of calling `installRuntime(self)`, ensuring the global `self.SWG` is initialized for any legacy snippets that might coexist on the page. + +### 4.1. Manual vs Auto Initialization +SwG supports an "auto-initialization" mode where it scans the DOM for configuration (e.g., meta tags) and starts automatically unless suppressed. + +- **IIFE Behavior:** Remains "Active-by-Default". It will attempt to auto-start immediately to support zero-config markup-only implementations. +- **ESM Behavior:** Becomes "Passive-by-Default". The `import` side-effect will install the runtime and support legacy snippets, but it will **not** trigger an automatic `start()`. +- **The Benefit:** This eliminates the need for ESM developers to use the `subscriptions-control="manual"` flag. They can simply `import` and `init()` without worrying about race conditions with an autonomous auto-start process. +- **Backwards Compatibility:** + - **Legacy Snippets:** Will still work perfectly. If a snippet calls `api.init()`, the runtime will initialize as expected. + - **Markup-Only:** If a publisher wants SwG to handle everything via markup without writing JS, they should continue using the IIFE `