-
Notifications
You must be signed in to change notification settings - Fork 114
feat: complete ESM core implementation with 100% IIFE compatibility #3978
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
everyplace
wants to merge
5
commits into
subscriptions-project:main
Choose a base branch
from
everyplace:feature/esm
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
7b114a8
feat: complete ESM core implementation with 100% IIFE compatibility
everyplace c3880ab
Merge branch 'subscriptions-project:main' into feature/esm
everyplace 0d55c04
Merge remote-tracking branch 'upstream/main' into feature/esm
everyplace 7b0c8f6
feat: implement autoStart logic in installBasicRuntime
everyplace 262bea6
Updated docs to be current and more readable
everyplace File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 \ | ||
|
|
@@ -80,3 +98,4 @@ wait | |
|
|
||
| # Remove template binaries. | ||
| rm dist/*template.js* | ||
| rm dist/*template.mjs* | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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