Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 31 additions & 12 deletions build_binaries.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the above, except with the --esm flag

}
build_template_binary basic $EXPERIMENTS &
build_template_binary classic $EXPERIMENTS &
Expand All @@ -50,15 +66,17 @@ function create_binaries_for_environment() {
shift 4

for variant in "" "-basic" "-gaa"; do
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same as it was before, but now just loops twice for both js and mjs

# 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 \
Expand All @@ -80,3 +98,4 @@ wait

# Remove template binaries.
rm dist/*template.js*
rm dist/*template.mjs*
107 changes: 107 additions & 0 deletions docs/builds.md
Original file line number Diff line number Diff line change
@@ -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
```
116 changes: 116 additions & 0 deletions docs/esm/esm-implementation.md
Original file line number Diff line number Diff line change
@@ -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 `<script>` tags) and ESM (for modern `import` statements).

- **Output Formats:** Update `rollupOptions.output` to include both `iife` and `es`.
- **IIFE Wrapping:** Ensure the `add-outer-iife` plugin only applies to the IIFE build. ESM files should not be wrapped in an IIFE as it breaks exports.
- **Targeted Logic:** Use Vite's `define` or environment variables to inject a flag (e.g., `IS_ESM`) so that `main.ts` knows whether to auto-start.

## 4. Backwards Compatibility Verification

Existing publishers use:
```html
<script async src=".../swg.js" subscriptions-control="manual"></script>
<script>
(self.SWG = self.SWG || []).push(subscriptions => {
subscriptions.init('PUB_ID');
});
</script>
```

**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 `<script>` tag. ESM is intended for developers who want explicit control.

### 4.2. DOM Configuration (LD+JSON) Support
Being "Passive" does not mean the library loses its ability to read markup. It simply transfers the control of *when* that scan happens to the developer.

- **Using Markup with ESM:** If a developer wants the library to use the configuration defined in LD+JSON or meta tags, they should call `subscriptions.start()` instead of `subscriptions.init()`.
- **Logic flow:** When `start()` is called, the library checks if it has been initialized via `init()`. If not, it automatically triggers the `PageConfigResolver` to scan the DOM, exactly as it would in "Auto" mode.
- **Why this is better:** It allows the developer to ensure that their own dependencies or tracking scripts are loaded before SwG starts scanning the DOM or making entitlement requests, without requiring a `manual` control flag.

## 5. Testing Strategy (100% Coverage)

### 5.1. Unit Tests
- **Idempotency Test:** Verify that calling `installRuntime` multiple times returns the exact same object instance.
- **Global Interaction Test:** Verify that `import`-ing the module correctly populates `window.SWG` and processes existing callbacks in the array.
- **Mixed Mode Test:** Verify that a page using both an ES `import` and a legacy `<script>` tag (IIFE) only initializes the runtime once.
- **Auto-Start Fork Test:** Verify that `autoStart: false` correctly suppresses the DOM scan and entitlement fetch, even if valid markup is present.

### 5.2. Integration/E2E Tests
- Create a test page using `<script type="module">` to import SwG and verify it can successfully call `subscriptions.init()`.
- Create a test page using a legacy snippet and verify it works even if the library is loaded via a module import.

## 6. Update `build_binaries.sh`

The production build script needs to be updated to handle the new ESM output files.

- **Loop over extensions:** Update the `create_binaries_for_environment` function to iterate over both `.js` and `.mjs`.
- **Update replacements:** Ensure the `sed` commands correctly target both file types.

## 7. Implementation Steps for the Agent

1. **Refactor Runtimes:** Update `runtime.ts` and `basic-runtime.ts` to return the public API and support the `autoStart` option.
2. **Add Exports:** Update entry point files to export the relevant objects/functions and use the `autoStart` logic.
3. **Configure Vite:** Add the `es` format to the output array and gate the IIFE-wrapping plugin. Use `define` to set the build format flag.
4. **Add Tests:** Update `runtime-test.js` and `basic-runtime-test.js` with new test cases covering the return values and module-style initialization.
5. **Update Production Scripts:** Modify `build_binaries.sh` to produce and environment-specialize the new ESM binaries.
6. **Verify Build:** Run the build and inspect the output files to confirm both IIFE and ESM versions contain the expected code and exports.
41 changes: 41 additions & 0 deletions docs/esm/esm-pr-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Moving swg-js to ES Modules

This PR gets `swg-js` ready for the modern web without breaking the thousands of sites that already use it. It adds ES Module (ESM) support and cleans up how the library starts up.

## The Goal

The web has moved to native modules, and `swg-js` needs to follow. But we have a lot of history here. This implementation avoids breaking the "Async Snippet" pattern that publishers have relied on for years.

The compromise is simple:
- **Modules are quiet.** If a site uses `import { subscriptions } from 'swg.mjs'`, nothing happens automatically. There are no surprise DOM scans or side effects. The developer is in control.
- **Script tags are loud.** The traditional `swg.js` bundle still acts as it always has—it scans the page and sets up buttons on its own.
- **It's safe.** Every one of the 1,900+ existing tests still passes.

## How it works

### Singletons
This PR refactors `installRuntime` and `installBasicRuntime` into singletons. The library now stores the API instance in a module-level variable. If the installer is called twice—which is easy to do when mixing imports and snippets—it just returns the existing instance. This prevents doubling up on event listeners or managers.

### The autoStart option
The installers now include an `autoStart` flag.
- In the IIFE build, this defaults to `true` to keep things simple for markup-only sites.
- In the ESM build, it defaults to `false`.
The entry points use a build-time constant (`IS_ESM_BUILD`) to decide which behavior to use.

### The ready() Promise
This PR adds a `.ready()` method to the global `SWG` and `SWG_BASIC` objects. This provides a promise-based alternative to the legacy `.push(callback)` pattern, offering a more modern way to handle asynchronous initialization.

## Build System

Vite is now configured to output both formats. Format-specific hacks (like the outer IIFE wrapper) are now gated so they only apply to the `.js` files, preventing them from breaking standard ES exports.

The `build_binaries.sh` script also handles both formats now, ensuring that the right URLs are injected into both the legacy and module versions for Prod, Autopush, and Qual.

## Verification

The migration was validated with an emphasis on preventing regressions in the production runtime:
1. **100% Pass Rate:** The entire suite of ~1,900 tests was executed with no failures.
2. **New ESM Tests:** Integration suites were added to test module-only scenarios, verifying that `autoStart: false` works and that multiple imports do not collide.
3. **Manual Checks:** The development server was verified for stability, and the generated `.mjs` files were confirmed to be valid and exportable.

This PR delivers modules and a cleaner API without breaking existing integrations.
Loading