Architecture
Context document for anyone (human or agent) touching this codebase. Read this before adding a module or touching anything that talks to Sitecore.
The two worlds
Since Manifest V3 the extension runs in two JavaScript worlds inside every Sitecore page:
| World | Bundle | Runs at | Can touch |
|---|---|---|---|
| Isolated | sc_ext/Application.js | document_end | DOM, chrome.* APIs, extension options |
| Main (page) | chrome/sitecoreBridge.js | document_start | DOM, page globals (Sitecore, scForm, scSitecore) |
They share the DOM and nothing else. The isolated world cannot see Sitecore or scForm; the main world cannot see chrome.storage or the options. Referencing Sitecore from isolated code throws ReferenceError: Sitecore is not defined.
The extension’s Manifest V3 content-script configuration is the source of this split: chrome/sitecoreBridge.js is the MAIN-world entry point at document_start; chrome/contentscript.js, common/optionsProvider.js, the library bundle, and sc_ext/Application.js are isolated-world entry points at document_end. The background service worker is chrome/background.js.
Modules own both halves
A module is a folder under app/sc_ext/modules/. If it needs page globals it gets a second file in the same folder, named *.page.ts, which is compiled into the main world bundle:
modules/placeholder/
_all.ts references for the isolated bundle
PlaceholderModule.ts isolated: options, Location check, DOM, UI
Placeholder.page.ts main world: Sitecore.PageModes, LayoutDefinition
Gulp decides which bundle a file lands in purely by name:
app/sc_ext/page/*.page.tsandapp/sc_ext/**/*.page.ts→app/chrome/sitecoreBridge.js(main world), tasktypescript_page- every other
app/sc_ext/**/*.tsexcept typings →app/sc_ext/Application.js(isolated), tasktypescript_sc_ext
The two bundles are separate compilations: *.page.ts files cannot import isolated types and vice versa. Cross-world types are plain JSON, nothing else.
The bridge
app/sc_ext/page/PageBridge.page.ts is transport only and knows no feature names. It offers three primitives to page-world code:
ScExtPage.register(name, handlers); // expose an agent to the isolated world
ScExtPage.emit(name, payload); // page -> isolated notification
ScExtPage.proxy(target, method, after); // wrap a Sitecore function, run after it
and app/sc_ext/SitecorePageBridge.ts is the isolated-world end:
SitecorePageBridge.invoke('placeholder', 'initialize', [args]); // isolated -> page call
SitecorePageBridge.on('page:changed', () => this.refresh()); // page -> isolated event
Registration is the allowlist. A target/method pair that nobody registered is unroutable, so there is no separate list of permitted calls to keep in sync. Handlers must be own properties; inherited members such as constructor or toString are rejected.
Built-in agents and events
| Name | Kind | Meaning |
|---|---|---|
scForm, scSitecore | agents | forward invoke / postEvent / postRequest / resume to the real page globals |
page:changed | event | Sitecore rerendered part of the page (scForm.invoke, scForm.resume, scSitecore.postEvent) |
Anything that decorates Content Editor DOM must re-run on page:changed; Sitecore replaces whole chunks of markup on every request.
Adding a module
- Create
app/sc_ext/modules/<name>/with_all.tsand<Name>Module.ts. - Extend
ModuleBase, implementISitecoreExtensionsModule:canExecute()— options plus aContext.Location()check. It runs outside the manager’s try/catch, so it must never throw and must never touch page globals.initialize()— wire up DOM, events and bridge calls.
- Reference the folder’s
_all.tsfromapp/sc_ext/_all.ts. - Construct and
addModule()it inapp/sc_ext/Application.ts, passingwrapper.getModuleOptions('<Display Name>'). The display name string is the options key — it must match the options page. - Add the module to the options page under
app/options/if it has settings. - If it needs
SitecoreorscForm, add the page half below.
Adding a page-world half
/// <reference path='../../page/PageBridge.page.ts'/>
namespace ScExtPage.MyAgent {
ScExtPage.register('myFeature', {
initialize: function () {
var sitecore = window['Sitecore'];
if (!sitecore || !sitecore.PageModes) {
return; // older Sitecore versions: degrade, never throw
}
ScExtPage.proxy(sitecore.PageModes.DesignManager, 'insertionStart', function () {
ScExtPage.emit('myFeature:changed');
});
}
});
}
The module then does:
initialize(): void {
SitecorePageBridge.on('myFeature:changed', () => this.refresh());
SitecorePageBridge.invoke('myFeature', 'initialize');
}
Rules that bite:
- Everything crossing the boundary is structured-cloned. No DOM nodes, no functions, no Sitecore objects. A callback cannot be passed; emit an event instead.
postEventarguments are nulled on the isolated side and re-filled withdocumenton the page side for this reason. - Agents must be idempotent.
initializecan be called again; guard the proxies you install. - Messages are forgeable. A hostile page can post a
sc_ext_page_callorsc_ext_page_event, so a handler may only do things the page could already do to itself. Never route options, storage orchrome.*access through the bridge. - Never give a
*.page.tsbundle output a name that collides withapp/chrome/*.ts. Both tasks write intoapp/chrome/, andtypescript_chromeruns last.
Checks
npm run build # compile and publish development output
npm run selfcheck # run all self-check scripts
scripts/selfcheck.js discovers every scripts/*.selfcheck.js file. The pageBridge.selfcheck.js check loads app/chrome/sitecoreBridge.js in a vm sandbox and verifies routing, own-property guarding, rejection of messages from other windows, event shape, and proxy semantics. Run npm run build first so the bundle exists.