Which templates a site can actually reach, and therefore which classes its stylesheet must carry.
A UI kit is installed once and shared by every site, so its components are written to
runtime/__global__/component/*.template and the CSS compiler used to glob the whole directory.
With Chelekom that is 251 templates and 2.1 MB — the great majority of everything the compiler
scans — and a site that uses one alert paid for all of it on every page load.
Reachability is decidable here, because a component reference in a runtime template is a literal:
<.component component_name="chelekom-alert" site="localhost">So the reachable set is a graph walk from a site's ROOTS: the pages and error pages a visitor can ask for, plus the layouts those pages use. Everything else earns its place by being named. A component's scope decides where it may be USED, never whether its CSS ships — a global component reaches a site's stylesheet on exactly the same terms as one of the site's own, by that site rendering it.
Components compose, so the walk is transitive: chelekom-form-wrapper pulls in chelekom-error,
which pulls in chelekom-icon.
Roots
A page and an error page are roots because a visitor can ask for either directly. A LAYOUT is not:
it is chosen by a database relation rather than named in markup, so it cannot be walked to from
the text alone. Treating every layout as a root meant a site with thirty layouts and five pages
scanned twenty-five it can never render, and put every GLOBAL layout into every site's stylesheet.
MishkaCmsCore.Runtime.Compilers.TailwindCompiler.closure_roots/2 reads pages.layout_id and
passes the layouts a site's pages actually name; the default here is what a bare directory can
know on its own, which is the pages and error pages.
An error page needs no layout root: BootHandler.build_error_template_content/1 already writes
its layouts' markup into the error template itself.
A component named through its own attribute
A component may draw another it is told the name of:
<.component component_name={@card} site={site_name()} doc={row.doc} />When card is an attribute the component declares and never assigns itself, the names it can
take are known: its default, when that is a non-empty string, and every literal a reached caller
writes for it — <.component component_name="doc-workspace-list" card="doc-card" …>. Those names
are walked as literal references are. The component they name may hand a name on in turn, so the
walk repeats until nothing new is reached.
WHICH ATTRIBUTES QUALIFY IS DECIDED WHERE THE COMPONENT'S CODE IS AT HAND, when its template file
is written: attribute_marks/1 writes a line per attribute into the file and the walk reads it
back, so it asks nothing of the database. component_name={@name} in a file with no line for
name — not declared, assigned by the component's own code, read inside a helper, or a file
written before these lines existed — is a reference the walk cannot resolve.
When the answer is "everything"
A reference the walk cannot resolve makes the closure unsound, and for_site/2 returns
:unbounded. Callers must fall back to globbing the whole directory. This is the whole safety
story: an under-approximated closure means a component renders with no CSS at all, which is worse
than a large stylesheet, and it would show up in production rather than in a build.
A reference cannot be resolved when, in a reached template:
component_name={…}is any expression but{@name}, or{@name}sits in a page, a layout or an error page, or a<.component>tag names nothing and spreads{…};{@name}names an attribute the file has no line for;- a call to a component that names one through
namehandsnameover as an expression (card={…}), or spreads{…}into the tag, or draws the component from stored content, whose markers an author writes — that CALLER is the template named; - once anything is named through an attribute, a template calls
component(…)orsend_update(…)from code, or holds a<.component>tag the walk cannot read.
Each degrades to correct-and-large rather than small-and-broken.
reached_components/2 answers what the walk did reach either way, for a caller that needs only
what is certainly reached — the components a page builds before it draws them
(MishkaCmsCore.Runtime.ComponentPrefetch).
Which copy of a component
A stylesheet looks a name up in the site's own components first and in the global ones after, and
either copy found is scanned. reached_components/2 answers which MODULE a render draws, so it
reads the tag's site as MishkaCmsCore.Runtime.LiveViewHelpers.component/1 does:
- no
site, orsite="Global"— the global component; site="<this site>"— the site's own, and nothing when it has none;site="<another site>"— nothing, as the render refuses it;site={assigns[:site]}orsite={@site}in a component — the scope that component was found in;- any other expression, and a name written outside a tag — the site's own, else the global one.
Components named in stored content
A template that draws stored content — <.rich_content …/> — reaches the components the
content's markers name, which no template shows (MishkaCmsCore.Runtime.ContentComponents). The
caller passes those names as :content_components, and such a template names exactly them, each
resolved as a component_name="…" is. A site where nothing reached draws content reaches none of
them.
Usage
CssClosure.for_site("localhost")
#=> %{components: ["chelekom-alert", "chelekom-icon"], templates: [...], scanned: 8, dynamic: [],
#=> resolved: []}See MishkaCmsCore.Runtime.Compilers.TailwindCompiler, which turns the result into @source
lines, and Mix.Tasks.Mishka.Css.Closure, which prints it.
Summary
Types
A component the walk reached: its name, and the site a <.component> tag names to draw it — the
site's own name for a component of its own, "Global" for a global one.
What one template names, read by references/1 where the template is in hand, for
reached_components/2 to walk from without a file.
A component, the attribute it names a component through, and every name that takes.
Everything a site's stylesheet has to be built from, or :unbounded when a reference the walk
cannot resolve makes it unsound.
Functions
Every global component, for callers that need the fallback set.
The lines a component's template file carries so for_site/2 can follow
component_name={@name} through name: one for each attribute the component's markup names a
component through, that it declares, and that none of its code assigns or reads inside a helper.
"" when there are none.
The components references names in a tag that says which copy it draws — site="Global", no
site, or site the name site_name — each as %{name: name, site: site}. A tag naming another
site, naming its component through an expression, or saying its site through one is left out.
The runtime template tree the compiler writes to.
The pages and error pages of scopes — the roots a bare directory tree can work out for itself.
Walks a site's roots and returns every template its stylesheet has to be built from.
Every component the walk is sure a site reaches, each with the site its tag draws it for, and whether that is all of them.
What the template source names, for reached_components/2 to walk from as :references — the
page or layout module built from it keeps the answer, so a render can walk what it names without
reading a file. Read as a page's markup is: no attribute of it names a component.
Types
A component the walk reached: its name, and the site a <.component> tag names to draw it — the
site's own name for a component of its own, "Global" for a global one.
@opaque references()
What one template names, read by references/1 where the template is in hand, for
reached_components/2 to walk from without a file.
A component, the attribute it names a component through, and every name that takes.
@type result() :: %{ components: [String.t()], templates: [String.t()], scanned: non_neg_integer(), dynamic: [String.t()], resolved: [resolved()] } | :unbounded
Everything a site's stylesheet has to be built from, or :unbounded when a reference the walk
cannot resolve makes it unsound.
components are the reachable component names, for a reader. templates are the resolved paths
of EVERYTHING that must be scanned — the roots, the layouts they use and the components they
reach — because a name is not enough to point a compiler at a file: it may resolve into the
site's own component directory or into the global one, and only the walk knows which.
resolved is every reached component that names a component through one of its attributes, with
the names that attribute was found to take.
Functions
Every global component, for callers that need the fallback set.
The lines a component's template file carries so for_site/2 can follow
component_name={@name} through name: one for each attribute the component's markup names a
component through, that it declares, and that none of its code assigns or reads inside a helper.
"" when there are none.
<%!-- css-closure attr=card default="doc-workspace-card" --%>The default is written when it is a non-empty string without a quote or a line break. Code that
does not parse, or that assigns an attribute whose name it works out at runtime, marks nothing —
see MishkaCmsCore.Runtime.PageBuilder.CodeReads.assigned/1.
component is the component's row, or the same record decoded from a job, with string keys.
MishkaCmsCore.Runtime.Handlers.BootHandler.build_component_class_content/1 writes the lines into
the file.
@spec certain_components(references(), String.t() | nil) :: [reached()]
The components references names in a tag that says which copy it draws — site="Global", no
site, or site the name site_name — each as %{name: name, site: site}. A tag naming another
site, naming its component through an expression, or saying its site through one is left out.
site_name is the site of the page or layout the template belongs to, nil for a global one.
references = CssClosure.references(~s|<.component component_name="card" />|)
CssClosure.certain_components(references, "acme")
#=> [%{name: "card", site: "Global"}]
@spec default_runtime_dir() :: String.t()
The runtime template tree the compiler writes to.
The pages and error pages of scopes — the roots a bare directory tree can work out for itself.
Walks a site's roots and returns every template its stylesheet has to be built from.
A SITE'S OWN COMPONENTS ARE WALKED, NOT ASSUMED. They used to be roots: every template under the site's directory seeded the walk, so all of them were reachable by definition and all of them were scanned. On a real install that is 176 components of which the pages reference a handful, and the rest contributed their classes to the stylesheet every visitor downloads.
:unbounded means a reached template names a component in a way the walk cannot resolve — see
"When the answer is everything" above. The caller must scan every component instead, and the
templates responsible are logged.
Options
:runtime_dir— the template tree to walk. Defaults todefault_runtime_dir/0.:roots— the templates a visitor can reach. Defaults to every page and error template in the site's own scopes;TailwindCompiler.closure_roots/2adds the layouts those pages use.:extra_scopes— sibling scope directories to walk as roots alongside the site's own. A draft preview writes its unsaved template into a throwaway namespace, and that draft may be the only thing referencing the component being previewed — a walk over the SAVED site would miss it and the preview would render unstyled, which is precisely the state the author is checking.:content_components— the component names the site's stored content uses: what a template that draws content names.[]unless given.
@spec reached_components(String.t() | nil, keyword()) :: %{ complete?: boolean(), components: [reached()] }
Every component the walk is sure a site reaches, each with the site its tag draws it for, and whether that is all of them.
The walk of for_site/2, with its options, and two differences. Where for_site/2 answers
:unbounded, this still answers what the walk did reach, with complete?: false: a reference it
cannot resolve may name more. And a name is resolved to the copy a render draws, by the tag's
site — see "Which copy of a component" above.
site_name may be nil for a render that is for no site, which reaches the global components
alone.
Options
Those of for_site/2, and:
:references— templates to walk from as well as:roots, each read byreferences/1.
Examples
CssClosure.reached_components("localhost")
#=> %{complete?: true, components: [%{name: "chelekom-alert", site: "Global"}]}
references = CssClosure.references(~s|<.component component_name="card" site="localhost" />|)
CssClosure.reached_components("localhost", roots: [], references: [references])
#=> %{complete?: true, components: [%{name: "card", site: "localhost"}]}
@spec references(String.t() | nil) :: references()
What the template source names, for reached_components/2 to walk from as :references — the
page or layout module built from it keeps the answer, so a render can walk what it names without
reading a file. Read as a page's markup is: no attribute of it names a component.