@modern-js/runtime 3.3.0 → 3.5.0

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.
@@ -86,7 +86,7 @@ async function buildShellBeforeTemplate(beforeAppTemplate, options) {
86
86
  if (asyncEntry) matchedRouteManifests?.push(asyncEntry);
87
87
  const cssChunks = matchedRouteManifests ? matchedRouteManifests?.reduce((chunks, routeManifest)=>{
88
88
  const { referenceCssAssets = [] } = routeManifest;
89
- const _cssChunks = referenceCssAssets.filter((asset)=>asset?.endsWith('.css') && !template.includes(asset));
89
+ const _cssChunks = referenceCssAssets.filter((asset)=>asset?.endsWith('.css') && !(0, external_utils_js_namespaceObject.hasStylesheetLink)(template, asset));
90
90
  return [
91
91
  ...chunks,
92
92
  ..._cssChunks
@@ -92,12 +92,14 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
92
92
  try {
93
93
  if (shellChunkStatus !== external_shared_js_namespaceObject.ShellChunkStatus.FINISH) {
94
94
  chunkVec.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
95
- const chunkStr = chunk.toString('utf-8');
96
- if (chunkStr.includes(external_common_js_namespaceObject.ESCAPED_SHELL_STREAM_END_MARK)) {
97
- let concatedChunk = Buffer.concat(chunkVec).toString('utf-8');
98
- concatedChunk = concatedChunk.replace(external_common_js_namespaceObject.ESCAPED_SHELL_STREAM_END_MARK, '');
95
+ const concatedChunk = Buffer.concat(chunkVec).toString('utf-8');
96
+ const markerIndex = concatedChunk.indexOf(external_common_js_namespaceObject.ESCAPED_SHELL_STREAM_END_MARK);
97
+ if (-1 !== markerIndex) {
98
+ const beforeMark = concatedChunk.slice(0, markerIndex);
99
+ const afterMark = concatedChunk.slice(markerIndex + external_common_js_namespaceObject.ESCAPED_SHELL_STREAM_END_MARK.length);
99
100
  shellChunkStatus = external_shared_js_namespaceObject.ShellChunkStatus.FINISH;
100
- this.push(`${shellBefore}${concatedChunk}${shellAfter}`);
101
+ this.push(`${shellBefore}${beforeMark}${shellAfter}`);
102
+ if (afterMark) this.push(afterMark);
101
103
  if (pendingScripts.length > 0) for (const s of pendingScripts)this.push(s);
102
104
  }
103
105
  } else this.push(chunk);
@@ -107,9 +107,13 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
107
107
  if (shellChunkStatus !== external_shared_js_namespaceObject.ShellChunkStatus.FINISH) {
108
108
  chunkVec.push(new TextDecoder().decode(value));
109
109
  const concatedChunk = chunkVec.join('');
110
- if (concatedChunk.includes(external_common_js_namespaceObject.ESCAPED_SHELL_STREAM_END_MARK)) {
110
+ const markerIndex = concatedChunk.indexOf(external_common_js_namespaceObject.ESCAPED_SHELL_STREAM_END_MARK);
111
+ if (-1 !== markerIndex) {
112
+ const beforeMark = concatedChunk.slice(0, markerIndex);
113
+ const afterMark = concatedChunk.slice(markerIndex + external_common_js_namespaceObject.ESCAPED_SHELL_STREAM_END_MARK.length);
111
114
  shellChunkStatus = external_shared_js_namespaceObject.ShellChunkStatus.FINISH;
112
- safeEnqueue((0, external_shared_js_namespaceObject.encodeForWebStream)(`${shellBefore}${concatedChunk.replace(external_common_js_namespaceObject.ESCAPED_SHELL_STREAM_END_MARK, '')}${shellAfter}`));
115
+ safeEnqueue((0, external_shared_js_namespaceObject.encodeForWebStream)(`${shellBefore}${beforeMark}${shellAfter}`));
116
+ if (afterMark) safeEnqueue((0, external_shared_js_namespaceObject.encodeForWebStream)(afterMark));
113
117
  flushPendingScripts();
114
118
  }
115
119
  } else safeEnqueue(value);
@@ -116,11 +116,7 @@ class LoadableCollector {
116
116
  const { template, chunkSet, config, entryName } = this.options;
117
117
  const { inlineStyles } = config;
118
118
  const atrributes = (0, external_utils_js_namespaceObject.attributesToString)(this.generateAttributes());
119
- const linkRegExp = /<link .*?href="([^"]+)".*?>/g;
120
- const matchs = template.matchAll(linkRegExp);
121
- const existedLinks = [];
122
- for (const match of matchs)existedLinks.push(match[1]);
123
- const css = await Promise.all(chunks.filter((chunk)=>!existedLinks.includes(chunk.url) && !this.existsAssets?.includes(chunk.path)).map(async (chunk)=>{
119
+ const css = await Promise.all(chunks.filter((chunk)=>!(0, external_utils_js_namespaceObject.hasStylesheetLink)(template, chunk.url) && !this.existsAssets?.includes(chunk.path)).map(async (chunk)=>{
124
120
  const link = `<link${atrributes} href="${chunk.url}" rel="stylesheet" />`;
125
121
  if ((0, external_utils_js_namespaceObject.checkIsNode)() && checkIsInline(chunk, inlineStyles)) return readAsset(chunk).then((content)=>`<style>${content}</style>`).catch((_)=>link);
126
122
  return link;
@@ -32,6 +32,7 @@ __webpack_require__.d(__webpack_exports__, {
32
32
  checkIsNode: ()=>checkIsNode,
33
33
  getSSRConfigByEntry: ()=>getSSRConfigByEntry,
34
34
  getSSRMode: ()=>getSSRMode,
35
+ hasStylesheetLink: ()=>hasStylesheetLink,
35
36
  safeReplace: ()=>safeReplace,
36
37
  serializeErrors: ()=>serializeErrors
37
38
  });
@@ -73,10 +74,31 @@ function getSSRMode(ssrConfig) {
73
74
  const result = ssrConfig?.mode === 'string' ? 'string' : 'stream';
74
75
  return result;
75
76
  }
77
+ const getLinkAttributes = (linkTag)=>{
78
+ const attributes = new Map();
79
+ const attributeRegExp = /([^\s"'<>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
80
+ let match;
81
+ while(match = attributeRegExp.exec(linkTag)){
82
+ const [, name, doubleQuotedValue, singleQuotedValue, unquotedValue] = match;
83
+ if ('link' === name.toLowerCase()) continue;
84
+ attributes.set(name.toLowerCase(), doubleQuotedValue ?? singleQuotedValue ?? unquotedValue ?? '');
85
+ }
86
+ return attributes;
87
+ };
88
+ const hasStylesheetLink = (template, href)=>{
89
+ const linkTags = template.match(/<link\b[^>]*>/gi) ?? [];
90
+ return linkTags.some((linkTag)=>{
91
+ const attributes = getLinkAttributes(linkTag);
92
+ const linkHref = attributes.get('href');
93
+ const rel = attributes.get('rel');
94
+ return linkHref === href && rel?.split(/\s+/).some((relToken)=>'stylesheet' === relToken.toLowerCase());
95
+ });
96
+ };
76
97
  exports.attributesToString = __webpack_exports__.attributesToString;
77
98
  exports.checkIsNode = __webpack_exports__.checkIsNode;
78
99
  exports.getSSRConfigByEntry = __webpack_exports__.getSSRConfigByEntry;
79
100
  exports.getSSRMode = __webpack_exports__.getSSRMode;
101
+ exports.hasStylesheetLink = __webpack_exports__.hasStylesheetLink;
80
102
  exports.safeReplace = __webpack_exports__.safeReplace;
81
103
  exports.serializeErrors = __webpack_exports__.serializeErrors;
82
104
  for(var __rspack_i in __webpack_exports__)if (-1 === [
@@ -84,6 +106,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
84
106
  "checkIsNode",
85
107
  "getSSRConfigByEntry",
86
108
  "getSSRMode",
109
+ "hasStylesheetLink",
87
110
  "safeReplace",
88
111
  "serializeErrors"
89
112
  ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
@@ -340,12 +340,13 @@ const documentPlugin = ()=>({
340
340
  processedHtml = processCommentPlaceholders(processedHtml);
341
341
  return `<!DOCTYPE html>${processedHtml}`.replace(external_constants_js_namespaceObject.DOCUMENT_META_PLACEHOLDER, ()=>metas).replace(external_constants_js_namespaceObject.DOCUMENT_SSR_PLACEHOLDER, ()=>external_constants_js_namespaceObject.HTML_SEPARATOR).replace(external_constants_js_namespaceObject.DOCUMENT_SCRIPTS_PLACEHOLDER, ()=>scripts).replace(external_constants_js_namespaceObject.DOCUMENT_LINKS_PLACEHOLDER, ()=>links).replace(external_constants_js_namespaceObject.DOCUMENT_CHUNKSMAP_PLACEHOLDER, ()=>external_constants_js_namespaceObject.PLACEHOLDER_REPLACER_MAP[external_constants_js_namespaceObject.DOCUMENT_CHUNKSMAP_PLACEHOLDER]).replace(external_constants_js_namespaceObject.DOCUMENT_SSRDATASCRIPT_PLACEHOLDER, ()=>external_constants_js_namespaceObject.PLACEHOLDER_REPLACER_MAP[external_constants_js_namespaceObject.DOCUMENT_SSRDATASCRIPT_PLACEHOLDER]).replace(external_constants_js_namespaceObject.DOCUMENT_TITLE_PLACEHOLDER, ()=>titles);
342
342
  };
343
- const documentEntry = (entryName, templateParameters)=>{
343
+ const documentEntry = (entryName)=>{
344
344
  const { entrypoints, internalDirectory, appDirectory } = api.getAppContext();
345
345
  const documentFilePath = getDocumentByEntryName(entrypoints, entryName, appDirectory);
346
346
  if (!documentFilePath) return null;
347
347
  return async (templateData)=>{
348
348
  const config = api.getNormalizedConfig();
349
+ const { compilation: _compilation, htmlPlugin, rspackConfig: _rspackConfig, ...templateParameters } = templateData;
349
350
  const documentParams = getDocParams({
350
351
  config: config,
351
352
  entryName,
@@ -363,7 +364,6 @@ const documentPlugin = ()=>({
363
364
  debug("entry %s's document jsx rendered html: %o", entryName, html);
364
365
  const { partialsByEntrypoint } = api.getAppContext();
365
366
  html = processPartials(html, entryName, partialsByEntrypoint || {});
366
- const htmlPlugin = templateData.htmlPlugin || templateData.htmlWebpackPlugin || templateData.htmlRspackPlugin;
367
367
  if (!htmlPlugin) throw new Error('Failed to get HTML plugin tags from template parameters.');
368
368
  const { scripts, links, metas, titles } = extractHtmlTags(htmlPlugin, templateParameters);
369
369
  return processPlaceholders(html, config, scripts, links, metas, titles);
@@ -375,10 +375,7 @@ const documentPlugin = ()=>({
375
375
  return {
376
376
  tools: {
377
377
  htmlPlugin: (options, entry)=>{
378
- const hackParameters = 'function' == typeof options?.templateParameters ? options?.templateParameters({}, {}, {}, {}) : {
379
- ...options?.templateParameters
380
- };
381
- const templateContent = documentEntry(entry.entryName, hackParameters);
378
+ const templateContent = documentEntry(entry.entryName);
382
379
  const documentHtmlOptions = templateContent ? {
383
380
  templateContent,
384
381
  inject: false
@@ -3,7 +3,7 @@ import react_helmet from "react-helmet";
3
3
  import { CHUNK_CSS_PLACEHOLDER } from "../constants.mjs";
4
4
  import { createReplaceHelemt } from "../helmet.mjs";
5
5
  import { buildHtml } from "../shared.mjs";
6
- import { checkIsNode, safeReplace } from "../utils.mjs";
6
+ import { checkIsNode, hasStylesheetLink, safeReplace } from "../utils.mjs";
7
7
  const readAsset = async (chunk)=>{
8
8
  const fs = await import("fs/promises");
9
9
  const path = await import("path");
@@ -44,7 +44,7 @@ async function buildShellBeforeTemplate(beforeAppTemplate, options) {
44
44
  if (asyncEntry) matchedRouteManifests?.push(asyncEntry);
45
45
  const cssChunks = matchedRouteManifests ? matchedRouteManifests?.reduce((chunks, routeManifest)=>{
46
46
  const { referenceCssAssets = [] } = routeManifest;
47
- const _cssChunks = referenceCssAssets.filter((asset)=>asset?.endsWith('.css') && !template.includes(asset));
47
+ const _cssChunks = referenceCssAssets.filter((asset)=>asset?.endsWith('.css') && !hasStylesheetLink(template, asset));
48
48
  return [
49
49
  ...chunks,
50
50
  ..._cssChunks
@@ -60,12 +60,14 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
60
60
  try {
61
61
  if (shellChunkStatus !== ShellChunkStatus.FINISH) {
62
62
  chunkVec.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
63
- const chunkStr = chunk.toString('utf-8');
64
- if (chunkStr.includes(ESCAPED_SHELL_STREAM_END_MARK)) {
65
- let concatedChunk = Buffer.concat(chunkVec).toString('utf-8');
66
- concatedChunk = concatedChunk.replace(ESCAPED_SHELL_STREAM_END_MARK, '');
63
+ const concatedChunk = Buffer.concat(chunkVec).toString('utf-8');
64
+ const markerIndex = concatedChunk.indexOf(ESCAPED_SHELL_STREAM_END_MARK);
65
+ if (-1 !== markerIndex) {
66
+ const beforeMark = concatedChunk.slice(0, markerIndex);
67
+ const afterMark = concatedChunk.slice(markerIndex + ESCAPED_SHELL_STREAM_END_MARK.length);
67
68
  shellChunkStatus = ShellChunkStatus.FINISH;
68
- this.push(`${shellBefore}${concatedChunk}${shellAfter}`);
69
+ this.push(`${shellBefore}${beforeMark}${shellAfter}`);
70
+ if (afterMark) this.push(afterMark);
69
71
  if (pendingScripts.length > 0) for (const s of pendingScripts)this.push(s);
70
72
  }
71
73
  } else this.push(chunk);
@@ -75,9 +75,13 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
75
75
  if (shellChunkStatus !== ShellChunkStatus.FINISH) {
76
76
  chunkVec.push(new TextDecoder().decode(value));
77
77
  const concatedChunk = chunkVec.join('');
78
- if (concatedChunk.includes(ESCAPED_SHELL_STREAM_END_MARK)) {
78
+ const markerIndex = concatedChunk.indexOf(ESCAPED_SHELL_STREAM_END_MARK);
79
+ if (-1 !== markerIndex) {
80
+ const beforeMark = concatedChunk.slice(0, markerIndex);
81
+ const afterMark = concatedChunk.slice(markerIndex + ESCAPED_SHELL_STREAM_END_MARK.length);
79
82
  shellChunkStatus = ShellChunkStatus.FINISH;
80
- safeEnqueue(encodeForWebStream(`${shellBefore}${concatedChunk.replace(ESCAPED_SHELL_STREAM_END_MARK, '')}${shellAfter}`));
83
+ safeEnqueue(encodeForWebStream(`${shellBefore}${beforeMark}${shellAfter}`));
84
+ if (afterMark) safeEnqueue(encodeForWebStream(afterMark));
81
85
  flushPendingScripts();
82
86
  }
83
87
  } else safeEnqueue(value);
@@ -1,5 +1,5 @@
1
1
  import { ChunkExtractor } from "@loadable/server";
2
- import { attributesToString, checkIsNode } from "../utils.mjs";
2
+ import { attributesToString, checkIsNode, hasStylesheetLink } from "../utils.mjs";
3
3
  const extname = (uri)=>{
4
4
  if ('string' != typeof uri || !uri.includes('.')) return '';
5
5
  return `.${uri?.split('.').pop()}` || '';
@@ -84,11 +84,7 @@ class LoadableCollector {
84
84
  const { template, chunkSet, config, entryName } = this.options;
85
85
  const { inlineStyles } = config;
86
86
  const atrributes = attributesToString(this.generateAttributes());
87
- const linkRegExp = /<link .*?href="([^"]+)".*?>/g;
88
- const matchs = template.matchAll(linkRegExp);
89
- const existedLinks = [];
90
- for (const match of matchs)existedLinks.push(match[1]);
91
- const css = await Promise.all(chunks.filter((chunk)=>!existedLinks.includes(chunk.url) && !this.existsAssets?.includes(chunk.path)).map(async (chunk)=>{
87
+ const css = await Promise.all(chunks.filter((chunk)=>!hasStylesheetLink(template, chunk.url) && !this.existsAssets?.includes(chunk.path)).map(async (chunk)=>{
92
88
  const link = `<link${atrributes} href="${chunk.url}" rel="stylesheet" />`;
93
89
  if (checkIsNode() && checkIsInline(chunk, inlineStyles)) return readAsset(chunk).then((content)=>`<style>${content}</style>`).catch((_)=>link);
94
90
  return link;
@@ -36,4 +36,24 @@ function getSSRMode(ssrConfig) {
36
36
  const result = ssrConfig?.mode === 'string' ? 'string' : 'stream';
37
37
  return result;
38
38
  }
39
- export { attributesToString, checkIsNode, getSSRConfigByEntry, getSSRMode, safeReplace, serializeErrors };
39
+ const getLinkAttributes = (linkTag)=>{
40
+ const attributes = new Map();
41
+ const attributeRegExp = /([^\s"'<>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
42
+ let match;
43
+ while(match = attributeRegExp.exec(linkTag)){
44
+ const [, name, doubleQuotedValue, singleQuotedValue, unquotedValue] = match;
45
+ if ('link' === name.toLowerCase()) continue;
46
+ attributes.set(name.toLowerCase(), doubleQuotedValue ?? singleQuotedValue ?? unquotedValue ?? '');
47
+ }
48
+ return attributes;
49
+ };
50
+ const hasStylesheetLink = (template, href)=>{
51
+ const linkTags = template.match(/<link\b[^>]*>/gi) ?? [];
52
+ return linkTags.some((linkTag)=>{
53
+ const attributes = getLinkAttributes(linkTag);
54
+ const linkHref = attributes.get('href');
55
+ const rel = attributes.get('rel');
56
+ return linkHref === href && rel?.split(/\s+/).some((relToken)=>'stylesheet' === relToken.toLowerCase());
57
+ });
58
+ };
59
+ export { attributesToString, checkIsNode, getSSRConfigByEntry, getSSRMode, hasStylesheetLink, safeReplace, serializeErrors };
@@ -295,12 +295,13 @@ const documentPlugin = ()=>({
295
295
  processedHtml = processCommentPlaceholders(processedHtml);
296
296
  return `<!DOCTYPE html>${processedHtml}`.replace(DOCUMENT_META_PLACEHOLDER, ()=>metas).replace(DOCUMENT_SSR_PLACEHOLDER, ()=>HTML_SEPARATOR).replace(DOCUMENT_SCRIPTS_PLACEHOLDER, ()=>scripts).replace(DOCUMENT_LINKS_PLACEHOLDER, ()=>links).replace(DOCUMENT_CHUNKSMAP_PLACEHOLDER, ()=>PLACEHOLDER_REPLACER_MAP[DOCUMENT_CHUNKSMAP_PLACEHOLDER]).replace(DOCUMENT_SSRDATASCRIPT_PLACEHOLDER, ()=>PLACEHOLDER_REPLACER_MAP[DOCUMENT_SSRDATASCRIPT_PLACEHOLDER]).replace(DOCUMENT_TITLE_PLACEHOLDER, ()=>titles);
297
297
  };
298
- const documentEntry = (entryName, templateParameters)=>{
298
+ const documentEntry = (entryName)=>{
299
299
  const { entrypoints, internalDirectory, appDirectory } = api.getAppContext();
300
300
  const documentFilePath = getDocumentByEntryName(entrypoints, entryName, appDirectory);
301
301
  if (!documentFilePath) return null;
302
302
  return async (templateData)=>{
303
303
  const config = api.getNormalizedConfig();
304
+ const { compilation: _compilation, htmlPlugin, rspackConfig: _rspackConfig, ...templateParameters } = templateData;
304
305
  const documentParams = getDocParams({
305
306
  config: config,
306
307
  entryName,
@@ -318,7 +319,6 @@ const documentPlugin = ()=>({
318
319
  debug("entry %s's document jsx rendered html: %o", entryName, html);
319
320
  const { partialsByEntrypoint } = api.getAppContext();
320
321
  html = processPartials(html, entryName, partialsByEntrypoint || {});
321
- const htmlPlugin = templateData.htmlPlugin || templateData.htmlWebpackPlugin || templateData.htmlRspackPlugin;
322
322
  if (!htmlPlugin) throw new Error('Failed to get HTML plugin tags from template parameters.');
323
323
  const { scripts, links, metas, titles } = extractHtmlTags(htmlPlugin, templateParameters);
324
324
  return processPlaceholders(html, config, scripts, links, metas, titles);
@@ -330,10 +330,7 @@ const documentPlugin = ()=>({
330
330
  return {
331
331
  tools: {
332
332
  htmlPlugin: (options, entry)=>{
333
- const hackParameters = 'function' == typeof options?.templateParameters ? options?.templateParameters({}, {}, {}, {}) : {
334
- ...options?.templateParameters
335
- };
336
- const templateContent = documentEntry(entry.entryName, hackParameters);
333
+ const templateContent = documentEntry(entry.entryName);
337
334
  const documentHtmlOptions = templateContent ? {
338
335
  templateContent,
339
336
  inject: false
@@ -4,7 +4,7 @@ import react_helmet from "react-helmet";
4
4
  import { CHUNK_CSS_PLACEHOLDER } from "../constants.mjs";
5
5
  import { createReplaceHelemt } from "../helmet.mjs";
6
6
  import { buildHtml } from "../shared.mjs";
7
- import { checkIsNode, safeReplace } from "../utils.mjs";
7
+ import { checkIsNode, hasStylesheetLink, safeReplace } from "../utils.mjs";
8
8
  import { fileURLToPath as __rspack_fileURLToPath } from "node:url";
9
9
  import { dirname as __rspack_dirname } from "node:path";
10
10
  var beforeTemplate_dirname = __rspack_dirname(__rspack_fileURLToPath(import.meta.url));
@@ -48,7 +48,7 @@ async function buildShellBeforeTemplate(beforeAppTemplate, options) {
48
48
  if (asyncEntry) matchedRouteManifests?.push(asyncEntry);
49
49
  const cssChunks = matchedRouteManifests ? matchedRouteManifests?.reduce((chunks, routeManifest)=>{
50
50
  const { referenceCssAssets = [] } = routeManifest;
51
- const _cssChunks = referenceCssAssets.filter((asset)=>asset?.endsWith('.css') && !template.includes(asset));
51
+ const _cssChunks = referenceCssAssets.filter((asset)=>asset?.endsWith('.css') && !hasStylesheetLink(template, asset));
52
52
  return [
53
53
  ...chunks,
54
54
  ..._cssChunks
@@ -61,12 +61,14 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
61
61
  try {
62
62
  if (shellChunkStatus !== ShellChunkStatus.FINISH) {
63
63
  chunkVec.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
64
- const chunkStr = chunk.toString('utf-8');
65
- if (chunkStr.includes(ESCAPED_SHELL_STREAM_END_MARK)) {
66
- let concatedChunk = Buffer.concat(chunkVec).toString('utf-8');
67
- concatedChunk = concatedChunk.replace(ESCAPED_SHELL_STREAM_END_MARK, '');
64
+ const concatedChunk = Buffer.concat(chunkVec).toString('utf-8');
65
+ const markerIndex = concatedChunk.indexOf(ESCAPED_SHELL_STREAM_END_MARK);
66
+ if (-1 !== markerIndex) {
67
+ const beforeMark = concatedChunk.slice(0, markerIndex);
68
+ const afterMark = concatedChunk.slice(markerIndex + ESCAPED_SHELL_STREAM_END_MARK.length);
68
69
  shellChunkStatus = ShellChunkStatus.FINISH;
69
- this.push(`${shellBefore}${concatedChunk}${shellAfter}`);
70
+ this.push(`${shellBefore}${beforeMark}${shellAfter}`);
71
+ if (afterMark) this.push(afterMark);
70
72
  if (pendingScripts.length > 0) for (const s of pendingScripts)this.push(s);
71
73
  }
72
74
  } else this.push(chunk);
@@ -76,9 +76,13 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
76
76
  if (shellChunkStatus !== ShellChunkStatus.FINISH) {
77
77
  chunkVec.push(new TextDecoder().decode(value));
78
78
  const concatedChunk = chunkVec.join('');
79
- if (concatedChunk.includes(ESCAPED_SHELL_STREAM_END_MARK)) {
79
+ const markerIndex = concatedChunk.indexOf(ESCAPED_SHELL_STREAM_END_MARK);
80
+ if (-1 !== markerIndex) {
81
+ const beforeMark = concatedChunk.slice(0, markerIndex);
82
+ const afterMark = concatedChunk.slice(markerIndex + ESCAPED_SHELL_STREAM_END_MARK.length);
80
83
  shellChunkStatus = ShellChunkStatus.FINISH;
81
- safeEnqueue(encodeForWebStream(`${shellBefore}${concatedChunk.replace(ESCAPED_SHELL_STREAM_END_MARK, '')}${shellAfter}`));
84
+ safeEnqueue(encodeForWebStream(`${shellBefore}${beforeMark}${shellAfter}`));
85
+ if (afterMark) safeEnqueue(encodeForWebStream(afterMark));
82
86
  flushPendingScripts();
83
87
  }
84
88
  } else safeEnqueue(value);
@@ -1,6 +1,6 @@
1
1
  import "node:module";
2
2
  import { ChunkExtractor } from "@loadable/server";
3
- import { attributesToString, checkIsNode } from "../utils.mjs";
3
+ import { attributesToString, checkIsNode, hasStylesheetLink } from "../utils.mjs";
4
4
  import { fileURLToPath as __rspack_fileURLToPath } from "node:url";
5
5
  import { dirname as __rspack_dirname } from "node:path";
6
6
  var loadable_dirname = __rspack_dirname(__rspack_fileURLToPath(import.meta.url));
@@ -88,11 +88,7 @@ class LoadableCollector {
88
88
  const { template, chunkSet, config, entryName } = this.options;
89
89
  const { inlineStyles } = config;
90
90
  const atrributes = attributesToString(this.generateAttributes());
91
- const linkRegExp = /<link .*?href="([^"]+)".*?>/g;
92
- const matchs = template.matchAll(linkRegExp);
93
- const existedLinks = [];
94
- for (const match of matchs)existedLinks.push(match[1]);
95
- const css = await Promise.all(chunks.filter((chunk)=>!existedLinks.includes(chunk.url) && !this.existsAssets?.includes(chunk.path)).map(async (chunk)=>{
91
+ const css = await Promise.all(chunks.filter((chunk)=>!hasStylesheetLink(template, chunk.url) && !this.existsAssets?.includes(chunk.path)).map(async (chunk)=>{
96
92
  const link = `<link${atrributes} href="${chunk.url}" rel="stylesheet" />`;
97
93
  if (checkIsNode() && checkIsInline(chunk, inlineStyles)) return readAsset(chunk).then((content)=>`<style>${content}</style>`).catch((_)=>link);
98
94
  return link;
@@ -37,4 +37,24 @@ function getSSRMode(ssrConfig) {
37
37
  const result = ssrConfig?.mode === 'string' ? 'string' : 'stream';
38
38
  return result;
39
39
  }
40
- export { attributesToString, checkIsNode, getSSRConfigByEntry, getSSRMode, safeReplace, serializeErrors };
40
+ const getLinkAttributes = (linkTag)=>{
41
+ const attributes = new Map();
42
+ const attributeRegExp = /([^\s"'<>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
43
+ let match;
44
+ while(match = attributeRegExp.exec(linkTag)){
45
+ const [, name, doubleQuotedValue, singleQuotedValue, unquotedValue] = match;
46
+ if ('link' === name.toLowerCase()) continue;
47
+ attributes.set(name.toLowerCase(), doubleQuotedValue ?? singleQuotedValue ?? unquotedValue ?? '');
48
+ }
49
+ return attributes;
50
+ };
51
+ const hasStylesheetLink = (template, href)=>{
52
+ const linkTags = template.match(/<link\b[^>]*>/gi) ?? [];
53
+ return linkTags.some((linkTag)=>{
54
+ const attributes = getLinkAttributes(linkTag);
55
+ const linkHref = attributes.get('href');
56
+ const rel = attributes.get('rel');
57
+ return linkHref === href && rel?.split(/\s+/).some((relToken)=>'stylesheet' === relToken.toLowerCase());
58
+ });
59
+ };
60
+ export { attributesToString, checkIsNode, getSSRConfigByEntry, getSSRMode, hasStylesheetLink, safeReplace, serializeErrors };
@@ -297,12 +297,13 @@ const documentPlugin = ()=>({
297
297
  processedHtml = processCommentPlaceholders(processedHtml);
298
298
  return `<!DOCTYPE html>${processedHtml}`.replace(DOCUMENT_META_PLACEHOLDER, ()=>metas).replace(DOCUMENT_SSR_PLACEHOLDER, ()=>HTML_SEPARATOR).replace(DOCUMENT_SCRIPTS_PLACEHOLDER, ()=>scripts).replace(DOCUMENT_LINKS_PLACEHOLDER, ()=>links).replace(DOCUMENT_CHUNKSMAP_PLACEHOLDER, ()=>PLACEHOLDER_REPLACER_MAP[DOCUMENT_CHUNKSMAP_PLACEHOLDER]).replace(DOCUMENT_SSRDATASCRIPT_PLACEHOLDER, ()=>PLACEHOLDER_REPLACER_MAP[DOCUMENT_SSRDATASCRIPT_PLACEHOLDER]).replace(DOCUMENT_TITLE_PLACEHOLDER, ()=>titles);
299
299
  };
300
- const documentEntry = (entryName, templateParameters)=>{
300
+ const documentEntry = (entryName)=>{
301
301
  const { entrypoints, internalDirectory, appDirectory } = api.getAppContext();
302
302
  const documentFilePath = getDocumentByEntryName(entrypoints, entryName, appDirectory);
303
303
  if (!documentFilePath) return null;
304
304
  return async (templateData)=>{
305
305
  const config = api.getNormalizedConfig();
306
+ const { compilation: _compilation, htmlPlugin, rspackConfig: _rspackConfig, ...templateParameters } = templateData;
306
307
  const documentParams = getDocParams({
307
308
  config: config,
308
309
  entryName,
@@ -320,7 +321,6 @@ const documentPlugin = ()=>({
320
321
  debug("entry %s's document jsx rendered html: %o", entryName, html);
321
322
  const { partialsByEntrypoint } = api.getAppContext();
322
323
  html = processPartials(html, entryName, partialsByEntrypoint || {});
323
- const htmlPlugin = templateData.htmlPlugin || templateData.htmlWebpackPlugin || templateData.htmlRspackPlugin;
324
324
  if (!htmlPlugin) throw new Error('Failed to get HTML plugin tags from template parameters.');
325
325
  const { scripts, links, metas, titles } = extractHtmlTags(htmlPlugin, templateParameters);
326
326
  return processPlaceholders(html, config, scripts, links, metas, titles);
@@ -332,10 +332,7 @@ const documentPlugin = ()=>({
332
332
  return {
333
333
  tools: {
334
334
  htmlPlugin: (options, entry)=>{
335
- const hackParameters = 'function' == typeof options?.templateParameters ? options?.templateParameters({}, {}, {}, {}) : {
336
- ...options?.templateParameters
337
- };
338
- const templateContent = documentEntry(entry.entryName, hackParameters);
335
+ const templateContent = documentEntry(entry.entryName);
339
336
  const documentHtmlOptions = templateContent ? {
340
337
  templateContent,
341
338
  inject: false
@@ -23,3 +23,10 @@ export declare function getSSRConfigByEntry(entryName: string, ssr?: ServerUserC
23
23
  loaderFailureMode?: "clientRender" | "errorBoundary";
24
24
  };
25
25
  export declare function getSSRMode(ssrConfig?: SSRConfig): 'string' | 'stream' | false;
26
+ /**
27
+ * Whether the template already contains a `<link rel="stylesheet">` for `href`.
28
+ * Other link rels (e.g. `<link rel="prefetch">` emitted by `performance.prefetch`,
29
+ * or `<link rel="preload" as="style">`) may reference the same css URL but do not
30
+ * apply styles, so they must not block stylesheet injection during SSR.
31
+ */
32
+ export declare const hasStylesheetLink: (template: string, href: string) => boolean;
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "modern",
16
16
  "modern.js"
17
17
  ],
18
- "version": "3.3.0",
18
+ "version": "3.5.0",
19
19
  "engines": {
20
20
  "node": ">=20"
21
21
  },
@@ -203,7 +203,7 @@
203
203
  "dependencies": {
204
204
  "@loadable/component": "5.16.7",
205
205
  "@loadable/server": "5.16.7",
206
- "@swc/core": "1.15.40",
206
+ "@swc/core": "1.15.41",
207
207
  "@swc/helpers": "^0.5.17",
208
208
  "@swc/plugin-loadable-components": "^11.12.0",
209
209
  "@types/loadable__component": "^5.13.10",
@@ -215,12 +215,12 @@
215
215
  "isbot": "3.8.0",
216
216
  "react-helmet": "^6.1.0",
217
217
  "react-is": "^18.3.1",
218
- "@modern-js/plugin": "3.3.0",
219
- "@modern-js/render": "3.3.0",
220
- "@modern-js/plugin-data-loader": "3.3.0",
221
- "@modern-js/runtime-utils": "3.3.0",
222
- "@modern-js/types": "3.3.0",
223
- "@modern-js/utils": "3.3.0"
218
+ "@modern-js/plugin": "3.5.0",
219
+ "@modern-js/plugin-data-loader": "3.5.0",
220
+ "@modern-js/render": "3.5.0",
221
+ "@modern-js/runtime-utils": "3.5.0",
222
+ "@modern-js/types": "3.5.0",
223
+ "@modern-js/utils": "3.5.0"
224
224
  },
225
225
  "peerDependencies": {
226
226
  "react": ">=17.0.2",
@@ -228,8 +228,8 @@
228
228
  },
229
229
  "devDependencies": {
230
230
  "@remix-run/web-fetch": "^4.1.3",
231
- "@rsbuild/core": "2.0.10",
232
- "@rslib/core": "0.22.0",
231
+ "@rsbuild/core": "2.1.0",
232
+ "@rslib/core": "0.23.0",
233
233
  "@testing-library/dom": "^10.4.1",
234
234
  "@testing-library/react": "^16.3.2",
235
235
  "@types/cookie": "0.6.0",
@@ -240,7 +240,7 @@
240
240
  "react-dom": "^19.2.7",
241
241
  "ts-node": "^10.9.2",
242
242
  "typescript": "^5",
243
- "@modern-js/app-tools": "3.3.0",
243
+ "@modern-js/app-tools": "3.5.0",
244
244
  "@modern-js/rslib": "2.68.10",
245
245
  "@scripts/rstest-config": "2.66.0"
246
246
  },