From 73953dea7ae2c2687bfffcfe6f966bd5d699a1fd Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sun, 14 Jul 2024 12:49:35 +0200 Subject: [PATCH] @codesplit (#519) * @autoCodesplit * node version in CI * handle conditionals via variables * handle conditionals * ci * up ncc * cache * ci * ci * adapt to aliased fragments * autoCodesplit -> codesplit * changelog * use main branch * ci * ci --- .github/workflows/bindings.yml | 4 +- .github/workflows/build-release.yml | 14 +- .github/workflows/cli.yml | 2 +- .github/workflows/integration-tests.yml | 6 +- .github/workflows/ppx.yml | 6 +- CHANGELOG.md | 1 + packages/relay | 2 +- packages/rescript-relay-cli/package.json | 2 +- packages/rescript-relay-cli/yarn.lock | 8 +- .../rescript-relay/__tests__/GroupAvatar.res | 11 + .../__tests__/HasNameComponent.res | 12 + .../rescript-relay/__tests__/RelayEnv.res | 4 +- .../rescript-relay/__tests__/RichContent.res | 12 + .../__tests__/Test_aliasedFragments.res | 8 +- .../__tests__/Test_codesplit-tests.js | 261 ++++++++++ .../__tests__/Test_codesplit.res | 91 ++++ .../rescript-relay/__tests__/UserAvatar.res | 20 + .../rescript-relay/__tests__/UserName.res | 12 + .../GroupAvatar_group_graphql.res | 60 +++ .../HasNameComponent_hasName_graphql.res | 68 +++ .../RichContent_content_graphql.res | 60 +++ .../TestAliasedFragmentsQuery_graphql.res | 16 +- .../TestCodesplitQuery_graphql.res | 480 ++++++++++++++++++ .../__generated__/UserAvatar_user_graphql.res | 79 +++ .../__generated__/UserName_user_graphql.res | 68 +++ .../rescript-relay/__tests__/schema.graphql | 5 + packages/rescript-relay/package.json | 4 +- .../rescript-relay-ppx/library/Fragment.ml | 11 +- .../rescript-relay-ppx/library/Query.ml | 11 +- .../library/RescriptRelayPpxLibrary.ml | 6 +- .../rescript-relay-ppx/library/Util.ml | 23 + packages/rescript-relay/src/RescriptRelay.res | 78 +++ .../rescript-relay/src/RescriptRelay.resi | 7 + .../src/RescriptRelay_Internal.res | 14 + .../src/RescriptRelay_Internal.resi | 2 + 35 files changed, 1423 insertions(+), 45 deletions(-) create mode 100644 packages/rescript-relay/__tests__/GroupAvatar.res create mode 100644 packages/rescript-relay/__tests__/HasNameComponent.res create mode 100644 packages/rescript-relay/__tests__/RichContent.res create mode 100644 packages/rescript-relay/__tests__/Test_codesplit-tests.js create mode 100644 packages/rescript-relay/__tests__/Test_codesplit.res create mode 100644 packages/rescript-relay/__tests__/UserAvatar.res create mode 100644 packages/rescript-relay/__tests__/UserName.res create mode 100644 packages/rescript-relay/__tests__/__generated__/GroupAvatar_group_graphql.res create mode 100644 packages/rescript-relay/__tests__/__generated__/HasNameComponent_hasName_graphql.res create mode 100644 packages/rescript-relay/__tests__/__generated__/RichContent_content_graphql.res create mode 100644 packages/rescript-relay/__tests__/__generated__/TestCodesplitQuery_graphql.res create mode 100644 packages/rescript-relay/__tests__/__generated__/UserAvatar_user_graphql.res create mode 100644 packages/rescript-relay/__tests__/__generated__/UserName_user_graphql.res 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("Has name: " ++ hasName.name)}
+} diff --git a/packages/rescript-relay/__tests__/RelayEnv.res b/packages/rescript-relay/__tests__/RelayEnv.res index 85db0ad6..573c856e 100644 --- a/packages/rescript-relay/__tests__/RelayEnv.res +++ b/packages/rescript-relay/__tests__/RelayEnv.res @@ -23,7 +23,9 @@ let fetchQuery: RescriptRelay.Network.fetchFunctionPromise = async ( ) if Response.ok(resp) { - await Response.json(resp) + let json = await Response.json(resp) + RescriptRelay.Network.preloadResources(~operation, ~variables, ~response=json) + json } else { raise(Graphql_error("Request failed: " ++ Response.statusText(resp))) } diff --git a/packages/rescript-relay/__tests__/RichContent.res b/packages/rescript-relay/__tests__/RichContent.res new file mode 100644 index 00000000..b5d08c87 --- /dev/null +++ b/packages/rescript-relay/__tests__/RichContent.res @@ -0,0 +1,12 @@ +module Fragment = %relay(` + fragment RichContent_content on RichContent { + content + } +`) + +@react.component +let make = (~content: RescriptRelay.fragmentRefs<[#RichContent_content]>) => { + let content = Fragment.use(content) + +
{React.string("Rich content: " ++ content.content)}
+} diff --git a/packages/rescript-relay/__tests__/Test_aliasedFragments.res b/packages/rescript-relay/__tests__/Test_aliasedFragments.res index acce023c..705dd87f 100644 --- a/packages/rescript-relay/__tests__/Test_aliasedFragments.res +++ b/packages/rescript-relay/__tests__/Test_aliasedFragments.res @@ -23,12 +23,8 @@ module Test = { @react.component let make = () => { let query = Query.use(~variables={skipThing: false}) - let firstNameData = FragmentFirstName.use( - query.loggedInUser.testAliasedFragments_userFirstName.fragmentRefs, - ) - let lastNameData = FragmentLastName.useOpt( - query.loggedInUser.testAliasedFragments_userLastName->Belt.Option.map(f => f.fragmentRefs), - ) + let firstNameData = FragmentFirstName.use(query.loggedInUser.testAliasedFragments_userFirstName) + let lastNameData = FragmentLastName.useOpt(query.loggedInUser.testAliasedFragments_userLastName)
{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