sfcc-sfra-client-side-js — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited sfcc-sfra-client-side-js (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Guide for building and extending client-side functionality in SFRA storefronts.
[ ] Use `paths` alias for base extension, never edit app_storefront_base
[ ] Register assets in main request context, not remote includes
[ ] Compose client overrides—no `module.superModule` (server-only)
[ ] Generate controller URLs server-side; pass via `data-*`
[ ] Always include CSRF token on POST (form.serialize() or manual)
[ ] Cache selectors & delegate events for dynamic regions
[ ] Batch DOM updates; debounce high-frequency handlers
[ ] Escape dynamic data inserted into DOM (XSS prevention)
[ ] Provide keyboard & screen reader accessible interactions
[ ] Namespace events for clean teardownSFRA separates:
paths aliases) – affects client/default/Client behavior is determined at build time, not by the live cartridge path.
app_custom_mybrand/
cartridge/
client/
default/
js/ # Feature modules
scss/ # Sass sourceGuidelines:
app_storefront_base directlyproduct/, checkout/)npm run compile:js
npm run compile:scssKey distinction: Changing the BM cartridge path does not affect already compiled bundles. Recompile after structural changes.
Register assets in ISML:
assets.addJs('/js/product/detail.js');
assets.addCss('/css/product.css');Caveat: Assets added inside remote includes live only in that request and won't appear in the parent page.
Extension uses build-time composition, not runtime inheritance.
paths in package.json{
"paths": {
"base": "../storefront-reference-architecture/cartridges/app_storefront_base/"
}
}'use strict';
var base = require('base/product/detail');
// Override existing
base.updateAddToCartButton = function (update) {
$('button.add-to-cart').attr('disabled', !update.readyToOrder);
// Custom enhancement
};
// Add new
base.initNotifyMe = function () {
$('body').on('click', '.notify-me', handleNotifyMe);
};
module.exports = base;Note: module.superModule does NOT exist client-side.
var $price = $('.product-price');
$price.text('$99.99');
$price.addClass('updated');// Robust (survives DOM replacement)
$('.product-grid').on('click', '.add-to-cart', handler);var html = products.map(p => `<li>${escapeHtml(p.name)}</li>`).join('');
$('#product-list').html(html);$('#newsletter-form').on('submit', function (e) {
e.preventDefault();
var $form = $(this);
$form.spinner().start();
$.ajax({
url: $form.data('action-url'),
type: 'post',
data: $form.serialize(), // Includes CSRF
success: function (data) {
$form.spinner().stop();
showMessage(data.success ? 'success' : 'danger', data.message);
},
error: function (err) {
$form.spinner().stop();
var msg = (err.responseJSON && err.responseJSON.message) || 'Error occurred';
showMessage('danger', msg);
}
});
});| Technique | Benefit |
|---|---|
| Debounce typing | Reduces AJAX bursts (250-400ms) |
| Cache selectors | Lower DOM traversal cost |
| Build HTML in memory | Fewer reflows |
| Event delegation | Handles dynamic content |
function debounce(fn, wait) {
var t;
return function () {
clearTimeout(t);
var args = arguments, ctx = this;
t = setTimeout(function () { fn.apply(ctx, args); }, wait);
};
}
$('#search').on('input', debounce(fetchSuggestions, 300));<button>, <nav>)aria-live="polite" to AJAX-updated regionsaria-expanded, aria-pressed)$('.filter-toggle').on('click', function () {
var $panel = $('#filter-panel');
var expanded = $(this).attr('aria-expanded') === 'true';
$(this).attr('aria-expanded', !expanded);
$panel.toggleClass('is-open', !expanded);
if (!expanded) {
$panel.find('input,button,a').first().focus();
}
});Always escape dynamic data:
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
// Or use jQuery
$('<div>').text(userInput); // Auto-escapesNever trust: query params, form inputs, server-returned user data.
'use strict';
var selectors = { container: '.favorites-container' };
function bindEvents($root) {
$root.on('click.favoritesAdd', '.favorite-add', onAdd);
}
function unbindEvents($root) {
$root.off('.favoritesAdd');
}
module.exports = {
init: function () { bindEvents($(selectors.container)); },
destroy: function () { unbindEvents($(selectors.container)); }
};Guidelines:
.featureAction)init() + optional destroy()User Input → HTML5 check → (fail) prevent + style
→ (pass) AJAX submit → Server re-validate → Success OR Error JSON
→ Error: map messages → show per-field + global feedbackAlways validate both client (UX) and server (authority).
Resource.msgf with placeholdersvar template = $('#banner').data('welcome-template'); // "Welcome, {name}!"
$('#banner .text').text(template.replace('{name}', firstName));~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.