Skip to content

feat: import shared modules and module-level values - #69

Merged
alexgrozav merged 7 commits into
milestone-m5-uxf-443from
shared-modules-uxf-457
Oct 28, 2026
Merged

alexgrozav merged 7 commits into
milestone-m5-uxf-443from
shared-modules-uxf-457

Conversation

@alexgrozav

@alexgrozav alexgrozav commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

What changed

Components can read values from shared .ts modules and from their own module's top-level consts (ADR-0070).

  • Analyser: bindings. A value import of a relative .ts module is an import binding. A top-level const is a moduleConst binding. Both are read-only: code that writes one is UF2011, and code that changes one in place is UF2004. The module records moduleImports and the constants its components read.
  • Analyser: module constants. A constant is a plain value. It is either a function, whose body runs once called, or any other value, which runs as the module loads.
    • It reads the module's other constants, its imports and the pure globals (UF3020).
    • As the module loads, it reads no later constant, either directly or through a function it calls (UF2023).
    • It changes in place only a value it builds itself: a literal, a new, a copy (filter, map, slice, … of anything but an object literal), or a function's local that only ever holds one. A parameter never counts, except a reduce accumulator. So Object.freeze(…), "filter, then sort", and a helper that pushes to its own array are fine. Writes, delete, mutating methods and Object.assign on anything else are UF2004, and a function writes only its own locals (UF2011).
    • An arrow runs at load only where it is called in place, passed to a call or called through a member (O.m()). One that a value holds runs once called, and a table of functions passed to Object.keys is only read. A local type a constant names is declared with it.
    • JSX is UF3012. this, import.meta, import(), function expressions, classes and computed calls are UF1002.
    • Chance, the clock and the locale are UF3019 as the module loads, and a global only client code may read is UF3020. Inside a function, both are reported where a template, a getter or an initial value calls or reads it, or a value holding it (XS.map(fmt), O.fmt()), as UF3019 or UF2014, as setup does. Client code may call it.
    • A let, a destructured or exported const, and a const that declares two names are UF1002.
  • Resolver and compile(). Resolver.module reads a shared module and follows its relative imports through every .ts module they reach, including import() and import x = require(). compile() copies each module, unchanged, into the output trees beside the component files. It also lists each one in CompileResult.dependencies.
  • Diagnostics.
    • A shared module that imports a framework or unframework, directly or through another .ts, is UF1204, and its component compiles for no target. The code's case is diagnostics/framework-in-shared-module.
    • A value import of a module the resolver cannot read, or a load it cannot follow, is UF1202.
    • A copy named like a component's output file is UF1104. That covers the module's own components and its children, and outputs import a component's file without the extension.
  • Unplugin. A target that joins a file's outputs (Angular, React, Solid, Qwik) leaves the copies out of the module.
  • Harness. caseOutputs keeps a path that two sources produce once, when the contents are the same. The analyser's corpus test reads a case's .ts value modules.
  • Wording. UF2033's description no longer names a milestone. UF1104, UF1202, UF2004, UF2023 and UF3020 now cover shared modules and module constants, in the catalogue and on the diagnostics page.

Framework output: no component's output changes on any target. Each output tree gains a copy of the .ts modules its source reaches. Until each target's composables lane lands, a target still reports UF1002 where a component reads an import or moduleConst binding, and no corpus case does that yet.

Checklist

  • The title follows Conventional Commits, as in feat(web): sell the next framework on the homepage.
  • pnpm format:check, pnpm lint, pnpm check-types and pnpm test pass. The first three pass. The tests of every touched package pass, and so does the full integration suite on all seven targets, plus the L1 and L2 canaries. The root pnpm test was not run as one command.
  • Changes to a published package: a changeset from pnpm changeset (see .changeset/README.md). Not needed: every package is private.
  • Homepage changes: pnpm build, then pnpm --filter web lighthouse, still scores at least 0.95 in every category. Not needed: no homepage change.

A component reads values from relative .ts modules and its module's
top-level consts, each through a read-only binding of its own (import,
moduleConst). The module records its value imports and the constants its
components read, and a constant's value reads only the module's other
constants, its imports and the pure globals (ADR-0070).

The resolver reads a shared module and follows its relative imports
through every .ts module they reach. compile() copies each into the
output trees beside a component's files, unchanged, and lists it in
CompileResult.dependencies. A shared module that imports a framework or
unframework, itself or through another .ts, is UF1204, with its case
diagnostics/framework-in-shared-module, and the component compiles for no
target. A value import of a module the resolver cannot read is UF1202.

The harness keeps a path two sources produce once when the contents are
the same, and the analyser's corpus test reads a case's .ts value
modules. UF2033's description says where a key lives instead of naming
M5.

Co-authored-by: multica-agent <github@multica.ai>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://shared-modules-uxf-457-unframework.blue-water-0397.workers.dev (commit bceb2e5)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://caac3882-unframework.blue-water-0397.workers.dev bceb2e5 2026-10-28T08:16:06.089Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://738ff354-unframework.blue-water-0397.workers.dev d45e97d 2026-10-28T08:00:28.771Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://4bcbbdc9-unframework.blue-water-0397.workers.dev cd54782 2026-10-28T07:40:23.660Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://920ee64b-unframework.blue-water-0397.workers.dev 81598c0 2026-10-28T07:35:09.312Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://fac36ffa-unframework.blue-water-0397.workers.dev d889612 2026-10-27T22:19:47.925Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://7b662e73-unframework.blue-water-0397.workers.dev 9a0b3c3 2026-10-27T21:50:29.286Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://e86cf62d-unframework.blue-water-0397.workers.dev 7836262 2026-10-27T21:18:21.636Z Visit the dashboard ↗

alexgrozav and others added 6 commits October 27, 2026 23:42
… of modules

A module constant's value is checked as setup code is: JSX is UF3012,
chance, the clock and the locale UF3019, import.meta and import() UF1002,
and a write of a module's value UF2011, or a change in place UF2004. A
read of a constant declared after it, outside a function, is UF2023.

A shared module's imports include import() with a literal specifier and
import x = require(), so UF1204 sees a framework loaded either way, and
the module a lazy import names is copied too. The resolver reads each
module once, and tests pin the unframework arm of UF1204.

The unplugin joins only components' files, so an Angular module no
longer holds the copies. A copy at a component's output file is UF1104
on that target, at the import that reaches it.

Co-authored-by: multica-agent <github@multica.ai>
…om outputs

A module constant is a function, whose body runs once called, or any
other value, all of which runs as the module loads. Nothing in it
changes a value in place, through as, !, patterns, aliases and computed
calls too (UF2004), and a function writes only its own locals (UF2011).
this, function expressions and computed calls are UF1002.

As the module loads, a value reads no later constant, itself or through
the functions it calls (UF2023), and a type it names reads nothing.
Chance, the clock and the locale in a function mark it, and a template,
a getter or an initial value that calls it reports UF3019, as setup
does; client code may call it.

A copy named like a component's output file without its extension, the
module's own or an imported child's, is UF1104 for every target. A
shared module's import() of a template without holes is followed, and
any other import() or import.meta.glob is UF1202.

Co-authored-by: multica-agent <github@multica.ai>
A module constant changes in place only a value it builds itself: a
literal, a new, a copy, or a function's local that only ever holds one,
a reduce accumulator too. So Object.freeze({ ... }) and a helper that
pushes to its own array are plain.

A global only client code may read, in a module function, waits for its
call: a template, a getter or an initial value that calls it reports
UF2014, as setup does. An arrow a value holds runs once called, so it
may read a later constant. A local type a constant writes is declared
with it. A test pins UF1104's kebab-case name.

Co-authored-by: multica-agent <github@multica.ai>
A parameter holds what its caller gives, so only a reduce accumulator
counts as fresh, and a copying method gives a fresh value only on an
array. Defaulting a parameter, then filling it, no longer changes a
module's value silently (UF2004).

An arrow a value holds runs where a value calls it through a member as
the module loads (O.m()), so what it reads counts (UF2023). A module
function a template, a getter or an initial value reads, or a value
that holds one, is judged as a call (UF2014, UF3019), and so is one
passed to a call as the module loads.

Co-authored-by: multica-agent <github@multica.ai>
A copying method's result is fresh whatever its receiver, but an object
literal, whose own method may give what it holds: filter, then sort, on
a parameter's member or a module table no longer reads as a change of
it. Freezing a value changes nothing anyone reads.

A value passed to a call as the module loads counts as called only when
it is a function constant, and a call whose result cannot hold it
(Object.keys, JSON.stringify, String) only reads it: a table of functions
passed to Object.keys neither runs nor carries them.

Co-authored-by: multica-agent <github@multica.ai>
@alexgrozav
alexgrozav merged commit 7a2f9ee into milestone-m5-uxf-443 Oct 28, 2026
49 checks passed
@alexgrozav
alexgrozav deleted the shared-modules-uxf-457 branch October 28, 2026 08:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant