Skip to content

feat: add <Script> component for build-time bundled client scripts#97

Draft
khromov wants to merge 32 commits into
mainfrom
script-component
Draft

feat: add <Script> component for build-time bundled client scripts#97
khromov wants to merge 32 commits into
mainfrom
script-component

Conversation

@khromov

@khromov khromov commented Jun 14, 2026

Copy link
Copy Markdown
Owner

What

Adds a <Script> Svelte component that loads client-side scripts via dynamic import(), where the referenced paths are bundled at build time through Mochi's existing Bun.build() client pass — not fetched or transpiled at runtime.

<script>
  import { Script } from 'mochi-framework/components';
</script>
<Script src="./analytics.ts" />
<Script scripts={['./a.ts', './b.ts']} />

How it works

Reuses the hydratable-island machinery (discover → bundle → swap URL placeholder):

  1. DiscoversvelteAstPreprocess.ts recognizes Script imported from mochi-framework/components, reads the static literal path(s), resolves them relative to the .svelte file, verifies each exists, and rewrites the tag to carry injected placeholder tokens.
  2. BundleComponentRegistry.buildClientBundle() adds each unique source path as an entrypoint to the same browser Bun.build() as the islands (TS transpile, code-split, content-hash); the metafile maps outputs back to scriptEntryUrls.
  3. ResolverenderComponent() swaps __MOCHI_SCRIPT_URL__<key>__ for the hashed URL; the component emits <script type="module">import("/_mochi/client/…js")</script>.

Threaded through HMR (rebuildHydratables, recompileChanged/All) and the prebuilt manifest (toManifest/fromManifest).

Behavior

  • src (single) or scripts (array) — paths resolved relative to the component file (absolute paths work too).
  • Eager load only (runs import() immediately); no timing option, by design.
  • SSR-only — throws if hydrated.
  • Fails loudly at build/preprocess time if a path can't be resolved or isn't a static literal.

Verification

  • New tests: 7 preprocess cases + 5 component e2e/manifest cases; updated the white-box recompileBundle.test.ts stubs for the new scriptEntries field.
  • Live smoke test on the demo site: bundle emitted, transpiled, served over HTTP, no leftover placeholder.
  • bun run checks passes (lint, format, typecheck, all workspace tests).
  • Docs: packages/docs/151-script.md.

Notes

  • The module-script string is built in a small .ts helper (buildScriptTag.ts) because a literal <script>/</script> inside a Svelte <script> block breaks the parser.
  • svelte-eslint-parser treats capitalized <Script> as the HTML <script> element, so it false-flags the import as unused (no-unused-vars). The component compiles fine; suppressed in fixtures and documented (alias or eslint-disable). Open question: rename to e.g. <ClientScript> to avoid the collision entirely?

khromov added 2 commits June 14, 2026 03:36
Add a <Script src="./foo.ts" /> component that loads client scripts via
dynamic import(), where the referenced paths are bundled at build time
through Mochi's existing Bun.build() client pass (TS transpile, code-split,
content-hash) rather than fetched or transpiled at runtime.

Reuses the island discovery machinery: the preprocessor detects <Script>
imported from mochi-framework/components, resolves its static literal
path(s) relative to the .svelte file, and registers them as client-bundle
entrypoints; renderComponent swaps a placeholder for the hashed output URL.
Build fails loudly when a path can't be resolved or isn't a static literal.
@github-actions

Copy link
Copy Markdown
Contributor

Mochi review report

Try this PR

Expand instructions
gh run download -R khromov/mochi 27485750533 -n mochi-framework-pr -D /tmp/mochi-pr && bun i /tmp/mochi-pr/mochi-framework-pr.tgz

Download manually

Dependency report

Expand report
Direct: 10
Peer:   3 (svelte, @tailwindcss/node, @tailwindcss/oxide)
Dev:    8
Total unique packages reachable from production deps (roots + transitive): 27
Total on-disk size of those packages: 5.18 MB

Toplist — direct deps ranked by total size (self + transitive):
      total       self  count  package
    4.76 MB    2.71 MB     19  svelte
   680.0 kB   141.4 kB      3  svelte-shaker
   526.4 kB   441.4 kB      1  magic-string
   180.6 kB   145.3 kB      1  chokidar
    51.2 kB    51.2 kB      0  devalue
    30.4 kB    30.4 kB      0  deepmerge
    28.0 kB    28.0 kB      0  negotiator
    25.8 kB    25.8 kB      0  mitt
    25.3 kB    25.3 kB      0  js-cookie
    12.3 kB    12.3 kB      0  zimmerframe

Transitive breakdown for the heaviest deps:

  svelte (19, 2.05 MB transitive): @jridgewell/gen-mapping (91.6 kB), @jridgewell/remapping (58.0 kB), @jridgewell/resolve-uri (51.9 kB), @jridgewell/sourcemap-codec (85.0 kB), @jridgewell/trace-mapping (143.3 kB), @sveltejs/acorn-typescript (194.2 kB), @types/estree (25.5 kB), @types/trusted-types (8.4 kB), acorn (545.5 kB), aria-query (172.8 kB), axobject-query (108.3 kB), clsx (8.4 kB), devalue (51.2 kB), esm-env (3.7 kB), esrap (86.1 kB), is-reference (3.9 kB), locate-character (5.2 kB), magic-string (441.4 kB), zimmerframe (12.3 kB)

  svelte-shaker (3, 538.6 kB transitive): @jridgewell/sourcemap-codec (85.0 kB), magic-string (441.4 kB), zimmerframe (12.3 kB)

  magic-string (1, 85.0 kB transitive): @jridgewell/sourcemap-codec (85.0 kB)

  chokidar (1, 35.3 kB transitive): readdirp (35.3 kB)

Lines of code

packages/mochi
Category main PR Δ
src/**/*.test.ts 6883 7227 +344
src/ComponentRegistry.ts 1633 1700 +67
src/{types.ts,*.d.ts} 703 705 +2
Other 2922 3284 +362
Total 19759 20534 +775

Unchanged: src/Mochi.ts (1317), src/hooks.ts (235), src/{requestContext,forms,errors}.ts (299), src/{events,log,logger}.ts (351), src/consoleLogger.ts (377), src/cookies*.ts (157), src/extensions.ts (191), src/cache.ts (157), src/middleware/** (81), src/enhance*.ts (184), src/build*.ts (258), src/proxy.ts (125), src/cli* (120), src/{csrf,serverIslandCrypto}.ts (226), src/web-components/** (434), src/debug-bar/** (2316), src/templates/** (790).

packages/docs
Category main PR Δ
Docs 3840 4001 +161
Total 3840 4001 +161
packages/site
Category main PR Δ
src/demos/** 6073 6401 +328
src/components/** 2159 2177 +18
src/lib/** 965 981 +16
Other 1379 1383 +4
Total 10602 10968 +366

Unchanged: src/stores/** (26).

packages/demos
Category main PR Δ
Total 3100 3100 0

Unchanged: src/hn/** (1083), Other (2017).

packages/minimal
Category main PR Δ
Total 536 536 0

Unchanged: Other (536).

packages/cli
Category main PR Δ
Total 479 479 0

Unchanged: src/**/*.test.ts (117), src/cli* (138), src/{create,templates,utils}.ts (220), Other (4).

@khromov khromov changed the base branch from main to view-transitions June 14, 2026 12:26
Base automatically changed from view-transitions to main June 15, 2026 23:13
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