@quilted/rollup 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,119 @@
1
1
  # @quilted/rollup
2
2
 
3
+ ## 0.4.6
4
+
5
+ ### Patch Changes
6
+
7
+ - [#890](https://github.com/lemonmade/quilt/pull/890) [`b0f2334`](https://github.com/lemonmade/quilt/commit/b0f23340945280c951998bf77b3be8b8df13338c) Thanks [@lemonmade](https://github.com/lemonmade)! - Redesign asset loading APIs for better performance and clearer structure.
8
+
9
+ ## Breaking changes
10
+
11
+ ### `@quilted/assets`: `BrowserAssetsEntry` redesigned
12
+
13
+ The `BrowserAssetsEntry` type has been completely restructured. The previous flat `scripts` and `styles` arrays have been replaced with structured `script` and `style` objects that separate the entry asset from its dependencies:
14
+
15
+ ```ts
16
+ // Before
17
+ interface BrowserAssetsEntry {
18
+ scripts: Asset[];
19
+ styles: Asset[];
20
+ }
21
+
22
+ // After
23
+ interface BrowserAssetsEntry {
24
+ script?: {
25
+ asset: Asset;
26
+ syncDependencies: readonly Asset[];
27
+ asyncDependencies: readonly Asset[];
28
+ };
29
+ style?: {
30
+ asset: Asset;
31
+ syncDependencies: readonly Asset[];
32
+ asyncDependencies: readonly Asset[];
33
+ };
34
+ }
35
+ ```
36
+
37
+ This separation enables the renderer to treat the entry script, its preloadable sync dependencies, and its async dependencies differently in the HTML output.
38
+
39
+ ### `@quilted/assets`: `BrowserAssets.modules()` return type changed
40
+
41
+ `modules()` now returns `readonly BrowserAssetsEntry[]` (one entry per module ID) instead of a single merged `BrowserAssetsEntry`:
42
+
43
+ ```ts
44
+ // Before
45
+ modules(modules: Iterable<...>, options?): BrowserAssetsEntry;
46
+
47
+ // After
48
+ modules(modules: Iterable<string>, options?): readonly BrowserAssetsEntry[];
49
+ ```
50
+
51
+ ### `@quilted/assets`: `BrowserAssetModuleSelector` removed
52
+
53
+ The `BrowserAssetModuleSelector` interface and the `modules` field on `BrowserAssetSelector` have been removed. If you want to get the asset details for modules in addition to the entrypoints, use `BrowserAssets.modules()` instead.
54
+
55
+ ### `@quilted/assets`: `AssetBuildManifest` module entry format changed
56
+
57
+ `AssetBuildManifest.modules` values changed from `number[]` to the new `AssetBuildModuleEntry` tuple type. The tuple uses positional slots for each asset category and is serialized in JSON as an object with numeric string keys (omitting empty positions):
58
+
59
+ ```ts
60
+ type AssetBuildModuleEntry = [
61
+ script?: number, // [0] entry JS chunk
62
+ style?: number, // [1] entry CSS file
63
+ scriptSync?: number[], // [2] sync JS dependency indices
64
+ styleSync?: number[], // [3] CSS from sync JS dependencies
65
+ scriptAsync?: number[], // [4] dynamic JS dependency indices
66
+ styleAsync?: number[], // [5] CSS from dynamic JS dependencies
67
+ ];
68
+ ```
69
+
70
+ ### `@quilted/browser`: `BrowserResponseAssets.get()` return type changed
71
+
72
+ `get()` now returns `string[]` (module IDs) instead of `BrowserAssetModuleSelector[]`.
73
+
74
+ ## New features
75
+
76
+ ### `@quilted/preact-browser`: `BrowserApp` class
77
+
78
+ A new `BrowserApp` class simplifies constructing and running a browser app. It handles waiting for the `#app` DOM element via `MutationObserver`, which is necessary now that the entry script runs with the `async` attribute:
79
+
80
+ ```ts
81
+ import {BrowserApp} from '@quilted/quilt/browser';
82
+ import {BrowserAppContext} from '~/context/browser.ts';
83
+ import {App} from './App.tsx';
84
+
85
+ const context = new BrowserAppContext();
86
+ const app = new BrowserApp(<App context={context} />, {context});
87
+ await app.hydrate();
88
+ ```
89
+
90
+ ## Render behavior changes
91
+
92
+ ### Entry script rendered as `async` module
93
+
94
+ The browser entry script is now rendered as `<script type="module" async>` instead of a blocking `<script type="module">`. This means the script no longer blocks HTML parsing, and does not wait for DOMContentLoaded. This change is meant to allow streaming HTML responses to begin executing JavaScript earlier.
95
+
96
+ ### Sync dependencies rendered as `modulepreload` links
97
+
98
+ Sync JS dependencies (previously rendered as additional `<script type="module">` tags) are now rendered as `<link rel="modulepreload">` hints. This tells the browser to fetch them eagerly without executing them, since the entry script will import them when it runs.
99
+
100
+ ### Stylesheets rendered after all script references
101
+
102
+ Entry and async stylesheets are now emitted from the `<HTMLTemplate.Assets async />` placeholder, after all script and modulepreload tags. This ensures the stylesheet `<link>` elements appear after all JS references in the HTML.
103
+
104
+ ### Asset deduplication across streamed chunks
105
+
106
+ Asset deduplication (preventing the same `src`/`href` from appearing more than once) now works across all streamed HTML chunks within a single response, not just within a single placeholder.
107
+
108
+ - Updated dependencies [[`b0f2334`](https://github.com/lemonmade/quilt/commit/b0f23340945280c951998bf77b3be8b8df13338c)]:
109
+ - @quilted/assets@0.1.10
110
+
111
+ ## 0.4.5
112
+
113
+ ### Patch Changes
114
+
115
+ - [`3c4a5ac`](https://github.com/lemonmade/quilt/commit/3c4a5ac934e1648be6843f8cd880a53f7c29aeb4) Thanks [@lemonmade](https://github.com/lemonmade)! - Improve asset output for Cloudflare plugin
116
+
3
117
  ## 0.4.4
4
118
 
5
119
  ### Patch Changes
package/build/esm/app.mjs CHANGED
@@ -88,6 +88,7 @@ async function quiltApp({
88
88
  async function quiltAppBrowser(options = {}) {
89
89
  const { root = process.cwd(), assets, runtime } = options;
90
90
  const project = Project.load(root);
91
+ const baseURL = assets?.baseURL ?? "/assets/";
91
92
  const [plugins, browserGroup] = await Promise.all([
92
93
  quiltAppBrowserPlugins(options),
93
94
  getBrowserGroupTargetDetails(assets?.targets, {
@@ -99,13 +100,12 @@ async function quiltAppBrowser(options = {}) {
99
100
  targetsSupportModules(browserGroup.browsers),
100
101
  rollupGenerateOptionsForBrowsers(browserGroup.browsers)
101
102
  ]);
103
+ const outputDirectory = assets?.directory ? assets.directory : runtime?.assets?.directory ? typeof runtime.assets.directory === "function" ? runtime.assets.directory({ baseURL }) : runtime.assets.directory : `build/assets`;
102
104
  const rollupOptions = {
103
105
  plugins,
104
106
  output: {
105
107
  format: isESM ? "esm" : "systemjs",
106
- dir: project.resolve(
107
- assets?.directory ?? runtime?.assets?.directory ?? `build/assets`
108
- ),
108
+ dir: project.resolve(outputDirectory),
109
109
  entryFileNames: `[name]${targetFilenamePart}.[hash].js`,
110
110
  assetFileNames: `[name]${targetFilenamePart}.[hash].[ext]`,
111
111
  chunkFileNames: `[name]${targetFilenamePart}.[hash].js`,
@@ -118,7 +118,9 @@ async function quiltAppBrowser(options = {}) {
118
118
  preserveEntrySignatures: false
119
119
  };
120
120
  if (runtime?.browser?.rollup) {
121
- rollupOptions.plugins.push(runtime.browser.rollup(rollupOptions));
121
+ rollupOptions.plugins.push(
122
+ runtime.browser.rollup(rollupOptions, { assets: { baseURL, ...assets } })
123
+ );
122
124
  }
123
125
  return rollupOptions;
124
126
  }
@@ -31,6 +31,12 @@ async function writeManifestForBundle(bundle, {
31
31
  if (output.type !== "chunk") continue;
32
32
  dependencyMap.set(output.fileName, output.imports);
33
33
  }
34
+ const facadeToChunk = /* @__PURE__ */ new Map();
35
+ for (const output of outputs) {
36
+ if (output.type === "chunk" && output.facadeModuleId) {
37
+ facadeToChunk.set(output.facadeModuleId, output.fileName);
38
+ }
39
+ }
34
40
  const assets = [];
35
41
  const assetIdMap = /* @__PURE__ */ new Map();
36
42
  function getAssetId(file2) {
@@ -54,9 +60,8 @@ async function writeManifestForBundle(bundle, {
54
60
  for (const output of outputs) {
55
61
  if (output.type === "asset") {
56
62
  if (output.name && output.fileName.endsWith(".js")) {
57
- manifest.modules[output.name] = createAssetsEntry([output.fileName], {
58
- dependencyMap,
59
- getAssetId
63
+ manifest.modules[output.name] = makeEntry({
64
+ script: getAssetId(output.fileName)
60
65
  });
61
66
  manifest.entries[`./${output.name}`] = output.name;
62
67
  }
@@ -77,17 +82,23 @@ async function writeManifestForBundle(bundle, {
77
82
  manifest.entries[entry] = moduleID;
78
83
  }
79
84
  const isCSS = moduleID.endsWith(".css");
80
- const moduleFiles = [output.fileName, ...output.imports];
81
- manifest.modules[moduleID] = createAssetsEntry(
82
- // When an entrypoint is a CSS file, Rollup creates an unnecessary JavaScript file
83
- // as the entrypoint of the module. We will exclude the JavaScript file, so only
84
- // the CSS file is included in the final manifest.
85
- isCSS ? moduleFiles.filter((file2) => file2.endsWith(".css")) : moduleFiles,
86
- {
87
- dependencyMap,
88
- getAssetId
89
- }
90
- );
85
+ if (isCSS) {
86
+ const cssFiles = [output.fileName, ...output.imports].filter(
87
+ (f) => f.endsWith(".css")
88
+ );
89
+ manifest.modules[moduleID] = makeEntry({
90
+ style: cssFiles[0] != null ? getAssetId(cssFiles[0]) : void 0,
91
+ styleSync: cssFiles.length > 1 ? cssFiles.slice(1).map(getAssetId) : void 0
92
+ });
93
+ } else {
94
+ const asyncFileNames = (output.dynamicImports ?? []).map((mid) => facadeToChunk.get(mid)).filter((f) => f != null);
95
+ manifest.modules[moduleID] = buildModuleEntry(
96
+ output.fileName,
97
+ output.imports,
98
+ asyncFileNames,
99
+ { dependencyMap, getAssetId }
100
+ );
101
+ }
91
102
  }
92
103
  manifest.assets = await Promise.all(assets);
93
104
  await fs.mkdir(path.dirname(file), { recursive: true });
@@ -141,26 +152,61 @@ async function normalizeInlineSource({
141
152
  function defaultModuleID({ imported }) {
142
153
  return imported.startsWith("/") ? path.relative(process.cwd(), imported) : imported.startsWith("\0") ? imported.replace("\0", "") : imported;
143
154
  }
144
- function createAssetsEntry(files, {
155
+ function makeEntry({
156
+ script,
157
+ style,
158
+ scriptSync,
159
+ styleSync,
160
+ scriptAsync,
161
+ styleAsync
162
+ }) {
163
+ const entry = {};
164
+ if (script != null) entry[0] = script;
165
+ if (style != null) entry[1] = style;
166
+ if (scriptSync?.length) entry[2] = scriptSync;
167
+ if (styleSync?.length) entry[3] = styleSync;
168
+ if (scriptAsync?.length) entry[4] = scriptAsync;
169
+ if (styleAsync?.length) entry[5] = styleAsync;
170
+ return entry;
171
+ }
172
+ function buildModuleEntry(entryFileName, syncImports, asyncFileNames, {
145
173
  dependencyMap,
146
174
  getAssetId
147
175
  }) {
148
- const assets = [];
149
- const allFiles = /* @__PURE__ */ new Set();
150
- const addFile = (file) => {
151
- if (allFiles.has(file)) return;
152
- allFiles.add(file);
153
- for (const dependency of dependencyMap.get(file) ?? []) {
154
- addFile(dependency);
176
+ const syncJsFiles = /* @__PURE__ */ new Set();
177
+ const entryStyleFiles = [];
178
+ const syncStyleFiles = /* @__PURE__ */ new Set();
179
+ for (const file of syncImports) {
180
+ if (file.endsWith(".css")) {
181
+ entryStyleFiles.push(file);
182
+ } else {
183
+ visitJsDep(file, syncJsFiles, syncStyleFiles, dependencyMap);
155
184
  }
156
- };
157
- for (const file of files) {
158
- addFile(file);
159
185
  }
160
- for (const file of allFiles) {
161
- assets.push(getAssetId(file));
186
+ const asyncStyleFiles = /* @__PURE__ */ new Set();
187
+ const visitedAsync = /* @__PURE__ */ new Set();
188
+ for (const asyncFileName of asyncFileNames) {
189
+ visitJsDep(asyncFileName, visitedAsync, asyncStyleFiles, dependencyMap);
190
+ }
191
+ return makeEntry({
192
+ script: getAssetId(entryFileName),
193
+ style: entryStyleFiles[0] != null ? getAssetId(entryStyleFiles[0]) : void 0,
194
+ scriptSync: syncJsFiles.size > 0 ? [...syncJsFiles].map(getAssetId) : void 0,
195
+ styleSync: syncStyleFiles.size > 0 ? [...syncStyleFiles].map(getAssetId) : void 0,
196
+ scriptAsync: asyncFileNames.length > 0 ? asyncFileNames.map(getAssetId) : void 0,
197
+ styleAsync: asyncStyleFiles.size > 0 ? [...asyncStyleFiles].map(getAssetId) : void 0
198
+ });
199
+ }
200
+ function visitJsDep(file, visitedJs, styleFiles, dependencyMap) {
201
+ if (visitedJs.has(file)) return;
202
+ visitedJs.add(file);
203
+ for (const dep of dependencyMap.get(file) ?? []) {
204
+ if (dep.endsWith(".css")) {
205
+ styleFiles.add(dep);
206
+ } else {
207
+ visitJsDep(dep, visitedJs, styleFiles, dependencyMap);
208
+ }
162
209
  }
163
- return assets;
164
210
  }
165
211
  const QUERY_PATTERN = /\?.*$/s;
166
212
  const HASH_PATTERN = /#.*$/s;