diff --git a/crates/parcel-resolver/src/package_json.rs b/crates/parcel-resolver/src/package_json.rs index f3b63f22a71..0b507e57e25 100644 --- a/crates/parcel-resolver/src/package_json.rs +++ b/crates/parcel-resolver/src/package_json.rs @@ -584,9 +584,7 @@ impl PackageJson { for (key, value) in target { let matches = match key { ExportsKey::Condition(key) => { - *key == ExportsCondition::SOURCE - || *key == ExportsCondition::DEFAULT - || conditions.contains(*key) + *key == ExportsCondition::DEFAULT || conditions.contains(*key) } ExportsKey::CustomCondition(key) => custom_conditions.iter().any(|k| k == key), _ => false, diff --git a/packages/configs/default/index.json b/packages/configs/default/index.json index de133550b75..8c49b485d01 100644 --- a/packages/configs/default/index.json +++ b/packages/configs/default/index.json @@ -48,6 +48,7 @@ }, "namers": ["@parcel/namer-default"], "runtimes": [ + "@parcel/runtime-rsc", "@parcel/runtime-js", "@parcel/runtime-browser-hmr", "@parcel/runtime-service-worker" diff --git a/packages/configs/default/package.json b/packages/configs/default/package.json index a933aeaf0c4..686f6484360 100644 --- a/packages/configs/default/package.json +++ b/packages/configs/default/package.json @@ -36,6 +36,7 @@ "@parcel/resolver-default": "2.13.3", "@parcel/runtime-browser-hmr": "2.13.3", "@parcel/runtime-js": "2.13.3", + "@parcel/runtime-rsc": "2.13.3", "@parcel/runtime-service-worker": "2.13.3", "@parcel/transformer-babel": "2.13.3", "@parcel/transformer-css": "2.13.3", diff --git a/packages/configs/react-static/index.json b/packages/configs/react-static/index.json new file mode 100644 index 00000000000..7181328f5ad --- /dev/null +++ b/packages/configs/react-static/index.json @@ -0,0 +1,7 @@ +{ + "extends": "@parcel/config-default", + "namers": ["@parcel/namer-static", "..."], + "packagers": { + "*.html": "@parcel/packager-react-static" + } +} diff --git a/packages/configs/react-static/package.json b/packages/configs/react-static/package.json new file mode 100644 index 00000000000..0261a83aba7 --- /dev/null +++ b/packages/configs/react-static/package.json @@ -0,0 +1,15 @@ +{ + "name": "@parcel/config-react-static", + "version": "2.13.3", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/parcel-bundler/parcel.git" + }, + "main": "index.json", + "dependencies": { + "@parcel/config-default": "2.13.3", + "@parcel/namer-static": "2.13.3", + "@parcel/packager-react-static": "2.13.3" + } +} diff --git a/packages/core/core/src/PackagerRunner.js b/packages/core/core/src/PackagerRunner.js index a7dc9ac27fe..87d7a8be08e 100644 --- a/packages/core/core/src/PackagerRunner.js +++ b/packages/core/core/src/PackagerRunner.js @@ -72,7 +72,7 @@ type Opts = {| |}; export type RunPackagerRunnerResult = {| - bundleInfo: BundleInfo, + bundleInfo: BundleInfo[], configRequests: Array, devDepRequests: Array, invalidations: Array, @@ -91,7 +91,6 @@ export type BundleInfo = {| type CacheKeyMap = {| content: string, map: string, - info: string, |}; const BOUNDARY_LENGTH = HASH_REF_PREFIX.length + 32 - 1; @@ -146,14 +145,21 @@ export default class PackagerRunner { invalidateDevDeps(invalidDevDeps, this.options, this.config); let {configs, bundleConfigs} = await this.loadConfigs(bundleGraph, bundle); - let bundleInfo = - (await this.getBundleInfoFromCache( - bundleGraph, + let bundleInfo = await this.getBundleInfoFromCache( + bundleGraph, + bundle, + configs, + bundleConfigs, + ); + + if (bundleInfo.length === 0) { + bundleInfo = await this.getBundleInfo( bundle, + bundleGraph, configs, bundleConfigs, - )) ?? - (await this.getBundleInfo(bundle, bundleGraph, configs, bundleConfigs)); + ); + } let configRequests = getConfigRequests([ ...configs.values(), @@ -287,9 +293,9 @@ export default class PackagerRunner { bundle: InternalBundle, configs: Map, bundleConfigs: Map, - ): Async { + ): Promise { if (this.options.shouldDisableCache) { - return; + return []; } let cacheKey = await this.getCacheKey( @@ -300,7 +306,8 @@ export default class PackagerRunner { this.previousInvalidations, ); let infoKey = PackagerRunner.getInfoKey(cacheKey); - return this.options.cache.get(infoKey); + let res = await this.options.cache.get(infoKey); + return res ?? []; } async getBundleInfo( @@ -308,8 +315,8 @@ export default class PackagerRunner { bundleGraph: InternalBundleGraph, configs: Map, bundleConfigs: Map, - ): Promise { - let {type, contents, map} = await this.getBundleResult( + ): Promise { + let results = await this.getBundleResult( bundle, bundleGraph, configs, @@ -324,13 +331,8 @@ export default class PackagerRunner { bundleConfigs, [...this.invalidations.values()], ); - let cacheKeys = { - content: PackagerRunner.getContentKey(cacheKey), - map: PackagerRunner.getMapKey(cacheKey), - info: PackagerRunner.getInfoKey(cacheKey), - }; - return this.writeToCache(cacheKeys, type, contents, map); + return this.writeToCache(cacheKey, results); } async getBundleResult( @@ -338,35 +340,43 @@ export default class PackagerRunner { bundleGraph: InternalBundleGraph, configs: Map, bundleConfigs: Map, - ): Promise<{| - type: string, - contents: Blob, - map: ?string, - |}> { - let packaged = await this.package( + ): Promise< + {| + type: string, + contents: Blob, + map: ?string, + |}[], + > { + let packagedResults = await this.package( bundle, bundleGraph, configs, bundleConfigs, ); - let type = packaged.type ?? bundle.type; - let res = await this.optimize( - bundle, - bundleGraph, - type, - packaged.contents, - packaged.map, - configs, - bundleConfigs, - ); + return Promise.all( + packagedResults.map(async packaged => { + let type = packaged.type ?? bundle.type; + let res = await this.optimize( + bundle, + bundleGraph, + type, + packaged.contents, + packaged.map, + configs, + bundleConfigs, + ); - let map = - res.map != null ? await this.generateSourceMap(bundle, res.map) : null; - return { - type: res.type ?? type, - contents: res.contents, - map, - }; + let map = + res.map != null + ? await this.generateSourceMap(bundle, res.map) + : null; + return { + type: res.type ?? type, + contents: res.contents, + map, + }; + }), + ); } getSourceMapReference(bundle: NamedBundle, map: ?SourceMap): Async { @@ -386,7 +396,7 @@ export default class PackagerRunner { bundleGraph: InternalBundleGraph, configs: Map, bundleConfigs: Map, - ): Promise { + ): Promise { let bundle = NamedBundle.get(internalBundle, bundleGraph, this.options); this.report({ type: 'buildProgress', @@ -401,7 +411,7 @@ export default class PackagerRunner { measurement = tracer.createMeasurement(name, 'packaging', bundle.name, { type: bundle.type, }); - return await plugin.package({ + let res = await plugin.package({ config: configs.get(name)?.result, bundleConfig: bundleConfigs.get(name)?.result, bundle, @@ -434,9 +444,13 @@ export default class PackagerRunner { bundleConfigs, ); - return {contents: res.contents}; + return {contents: res[0].contents}; }, }); + if (Array.isArray(res)) { + return res; + } + return [res]; } catch (e) { throw new ThrowableDiagnostic({ diagnostic: errorToDiagnostic(e, { @@ -482,12 +496,16 @@ export default class PackagerRunner { NamedBundle.get.bind(NamedBundle), this.options, ); + let name = bundle.name; + if (type !== bundle.type) { + name = name.slice(0, -path.extname(name).length) + '.' + type; + } let optimizers = await this.config.getOptimizers( - bundle.name, + name, internalBundle.pipeline, ); if (!optimizers.length) { - return {type: bundle.type, contents, map}; + return {type, contents, map}; } this.report({ @@ -691,97 +709,82 @@ export default class PackagerRunner { return devDepHashes; } - async readFromCache(cacheKey: string): Promise { - let contentKey = PackagerRunner.getContentKey(cacheKey); - let mapKey = PackagerRunner.getMapKey(cacheKey); - - let isLargeBlob = await this.options.cache.hasLargeBlob(contentKey); - let contentExists = - isLargeBlob || (await this.options.cache.has(contentKey)); - if (!contentExists) { - return null; - } - - let mapExists = await this.options.cache.has(mapKey); - - return { - contents: isLargeBlob - ? this.options.cache.getStream(contentKey) - : blobToStream(await this.options.cache.getBlob(contentKey)), - map: mapExists - ? blobToStream(await this.options.cache.getBlob(mapKey)) - : null, - }; - } - async writeToCache( - cacheKeys: CacheKeyMap, - type: string, - contents: Blob, - map: ?string, - ): Promise { - let size = 0; - let hash; - let hashReferences = []; - let isLargeBlob = false; - - // TODO: don't replace hash references in binary files?? - if (contents instanceof Readable) { - isLargeBlob = true; - let boundaryStr = ''; - let h = new Hash(); - await this.options.cache.setStream( - cacheKeys.content, - blobToStream(contents).pipe( - new TapStream(buf => { - let str = boundaryStr + buf.toString(); - hashReferences = hashReferences.concat( - str.match(HASH_REF_REGEX) ?? [], - ); - size += buf.length; - h.writeBuffer(buf); - boundaryStr = str.slice(str.length - BOUNDARY_LENGTH); - }), - ), - ); - hash = h.finish(); - } else if (typeof contents === 'string') { - let buffer = Buffer.from(contents); - size = buffer.byteLength; - hash = hashBuffer(buffer); - hashReferences = contents.match(HASH_REF_REGEX) ?? []; - await this.options.cache.setBlob(cacheKeys.content, buffer); - } else { - size = contents.length; - hash = hashBuffer(contents); - hashReferences = contents.toString().match(HASH_REF_REGEX) ?? []; - await this.options.cache.setBlob(cacheKeys.content, contents); - } + cacheKey: string, + results: Array<{| + contents: Blob, + map: ?string, + type: string, + |}>, + ): Promise { + let info = await Promise.all( + results.map(async ({contents, map, type}, index) => { + let size = 0; + let hash; + let hashReferences = []; + let isLargeBlob = false; + let cacheKeys = { + content: PackagerRunner.getContentKey(cacheKey, index), + map: PackagerRunner.getMapKey(cacheKey, index), + }; - if (map != null) { - await this.options.cache.setBlob(cacheKeys.map, map); - } - let info = { - type, - size, - hash, - hashReferences, - cacheKeys, - isLargeBlob, - }; - await this.options.cache.set(cacheKeys.info, info); + // TODO: don't replace hash references in binary files?? + if (contents instanceof Readable) { + isLargeBlob = true; + let boundaryStr = ''; + let h = new Hash(); + await this.options.cache.setStream( + cacheKeys.content, + blobToStream(contents).pipe( + new TapStream(buf => { + let str = boundaryStr + buf.toString(); + hashReferences = hashReferences.concat( + str.match(HASH_REF_REGEX) ?? [], + ); + size += buf.length; + h.writeBuffer(buf); + boundaryStr = str.slice(str.length - BOUNDARY_LENGTH); + }), + ), + ); + hash = h.finish(); + } else if (typeof contents === 'string') { + let buffer = Buffer.from(contents); + size = buffer.byteLength; + hash = hashBuffer(buffer); + hashReferences = contents.match(HASH_REF_REGEX) ?? []; + await this.options.cache.setBlob(cacheKeys.content, buffer); + } else { + size = contents.length; + hash = hashBuffer(contents); + hashReferences = contents.toString().match(HASH_REF_REGEX) ?? []; + await this.options.cache.setBlob(cacheKeys.content, contents); + } + + if (map != null) { + await this.options.cache.setBlob(cacheKeys.map, map); + } + let info: BundleInfo = { + type, + size, + hash, + hashReferences, + cacheKeys, + isLargeBlob, + }; + return info; + }), + ); + await this.options.cache.set(PackagerRunner.getInfoKey(cacheKey), info); return info; } - static getContentKey(cacheKey: string): string { - return hashString(`${cacheKey}:content`); + static getContentKey(cacheKey: string, index: number): string { + return hashString(`${cacheKey}:${index}:content`); } - static getMapKey(cacheKey: string): string { - return hashString(`${cacheKey}:map`); + static getMapKey(cacheKey: string, index: number): string { + return hashString(`${cacheKey}:${index}:map`); } static getInfoKey(cacheKey: string): string { diff --git a/packages/core/core/src/public/Bundle.js b/packages/core/core/src/public/Bundle.js index 9e9b29dac91..6e1fda606d8 100644 --- a/packages/core/core/src/public/Bundle.js +++ b/packages/core/core/src/public/Bundle.js @@ -17,6 +17,7 @@ import type { Stats, Target as ITarget, BundleBehavior, + PackagedBundleFile, } from '@parcel/types'; import type BundleGraph from '../BundleGraph'; @@ -263,7 +264,7 @@ export class PackagedBundle extends NamedBundle implements IPackagedBundle { #bundle /*: InternalBundle */; #bundleGraph /*: BundleGraph */; #options /*: ParcelOptions */; - #bundleInfo /*: ?PackagedBundleInfo */; + #bundleInfo /*: ?PackagedBundleInfo[] */; constructor( sentinel: mixed, @@ -307,7 +308,7 @@ export class PackagedBundle extends NamedBundle implements IPackagedBundle { internalBundle: InternalBundle, bundleGraph: BundleGraph, options: ParcelOptions, - bundleInfo: ?PackagedBundleInfo, + bundleInfo: ?(PackagedBundleInfo[]), ): PackagedBundle { let packagedBundle = PackagedBundle.get( internalBundle, @@ -321,17 +322,26 @@ export class PackagedBundle extends NamedBundle implements IPackagedBundle { get filePath(): string { return fromProjectPath( this.#options.projectRoot, - nullthrows(this.#bundleInfo).filePath, + nullthrows(this.#bundleInfo)[0].filePath, ); } get type(): string { // The bundle type may be overridden in the packager. // However, inline bundles will not have a bundleInfo here since they are not written to the filesystem. - return this.#bundleInfo ? this.#bundleInfo.type : this.#bundle.type; + return this.#bundleInfo ? this.#bundleInfo[0].type : this.#bundle.type; } get stats(): Stats { - return nullthrows(this.#bundleInfo).stats; + return nullthrows(this.#bundleInfo)[0].stats; + } + + get files(): PackagedBundleFile[] { + return this.#bundleInfo + ? this.#bundleInfo.map(i => ({ + filePath: fromProjectPath(this.#options.projectRoot, i.filePath), + stats: i.stats, + })) + : []; } } diff --git a/packages/core/core/src/requests/PackageRequest.js b/packages/core/core/src/requests/PackageRequest.js index 666298cb780..2169a809bf0 100644 --- a/packages/core/core/src/requests/PackageRequest.js +++ b/packages/core/core/src/requests/PackageRequest.js @@ -24,7 +24,7 @@ type PackageRequestInput = {| useMainThread?: boolean, |}; -export type PackageRequestResult = BundleInfo; +export type PackageRequestResult = BundleInfo[]; type RunInput = {| input: PackageRequestInput, @@ -34,7 +34,7 @@ type RunInput = {| export type PackageRequest = {| id: ContentKey, +type: typeof requestTypes.package_request, - run: (RunInput) => Async, + run: (RunInput) => Async, input: PackageRequestInput, |}; @@ -95,8 +95,10 @@ async function run({input, api, farm}) { } } - // $FlowFixMe[cannot-write] time is marked read-only, but this is the exception - bundleInfo.time = Date.now() - start; + for (let info of bundleInfo) { + // $FlowFixMe[cannot-write] time is marked read-only, but this is the exception + info.time = Date.now() - start; + } api.storeResult(bundleInfo); return bundleInfo; diff --git a/packages/core/core/src/requests/ParcelBuildRequest.js b/packages/core/core/src/requests/ParcelBuildRequest.js index a3bb384c9ec..b80e584ecb4 100644 --- a/packages/core/core/src/requests/ParcelBuildRequest.js +++ b/packages/core/core/src/requests/ParcelBuildRequest.js @@ -31,7 +31,7 @@ type ParcelBuildRequestInput = {| export type ParcelBuildRequestResult = {| bundleGraph: BundleGraph, - bundleInfo: Map, + bundleInfo: Map, changedAssets: Map, assetRequests: Array, |}; diff --git a/packages/core/core/src/requests/WriteBundleRequest.js b/packages/core/core/src/requests/WriteBundleRequest.js index d6c7fe1d733..836fa5640ec 100644 --- a/packages/core/core/src/requests/WriteBundleRequest.js +++ b/packages/core/core/src/requests/WriteBundleRequest.js @@ -2,7 +2,7 @@ import type {FileSystem, FileOptions} from '@parcel/fs'; import type {ContentKey} from '@parcel/graph'; -import type {Async, FilePath, Compressor} from '@parcel/types'; +import type {Async, Compressor} from '@parcel/types'; import type {RunAPI, StaticRunOpts} from '../RequestTracker'; import type {Bundle, PackagedBundleInfo, ParcelOptions} from '../types'; @@ -51,7 +51,7 @@ type WriteBundleRequestInput = {| hashRefToNameHash: Map, |}; -export type WriteBundleRequestResult = PackagedBundleInfo; +export type WriteBundleRequestResult = PackagedBundleInfo[]; type RunInput = {| input: WriteBundleRequestInput, @@ -61,7 +61,7 @@ type RunInput = {| export type WriteBundleRequest = {| id: ContentKey, +type: typeof requestTypes.write_bundle_request, - run: (RunInput) => Async, + run: (RunInput) => Async, input: WriteBundleRequestInput, |}; @@ -137,12 +137,6 @@ async function run({input, options, api}) { await options.cache.getBlob(cacheKeys.content), ); } - let size = 0; - contentStream = contentStream.pipe( - new TapStream(buf => { - size += buf.length; - }), - ); let configResult = nullthrows( await api.runRequest(createParcelConfigRequest()), @@ -152,7 +146,7 @@ async function run({input, options, api}) { let {devDeps, invalidDevDeps} = await getDevDepRequests(api); invalidateDevDeps(invalidDevDeps, options, config); - await writeFiles( + let files = await writeFiles( contentStream, info, hashRefToNameHash, @@ -171,7 +165,7 @@ async function run({input, options, api}) { !bundle.env.sourceMap.inline && (await options.cache.has(mapKey)) ) { - await writeFiles( + let mapFiles = await writeFiles( blobToStream(await options.cache.getBlob(mapKey)), info, hashRefToNameHash, @@ -183,19 +177,11 @@ async function run({input, options, api}) { devDeps, api, ); + files.push(...mapFiles); } - let res = { - filePath, - type: info.type, - stats: { - size, - time: info.time ?? 0, - }, - }; - - api.storeResult(res); - return res; + api.storeResult(files); + return files; } async function writeFiles( @@ -208,12 +194,11 @@ async function writeFiles( filePath: ProjectPath, writeOptions: ?FileOptions, devDeps: Map, - api: RunAPI, -) { + api: RunAPI, +): Promise { let compressors = await config.getCompressors( fromProjectPathRelative(filePath), ); - let fullPath = fromProjectPath(options.projectRoot, filePath); let stream = info.hashReferences.length ? inputStream.pipe(replaceStream(hashRefToNameHash)) @@ -224,10 +209,11 @@ async function writeFiles( promises.push( runCompressor( compressor, + info, cloneStream(stream), options, outputFS, - fullPath, + filePath, writeOptions, devDeps, api, @@ -235,25 +221,27 @@ async function writeFiles( ); } - await Promise.all(promises); + let results = await Promise.all(promises); + return results.filter(Boolean); } async function runCompressor( compressor: LoadedPlugin, + info: BundleInfo, stream: stream$Readable, options: ParcelOptions, outputFS: FileSystem, - filePath: FilePath, + inputFilePath: ProjectPath, writeOptions: ?FileOptions, devDeps: Map, - api: RunAPI, -) { + api: RunAPI, +): Promise { let measurement; try { measurement = tracer.createMeasurement( compressor.name, 'compress', - path.relative(options.projectRoot, filePath), + fromProjectPathRelative(inputFilePath), ); let res = await compressor.plugin.compress({ stream, @@ -262,21 +250,45 @@ async function runCompressor( tracer: new PluginTracer({origin: compressor.name, category: 'compress'}), }); + let filePath = inputFilePath; if (res != null) { + if (res.type != null) { + let type = res.type; + filePath = toProjectPathUnsafe( + fromProjectPathRelative(filePath) + '.' + type, + ); + } + + let size = 0; + let stream = res.stream.pipe( + new TapStream(buf => { + size += buf.length; + }), + ); + + let fullPath = fromProjectPath(options.projectRoot, filePath); await new Promise((resolve, reject) => pipeline( - res.stream, - outputFS.createWriteStream( - filePath + (res.type != null ? '.' + res.type : ''), - writeOptions, - ), + stream, + outputFS.createWriteStream(fullPath, writeOptions), err => { if (err) reject(err); else resolve(); }, ), ); + + return { + filePath, + type: info.type, + stats: { + size, + time: info.time ?? 0, + }, + }; } + + return null; } catch (err) { throw new ThrowableDiagnostic({ diagnostic: errorToDiagnostic(err, { diff --git a/packages/core/core/src/requests/WriteBundlesRequest.js b/packages/core/core/src/requests/WriteBundlesRequest.js index 8a50b04e349..2da496e4a84 100644 --- a/packages/core/core/src/requests/WriteBundlesRequest.js +++ b/packages/core/core/src/requests/WriteBundlesRequest.js @@ -21,7 +21,7 @@ type WriteBundlesRequestInput = {| optionsRef: SharedReference, |}; -export type WriteBundlesRequestResult = Map; +export type WriteBundlesRequestResult = Map; type RunInput = {| input: WriteBundlesRequestInput, @@ -59,9 +59,9 @@ async function run({input, api, farm, options}) { let res = new Map(); let bundleInfoMap: {| - [string]: BundleInfo, + [string]: BundleInfo[], |} = {}; - let writeEarlyPromises = {}; + let writeEarlyPromises: {[string]: Promise} = {}; let hashRefToNameHash = new Map(); let bundles = bundleGraph.getBundles().filter(bundle => { // Do not package and write placeholder bundles to disk. We just @@ -73,14 +73,16 @@ async function run({input, api, farm, options}) { bundle.name, `Expected ${bundle.type} bundle to have a name`, ).replace(bundle.hashReference, hash); - res.set(bundle.id, { - filePath: joinProjectPath(bundle.target.distDir, name), - type: bundle.type, // FIXME: this is wrong if the packager changes the type... - stats: { - time: 0, - size: 0, + res.set(bundle.id, [ + { + filePath: joinProjectPath(bundle.target.distDir, name), + type: bundle.type, // FIXME: this is wrong if the packager changes the type... + stats: { + time: 0, + size: 0, + }, }, - }); + ]); return false; } @@ -105,7 +107,7 @@ async function run({input, api, farm, options}) { useMainThread, }); - let info = await api.runRequest(request); + let infos = await api.runRequest(request); if (!useMainThread) { // Force a refresh of the cache to avoid a race condition @@ -119,42 +121,51 @@ async function run({input, api, farm, options}) { options.cache.refresh(); } - bundleInfoMap[bundle.id] = info; - if (!info.hashReferences.length) { + bundleInfoMap[bundle.id] = infos; + if (infos.every(info => info.hashReferences.length === 0)) { hashRefToNameHash.set( bundle.hashReference, options.shouldContentHash - ? info.hash.slice(-8) + ? infos.length === 1 + ? infos[0].hash.slice(-8) + : hashString(infos.map(i => i.hash).join(':')).slice(-8) : bundle.id.slice(-8), ); - let writeBundleRequest = createWriteBundleRequest({ - bundle, - info, - hashRefToNameHash, - bundleGraph, - }); - let promise = api.runRequest(writeBundleRequest); - // If the promise rejects before we await it (below), we don't want to crash the build. - promise.catch(() => {}); - writeEarlyPromises[bundle.id] = promise; + for (let info of infos) { + let writeBundleRequest = createWriteBundleRequest({ + bundle, + info, + hashRefToNameHash, + bundleGraph, + }); + let promise = api.runRequest(writeBundleRequest); + // If the promise rejects before we await it (below), we don't want to crash the build. + promise.catch(() => {}); + writeEarlyPromises[info.cacheKeys.content] = promise; + } } }), ); assignComplexNameHashes(hashRefToNameHash, bundles, bundleInfoMap, options); await Promise.all( bundles.map(bundle => { - let promise = - writeEarlyPromises[bundle.id] ?? - api.runRequest( - createWriteBundleRequest({ - bundle, - info: bundleInfoMap[bundle.id], - hashRefToNameHash, - bundleGraph, - }), - ); - - return promise.then(r => res.set(bundle.id, r)); + let promise = Promise.all( + bundleInfoMap[bundle.id].map(info => { + return ( + writeEarlyPromises[info.cacheKeys.content] ?? + api.runRequest( + createWriteBundleRequest({ + bundle, + info, + hashRefToNameHash, + bundleGraph, + }), + ) + ); + }), + ); + + return promise.then(r => res.set(bundle.id, r.flat())); }), ); @@ -180,7 +191,7 @@ function assignComplexNameHashes( options.shouldContentHash ? hashString( [...getBundlesIncludedInHash(bundle.id, bundleInfoMap)] - .map(bundleId => bundleInfoMap[bundleId].hash) + .flatMap(bundleId => bundleInfoMap[bundleId].map(i => i.hash)) .join(':'), ).slice(-8) : bundle.id.slice(-8), @@ -194,10 +205,12 @@ function getBundlesIncludedInHash( included = new Set(), ) { included.add(bundleId); - for (let hashRef of bundleInfoMap[bundleId].hashReferences) { - let referencedId = getIdFromHashRef(hashRef); - if (!included.has(referencedId)) { - getBundlesIncludedInHash(referencedId, bundleInfoMap, included); + for (let info of bundleInfoMap[bundleId]) { + for (let hashRef of info.hashReferences) { + let referencedId = getIdFromHashRef(hashRef); + if (!included.has(referencedId)) { + getBundlesIncludedInHash(referencedId, bundleInfoMap, included); + } } } diff --git a/packages/core/integration-tests/test/react-server.js b/packages/core/integration-tests/test/react-server.js index b27193f4637..eeb73b119c5 100644 --- a/packages/core/integration-tests/test/react-server.js +++ b/packages/core/integration-tests/test/react-server.js @@ -43,14 +43,6 @@ describe('react server components', function () { }), ); - await overlayFS.writeFile( - path.join(dir, '.parcelrc'), - JSON.stringify({ - extends: '@parcel/config-default', - runtimes: ['@parcel/runtime-rsc', '...'], - }), - ); - await overlayFS.writeFile(path.join(dir, 'yarn.lock'), ''); }); diff --git a/packages/core/integration-tests/test/react-ssg.js b/packages/core/integration-tests/test/react-ssg.js new file mode 100644 index 00000000000..95e1bea1815 --- /dev/null +++ b/packages/core/integration-tests/test/react-ssg.js @@ -0,0 +1,193 @@ +// @flow +import assert from 'assert'; +import path from 'path'; +import {bundle, overlayFS, fsFixture, assertBundles} from '@parcel/test-utils'; + +describe('react static', function () { + let count = 0; + let dir; + beforeEach(async () => { + dir = path.join(__dirname, 'react-static', '' + ++count); + await overlayFS.mkdirp(dir); + await overlayFS.writeFile( + path.join(dir, 'package.json'), + JSON.stringify({ + name: 'react-static-test', + dependencies: { + react: '^19', + }, + targets: { + default: { + context: 'react-server', + scopeHoist: false, + }, + }, + }), + ); + + await overlayFS.writeFile( + path.join(dir, '.parcelrc'), + JSON.stringify({ + extends: '@parcel/config-react-static', + }), + ); + + await overlayFS.writeFile(path.join(dir, 'yarn.lock'), ''); + }); + + after(async () => { + await overlayFS.rimraf(path.join(__dirname, 'react-static')); + }); + + it('should render to HTML', async function () { + await fsFixture(overlayFS, dir)` + index.jsx: + import {Client} from './client'; + import {Resources} from "@parcel/runtime-rsc"; + import './bootstrap'; + + export default function Index() { + return ( + + + Static RSC + + + +

This is an RSC!

+ + + + ); + } + + client.jsx: + "use client"; + export function Client() { + return

Client

; + } + + bootstrap.js: + "use client-entry"; + `; + + let b = await bundle(path.join(dir, '/index.jsx'), { + inputFS: overlayFS, + targets: ['default'], + mode: 'production', + env: { + NODE_ENV: 'production', + }, + }); + + assertBundles( + b, + [ + { + name: 'index.html', + assets: ['index.jsx', 'resources.js'], + }, + { + assets: ['client.jsx', 'bootstrap.js'], + }, + ], + {skipNodeModules: true}, + ); + + let files = b.getBundles()[0].files; + assert.equal(files.length, 2); + assert.equal(path.basename(files[0].filePath), 'index.html'); + assert.equal(path.basename(files[1].filePath), 'index.rsc'); + + let output = await overlayFS.readFile(files[0].filePath, 'utf8'); + assert(output.includes('

This is an RSC!

Client

')); + assert(output.includes('