diff --git a/.github/workflows/bindings.yml b/.github/workflows/bindings.yml
index daf5e8bb..5d1005c1 100644
--- a/.github/workflows/bindings.yml
+++ b/.github/workflows/bindings.yml
@@ -17,13 +17,13 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
- node-version: 16
+ node-version: 20
env:
CI: true
- name: Install esy
run: npm install -g esy
- name: Build PPX
- uses: esy/github-action@master
+ uses: esy/github-action@6863524ed7748e7882e317e31ac2b8b107011744
with:
cache-key: ${{ hashFiles('esy.lock/index.json') }}
working-directory: packages/rescript-relay/rescript-relay-ppx
diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
index 0f01ba2d..008770ff 100644
--- a/.github/workflows/build-release.yml
+++ b/.github/workflows/build-release.yml
@@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
- node-version: 18.x
+ node-version: 20.x
env:
CI: true
- name: Esy install
@@ -70,15 +70,15 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
- node-version: 18.x
+ node-version: 20.x
env:
CI: true
- name: Install esy
run: npm install -g esy
- name: Build PPX
- uses: esy/github-action@master
+ uses: esy/github-action@6863524ed7748e7882e317e31ac2b8b107011744
with:
- cache-key: ${{ matrix.platform }}-${{ hashFiles('esy.lock/index.json') }}-v3
+ cache-key: ${{ matrix.platform }}-${{ hashFiles('esy.lock/index.json') }}-v4
working-directory: packages/rescript-relay/rescript-relay-ppx
- name: Strip PPX binary
if: runner.os != 'Windows'
@@ -154,15 +154,15 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
with:
- node-version: 16
+ node-version: 20
env:
CI: true
- name: Install esy
run: npm install -g esy
- name: Build PPX
- uses: esy/github-action@v1
+ uses: esy/github-action@6863524ed7748e7882e317e31ac2b8b107011744
with:
- cache-key: ${{ hashFiles('esy.lock/index.json') }}
+ cache-key: ${{ hashFiles('esy.lock/index.json') }}-v4
working-directory: packages/rescript-relay/rescript-relay-ppx
- name: Build assets
env:
diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml
index aa5d50f4..0c2c0722 100644
--- a/.github/workflows/cli.yml
+++ b/.github/workflows/cli.yml
@@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
- node-version: 12.19.0
+ node-version: 20
env:
CI: true
- name: Install
diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
index 8352ee4c..9d0a0722 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -16,7 +16,7 @@ jobs:
submodules: "true"
- uses: actions/setup-node@v3
with:
- node-version: 16
+ node-version: 20
env:
CI: true
- uses: actions-rs/toolchain@v1
@@ -25,12 +25,12 @@ jobs:
override: true
- name: Install esy
run: npm install -g esy
- - uses: esy/github-action@master
+ - uses: esy/github-action@6863524ed7748e7882e317e31ac2b8b107011744
with:
cache-key: ${{ hashFiles('esy.lock/index.json') }}
working-directory: packages/rescript-relay/rescript-relay-ppx
- name: Build PPX
- uses: esy/github-action@master
+ uses: esy/github-action@6863524ed7748e7882e317e31ac2b8b107011744
with:
cache-key: ${{ hashFiles('esy.lock/index.json') }}
working-directory: packages/rescript-relay/rescript-relay-ppx
diff --git a/.github/workflows/ppx.yml b/.github/workflows/ppx.yml
index 79c29021..06eddbde 100644
--- a/.github/workflows/ppx.yml
+++ b/.github/workflows/ppx.yml
@@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
- node-version: 14.x
+ node-version: 20.x
env:
CI: true
- name: Esy install
@@ -64,13 +64,13 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
- node-version: 16.x
+ node-version: 20.x
env:
CI: true
- name: Install esy
run: npm install -g esy
- name: Build PPX
- uses: esy/github-action@master
+ uses: esy/github-action@6863524ed7748e7882e317e31ac2b8b107011744
with:
cache-key: ${{ matrix.platform }}-${{ hashFiles('esy.lock/index.json') }}-v3
working-directory: packages/rescript-relay/rescript-relay-ppx
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3af2c396..50454a4c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
# Unreleased
- Support `@alias` fragments.
+- Experimental support for `@codesplit`, a RescriptRelay exclusive directive that's essentially a client side version of [Relay data-driven dependencies (3D)](https://relay.dev/docs/glossary/#3d).
# 3.0.0-rc.9
diff --git a/packages/relay b/packages/relay
index 7951c17e..686d7a39 160000
--- a/packages/relay
+++ b/packages/relay
@@ -1 +1 @@
-Subproject commit 7951c17e5dc260bd534c0d512db7159964fa5706
+Subproject commit 686d7a399bae3f8da73553ccd71ac23eb341ab6a
diff --git a/packages/rescript-relay-cli/package.json b/packages/rescript-relay-cli/package.json
index f9697822..d0449fee 100644
--- a/packages/rescript-relay-cli/package.json
+++ b/packages/rescript-relay-cli/package.json
@@ -18,7 +18,7 @@
"@types/node": "15.12.5",
"@types/prettier": "2.3.0",
"@types/relay-config": "6.0.1",
- "@vercel/ncc": "0.28.6",
+ "@vercel/ncc": "0.38.1",
"fast-glob": "3.2.6",
"jest": "27.0.6",
"ts-jest": "27.0.3",
diff --git a/packages/rescript-relay-cli/yarn.lock b/packages/rescript-relay-cli/yarn.lock
index 458186ef..0e8cd5cc 100644
--- a/packages/rescript-relay-cli/yarn.lock
+++ b/packages/rescript-relay-cli/yarn.lock
@@ -693,10 +693,10 @@
dependencies:
"@types/yargs-parser" "*"
-"@vercel/ncc@0.28.6":
- version "0.28.6"
- resolved "https://registry.yarnpkg.com/@vercel/ncc/-/ncc-0.28.6.tgz#073c0ce8e0269210c0a9f180fb0bf949eecc20e0"
- integrity sha512-t4BoSSuyK8BZaUE0gV18V6bkFs4st7baumtFGa50dv1tMu2GDBEBF8sUZaKBdKiL6DzJ2D2+XVCwYWWDcQOYdQ==
+"@vercel/ncc@0.38.1":
+ version "0.38.1"
+ resolved "https://registry.yarnpkg.com/@vercel/ncc/-/ncc-0.38.1.tgz#13f08738111e1d9e8a22fd6141f3590e54d9a60e"
+ integrity sha512-IBBb+iI2NLu4VQn3Vwldyi2QwaXt5+hTyh58ggAMoCGE6DJmPvwL3KPBWcJl1m9LYPChBLE980Jw+CS4Wokqxw==
abab@^2.0.3, abab@^2.0.5:
version "2.0.5"
diff --git a/packages/rescript-relay/__tests__/GroupAvatar.res b/packages/rescript-relay/__tests__/GroupAvatar.res
new file mode 100644
index 00000000..fbeb637b
--- /dev/null
+++ b/packages/rescript-relay/__tests__/GroupAvatar.res
@@ -0,0 +1,11 @@
+module Fragment = %relay(`
+ fragment GroupAvatar_group on Group {
+ name
+ }
+`)
+
+@react.component
+let make = (~group: RescriptRelay.fragmentRefs<[#GroupAvatar_group]>) => {
+ let group = Fragment.use(group)
+ React.string("Group name: " ++ group.name)
+}
diff --git a/packages/rescript-relay/__tests__/HasNameComponent.res b/packages/rescript-relay/__tests__/HasNameComponent.res
new file mode 100644
index 00000000..ab28745b
--- /dev/null
+++ b/packages/rescript-relay/__tests__/HasNameComponent.res
@@ -0,0 +1,12 @@
+module Fragment = %relay(`
+ fragment HasNameComponent_hasName on HasName {
+ name
+ }
+`)
+
+@react.component
+let make = (~hasName: RescriptRelay.fragmentRefs<[#HasNameComponent_hasName]>) => {
+ let hasName = Fragment.use(hasName)
+
+
{React.string(
diff --git a/packages/rescript-relay/__tests__/Test_codesplit-tests.js b/packages/rescript-relay/__tests__/Test_codesplit-tests.js
new file mode 100644
index 00000000..f64159db
--- /dev/null
+++ b/packages/rescript-relay/__tests__/Test_codesplit-tests.js
@@ -0,0 +1,261 @@
+require("@testing-library/jest-dom/extend-expect");
+const t = require("@testing-library/react");
+const React = require("react");
+const queryMock = require("./queryMock");
+const ReactTestUtils = require("react-dom/test-utils");
+const query = require("./__generated__/TestCodesplitQuery_graphql.bs");
+
+const { test_codesplit } = require("./Test_codesplit.bs");
+
+describe("Autocodesplits", () => {
+ let preloadUserHasRun = false;
+ let preloadUserFn;
+ const targetUserIndex = query.node.params.metadata.codesplits.findIndex(
+ ([p, _]) => p === "member.$$u$$User"
+ );
+
+ let preloadHasNameHasRun = false;
+ let preloadHasNameFn;
+ const targetHasNameIndex = query.node.params.metadata.codesplits.findIndex(
+ ([p, _]) => p === "member.$$i$$HasName"
+ );
+
+ let preloadLinkedFieldHasRun = false;
+ let preloadLinkedFieldFn;
+ const targetLinkedFieldIndex =
+ query.node.params.metadata.codesplits.findIndex(
+ ([p, _]) => p === "member.$$u$$User.description"
+ );
+
+ let preloadBestFriendDescriptionHasRun = false;
+ let preloadBestFriendDescriptionFn;
+ const targetBestFriendDescriptionIndex =
+ query.node.params.metadata.codesplits.findIndex(
+ ([p, _]) => p === "member.$$u$$User.bestFriend.description"
+ );
+
+ beforeEach(() => {
+ // Very hacky way to check if the preload functions has run
+ preloadUserFn = query.node.params.metadata.codesplits[targetUserIndex][1];
+ query.node.params.metadata.codesplits[targetUserIndex][1] = (v) => {
+ preloadUserHasRun = true;
+ return preloadUserFn(v);
+ };
+
+ preloadHasNameFn =
+ query.node.params.metadata.codesplits[targetHasNameIndex][1];
+ query.node.params.metadata.codesplits[targetHasNameIndex][1] = (v) => {
+ preloadHasNameHasRun = true;
+ return preloadHasNameFn(v);
+ };
+
+ preloadLinkedFieldFn =
+ query.node.params.metadata.codesplits[targetLinkedFieldIndex][1];
+ query.node.params.metadata.codesplits[targetLinkedFieldIndex][1] = (v) => {
+ preloadLinkedFieldHasRun = true;
+ return preloadLinkedFieldFn(v);
+ };
+
+ preloadBestFriendDescriptionFn =
+ query.node.params.metadata.codesplits[
+ targetBestFriendDescriptionIndex
+ ][1];
+ query.node.params.metadata.codesplits[targetBestFriendDescriptionIndex][1] =
+ (v) => {
+ if (v.includeBestFriendDescription) {
+ preloadBestFriendDescriptionHasRun = true;
+ return preloadBestFriendDescriptionFn(v);
+ }
+ };
+ });
+
+ afterEach(() => {
+ query.node.params.metadata.codesplits[targetUserIndex][1] = preloadUserFn;
+ preloadUserHasRun = false;
+
+ query.node.params.metadata.codesplits[targetHasNameIndex][1] =
+ preloadHasNameFn;
+ preloadHasNameHasRun = false;
+
+ query.node.params.metadata.codesplits[targetLinkedFieldIndex][1] =
+ preloadLinkedFieldFn;
+ preloadLinkedFieldHasRun = false;
+
+ query.node.params.metadata.codesplits[targetBestFriendDescriptionIndex][1] =
+ preloadBestFriendDescriptionFn;
+ preloadBestFriendDescriptionHasRun = false;
+ });
+
+ test("preload runs when query response matches", async () => {
+ queryMock.mockQuery({
+ name: "TestCodesplitQuery",
+ variables: {
+ includeBestFriendDescription: true,
+ },
+ data: {
+ member: {
+ __typename: "User",
+ id: "user-1",
+ firstName: "First",
+ lastName: "Last",
+ avatarUrl: "avatar-here",
+ description: {
+ content: "Rich content!",
+ },
+ bestFriend: {
+ __typename: "User",
+ id: "user-2",
+ description: {
+ content: "Rich content?",
+ },
+ },
+ },
+ },
+ });
+
+ // No preload yet
+ expect(preloadUserHasRun).toBe(false);
+ expect(preloadHasNameHasRun).toBe(false);
+ expect(preloadLinkedFieldHasRun).toBe(false);
+ expect(preloadBestFriendDescriptionHasRun).toBe(false);
+
+ t.render(test_codesplit());
+
+ await t.screen.findByText("Render");
+
+ // Preloads as soon as data has loaded
+ expect(preloadUserHasRun).toBe(true);
+ expect(preloadHasNameHasRun).toBe(false);
+ expect(preloadLinkedFieldHasRun).toBe(true);
+ expect(preloadBestFriendDescriptionHasRun).toBe(true);
+
+ ReactTestUtils.act(() => {
+ t.fireEvent.click(t.screen.getByText("Render"));
+ });
+
+ await t.screen.findByText("User avatarUrl: avatar-here");
+ await t.screen.findByText("User name: First Last");
+ await t.screen.findByText("Rich content: Rich content!");
+ await t.screen.findByText("Rich content: Rich content?");
+ });
+
+ test("handles when conditionals exclude but the underlying content matches", async () => {
+ queryMock.mockQuery({
+ name: "TestCodesplitQuery",
+ variables: {
+ includeBestFriendDescription: false,
+ },
+ data: {
+ member: {
+ __typename: "User",
+ id: "user-1",
+ firstName: "First",
+ lastName: "Last",
+ avatarUrl: "avatar-here",
+ description: {
+ content: "Rich content!",
+ },
+ bestFriend: {
+ __typename: "User",
+ id: "user-2",
+ description: {
+ content: "Rich content?",
+ },
+ },
+ },
+ },
+ });
+
+ // No preload yet
+ expect(preloadBestFriendDescriptionHasRun).toBe(false);
+
+ t.render(test_codesplit(false));
+
+ await t.screen.findByText("Render");
+
+ // Preloads as soon as data has loaded
+ expect(preloadBestFriendDescriptionHasRun).toBe(false);
+
+ ReactTestUtils.act(() => {
+ t.fireEvent.click(t.screen.getByText("Render"));
+ });
+
+ await t.screen.findByText("User avatarUrl: avatar-here");
+ });
+
+ test("preload does not run when linked field does not match", async () => {
+ queryMock.mockQuery({
+ name: "TestCodesplitQuery",
+ variables: {
+ includeBestFriendDescription: true,
+ },
+ data: {
+ member: {
+ __typename: "User",
+ id: "user-1",
+ firstName: "First",
+ lastName: "Last",
+ avatarUrl: "avatar-here",
+ description: null,
+ bestFriend: null,
+ },
+ },
+ });
+
+ expect(preloadUserHasRun).toBe(false);
+ expect(preloadHasNameHasRun).toBe(false);
+ expect(preloadLinkedFieldHasRun).toBe(false);
+
+ t.render(test_codesplit());
+
+ await t.screen.findByText("Render");
+
+ // Preloads as soon as data has loaded
+ expect(preloadUserHasRun).toBe(true);
+ expect(preloadHasNameHasRun).toBe(false);
+ expect(preloadLinkedFieldHasRun).toBe(false);
+
+ ReactTestUtils.act(() => {
+ t.fireEvent.click(t.screen.getByText("Render"));
+ });
+
+ await t.screen.findByText("User avatarUrl: avatar-here");
+ await t.screen.findByText("User name: First Last");
+ });
+
+ test("preload runs for interface", async () => {
+ queryMock.mockQuery({
+ name: "TestCodesplitQuery",
+ variables: {
+ includeBestFriendDescription: true,
+ },
+ data: {
+ member: {
+ __typename: "Group",
+ id: "group-1",
+ name: "A Group",
+ __isHasName: "Group",
+ },
+ },
+ });
+
+ // No preload yet
+ expect(preloadUserHasRun).toBe(false);
+ expect(preloadHasNameHasRun).toBe(false);
+
+ t.render(test_codesplit());
+
+ await t.screen.findByText("Render");
+
+ // No preloads now either
+ expect(preloadUserHasRun).toBe(false);
+ expect(preloadHasNameHasRun).toBe(true);
+
+ ReactTestUtils.act(() => {
+ t.fireEvent.click(t.screen.getByText("Render"));
+ });
+
+ await t.screen.findByText("Group name: A Group");
+ await t.screen.findByText("Has name: A Group");
+ });
+});
diff --git a/packages/rescript-relay/__tests__/Test_codesplit.res b/packages/rescript-relay/__tests__/Test_codesplit.res
new file mode 100644
index 00000000..b4c7ce33
--- /dev/null
+++ b/packages/rescript-relay/__tests__/Test_codesplit.res
@@ -0,0 +1,91 @@
+module Query = %relay(`
+ query TestCodesplitQuery($includeBestFriendDescription: Boolean!) {
+ member(id: "1") {
+ ...HasNameComponent_hasName @codesplit @alias
+ ... on User {
+ ...UserAvatar_user @codesplit @alias
+ description {
+ ...RichContent_content @codesplit @alias
+ }
+ bestFriend {
+ ...UserAvatar_user @codesplit @alias @skip(if: $includeBestFriendDescription)
+ description {
+ ...RichContent_content @codesplit @alias @include(if: $includeBestFriendDescription)
+ }
+ }
+ }
+ ... on Group {
+ ...GroupAvatar_group @codesplit @alias
+ }
+ }
+ }
+`)
+
+module Test = {
+ @react.component
+ let make = (~includeBestFriendDescription) => {
+ let query = Query.use(~variables={includeBestFriendDescription: includeBestFriendDescription})
+ let (shouldRender, setShouldRender) = React.useState(() => false)
+
+ switch (shouldRender, query.member) {
+ | (false, _) =>
+
+ | (true, None) => React.string("not found")
+ | (true, Some(member)) =>
+ open Query.CodesplitComponents
+
+
+ {switch member {
+ | {hasNameComponent_hasName: Some(hasNameComponent_hasName)} =>
+
+ | _ => React.null
+ }}
+ {switch member {
+ | {groupAvatar_group: Some(groupAvatar_group)} =>
+ | {userAvatar_user: Some(userAvatar_user), description, bestFriend} =>
+ <>
+
+ {switch description {
+ | Some({richContent_content}) =>
+ | None => React.null
+ }}
+ {switch bestFriend {
+ | Some({
+ description: Some({richContent_content: Some(richContent_content)}),
+ userAvatar_user,
+ }) =>
+ <>
+ {switch userAvatar_user {
+ | Some(userAvatar_user) =>
+ | None => React.null
+ }}
+
+ >
+ | _ => React.null
+ }}
+ >
+ | _ => React.null
+ }}
+
+ }
+ }
+}
+
+@live
+let test_codesplit = (~includeBestFriendDescription=true) => {
+ let network = RescriptRelay.Network.makePromiseBased(~fetchFunction=RelayEnv.fetchQuery)
+
+ let environment = RescriptRelay.Environment.make(
+ ~network,
+ ~store=RescriptRelay.Store.make(~source=RescriptRelay.RecordSource.make()),
+ )
+
+
+
+
+}
diff --git a/packages/rescript-relay/__tests__/UserAvatar.res b/packages/rescript-relay/__tests__/UserAvatar.res
new file mode 100644
index 00000000..c4e00399
--- /dev/null
+++ b/packages/rescript-relay/__tests__/UserAvatar.res
@@ -0,0 +1,20 @@
+module Fragment = %relay(`
+ fragment UserAvatar_user on User {
+ avatarUrl
+ ...UserName_user @codesplit @alias
+ }
+`)
+
+@react.component
+let make = (~user: RescriptRelay.fragmentRefs<[#UserAvatar_user]>) => {
+ let user = Fragment.use(user)
+
+ open Fragment.Operation.CodesplitComponents
+
+ <>
+
+ {React.string("User avatarUrl: " ++ user.avatarUrl->Belt.Option.getWithDefault("-"))}
+
+
+ >
+}
diff --git a/packages/rescript-relay/__tests__/UserName.res b/packages/rescript-relay/__tests__/UserName.res
new file mode 100644
index 00000000..896ffc27
--- /dev/null
+++ b/packages/rescript-relay/__tests__/UserName.res
@@ -0,0 +1,12 @@
+module Fragment = %relay(`
+ fragment UserName_user on User {
+ firstName
+ lastName
+ }
+`)
+
+@react.component
+let make = (~user: RescriptRelay.fragmentRefs<[#UserName_user]>) => {
+ let user = Fragment.use(user)
+
{React.string("User name: " ++ user.firstName ++ " " ++ user.lastName)}
+}
diff --git a/packages/rescript-relay/__tests__/__generated__/GroupAvatar_group_graphql.res b/packages/rescript-relay/__tests__/__generated__/GroupAvatar_group_graphql.res
new file mode 100644
index 00000000..b05d7c40
--- /dev/null
+++ b/packages/rescript-relay/__tests__/__generated__/GroupAvatar_group_graphql.res
@@ -0,0 +1,60 @@
+/* @sourceLoc GroupAvatar.res */
+/* @generated */
+%%raw("/* @generated */")
+module Types = {
+ @@warning("-30")
+
+ type fragment = {
+ name: string,
+ }
+}
+
+module Internal = {
+ @live
+ type fragmentRaw
+ @live
+ let fragmentConverter: Js.Dict.t
>> = %raw(
+ json`{}`
+ )
+ @live
+ let fragmentConverterMap = ()
+ @live
+ let convertFragment = v => v->RescriptRelay.convertObj(
+ fragmentConverter,
+ fragmentConverterMap,
+ Js.undefined
+ )
+}
+
+type t
+type fragmentRef
+external getFragmentRef:
+ RescriptRelay.fragmentRefs<[> | #GroupAvatar_group]> => fragmentRef = "%identity"
+
+module Utils = {
+ @@warning("-33")
+ open Types
+}
+
+type relayOperationNode
+type operationType = RescriptRelay.fragmentNode
+
+
+let node: operationType = %raw(json` {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "GroupAvatar_group",
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "name",
+ "storageKey": null
+ }
+ ],
+ "type": "Group",
+ "abstractKey": null
+} `)
+
diff --git a/packages/rescript-relay/__tests__/__generated__/HasNameComponent_hasName_graphql.res b/packages/rescript-relay/__tests__/__generated__/HasNameComponent_hasName_graphql.res
new file mode 100644
index 00000000..d067e511
--- /dev/null
+++ b/packages/rescript-relay/__tests__/__generated__/HasNameComponent_hasName_graphql.res
@@ -0,0 +1,68 @@
+/* @sourceLoc HasNameComponent.res */
+/* @generated */
+%%raw("/* @generated */")
+module Types = {
+ @@warning("-30")
+
+ type fragment = {
+ @live __typename: string,
+ name: string,
+ }
+}
+
+module Internal = {
+ @live
+ type fragmentRaw
+ @live
+ let fragmentConverter: Js.Dict.t>> = %raw(
+ json`{}`
+ )
+ @live
+ let fragmentConverterMap = ()
+ @live
+ let convertFragment = v => v->RescriptRelay.convertObj(
+ fragmentConverter,
+ fragmentConverterMap,
+ Js.undefined
+ )
+}
+
+type t
+type fragmentRef
+external getFragmentRef:
+ RescriptRelay.fragmentRefs<[> | #HasNameComponent_hasName]> => fragmentRef = "%identity"
+
+module Utils = {
+ @@warning("-33")
+ open Types
+}
+
+type relayOperationNode
+type operationType = RescriptRelay.fragmentNode
+
+
+let node: operationType = %raw(json` {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "HasNameComponent_hasName",
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "__typename",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "name",
+ "storageKey": null
+ }
+ ],
+ "type": "HasName",
+ "abstractKey": "__isHasName"
+} `)
+
diff --git a/packages/rescript-relay/__tests__/__generated__/RichContent_content_graphql.res b/packages/rescript-relay/__tests__/__generated__/RichContent_content_graphql.res
new file mode 100644
index 00000000..c05c3b7c
--- /dev/null
+++ b/packages/rescript-relay/__tests__/__generated__/RichContent_content_graphql.res
@@ -0,0 +1,60 @@
+/* @sourceLoc RichContent.res */
+/* @generated */
+%%raw("/* @generated */")
+module Types = {
+ @@warning("-30")
+
+ type fragment = {
+ content: string,
+ }
+}
+
+module Internal = {
+ @live
+ type fragmentRaw
+ @live
+ let fragmentConverter: Js.Dict.t>> = %raw(
+ json`{}`
+ )
+ @live
+ let fragmentConverterMap = ()
+ @live
+ let convertFragment = v => v->RescriptRelay.convertObj(
+ fragmentConverter,
+ fragmentConverterMap,
+ Js.undefined
+ )
+}
+
+type t
+type fragmentRef
+external getFragmentRef:
+ RescriptRelay.fragmentRefs<[> | #RichContent_content]> => fragmentRef = "%identity"
+
+module Utils = {
+ @@warning("-33")
+ open Types
+}
+
+type relayOperationNode
+type operationType = RescriptRelay.fragmentNode
+
+
+let node: operationType = %raw(json` {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "RichContent_content",
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "content",
+ "storageKey": null
+ }
+ ],
+ "type": "RichContent",
+ "abstractKey": null
+} `)
+
diff --git a/packages/rescript-relay/__tests__/__generated__/TestAliasedFragmentsQuery_graphql.res b/packages/rescript-relay/__tests__/__generated__/TestAliasedFragmentsQuery_graphql.res
index e5f86b28..19c20560 100644
--- a/packages/rescript-relay/__tests__/__generated__/TestAliasedFragmentsQuery_graphql.res
+++ b/packages/rescript-relay/__tests__/__generated__/TestAliasedFragmentsQuery_graphql.res
@@ -4,15 +4,9 @@
module Types = {
@@warning("-30")
- type rec response_loggedInUser_TestAliasedFragments_userFirstName = {
- fragmentRefs: RescriptRelay.fragmentRefs<[ | #TestAliasedFragments_userFirstName]>,
- }
- and response_loggedInUser_TestAliasedFragments_userLastName = {
- fragmentRefs: RescriptRelay.fragmentRefs<[ | #TestAliasedFragments_userLastName]>,
- }
- and response_loggedInUser = {
- @as("TestAliasedFragments_userFirstName") testAliasedFragments_userFirstName: response_loggedInUser_TestAliasedFragments_userFirstName,
- @as("TestAliasedFragments_userLastName") testAliasedFragments_userLastName: option,
+ type rec response_loggedInUser = {
+ @as("TestAliasedFragments_userFirstName") testAliasedFragments_userFirstName: RescriptRelay.fragmentRefs<[ | #TestAliasedFragments_userFirstName]>,
+ @as("TestAliasedFragments_userLastName") testAliasedFragments_userLastName: option>,
}
type response = {
loggedInUser: response_loggedInUser,
@@ -55,7 +49,7 @@ module Internal = {
type wrapResponseRaw
@live
let wrapResponseConverter: Js.Dict.t>> = %raw(
- json`{"__root":{"loggedInUser_TestAliasedFragments_userLastName":{"f":""},"loggedInUser_TestAliasedFragments_userFirstName":{"f":""}}}`
+ json`{}`
)
@live
let wrapResponseConverterMap = ()
@@ -69,7 +63,7 @@ module Internal = {
type responseRaw
@live
let responseConverter: Js.Dict.t>> = %raw(
- json`{"__root":{"loggedInUser_TestAliasedFragments_userLastName":{"f":""},"loggedInUser_TestAliasedFragments_userFirstName":{"f":""}}}`
+ json`{}`
)
@live
let responseConverterMap = ()
diff --git a/packages/rescript-relay/__tests__/__generated__/TestCodesplitQuery_graphql.res b/packages/rescript-relay/__tests__/__generated__/TestCodesplitQuery_graphql.res
new file mode 100644
index 00000000..8cbe696e
--- /dev/null
+++ b/packages/rescript-relay/__tests__/__generated__/TestCodesplitQuery_graphql.res
@@ -0,0 +1,480 @@
+/* @sourceLoc Test_codesplit.res */
+/* @generated */
+%%raw("/* @generated */")
+module Types = {
+ @@warning("-30")
+
+ type rec response_member_bestFriend_description = {
+ @as("RichContent_content") richContent_content: option>,
+ }
+ and response_member_bestFriend = {
+ @as("UserAvatar_user") userAvatar_user: option>,
+ description: option,
+ }
+ and response_member_description = {
+ @as("RichContent_content") richContent_content: RescriptRelay.fragmentRefs<[ | #RichContent_content]>,
+ }
+ and response_member = {
+ @live __typename: string,
+ @as("GroupAvatar_group") groupAvatar_group: option>,
+ @as("HasNameComponent_hasName") hasNameComponent_hasName: option>,
+ @as("UserAvatar_user") userAvatar_user: option>,
+ bestFriend: option,
+ description: option,
+ }
+ type response = {
+ member: option,
+ }
+ @live
+ type rawResponse = response
+ @live
+ type variables = {
+ includeBestFriendDescription: bool,
+ }
+ @live
+ type refetchVariables = {
+ includeBestFriendDescription: option,
+ }
+ @live let makeRefetchVariables = (
+ ~includeBestFriendDescription=?,
+ ): refetchVariables => {
+ includeBestFriendDescription: includeBestFriendDescription
+ }
+
+}
+
+
+type queryRef
+
+module Internal = {
+ @live
+ let variablesConverter: Js.Dict.t>> = %raw(
+ json`{}`
+ )
+ @live
+ let variablesConverterMap = ()
+ @live
+ let convertVariables = v => v->RescriptRelay.convertObj(
+ variablesConverter,
+ variablesConverterMap,
+ Js.undefined
+ )
+ @live
+ type wrapResponseRaw
+ @live
+ let wrapResponseConverter: Js.Dict.t>> = %raw(
+ json`{}`
+ )
+ @live
+ let wrapResponseConverterMap = ()
+ @live
+ let convertWrapResponse = v => v->RescriptRelay.convertObj(
+ wrapResponseConverter,
+ wrapResponseConverterMap,
+ Js.null
+ )
+ @live
+ type responseRaw
+ @live
+ let responseConverter: Js.Dict.t>> = %raw(
+ json`{}`
+ )
+ @live
+ let responseConverterMap = ()
+ @live
+ let convertResponse = v => v->RescriptRelay.convertObj(
+ responseConverter,
+ responseConverterMap,
+ Js.undefined
+ )
+ type wrapRawResponseRaw = wrapResponseRaw
+ @live
+ let convertWrapRawResponse = convertWrapResponse
+ type rawResponseRaw = responseRaw
+ @live
+ let convertRawResponse = convertResponse
+ type rawPreloadToken<'response> = {source: Js.Nullable.t>}
+ external tokenToRaw: queryRef => rawPreloadToken = "%identity"
+}
+module Utils = {
+ @@warning("-33")
+ open Types
+}
+
+module CodesplitComponents = {
+ module HasNameComponent = {
+ let make = React.lazy_(() => Js.import(HasNameComponent.make))
+ }
+ module UserAvatar = {
+ let make = React.lazy_(() => Js.import(UserAvatar.make))
+ }
+ module RichContent = {
+ let make = React.lazy_(() => Js.import(RichContent.make))
+ }
+ module GroupAvatar = {
+ let make = React.lazy_(() => Js.import(GroupAvatar.make))
+ }
+}
+
+
+type relayOperationNode
+type operationType = RescriptRelay.queryNode
+
+
+let node: operationType = %raw(json` (function(){
+var v0 = [
+ {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "includeBestFriendDescription"
+ }
+],
+v1 = [
+ {
+ "kind": "Literal",
+ "name": "id",
+ "value": "1"
+ }
+],
+v2 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "__typename",
+ "storageKey": null
+},
+v3 = {
+ "fragment": {
+ "args": null,
+ "kind": "FragmentSpread",
+ "name": "UserAvatar_user"
+ },
+ "kind": "AliasedFragmentSpread",
+ "name": "UserAvatar_user",
+ "type": "User",
+ "abstractKey": null
+},
+v4 = [
+ {
+ "fragment": {
+ "args": null,
+ "kind": "FragmentSpread",
+ "name": "RichContent_content"
+ },
+ "kind": "AliasedFragmentSpread",
+ "name": "RichContent_content",
+ "type": "RichContent",
+ "abstractKey": null
+ }
+],
+v5 = [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "name",
+ "storageKey": null
+ }
+],
+v6 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "avatarUrl",
+ "storageKey": null
+},
+v7 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "firstName",
+ "storageKey": null
+},
+v8 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "lastName",
+ "storageKey": null
+},
+v9 = [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "content",
+ "storageKey": null
+ }
+],
+v10 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+};
+return {
+ "fragment": {
+ "argumentDefinitions": (v0/*: any*/),
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "TestCodesplitQuery",
+ "selections": [
+ {
+ "alias": null,
+ "args": (v1/*: any*/),
+ "concreteType": null,
+ "kind": "LinkedField",
+ "name": "member",
+ "plural": false,
+ "selections": [
+ (v2/*: any*/),
+ {
+ "fragment": {
+ "args": null,
+ "kind": "FragmentSpread",
+ "name": "HasNameComponent_hasName"
+ },
+ "kind": "AliasedFragmentSpread",
+ "name": "HasNameComponent_hasName",
+ "type": "HasName",
+ "abstractKey": "__isHasName"
+ },
+ {
+ "kind": "InlineFragment",
+ "selections": [
+ (v3/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "RichContent",
+ "kind": "LinkedField",
+ "name": "description",
+ "plural": false,
+ "selections": (v4/*: any*/),
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "User",
+ "kind": "LinkedField",
+ "name": "bestFriend",
+ "plural": false,
+ "selections": [
+ {
+ "condition": "includeBestFriendDescription",
+ "kind": "Condition",
+ "passingValue": false,
+ "selections": [
+ (v3/*: any*/)
+ ]
+ },
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "RichContent",
+ "kind": "LinkedField",
+ "name": "description",
+ "plural": false,
+ "selections": [
+ {
+ "condition": "includeBestFriendDescription",
+ "kind": "Condition",
+ "passingValue": true,
+ "selections": (v4/*: any*/)
+ }
+ ],
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+ ],
+ "type": "User",
+ "abstractKey": null
+ },
+ {
+ "kind": "InlineFragment",
+ "selections": [
+ {
+ "fragment": {
+ "args": null,
+ "kind": "FragmentSpread",
+ "name": "GroupAvatar_group"
+ },
+ "kind": "AliasedFragmentSpread",
+ "name": "GroupAvatar_group",
+ "type": "Group",
+ "abstractKey": null
+ }
+ ],
+ "type": "Group",
+ "abstractKey": null
+ }
+ ],
+ "storageKey": "member(id:\"1\")"
+ }
+ ],
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": (v0/*: any*/),
+ "kind": "Operation",
+ "name": "TestCodesplitQuery",
+ "selections": [
+ {
+ "alias": null,
+ "args": (v1/*: any*/),
+ "concreteType": null,
+ "kind": "LinkedField",
+ "name": "member",
+ "plural": false,
+ "selections": [
+ (v2/*: any*/),
+ {
+ "kind": "InlineFragment",
+ "selections": (v5/*: any*/),
+ "type": "HasName",
+ "abstractKey": "__isHasName"
+ },
+ {
+ "kind": "InlineFragment",
+ "selections": [
+ (v6/*: any*/),
+ (v7/*: any*/),
+ (v8/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "RichContent",
+ "kind": "LinkedField",
+ "name": "description",
+ "plural": false,
+ "selections": (v9/*: any*/),
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "User",
+ "kind": "LinkedField",
+ "name": "bestFriend",
+ "plural": false,
+ "selections": [
+ {
+ "condition": "includeBestFriendDescription",
+ "kind": "Condition",
+ "passingValue": false,
+ "selections": [
+ (v6/*: any*/),
+ (v7/*: any*/),
+ (v8/*: any*/)
+ ]
+ },
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "RichContent",
+ "kind": "LinkedField",
+ "name": "description",
+ "plural": false,
+ "selections": [
+ {
+ "condition": "includeBestFriendDescription",
+ "kind": "Condition",
+ "passingValue": true,
+ "selections": (v9/*: any*/)
+ }
+ ],
+ "storageKey": null
+ },
+ (v10/*: any*/)
+ ],
+ "storageKey": null
+ }
+ ],
+ "type": "User",
+ "abstractKey": null
+ },
+ {
+ "kind": "InlineFragment",
+ "selections": (v5/*: any*/),
+ "type": "Group",
+ "abstractKey": null
+ },
+ {
+ "kind": "InlineFragment",
+ "selections": [
+ (v10/*: any*/)
+ ],
+ "type": "Node",
+ "abstractKey": "__isNode"
+ }
+ ],
+ "storageKey": "member(id:\"1\")"
+ }
+ ]
+ },
+ "params": {
+ "cacheID": "cfb15896d28abad88b1960eeca4222d7",
+ "id": null,
+ "metadata": {},
+ "name": "TestCodesplitQuery",
+ "operationKind": "query",
+ "text": "query TestCodesplitQuery(\n $includeBestFriendDescription: Boolean!\n) {\n member(id: \"1\") {\n __typename\n ...HasNameComponent_hasName\n ... on User {\n ...UserAvatar_user\n description {\n ...RichContent_content\n }\n bestFriend {\n ...UserAvatar_user @skip(if: $includeBestFriendDescription)\n description {\n ...RichContent_content @include(if: $includeBestFriendDescription)\n }\n id\n }\n }\n ... on Group {\n ...GroupAvatar_group\n }\n ... on Node {\n __isNode: __typename\n __typename\n id\n }\n }\n}\n\nfragment GroupAvatar_group on Group {\n name\n}\n\nfragment HasNameComponent_hasName on HasName {\n __isHasName: __typename\n __typename\n name\n}\n\nfragment RichContent_content on RichContent {\n content\n}\n\nfragment UserAvatar_user on User {\n avatarUrl\n ...UserName_user\n}\n\nfragment UserName_user on User {\n firstName\n lastName\n}\n"
+ }
+};
+})() `)
+
+let node = RescriptRelay_Internal.applyCodesplitMetadata(node, [
+ ("member.$$i$$HasName", (_variables: dict) => {Js.import(HasNameComponent.make)->ignore}),
+ ("member.$$u$$User", (_variables: dict) => {Js.import(UserAvatar.make)->ignore; Js.import(UserName.make)->ignore}),
+ ("member.$$u$$User.description", (_variables: dict) => {Js.import(RichContent.make)->ignore}),
+ ("member.$$u$$User.bestFriend", (variables: dict) => {if variables->Js.Dict.get("includeBestFriendDescription") === Some(Js.Json.Boolean(false)) {Js.import(UserAvatar.make)->ignore}}),
+ ("member.$$u$$User.bestFriend.description", (variables: dict) => {if variables->Js.Dict.get("includeBestFriendDescription") === Some(Js.Json.Boolean(true)) {Js.import(RichContent.make)->ignore}}),
+ ("member.$$u$$Group", (_variables: dict) => {Js.import(GroupAvatar.make)->ignore}),
+])
+@live let load: (
+ ~environment: RescriptRelay.Environment.t,
+ ~variables: Types.variables,
+ ~fetchPolicy: RescriptRelay.fetchPolicy=?,
+ ~fetchKey: string=?,
+ ~networkCacheConfig: RescriptRelay.cacheConfig=?,
+) => queryRef = (
+ ~environment,
+ ~variables,
+ ~fetchPolicy=?,
+ ~fetchKey=?,
+ ~networkCacheConfig=?,
+) =>
+ RescriptRelay.loadQuery(
+ environment,
+ node,
+ variables->Internal.convertVariables,
+ {
+ fetchKey,
+ fetchPolicy,
+ networkCacheConfig,
+ },
+ )
+
+@live
+let queryRefToObservable = token => {
+ let raw = token->Internal.tokenToRaw
+ raw.source->Js.Nullable.toOption
+}
+
+@live
+let queryRefToPromise = token => {
+ Js.Promise.make((~resolve, ~reject as _) => {
+ switch token->queryRefToObservable {
+ | None => resolve(Error())
+ | Some(o) =>
+ open RescriptRelay.Observable
+ let _: subscription = o->subscribe(makeObserver(~complete=() => resolve(Ok())))
+ }
+ })
+}
diff --git a/packages/rescript-relay/__tests__/__generated__/UserAvatar_user_graphql.res b/packages/rescript-relay/__tests__/__generated__/UserAvatar_user_graphql.res
new file mode 100644
index 00000000..d57bd17b
--- /dev/null
+++ b/packages/rescript-relay/__tests__/__generated__/UserAvatar_user_graphql.res
@@ -0,0 +1,79 @@
+/* @sourceLoc UserAvatar.res */
+/* @generated */
+%%raw("/* @generated */")
+module Types = {
+ @@warning("-30")
+
+ type fragment = {
+ @as("UserName_user") userName_user: RescriptRelay.fragmentRefs<[ | #UserName_user]>,
+ avatarUrl: option,
+ }
+}
+
+module Internal = {
+ @live
+ type fragmentRaw
+ @live
+ let fragmentConverter: Js.Dict.t>> = %raw(
+ json`{}`
+ )
+ @live
+ let fragmentConverterMap = ()
+ @live
+ let convertFragment = v => v->RescriptRelay.convertObj(
+ fragmentConverter,
+ fragmentConverterMap,
+ Js.undefined
+ )
+}
+
+type t
+type fragmentRef
+external getFragmentRef:
+ RescriptRelay.fragmentRefs<[> | #UserAvatar_user]> => fragmentRef = "%identity"
+
+module Utils = {
+ @@warning("-33")
+ open Types
+}
+
+type relayOperationNode
+type operationType = RescriptRelay.fragmentNode
+
+
+
+module CodesplitComponents = {
+ module UserName = {
+ let make = React.lazy_(() => Js.import(UserName.make))
+ }
+}
+
+let node: operationType = %raw(json` {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "UserAvatar_user",
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "avatarUrl",
+ "storageKey": null
+ },
+ {
+ "fragment": {
+ "args": null,
+ "kind": "FragmentSpread",
+ "name": "UserName_user"
+ },
+ "kind": "AliasedFragmentSpread",
+ "name": "UserName_user",
+ "type": "User",
+ "abstractKey": null
+ }
+ ],
+ "type": "User",
+ "abstractKey": null
+} `)
+
diff --git a/packages/rescript-relay/__tests__/__generated__/UserName_user_graphql.res b/packages/rescript-relay/__tests__/__generated__/UserName_user_graphql.res
new file mode 100644
index 00000000..071dec63
--- /dev/null
+++ b/packages/rescript-relay/__tests__/__generated__/UserName_user_graphql.res
@@ -0,0 +1,68 @@
+/* @sourceLoc UserName.res */
+/* @generated */
+%%raw("/* @generated */")
+module Types = {
+ @@warning("-30")
+
+ type fragment = {
+ firstName: string,
+ lastName: string,
+ }
+}
+
+module Internal = {
+ @live
+ type fragmentRaw
+ @live
+ let fragmentConverter: Js.Dict.t>> = %raw(
+ json`{}`
+ )
+ @live
+ let fragmentConverterMap = ()
+ @live
+ let convertFragment = v => v->RescriptRelay.convertObj(
+ fragmentConverter,
+ fragmentConverterMap,
+ Js.undefined
+ )
+}
+
+type t
+type fragmentRef
+external getFragmentRef:
+ RescriptRelay.fragmentRefs<[> | #UserName_user]> => fragmentRef = "%identity"
+
+module Utils = {
+ @@warning("-33")
+ open Types
+}
+
+type relayOperationNode
+type operationType = RescriptRelay.fragmentNode
+
+
+let node: operationType = %raw(json` {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "UserName_user",
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "firstName",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "lastName",
+ "storageKey": null
+ }
+ ],
+ "type": "User",
+ "abstractKey": null
+} `)
+
diff --git a/packages/rescript-relay/__tests__/schema.graphql b/packages/rescript-relay/__tests__/schema.graphql
index bfd01bc6..1e7982d2 100644
--- a/packages/rescript-relay/__tests__/schema.graphql
+++ b/packages/rescript-relay/__tests__/schema.graphql
@@ -78,12 +78,17 @@ interface HasName {
name: String!
}
+type RichContent {
+ content: String!
+}
+
type User implements Node {
id: ID!
firstName: String!
lastName: String!
avatarUrl: String
isOnline: Boolean
+ description: RichContent
onlineStatus: OnlineStatus
nicknames: [String!]!
createdAt: Datetime!
diff --git a/packages/rescript-relay/package.json b/packages/rescript-relay/package.json
index c1d30d01..220378a3 100644
--- a/packages/rescript-relay/package.json
+++ b/packages/rescript-relay/package.json
@@ -35,8 +35,8 @@
"build": "rescript",
"build:test": "./build-compiler-dev.sh && ./rescript-relay-compiler",
"postinstall": "node postinstall.js",
- "test": "jest",
- "test:ci": "jest --ci --runInBand"
+ "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest",
+ "test:ci": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --ci --runInBand"
},
"devDependencies": {
"@glennsl/rescript-fetch": "^0.2.0",
diff --git a/packages/rescript-relay/rescript-relay-ppx/library/Fragment.ml b/packages/rescript-relay/rescript-relay-ppx/library/Fragment.ml
index d3226c48..3090f32d 100644
--- a/packages/rescript-relay/rescript-relay-ppx/library/Fragment.ml
+++ b/packages/rescript-relay/rescript-relay-ppx/library/Fragment.ml
@@ -2,7 +2,7 @@ open Ppxlib
open Util
let make ~loc ~moduleName ~refetchableQueryName ~extractedConnectionInfo
- ~hasInlineDirective ~isPlural =
+ ~hasInlineDirective ~isPlural ~hasAutocodesplitDirective =
let typeFromGeneratedModule = makeTypeAccessor ~loc ~moduleName in
let valFromGeneratedModule = makeExprAccessor ~loc ~moduleName in
let moduleIdentFromGeneratedModule = makeModuleIdent ~loc ~moduleName in
@@ -23,6 +23,15 @@ let make ~loc ~moduleName ~refetchableQueryName ~extractedConnectionInfo
[%t typeFromGeneratedModule ["Types"; "fragment"]] =
[%e valFromGeneratedModule ["Internal"; "convertFragment"]]];
];
+ (match hasAutocodesplitDirective with
+ | true ->
+ [
+ [%stri
+ module CodesplitComponents =
+ [%m
+ moduleIdentFromGeneratedModule ["CodesplitComponents"]]];
+ ]
+ | false -> []);
(match hasInlineDirective with
| false ->
[
diff --git a/packages/rescript-relay/rescript-relay-ppx/library/Query.ml b/packages/rescript-relay/rescript-relay-ppx/library/Query.ml
index ffa80234..43819151 100644
--- a/packages/rescript-relay/rescript-relay-ppx/library/Query.ml
+++ b/packages/rescript-relay/rescript-relay-ppx/library/Query.ml
@@ -1,7 +1,7 @@
open Ppxlib
open Util
-let make ~loc ~moduleName ~hasRawResponseType =
+let make ~loc ~moduleName ~hasRawResponseType ~hasAutocodesplitDirective =
let typeFromGeneratedModule = makeTypeAccessor ~loc ~moduleName in
let valFromGeneratedModule = makeExprAccessor ~loc ~moduleName in
let moduleIdentFromGeneratedModule = makeModuleIdent ~loc ~moduleName in
@@ -76,5 +76,14 @@ let make ~loc ~moduleName ~hasRawResponseType =
~node:[%e valFromGeneratedModule ["node"]]]
| false -> [%stri ()]);
];
+ (match hasAutocodesplitDirective with
+ | true ->
+ [
+ [%stri
+ module CodesplitComponents =
+ [%m
+ moduleIdentFromGeneratedModule ["CodesplitComponents"]]];
+ ]
+ | false -> []);
]
|> List.map UncurriedUtils.mapStructureItem))
diff --git a/packages/rescript-relay/rescript-relay-ppx/library/RescriptRelayPpxLibrary.ml b/packages/rescript-relay/rescript-relay-ppx/library/RescriptRelayPpxLibrary.ml
index ce5a7d77..3c732002 100644
--- a/packages/rescript-relay/rescript-relay-ppx/library/RescriptRelayPpxLibrary.ml
+++ b/packages/rescript-relay/rescript-relay-ppx/library/RescriptRelayPpxLibrary.ml
@@ -22,17 +22,21 @@ let commonExtension =
op |> extractFragmentRefetchableQueryName ~loc
in
Fragment.make
+ ~hasAutocodesplitDirective:
+ (selection_set |> Util.hasAutocodesplitDirective)
~moduleName:(op |> extractTheFragmentName ~loc)
~refetchableQueryName
~extractedConnectionInfo:(op |> extractFragmentConnectionInfo ~loc)
~hasInlineDirective:(op |> fragmentHasInlineDirective ~loc)
~isPlural:(op |> fragmentIsPlural ~loc)
~loc
- | Operation {optype = Query} ->
+ | Operation {optype = Query; selection_set} ->
if Util.queryIsUpdatable op then
UpdatableQuery.make ~loc ~moduleName:(op |> extractTheQueryName ~loc)
else
Query.make
+ ~hasAutocodesplitDirective:
+ (selection_set |> Util.hasAutocodesplitDirective)
~moduleName:(op |> extractTheQueryName ~loc)
~hasRawResponseType:(op |> queryHasRawResponseTypeDirective ~loc)
~loc
diff --git a/packages/rescript-relay/rescript-relay-ppx/library/Util.ml b/packages/rescript-relay/rescript-relay-ppx/library/Util.ml
index 2e643016..6e8d2276 100755
--- a/packages/rescript-relay/rescript-relay-ppx/library/Util.ml
+++ b/packages/rescript-relay/rescript-relay-ppx/library/Util.ml
@@ -153,3 +153,26 @@ let makeModuleIdent ~loc ~moduleName path =
let gqlModuleName = getGraphQLModuleName moduleName in
let path = gqlModuleName :: path |> List.rev in
Ppxlib.Ast_helper.Mod.ident ~loc {txt = longidentFromStrings path; loc}
+
+let rec hasAutocodesplitDirective selections =
+ match
+ selections
+ |> List.find_opt (fun sel ->
+ match sel with
+ | Graphql_parser.FragmentSpread {directives} -> (
+ match
+ directives
+ |> List.find_opt (fun (dir : Graphql_parser.directive) ->
+ match dir with
+ | {name = "codesplit"} -> true
+ | _ -> false)
+ with
+ | Some _ -> true
+ | None -> false)
+ | Graphql_parser.Field {selection_set} ->
+ hasAutocodesplitDirective selection_set
+ | InlineFragment {selection_set} ->
+ hasAutocodesplitDirective selection_set)
+ with
+ | Some _ -> true
+ | None -> false
diff --git a/packages/rescript-relay/src/RescriptRelay.res b/packages/rescript-relay/src/RescriptRelay.res
index 98d076ef..8c0a461c 100644
--- a/packages/rescript-relay/src/RescriptRelay.res
+++ b/packages/rescript-relay/src/RescriptRelay.res
@@ -504,11 +504,16 @@ module Observable = {
module Network = {
type t
+ type codesplitsMetadata = (string, unit => unit)
+
+ type operationMetadata = {codesplits?: array}
+
type operation = {
id: string,
text: string,
name: string,
operationKind: string,
+ metadata: Js.Nullable.t,
}
type subscribeFn = (operation, Js.Json.t, cacheConfig) => Observable.t
@@ -538,6 +543,79 @@ module Network = {
~observableFunction: fetchFunctionObservable,
~subscriptionFunction: subscribeFn=?,
) => t = "create"
+
+ let preloadResources: (
+ ~operation: operation,
+ ~variables: Js.Json.t,
+ ~response: Js.Json.t,
+ ) => unit = %raw(`
+function preloadResources(operation, variables, response) {
+ let metadata = operation.metadata;
+ if (metadata == null) return;
+ let codesplits = metadata.codesplits;
+ if (codesplits == null) return;
+ let data = response.data;
+
+ function pathExists(obj, path) {
+ let current = obj;
+
+ for (let i = 0; i < path.length; i++) {
+ let segment = path[i];
+
+ if (Array.isArray(current)) {
+ return current.some((item) => pathExists(item, path.slice(i)));
+ } else if (current != null && current.hasOwnProperty(segment)) {
+ current = current[segment];
+ } else if (current != null && (segment.startsWith("$$u$$") || segment.startsWith("$$i$$"))) {
+ let isInterface = segment.startsWith("$$i$$");
+ let expectedTypename = segment.slice(5);
+ if (
+ (
+ !isInterface &&
+ current.hasOwnProperty("__typename") &&
+ current.__typename === expectedTypename) ||
+ (
+ isInterface &&
+ current.hasOwnProperty("__is" + expectedTypename) &&
+ current["__is" + expectedTypename] != null
+ )
+ ) {
+ if (i + 1 === path.length) {
+ // End
+ return true;
+ } else {
+ continue;
+ }
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ return current != null;
+ }
+
+ function run() {
+ for (let instruction of codesplits) {
+ let path = instruction[0];
+ let func = instruction[1];
+ if (pathExists(data, path.split("."))) {
+ func(variables);
+ }
+ }
+ }
+
+ if ("requestIdleCallback" in window) {
+ requestIdleCallback(run);
+ } else {
+ setTimeout(() => {
+ Promise.resolve().then(run);
+ }, 1);
+ }
+}
+`)
}
module RecordSource = {
diff --git a/packages/rescript-relay/src/RescriptRelay.resi b/packages/rescript-relay/src/RescriptRelay.resi
index ec44a54d..67d5c238 100644
--- a/packages/rescript-relay/src/RescriptRelay.resi
+++ b/packages/rescript-relay/src/RescriptRelay.resi
@@ -611,6 +611,10 @@ module Observable: {
/**Represents the network layer.*/
module Network: {
+ type codesplitsMetadata = (string, unit => unit)
+
+ type operationMetadata = {codesplits?: array}
+
/**The type representing an instantiated `NetworkLayer`.*/
type t
@@ -620,6 +624,7 @@ module Network: {
text: string,
name: string,
operationKind: string,
+ metadata: Js.Nullable.t,
}
/**The shape of the function Relay expects for creating a subscription.*/
@@ -656,6 +661,8 @@ module Network: {
~observableFunction: fetchFunctionObservable,
~subscriptionFunction: subscribeFn=?,
) => t = "create"
+
+ let preloadResources: (~operation: operation, ~variables: Js.Json.t, ~response: Js.Json.t) => unit
}
/**RecordSource is the source of records used by the store. Can be initiated with or without prior records; eg. hydrating the store with prior data.*/
diff --git a/packages/rescript-relay/src/RescriptRelay_Internal.res b/packages/rescript-relay/src/RescriptRelay_Internal.res
index fffdbd92..ac1e9f79 100644
--- a/packages/rescript-relay/src/RescriptRelay_Internal.res
+++ b/packages/rescript-relay/src/RescriptRelay_Internal.res
@@ -55,3 +55,17 @@ external internal_resolverFragmentRefsToFragmentRefs: RescriptRelay.resolverFrag
external internal_resolverFragmentRefsToFragmentRefsPlural: RescriptRelay.resolverFragmentRefs<
'a,
> => array> = "%identity"
+
+let applyCodesplitMetadata: ('node, array<(string, dict => unit)>) => 'node = %raw(`
+ function applyCodesplitMetadata(node, meta) {
+ if (node != null && node.params != null) {
+ let metadata = node.params.metadata;
+ if (metadata == null) {
+ node.params.metadata = {codesplits: meta}
+ } else if (typeof metadata === "object") {
+ node.params.metadata.codesplits = meta
+ }
+ }
+ return node;
+ }
+`)
diff --git a/packages/rescript-relay/src/RescriptRelay_Internal.resi b/packages/rescript-relay/src/RescriptRelay_Internal.resi
index 4496c2f5..477d863f 100644
--- a/packages/rescript-relay/src/RescriptRelay_Internal.resi
+++ b/packages/rescript-relay/src/RescriptRelay_Internal.resi
@@ -16,3 +16,5 @@ external internal_resolverFragmentRefsToFragmentRefs: RescriptRelay.resolverFrag
external internal_resolverFragmentRefsToFragmentRefsPlural: RescriptRelay.resolverFragmentRefs<
'a,
> => array> = "%identity"
+
+let applyCodesplitMetadata: ('node, array<(string, dict => unit)>) => 'node