A component package is a set of Smart components served from its own base and
carrying its own rules, loaded by the same Loader as everything else. It is how
the Framework is extended: a component lives with its owner, and a product or
an external author ships its components as a package rather than adding them
to the Framework (owner's decisions of 2026-10-11). The standard is
simai.component-package 1.0.0 (contracts/standards/simai.component-package/).
Larena's components are the first package, larena: sf-table,
sf-data-view, sf-property-view and sf-admin-menu are not in Core's rule
list or in ui; they live in the component folder of simai/larena-ui, open
under Apache-2.0, and arrive this way. They went to a package named pro
first; SIMAI Framework Pro was dropped the same day, and the change journal
records both moves (component-pro-package-move,
component-larena-package-move). The Loader side is in
src/core/js/loader.js (registerPackage, resolveSmartBase,
getMissingFileKey).
The ui distribution carries Smart in distr/smart, beside distr/core
(release ui-6b4a0c3887d6 on). A page sets window.sfPath to its copy of
distr/ and nothing else; Smart resolves to <sfPath>/smart.
window.sfSmartPath still works for a page that serves the separate ui-smart
repository from its own root, and an empty value counts as not set.
tests/loader-packages.contract.test.mjs.
The page declares where each package is served, in the boot configuration,
before core.js:
<script>
window.sfPath = '/assets/ui/distr/';
window.SF_BOOT_CONFIG = {
packages: {
larena: {base: '/assets/larena/smart'},
},
};
</script>
<script src="/assets/ui/distr/core/js/core.js"></script>- Name: lower-case letters, digits and hyphens. It is the key the package's rules are filed under and the prefix of its missing-file entries.
base: the directory holding the package's component folders, laid out likedistr/smart:<base>/<folder>/js/<folder>.jsand<base>/<folder>/css/<folder>.css, where the folder is the rule name without itscl-prefix.rule(optional): the URL of the package's rule file. Default<base>/rule.js.
There is one place to declare a package. SF_BOOT_CONFIG already holds the
structured settings (smart, preloader, icons, the cache versions); the
sf*Path globals are single strings and stay as they are.
The Loader fetches each declared package's rule file itself, before its first
scan of the page. The page does not add it as a script. Two reasons: Core
creates the rule map (SF.RuleLoader) when it starts, so a rule file that ran
before Core would have nowhere to register; and with the rules in place before
the first scan, the package's tags are known when the page is first read and a
cached plugin list never names a rule that has not arrived. A page that
declares no package starts exactly as before, synchronously.
The file is a plain script with one call:
SF.RuleLoader.registerPackage('larena', {
rules: {
'cl-table': {
tags: ['sf-table'],
type: 'smart',
mode: 'smart',
js: true,
css: true,
relation: [{name: 'cl-icons'}, {name: 'cl-pagination'}],
},
},
});- A rule is written exactly as a Core Smart rule is; the Loader stamps
package: '<name>'on it, and that field is what sends its files to the package base. Relations name rules by key and each resolves from its own package, so a larena rule relating tocl-iconsfetches the icons from the Smart base. - A relation the element renders only later -- a filter editor, a date
picker, a cell renderer for a type a page may not hold -- is marked
{name: 'cl-datepicker', lazy: true}. The element does not wait for it and the page does not hold its first paint for it: the Loader fetches it aftersf-loader-ready, in the browser's idle time, and at once when it is asked for (SF.Loader.ensurePlugin('cl-datepicker'), or the element appearing in the document). An element that creates a lazy one and calls it straight away waits forcustomElements.whenDefinedfirst. - A package's elements may carry its owner's prefix (
lr-records-list) rather than the Framework'ssf-, and a new element should (standardsimai.component-package, own-element-prefix). The Loader loads any tag a package rule lists; a tag outsidesf-is claimed only by a package rule, so the Framework's names stay its own.SF.create('lr-records-list')keeps the full name; a short name (SF.create('button')) is still a Framework one. registerPackage(name, {rules, base})may also take abase. The page's declaration wins when both are given: where the bytes are served is the deployment's decision, not the bundle's.- It returns the rule keys it registered. It is not enumerable on
SF.RuleLoader, so nothing that walks the rules mistakes it for one. - A package may take over a Core rule of the same name that has not loaded yet. While a component moves into a package, Core still ships its old rule, and a page that declared the package chose the package's bytes. A rule that has already loaded is kept, with a warning (a custom element cannot be defined twice), and so is a rule another package owns.
registerPackage can also be called after the page is up, by a page that
loads a package of its own. The Loader drops what it derived from the rule map
(the attribute index, the regexp scan list, both match caches) and reads the
page again, so an element that was in the document before its rule arrived is
loaded. tests/browser/loader-packages.browser.mjs.
Nothing is fetched from the Smart base in a package's place: that would load whatever an older Core shipped under the same name, or 404 and be cached as missing there. Instead the Loader says, once per rule, which tag could not load and why:
- the rule names a package the page has not declared;
- the package has no base;
- the rule file could not be loaded, or loaded and registered nothing.
A file missing from one base is not missing everywhere. Entries for a
package's rules are kept as <package>::<rule> in SF_MISSING_PLUGINS, so a
404 from a package base never suppresses the same name in the Smart base, or
the other way round. Smart-base entries keep their bare names, the format
browsers already hold. Revalidation forgets a package entry only when a rule of
that package is registered under the name.
A package bundle carries its own copy of src/core/js/page-language.js, as
every Smart bundle does. The language state (registered words, listeners, the
chosen language) lives on globalThis.__sfPageLanguage, never in module scope,
so every copy reads and writes the one registry: a product that registers a
language, or calls SF.language.use(), changes base and package components
alike. tests/page-language-shared-state.contract.test.mjs fails if that
state moves back into the module.
A package's source is a folder laid out like src/smart: one folder per
component with its index.js, rule.js (writing exactly its own cl-<folder>
rule as plain data), manifest and templates, plus tests/, docs/ and a
framework.lock.json pinning one ui-source revision
(simai.framework.product-package-source-lock.v1). The folder may be a
repository's root or a folder inside a product's own repository; Larena's is
the component folder of simai/larena-ui.
ui-builder builds it over the pinned ui-source, from committed bytes, twice,
and the two waves must be identical (docs/developer/product-packages.md in
ui-builder). The output is laid out for exactly the declaration above: a
smart/ directory that is the package base, with smart/rule.js -- one
SF.RuleLoader.registerPackage('<name>', {rules}) call generated from the
components' rule files -- at the default place, so a page needs only
window.SF_BOOT_CONFIG = {packages: {larena: {base: '/assets/larena/smart'}}};Every build a consumer uses has a receipt, package-receipt.json: the package,
its source repository, revision and path, the ui-source revision and the free
release ui-<12> it was built over, the builder, and the hashes of what it
wrote. The owner delivers the build inside its own distribution; there is no
separate distribution repository and no built bytes in git.
While the free recipe recipe.admin.collection still places Larena's data
view, src/core/contracts/packages/larena.package.json names the package's
components so the free registry can place them. It carries no bytes, and it
leaves the free Framework together with the recipe once Larena reads both from
its own folder.
- A package's files are versioned with Core's
cacheVersion; a package with its own release cadence has no separate cache version yet. A page that serves each release under its own path (as ui-admin does) is not affected.