ReviewOS

stacks/stx

@stacksjs/components names do not resolve by bare tag, so <Dialog /> silently renders nothing

#2011
Closed glennmichael123 opened this 9 days ago ·
9 days ago

Split from #2004, which was closed once the dangerous half of this was fixed. That issue's first and preferred expectation was to make the names resolve, and that part is untouched.

What happens

@stacksjs/defaults' components answer to a bare tag. @stacksjs/components' do not, and need an explicit import in a <script client> block.

bare <Name>resolves?
Badge, Card, Avatar, Image, Videoyes, because they are also in @stacksjs/defaults
Skeleton, Spinner, Switch, Progress, Dialogno

Since 988a0e627f the failure is quiet rather than hazardous: an HTML comment in the page and a console.error carrying the searched paths, instead of a block of absolute filesystem paths in the body. So this is no longer a production incident, just an invisible one.

Dialog is still the one that hurts. It is the documented way to build a modal, it is in the non-resolving group, and an app can ship <Dialog :open="…"> with nothing where the modal should be.

Why

Resolution searches project directories only:

// packages/stx/src/utils.ts:748
push(path.resolve(configuredRoot, 'resources/views/components'))
push(path.resolve(configuredRoot, 'resources/components'))

Nothing consults an installed package, so no amount of having @stacksjs/components in package.json makes a bare <Dialog /> resolve.

Options

  1. Let an installed component package register its own names, so @stacksjs/components answers to a bare tag the way the defaults do. This is what the docs already imply, since they show the components without an import.
  2. Document the import requirement where the components are documented. Cheaper, and it leaves the two packages behaving differently for no reason a reader can see.

(1) is the one worth doing. The current split is not a design, it is an accident of which package the defaults happen to live in.

Reproducing

<!-- resources/views/anything.stx -->
<Dialog :open="true" />

curl the page: an HTML comment where the dialog should be, and the paths on the dev server's console. Add import { Dialog } from '@stacksjs/components' in a <script client> block and it renders.

Measured on main at 0.2.345.

9 days ago

Fixed in a511c766b3.

Option 1, as the issue argued. The mechanism already existed — @stacksjs/components ships ./stx-plugin registering src/ui and src/components — it just required a config line nobody was told about. componentPackages, defaulting to ['@stacksjs/components'], is appended to whatever the config lists, loaded through each package's own stx-plugin export and only when the project actually has it installed.

Measured on a throwaway project with no plugins line, before and after:

beforeafter
Dialogunresolvedrendered
Spinnerunresolvedrendered
Switchunresolvedrendered
Progressunresolvedrendered
Skeletonunresolvedrendered

Zero unresolved markers after.

Two ordering properties make this safe to do by default rather than on request:

  • The dirs land last in _pluginComponentDirs, which is itself searched after every project directory. A component you wrote by the same name still wins, so adding this dependency cannot restyle an app — pinned by a test that puts a project Spinner.stx beside a library <Dialog> and checks the project copy renders while the package still supplies what the project did not write.
  • A plugin the config already lists is skipped, not registered twice. The component dirs are pushed into one array, so a duplicate would make every resolution search the same two directories again.

Escape hatches, both in the stx config: componentPackages: [] opts out entirely, and adding a name opts another package in.

One extra change was needed. The in-loop plugin resolve now passes paths: [effectiveCwd], so a plugin resolves against the project rather than against stx — the wrong answer in a monorepo, and the whole question being asked here.

A test that was passing for the wrong reason

My absent-package case built its project inside this checkout, and require.resolve walks up the directory tree, so the package always resolved and the assertion (Array.isArray(...)) could not fail. It uses a directory outside the repo now and asserts the real behaviour: nothing registered, nothing thrown, no warning, and the component left unresolved — an app that does not depend on the package should not hear about it.

Both docs corrected. docs/guide/using-components.md had plugins: ['@stacksjs/components/stx-plugin'] as its Setup section; that section now says there isn't any setup, and records why the old line was load-bearing.

Sign in to comment on this issue.