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 `