Skip to content

ship compiled hooks behind exports condition, inline React Compiler runtime #12731

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: release-4.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .api-reports/api-report-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,9 @@ interface QueryResult_2<TData = unknown> {
error?: ErrorLike;
}

// @public (undocumented)
export const reactCompilerVersion: string;

// @public (undocumented)
type RefetchQueriesInclude = RefetchQueryDescriptor[] | RefetchQueriesIncludeShorthand;

Expand Down
12 changes: 12 additions & 0 deletions .api-reports/api-report-react_internal_compiler-runtime.api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## API Report File for "@apollo/client"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).

```ts

// @public
export const c: any;

// (No @packageDocumentation comment for this package)

```
8 changes: 4 additions & 4 deletions .size-limits.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 43689,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 38663,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33356,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27606
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 43643,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 38635,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33333,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27618
}
30 changes: 0 additions & 30 deletions config/babel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,36 +25,6 @@ export const babelTransform: BuildStep = async (options) => {
} satisfies import("@babel/preset-env").Options,
],
],
plugins:
(
// apply the compiler only to the react hooks, not test helper components etc.
relativeSourcePath.match(/react\/hooks/)
) ?
// compiler will insert `"import"` statements, so it's not CJS compatible
options.type === "esm" ?
[
[
"babel-plugin-react-compiler",
{
target: "17",
},
],
]
//For now, the compiler doesn't seem to work in CJS files
/*
[
"babel-plugin-react-compiler",
{
target: "17",
},
],
[
"@babel/plugin-transform-modules-commonjs",
{ importInterop: "none" },
],
*/
: []
: [],
});
return { ast: result.ast!, map: result.map };
},
Expand Down
2 changes: 2 additions & 0 deletions config/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { $ } from "zx";

import { babelTransform } from "./babel.ts";
import { compileTs } from "./compileTs.ts";
import { reactCompiler } from "./react-compiler.ts";
import { deprecateInternals } from "./deprecateInternals.ts";
import { addExports } from "./exports.ts";
import { distDir } from "./helpers.ts";
Expand Down Expand Up @@ -42,6 +43,7 @@ const buildSteps = {
inlineInheritDoc,
deprecateInternals,
processInvariants,
reactCompiler,
verifyVersion,
verifySourceMaps,
} satisfies BuildSteps;
Expand Down
13 changes: 8 additions & 5 deletions config/compileTs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { $ } from "zx";

import type { BuildStep, BuildStepOptions } from "./build.ts";
import type { ExportsCondition } from "./entryPoints.ts";
import { applyRecast } from "./helpers.ts";
import { applyRecast, updatePackageJson } from "./helpers.ts";

export const compileTs: BuildStep = async (options) => {
// TODO use `await using` instead of the `try..finally` here once Node supports it
Expand All @@ -18,15 +18,18 @@ export const compileTs: BuildStep = async (options) => {
await $`npx tsc --project tsconfig.build.json --outDir ${options.targetDir}`;
} else {
const packageJsonPath = join(import.meta.dirname, "..", `package.json`);
const originalPackageJson = await readFile(packageJsonPath, "utf-8");
const originalPackageJson = await readFile(
join(options.rootDir, "package.json"),
"utf-8"
);
try {
// module `node18` will compile to CommonJS if the [detected module format](https://www.typescriptlang.org/docs/handbook/modules/reference.html#module-format-detection)
// is CommonJS, so we temporarily overwrite the `package.json` file
// this is the right way to build CommonJS, the `commonjs` module option should actually not be used
// see https://www.typescriptlang.org/docs/handbook/modules/reference.html#commonjs
const packageJson = JSON.parse(originalPackageJson);
packageJson.type = "commonjs";
writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
await updatePackageJson(options.rootDir, (packageJson) => {
packageJson.type = "commonjs";
});
// noCheck is required to suppress errors like
// error TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("@wry/equality")' call instead.
await $`npx tsc --project tsconfig.build.json --outDir ${options.targetDir} --module node16 --moduleResolution node16 --noCheck`;
Expand Down
51 changes: 26 additions & 25 deletions config/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { join } from "node:path";

import type { BuildStep } from "./build.ts";
import { entryPoints } from "./entryPoints.ts";
import { updatePackageJson } from "./helpers.ts";

type ConditionRoot = {
import?: string;
Expand All @@ -14,34 +15,33 @@ type ConditionRoot = {
};

export const addExports: BuildStep = async (options) => {
const pkgFileName = join(options.packageRoot, "package.json");
const pkg = JSON.parse(await readFile(pkgFileName, "utf-8"));
// normal entry points a la `@apollo/client` and `@apollo/client/core`.
// these entry points will be used in most cases and point to the right file depending
// on how the user is consuming the package.
for (const entryPoint of entryPoints) {
if (typeof entryPoint.value === "string") {
pkg.exports[entryPoint.key] = processEntryPoint(
entryPoint.value,
pkg.exports[entryPoint.key]
);
} else {
for (const [key, value] of Object.entries(entryPoint.value)) {
if (!pkg.exports[entryPoint.key]) {
pkg.exports[entryPoint.key] = {};
}
assert(
typeof value === "string",
"nesting of this complexity is not supported yet"
);
pkg.exports[entryPoint.key][key] = processEntryPoint(
value,
pkg.exports[entryPoint.key][key]
await updatePackageJson(options.packageRoot, (pkg) => {
// normal entry points a la `@apollo/client` and `@apollo/client/core`.
// these entry points will be used in most cases and point to the right file depending
// on how the user is consuming the package.
for (const entryPoint of entryPoints) {
if (typeof entryPoint.value === "string") {
pkg.exports[entryPoint.key] = processEntryPoint(
entryPoint.value,
pkg.exports[entryPoint.key]
);
} else {
for (const [key, value] of Object.entries(entryPoint.value)) {
if (!pkg.exports[entryPoint.key]) {
pkg.exports[entryPoint.key] = {};
}
assert(
typeof value === "string",
"nesting of this complexity is not supported yet"
);
pkg.exports[entryPoint.key][key] = processEntryPoint(
value,
pkg.exports[entryPoint.key][key]
);
}
}
}
}
await writeFile(pkgFileName, JSON.stringify(pkg, null, 2));
});

// add legacy-style exports for `@apollo/client/index.js`, `@apollo/client/core/index.js`,
// `@apollo/client/main.cjs`, `@apollo/client/core/core.cjs`, etc.
Expand Down Expand Up @@ -84,6 +84,7 @@ export const addExports: BuildStep = async (options) => {
return JSON.parse(
JSON.stringify(existing, [
// ensure the order of keys is consistent
"react-compiler",
"module",
"module-sync",
"require",
Expand Down
45 changes: 38 additions & 7 deletions config/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as assert from "node:assert";
import { glob, unlink } from "node:fs/promises";
import { glob, unlink, writeFile } from "node:fs/promises";
import { mkdir, readFile, rm, symlink } from "node:fs/promises";
import { relative } from "node:path";
import * as path from "path";
Expand All @@ -10,6 +10,7 @@ import * as tsParser from "recast/parsers/typescript.js";
// @ts-ignore unfortunately we don't have types for this as it's JS with JSDoc
// eslint-disable-next-line import/no-unresolved
import * as sorcery from "sorcery";
import type { JSONSchemaForNPMPackageJsonFiles } from "./schema.package.json.ts";

export const distDir = path.resolve(import.meta.dirname, "..", "dist");

Expand All @@ -34,7 +35,11 @@ export async function applyRecast({
ast: recast.types.ASTNode;
sourceName: string;
relativeSourcePath: string;
}) => MaybePromise<{ ast: recast.types.ASTNode; targetFileName?: string }>;
}) => MaybePromise<{
ast: recast.types.ASTNode;
targetFileName?: string;
copy?: boolean;
}>;
}) {
for await (let sourceFile of glob(globString, {
withFileTypes: true,
Expand Down Expand Up @@ -81,11 +86,17 @@ export async function applyRecast({
sourceMapName: `${targetFileName}.map`,
});

if (targetFileName !== sourceFileName) {
// we are renaming the files - as we won't be overriding in place,
// delete the old files
await rm(sourcePath);
await rm(sourceMapPath);
if (!transformResult.copy) {
if (targetFileName !== sourceFileName) {
// we are renaming the files - as we won't be overriding in place,
// delete the old files
await rm(sourcePath);
await rm(sourceMapPath);
} else if (result.code === source) {
// no changes, so we can skip writing the file, which guarantees no further
// changes to the source map
continue;
}
}

// load the resulting "targetFileName" and the intermediate file into sorcery
Expand Down Expand Up @@ -137,3 +148,23 @@ export function frameComment(text: string) {
.replaceAll(/(^(\s*\*\s*\n)*|(\n\s*\*\s*)*$)/g, "");
return `*\n${framed}\n`;
}

type PackageJson = Omit<JSONSchemaForNPMPackageJsonFiles, "author"> & {
author: string;
};

export async function updatePackageJson(
dirname: string,
updater: (pkg: PackageJson) => PackageJson | void,
replacer: null | ((this: any, key: string, value: any) => any) = null
) {
const packageJsonPath = path.join(dirname, "package.json");
const pkg = JSON.parse(
await readFile(packageJsonPath, "utf8")
) as PackageJson;
const newContents = updater(pkg) ?? pkg;
await writeFile(
packageJsonPath,
JSON.stringify(newContents, replacer, 2) + "\n"
);
}
100 changes: 43 additions & 57 deletions config/prepareDist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,81 +11,67 @@
// - Create a new `package.json` for each sub-set bundle we support, and
// store it in the appropriate dist sub-directory.

import fs from "node:fs";
import { mkdir } from "node:fs/promises";
import path from "node:path";

/* @apollo/client */

import pkg from "../package.json" with { type: "json" };
import { copyFile, mkdir } from "node:fs/promises";
import { join } from "node:path";

import type { BuildStep } from "./build.ts";
import type { JSONSchemaForNPMPackageJsonFiles } from "./schema.package.json.ts";

// the generated `Person` type is a bit weird - `author` as a string is valid
const packageJson: Omit<JSONSchemaForNPMPackageJsonFiles, "author"> & {
author: string;
} = pkg;
import { updatePackageJson } from "./helpers.ts";

export const prepareDist: BuildStep = async (options) => {
if (!options.first) return;

await mkdir(options.packageRoot, { recursive: true });
await copyFile(
join(options.rootDir, "package.json"),
join(options.packageRoot, "package.json")
);
await updatePackageJson(
options.packageRoot,
(packageJson) => {
// The root package.json is marked as private to prevent publishing
// from happening in the root of the project. This sets the package back to
// public so it can be published from the "dist" directory.
packageJson.private = false;

// The root package.json is marked as private to prevent publishing
// from happening in the root of the project. This sets the package back to
// public so it can be published from the "dist" directory.
packageJson.private = false;

// Remove package.json items that we don't need to publish
delete packageJson.scripts;
delete packageJson.bundlesize;
delete packageJson.devEngines;
delete packageJson.devDependencies;
delete packageJson.overrides;

packageJson.exports = {
"./package.json": "./package.json",
"./*.js": "./legacyEntryPoints/*.js",
"./*.cjs": "./legacyEntryPoints/*.cjs",
"./*.d.ts": "./legacyEntryPoints/*.d.ts",
"./*.d.cts": "./legacyEntryPoints/*.d.cts",
};

// The root package.json points to the CJS/ESM source in "dist", to support
// on-going package development (e.g. running tests, supporting npm link, etc.).
// When publishing from "dist" however, we need to update the package.json
// to point to the files within the same directory.
const distPackageJson =
JSON.stringify(
packageJson,
(_key, value) => {
if (typeof value === "string" && value.startsWith("./dist/")) {
const parts = value.split("/");
parts.splice(1, 1); // remove dist
return parts.join("/");
}
return value;
},
2
) + "\n";
// Remove package.json items that we don't need to publish
delete packageJson.scripts;
delete packageJson.bundlesize;
delete packageJson.devEngines;
delete packageJson.devDependencies;
delete packageJson.overrides;

// Save the modified package.json to "dist"
fs.writeFileSync(
path.join(options.packageRoot, `package.json`),
distPackageJson
packageJson.exports = {
"./package.json": "./package.json",
"./*.js": "./legacyEntryPoints/*.js",
"./*.cjs": "./legacyEntryPoints/*.cjs",
"./*.d.ts": "./legacyEntryPoints/*.d.ts",
"./*.d.cts": "./legacyEntryPoints/*.d.cts",
};
},
(_key: any, value: any) => {
// The root package.json points to the CJS/ESM source in "dist", to support
// on-going package development (e.g. running tests, supporting npm link, etc.).
// When publishing from "dist" however, we need to update the package.json
// to point to the files within the same directory.
if (typeof value === "string" && value.startsWith("./dist/")) {
const parts = value.split("/");
parts.splice(1, 1); // remove dist
return parts.join("/");
}
return value;
}
);

// Copy supporting files into "dist"
fs.copyFileSync(
await copyFile(
`${options.rootDir}/README.md`,
`${options.packageRoot}/README.md`
);
fs.copyFileSync(
await copyFile(
`${options.rootDir}/LICENSE`,
`${options.packageRoot}/LICENSE`
);
fs.copyFileSync(
await copyFile(
`${options.rootDir}/CHANGELOG.md`,
`${options.packageRoot}/CHANGELOG.md`
);
Expand Down
Loading