'
66 | exit(1)
67 | end
68 |
69 | raise 'Git working directory is not clean' unless git_working_directory_is_clean?
70 |
71 | version = Gem::Version.new(ARGV[0])
72 | next_version = version.bump.to_s + '.0'
73 |
74 | update_podspec(next_version)
75 | update_readme(next_version)
76 | update_test_workflow(version.to_s)
77 |
--------------------------------------------------------------------------------
/docs/badge.svg:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/docs/docsets/ApolloDeveloperKit.docset/Contents/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleIdentifier
6 | com.jazzy.apollodeveloperkit
7 | CFBundleName
8 | ApolloDeveloperKit
9 | DocSetPlatformFamily
10 | apollodeveloperkit
11 | isDashDocset
12 |
13 | dashIndexFilePath
14 | index.html
15 | isJavaScriptEnabled
16 |
17 | DashDocSetFamily
18 | dashtoc
19 |
20 |
21 |
--------------------------------------------------------------------------------
/docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/badge.svg:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/img/carat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/img/carat.png
--------------------------------------------------------------------------------
/docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/img/dash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/img/dash.png
--------------------------------------------------------------------------------
/docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/img/gh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/img/gh.png
--------------------------------------------------------------------------------
/docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/img/spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/img/spinner.gif
--------------------------------------------------------------------------------
/docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/js/jazzy.js:
--------------------------------------------------------------------------------
1 | window.jazzy = {'docset': false}
2 | if (typeof window.dash != 'undefined') {
3 | document.documentElement.className += ' dash'
4 | window.jazzy.docset = true
5 | }
6 | if (navigator.userAgent.match(/xcode/i)) {
7 | document.documentElement.className += ' xcode'
8 | window.jazzy.docset = true
9 | }
10 |
11 | function toggleItem($link, $content) {
12 | var animationDuration = 300;
13 | $link.toggleClass('token-open');
14 | $content.slideToggle(animationDuration);
15 | }
16 |
17 | function itemLinkToContent($link) {
18 | return $link.parent().parent().next();
19 | }
20 |
21 | // On doc load + hash-change, open any targetted item
22 | function openCurrentItemIfClosed() {
23 | if (window.jazzy.docset) {
24 | return;
25 | }
26 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token');
27 | $content = itemLinkToContent($link);
28 | if ($content.is(':hidden')) {
29 | toggleItem($link, $content);
30 | }
31 | }
32 |
33 | $(openCurrentItemIfClosed);
34 | $(window).on('hashchange', openCurrentItemIfClosed);
35 |
36 | // On item link ('token') click, toggle its discussion
37 | $('.token').on('click', function(event) {
38 | if (window.jazzy.docset) {
39 | return;
40 | }
41 | var $link = $(this);
42 | toggleItem($link, itemLinkToContent($link));
43 |
44 | // Keeps the document from jumping to the hash.
45 | var href = $link.attr('href');
46 | if (history.pushState) {
47 | history.pushState({}, '', href);
48 | } else {
49 | location.hash = href;
50 | }
51 | event.preventDefault();
52 | });
53 |
54 | // Clicks on links to the current, closed, item need to open the item
55 | $("a:not('.token')").on('click', function() {
56 | if (location == this.href) {
57 | openCurrentItemIfClosed();
58 | }
59 | });
60 |
61 | // KaTeX rendering
62 | if ("katex" in window) {
63 | $($('.math').each( (_, element) => {
64 | katex.render(element.textContent, element, {
65 | displayMode: $(element).hasClass('m-block'),
66 | throwOnError: false,
67 | trust: true
68 | });
69 | }))
70 | }
71 |
--------------------------------------------------------------------------------
/docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/js/jazzy.search.js:
--------------------------------------------------------------------------------
1 | $(function(){
2 | var $typeahead = $('[data-typeahead]');
3 | var $form = $typeahead.parents('form');
4 | var searchURL = $form.attr('action');
5 |
6 | function displayTemplate(result) {
7 | return result.name;
8 | }
9 |
10 | function suggestionTemplate(result) {
11 | var t = '';
12 | t += '' + result.name + '';
13 | if (result.parent_name) {
14 | t += '' + result.parent_name + '';
15 | }
16 | t += '
';
17 | return t;
18 | }
19 |
20 | $typeahead.one('focus', function() {
21 | $form.addClass('loading');
22 |
23 | $.getJSON(searchURL).then(function(searchData) {
24 | const searchIndex = lunr(function() {
25 | this.ref('url');
26 | this.field('name');
27 | this.field('abstract');
28 | for (const [url, doc] of Object.entries(searchData)) {
29 | this.add({url: url, name: doc.name, abstract: doc.abstract});
30 | }
31 | });
32 |
33 | $typeahead.typeahead(
34 | {
35 | highlight: true,
36 | minLength: 3,
37 | autoselect: true
38 | },
39 | {
40 | limit: 10,
41 | display: displayTemplate,
42 | templates: { suggestion: suggestionTemplate },
43 | source: function(query, sync) {
44 | const lcSearch = query.toLowerCase();
45 | const results = searchIndex.query(function(q) {
46 | q.term(lcSearch, { boost: 100 });
47 | q.term(lcSearch, {
48 | boost: 10,
49 | wildcard: lunr.Query.wildcard.TRAILING
50 | });
51 | }).map(function(result) {
52 | var doc = searchData[result.ref];
53 | doc.url = result.ref;
54 | return doc;
55 | });
56 | sync(results);
57 | }
58 | }
59 | );
60 | $form.removeClass('loading');
61 | $typeahead.trigger('focus');
62 | });
63 | });
64 |
65 | var baseURL = searchURL.slice(0, -"search.json".length);
66 |
67 | $typeahead.on('typeahead:select', function(e, result) {
68 | window.location = baseURL + result.url;
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/docSet.dsidx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/docSet.dsidx
--------------------------------------------------------------------------------
/docs/docsets/ApolloDeveloperKit.tgz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/docsets/ApolloDeveloperKit.tgz
--------------------------------------------------------------------------------
/docs/img/carat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/img/carat.png
--------------------------------------------------------------------------------
/docs/img/dash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/img/dash.png
--------------------------------------------------------------------------------
/docs/img/gh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/img/gh.png
--------------------------------------------------------------------------------
/docs/img/spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/img/spinner.gif
--------------------------------------------------------------------------------
/docs/js/jazzy.js:
--------------------------------------------------------------------------------
1 | window.jazzy = {'docset': false}
2 | if (typeof window.dash != 'undefined') {
3 | document.documentElement.className += ' dash'
4 | window.jazzy.docset = true
5 | }
6 | if (navigator.userAgent.match(/xcode/i)) {
7 | document.documentElement.className += ' xcode'
8 | window.jazzy.docset = true
9 | }
10 |
11 | function toggleItem($link, $content) {
12 | var animationDuration = 300;
13 | $link.toggleClass('token-open');
14 | $content.slideToggle(animationDuration);
15 | }
16 |
17 | function itemLinkToContent($link) {
18 | return $link.parent().parent().next();
19 | }
20 |
21 | // On doc load + hash-change, open any targetted item
22 | function openCurrentItemIfClosed() {
23 | if (window.jazzy.docset) {
24 | return;
25 | }
26 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token');
27 | $content = itemLinkToContent($link);
28 | if ($content.is(':hidden')) {
29 | toggleItem($link, $content);
30 | }
31 | }
32 |
33 | $(openCurrentItemIfClosed);
34 | $(window).on('hashchange', openCurrentItemIfClosed);
35 |
36 | // On item link ('token') click, toggle its discussion
37 | $('.token').on('click', function(event) {
38 | if (window.jazzy.docset) {
39 | return;
40 | }
41 | var $link = $(this);
42 | toggleItem($link, itemLinkToContent($link));
43 |
44 | // Keeps the document from jumping to the hash.
45 | var href = $link.attr('href');
46 | if (history.pushState) {
47 | history.pushState({}, '', href);
48 | } else {
49 | location.hash = href;
50 | }
51 | event.preventDefault();
52 | });
53 |
54 | // Clicks on links to the current, closed, item need to open the item
55 | $("a:not('.token')").on('click', function() {
56 | if (location == this.href) {
57 | openCurrentItemIfClosed();
58 | }
59 | });
60 |
61 | // KaTeX rendering
62 | if ("katex" in window) {
63 | $($('.math').each( (_, element) => {
64 | katex.render(element.textContent, element, {
65 | displayMode: $(element).hasClass('m-block'),
66 | throwOnError: false,
67 | trust: true
68 | });
69 | }))
70 | }
71 |
--------------------------------------------------------------------------------
/docs/js/jazzy.search.js:
--------------------------------------------------------------------------------
1 | $(function(){
2 | var $typeahead = $('[data-typeahead]');
3 | var $form = $typeahead.parents('form');
4 | var searchURL = $form.attr('action');
5 |
6 | function displayTemplate(result) {
7 | return result.name;
8 | }
9 |
10 | function suggestionTemplate(result) {
11 | var t = '';
12 | t += '' + result.name + '';
13 | if (result.parent_name) {
14 | t += '' + result.parent_name + '';
15 | }
16 | t += '
';
17 | return t;
18 | }
19 |
20 | $typeahead.one('focus', function() {
21 | $form.addClass('loading');
22 |
23 | $.getJSON(searchURL).then(function(searchData) {
24 | const searchIndex = lunr(function() {
25 | this.ref('url');
26 | this.field('name');
27 | this.field('abstract');
28 | for (const [url, doc] of Object.entries(searchData)) {
29 | this.add({url: url, name: doc.name, abstract: doc.abstract});
30 | }
31 | });
32 |
33 | $typeahead.typeahead(
34 | {
35 | highlight: true,
36 | minLength: 3,
37 | autoselect: true
38 | },
39 | {
40 | limit: 10,
41 | display: displayTemplate,
42 | templates: { suggestion: suggestionTemplate },
43 | source: function(query, sync) {
44 | const lcSearch = query.toLowerCase();
45 | const results = searchIndex.query(function(q) {
46 | q.term(lcSearch, { boost: 100 });
47 | q.term(lcSearch, {
48 | boost: 10,
49 | wildcard: lunr.Query.wildcard.TRAILING
50 | });
51 | }).map(function(result) {
52 | var doc = searchData[result.ref];
53 | doc.url = result.ref;
54 | return doc;
55 | });
56 | sync(results);
57 | }
58 | }
59 | );
60 | $form.removeClass('loading');
61 | $typeahead.trigger('focus');
62 | });
63 | });
64 |
65 | var baseURL = searchURL.slice(0, -"search.json".length);
66 |
67 | $typeahead.on('typeahead:select', function(e, result) {
68 | window.location = baseURL + result.url;
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "apollodeveloperkit",
3 | "version": "0.15.0",
4 | "description": "Visual debug your app, that is based on Apollo iOS",
5 | "repository": "https://github.com/manicmaniac/ApolloDeveloperKit",
6 | "author": "Ryosuke Ito ",
7 | "license": "MIT",
8 | "private": true,
9 | "module": "src/index.ts",
10 | "sideEffects": [
11 | "src/index.ts"
12 | ],
13 | "scripts": {
14 | "build": "webpack",
15 | "lint": "eslint . --ext .ts",
16 | "test": "jest",
17 | "generate:type": "npm run generate:type:swift & npm run generate:type:typescript & wait",
18 | "generate:type:swift": "quicktype -l swift --just-types -t Schema -s schema -o Sources/ApolloDeveloperKit/Schema/Schema.swift ApolloDeveloperKit.schema.json",
19 | "generate:type:typescript": "quicktype -l typescript --just-types -t Schema -s schema -o src/schema.ts ApolloDeveloperKit.schema.json"
20 | },
21 | "devDependencies": {
22 | "@types/jest": "^26.0.10",
23 | "@typescript-eslint/eslint-plugin": "^3.10.1",
24 | "@typescript-eslint/parser": "^3.10.1",
25 | "apollo-cache": "^1.3.5",
26 | "apollo-client": "^2.6.10",
27 | "apollo-client-devtools": "^2.3.1",
28 | "eslint": "^7.7.0",
29 | "eventsourcemock": "^2.0.0",
30 | "graphql": "^15.3.0",
31 | "jest": "^26.4.2",
32 | "quicktype": "^15.0.256",
33 | "ts-jest": "^26.3.0",
34 | "ts-loader": "^8.0.3",
35 | "typescript": "^4.0.2",
36 | "webpack": "^4.44.1",
37 | "webpack-cli": "^3.3.12"
38 | },
39 | "eslintConfig": {
40 | "root": true,
41 | "parser": "@typescript-eslint/parser",
42 | "plugins": [
43 | "@typescript-eslint"
44 | ],
45 | "extends": [
46 | "eslint:recommended",
47 | "plugin:@typescript-eslint/recommended"
48 | ],
49 | "ignorePatterns": [
50 | "Carthage",
51 | "node_modules",
52 | "src/schema.ts"
53 | ],
54 | "rules": {
55 | "@typescript-eslint/no-unused-vars": [
56 | "warn",
57 | {
58 | "argsIgnorePattern": "^_"
59 | }
60 | ]
61 | }
62 | },
63 | "jest": {
64 | "preset": "ts-jest/presets/js-with-ts",
65 | "testEnvironment": "jsdom",
66 | "transformIgnorePatterns": [
67 | "node_modules/(?!(apollo-client-devtools)/)"
68 | ]
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/ApolloCachePretender.ts:
--------------------------------------------------------------------------------
1 | import type { Cache, Transaction } from 'apollo-cache'
2 | import { ApolloCache } from 'apollo-cache'
3 |
4 | type CacheObject = Record
5 |
6 | export default class ApolloCachePretender extends ApolloCache {
7 | private onExtract?: () => void
8 |
9 | constructor(onExtract?: () => void) {
10 | super()
11 | this.onExtract = onExtract
12 | }
13 |
14 | read(_query: Cache.ReadOptions): null {
15 | return null
16 | }
17 |
18 | write(_write: Cache.WriteOptions): void {
19 | // do nothing
20 | }
21 |
22 | diff(_query: Cache.DiffOptions): Cache.DiffResult {
23 | return {}
24 | }
25 |
26 | watch(_watch: Cache.WatchOptions): () => void {
27 | return () => { /* do nothing */}
28 | }
29 |
30 | evict(_query: Cache.EvictOptions): Cache.EvictionResult {
31 | return { success: true }
32 | }
33 |
34 | async reset(): Promise {
35 | // do nothing
36 | }
37 |
38 | restore(_serializedState: unknown): this {
39 | return this
40 | }
41 |
42 | extract(_optimistic = false): CacheObject {
43 | this.onExtract?.()
44 | return {}
45 | }
46 |
47 | removeOptimistic(_id: string): void {
48 | // do nothing
49 | }
50 |
51 | performTransaction(transaction: Transaction): void {
52 | transaction(this)
53 | }
54 |
55 | recordOptimisticTransaction(transaction: Transaction, _id: string): void {
56 | transaction(this)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/ApolloClientPretender.ts:
--------------------------------------------------------------------------------
1 | import type { FetchResult, Operation as LinkOperation } from 'apollo-link'
2 | import type { DataProxy } from 'apollo-cache'
3 | import type { StateChange as DevtoolsStateChange } from 'apollo-client-devtools'
4 | import type { ConsoleEvent, Operation, StateChange as DeveloperKitStateChange } from './schema'
5 | import assert from 'assert'
6 | import { parse } from 'graphql/language/parser'
7 | import { print } from 'graphql/language/printer'
8 | import { ApolloLink, fromPromise } from 'apollo-link'
9 | import ApolloCachePretender from './ApolloCachePretender'
10 | import { ConsoleEventType } from './schema'
11 |
12 | export default class ApolloClientPretender implements DataProxy {
13 | readonly version = '2.0.0'
14 | readonly link = new ApolloLink((operation) => fromPromise(requestOperation(operation)))
15 | readonly cache = new ApolloCachePretender(this.startListening.bind(this))
16 |
17 | private devToolsHookCb?: (event: DevtoolsStateChange) => void
18 | private eventSource?: EventSource
19 |
20 | readQuery = this.cache.readQuery.bind(this.cache)
21 | readFragment = this.cache.readFragment.bind(this.cache)
22 | writeQuery = this.cache.writeQuery.bind(this.cache)
23 | writeFragment = this.cache.writeFragment.bind(this.cache)
24 | writeData = this.cache.writeData.bind(this.cache)
25 |
26 | startListening(): void {
27 | this.eventSource = new EventSource('/events')
28 | this.eventSource.onmessage = message => {
29 | const event = JSON.parse(message.data) as DeveloperKitStateChange
30 | const newEvent = translateApolloStateChangeEvent(event)
31 | this.devToolsHookCb?.(newEvent)
32 | }
33 | this.eventSource.addEventListener('stdout', event => onConsoleEventReceived(event))
34 | this.eventSource.addEventListener('stderr', event => onConsoleEventReceived(event))
35 | }
36 |
37 | stopListening(): void {
38 | this.eventSource?.close()
39 | }
40 |
41 | __actionHookForDevTools(cb: (event: DevtoolsStateChange) => void): void {
42 | this.devToolsHookCb = cb
43 | }
44 | }
45 |
46 | async function requestOperation(operation: LinkOperation): Promise {
47 | const body: Operation = {
48 | variables: operation.variables,
49 | operationName: operation.operationName,
50 | query: print(operation.query)
51 | }
52 | const options: RequestInit = {
53 | method: 'POST',
54 | headers: { 'Content-Type': 'application/json' },
55 | body: JSON.stringify(body)
56 | }
57 | const response = await fetch('/request', options)
58 | if (!response.ok) {
59 | throw new Error(response.statusText)
60 | }
61 | return await response.json()
62 | }
63 |
64 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
65 | function isConsoleEvent(object: any): object is ConsoleEvent {
66 | return Object.values(ConsoleEventType).includes(object?.type) && ((typeof object?.data === 'string') || object?.data instanceof String)
67 | }
68 |
69 | const consoleEventTypeColorMap: Readonly> = Object.freeze({
70 | 'stdout': 'cadetblue',
71 | 'stderr': 'tomato'
72 | })
73 |
74 | function onConsoleEventReceived(event: Event): void {
75 | assert(isConsoleEvent(event))
76 | console.log(`%c${event.data}`, `color: ${consoleEventTypeColorMap[event.type]}`)
77 | }
78 |
79 | function translateApolloStateChangeEvent(event: DeveloperKitStateChange): DevtoolsStateChange {
80 | return {
81 | ...event,
82 | state: {
83 | queries: event.state.queries.map(query => ({
84 | ...query,
85 | document: parse(query.document)
86 | })),
87 | mutations: event.state.mutations.map(mutation => ({
88 | ...mutation,
89 | mutation: parse(mutation.mutation)
90 | }))
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/__tests__/ApolloCachePretender.test.ts:
--------------------------------------------------------------------------------
1 | import type { DocumentNode } from 'graphql'
2 | import type { Transaction } from 'apollo-cache'
3 | import ApolloCachePretender from '../ApolloCachePretender'
4 |
5 | describe('ApolloCachePretender', () => {
6 | const query: DocumentNode = {
7 | kind: 'Document',
8 | definitions: []
9 | }
10 |
11 | let cache: ApolloCachePretender
12 | const onExtract = jest.fn()
13 |
14 | beforeEach(() => {
15 | cache = new ApolloCachePretender(onExtract)
16 | onExtract.mockClear()
17 | })
18 |
19 | describe('#extract', () => {
20 | it('returns some object', () => {
21 | expect(cache.extract()).toStrictEqual({})
22 | })
23 |
24 | it('invokes the callback', () => {
25 | cache.extract()
26 | expect(onExtract).toHaveBeenCalledTimes(1)
27 | })
28 | })
29 |
30 | describe('#read', () => {
31 | it('returns null', () => {
32 | expect(cache.read({ query, optimistic: true })).toBeNull()
33 | })
34 | })
35 |
36 | describe('#write', () => {
37 | it('does not throw any error', () => {
38 | expect(() => cache.write({ query, dataId: '', result: ''})).not.toThrow()
39 | })
40 | })
41 |
42 | describe('#diff', () => {
43 | it('returns empty object', () => {
44 | expect(cache.diff({ query, optimistic: true })).toMatchObject({})
45 | })
46 | })
47 |
48 | describe('#watch', () => {
49 | it('returns empty thunk', () => {
50 | const callback = jest.fn()
51 | expect(cache.watch({ query, callback, optimistic: true })).toBeInstanceOf(Function)
52 | expect(callback).not.toHaveBeenCalled()
53 | })
54 | })
55 |
56 | describe('#evict', () => {
57 | it('tells success', () => {
58 | expect(cache.evict({ query })).toMatchObject({ success: true })
59 | })
60 | })
61 |
62 | describe('#reset', () => {
63 | it('returns an empty promise', async () => {
64 | expect(async () => await cache.reset()).not.toThrow()
65 | })
66 | })
67 |
68 | describe('#restore', () => {
69 | it('returns itself', () => {
70 | expect(cache.restore(null)).toBe(cache)
71 | })
72 | })
73 |
74 | describe('#removeOptimistic', () => {
75 | it('does not throw any error', () => {
76 | expect(() => cache.removeOptimistic('')).not.toThrowError()
77 | })
78 | })
79 |
80 | describe('#performTransaction', () => {
81 | it('does not throw any error', () => {
82 | const transaction: Transaction = jest.fn()
83 | expect(() => cache.performTransaction(transaction)).not.toThrowError()
84 | expect(transaction).toHaveBeenCalledTimes(1)
85 | })
86 | })
87 |
88 | describe('#recordOptimisticTransaction', () => {
89 | it('does not throw any error', () => {
90 | const transaction: Transaction = jest.fn()
91 | expect(() => cache.recordOptimisticTransaction(transaction, '')).not.toThrowError()
92 | expect(transaction).toHaveBeenCalledTimes(1)
93 | })
94 | })
95 | })
96 |
--------------------------------------------------------------------------------
/src/__tests__/ApolloClientPretender.test.ts:
--------------------------------------------------------------------------------
1 | import type { DataProxy } from 'apollo-cache'
2 | import type { DocumentNode } from 'graphql'
3 | import EventSourceMock, { sources } from 'eventsourcemock'
4 | import { mocked } from 'ts-jest/utils'
5 | import ApolloClientPretender from '../ApolloClientPretender'
6 | import ApolloCachePretender from '../ApolloCachePretender'
7 |
8 | jest.mock('../ApolloCachePretender')
9 | jest.spyOn(console, 'log')
10 |
11 | describe('ApolloClientPretender', () => {
12 | const query: DocumentNode = {
13 | kind: 'Document',
14 | definitions: []
15 | }
16 |
17 | let originalEventSource: typeof EventSource
18 | let client: ApolloClientPretender
19 | let cache: ApolloCachePretender
20 |
21 | beforeEach(() => {
22 | mocked(ApolloCachePretender).mockClear()
23 | mocked(console.log).mockClear()
24 | originalEventSource = window.EventSource
25 | window.EventSource = EventSourceMock as typeof EventSource
26 | client = new ApolloClientPretender()
27 | cache = mocked(ApolloCachePretender).mock.instances[0]
28 | })
29 |
30 | afterEach(() => {
31 | window.EventSource = originalEventSource
32 | })
33 |
34 | describe('#version', () => {
35 | it('is 2.0.0', () => {
36 | expect(client.version).toBe('2.0.0')
37 | })
38 | })
39 |
40 | describe('#cache', () => {
41 | it('returns its own cache', () => {
42 | expect(client.cache).toStrictEqual(cache)
43 | })
44 | })
45 |
46 | describe('#readQuery', () => {
47 | it('proxies method call to its own cache', () => {
48 | const options: DataProxy.Query = { query }
49 | client.readQuery(options)
50 | expect(cache.readQuery).toHaveBeenCalledWith(options)
51 | })
52 | })
53 |
54 | describe('#readFragment', () => {
55 | it('proxies method call to its own cache', () => {
56 | const options: DataProxy.Fragment = {
57 | id: '',
58 | fragment: query
59 | }
60 | client.readFragment(options)
61 | expect(cache.readFragment).toHaveBeenCalledWith(options)
62 | })
63 | })
64 |
65 | describe('#writeQuery', () => {
66 | it('proxies method call to its own cache', () => {
67 | const options: DataProxy.WriteQueryOptions = {
68 | query,
69 | data: ''
70 | }
71 | client.writeQuery(options)
72 | expect(cache.writeQuery).toHaveBeenCalledWith(options)
73 | })
74 | })
75 |
76 | describe('#writeFragment', () => {
77 | it('proxies method call to its own cache', () => {
78 | const options: DataProxy.WriteFragmentOptions = {
79 | id: '',
80 | fragment: query,
81 | data: ''
82 | }
83 | client.writeFragment(options)
84 | expect(cache.writeFragment).toHaveBeenCalledWith(options)
85 | })
86 | })
87 |
88 | describe('#writeData', () => {
89 | it('proxies method call to its own cache', () => {
90 | const options: DataProxy.WriteDataOptions = { data: '' }
91 | client.writeData(options)
92 | expect(cache.writeData).toHaveBeenCalledWith(options)
93 | })
94 | })
95 |
96 | describe('#startListening', () => {
97 | const hook = jest.fn()
98 |
99 | beforeEach(() => {
100 | hook.mockClear()
101 | client.__actionHookForDevTools(hook)
102 | client.startListening()
103 | })
104 |
105 | afterEach(() => {
106 | client.stopListening()
107 | })
108 |
109 | it('starts event source', () => {
110 | expect(sources['/events'].readyState).toBe(0)
111 | })
112 |
113 | it('calls hook callback when it receives state change event', () => {
114 | const data = {
115 | state: {
116 | queries: [],
117 | mutations: []
118 | },
119 | dataWithOptimisticResults: {}
120 | }
121 | const event = {
122 | type: 'message',
123 | data: JSON.stringify(data)
124 | } as MessageEvent
125 | sources['/events'].emitMessage(event)
126 | expect(hook).toHaveBeenCalledWith(data)
127 | })
128 |
129 | it('writes data to console when it receives stdout event', () => {
130 | const event = {
131 | type: 'stdout',
132 | data: 'blah',
133 | } as MessageEvent
134 | sources['/events'].emit('stdout', event)
135 | expect(console.log).toHaveBeenCalledWith('%cblah', 'color: cadetblue')
136 | })
137 |
138 | it('writes data to console when it receives stderr event', () => {
139 | const event = {
140 | type: 'stderr',
141 | data: 'blah',
142 | } as MessageEvent
143 | sources['/events'].emit('stderr', event)
144 | expect(console.log).toHaveBeenCalledWith('%cblah', 'color: tomato')
145 | })
146 | })
147 |
148 | describe('#stopListening', () => {
149 | it('stops event source', () => {
150 | client.startListening()
151 | client.stopListening()
152 | expect(sources['/events'].readyState).toBe(2)
153 | })
154 | })
155 | })
156 |
--------------------------------------------------------------------------------
/src/__tests__/IntegrationTests.test.ts:
--------------------------------------------------------------------------------
1 | import type { /* global */ } from 'apollo-client-devtools'
2 | import Bridge from 'apollo-client-devtools/src/bridge'
3 | import { initBackend } from 'apollo-client-devtools/src/backend'
4 | import { installHook } from 'apollo-client-devtools/src/backend/hook'
5 | import ApolloClientPretender from '../ApolloClientPretender'
6 |
7 | describe('integration', () => {
8 | let bridge: Bridge
9 |
10 | beforeAll(done => {
11 | window.__APOLLO_CLIENT__ = new ApolloClientPretender()
12 | bridge = new Bridge({
13 | listen(fn) {
14 | const listener = (evt: MessageEvent) => {
15 | fn(evt.data.payload)
16 | }
17 | window.addEventListener('message', listener)
18 | },
19 | send(payload) {
20 | window.postMessage({ payload }, '*')
21 | }
22 | })
23 | installHook(window, 'test')
24 | setTimeout(done, 1000) // Wait until hook finds ApolloClient
25 | })
26 |
27 | describe('#initBackend', () => {
28 | it('emits `ready` event', done => {
29 | bridge.addListener('ready', (message: string) => {
30 | expect(message).toBe('2.0.0')
31 | done()
32 | })
33 | initBackend(bridge, window.__APOLLO_DEVTOOLS_GLOBAL_HOOK__, window.localStorage)
34 | })
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { /* global */ } from 'apollo-client-devtools'
2 | import ApolloClientPretender from "./ApolloClientPretender"
3 |
4 | window.__APOLLO_CLIENT__ = new ApolloClientPretender()
5 |
--------------------------------------------------------------------------------
/src/schema.ts:
--------------------------------------------------------------------------------
1 | export interface Schema {
2 | /**
3 | * Console event pushed from client to server.
4 | */
5 | consoleEvent?: ConsoleEvent;
6 | /**
7 | * GraphQL operation request passed from client to server.
8 | */
9 | operation?: Operation;
10 | /**
11 | * State change event pushed from server to client.
12 | */
13 | stateChange?: StateChange;
14 | }
15 |
16 | /**
17 | * Console event pushed from client to server.
18 | */
19 | export interface ConsoleEvent {
20 | data: string;
21 | type: ConsoleEventType;
22 | }
23 |
24 | export enum ConsoleEventType {
25 | Stderr = "stderr",
26 | Stdout = "stdout",
27 | }
28 |
29 | /**
30 | * GraphQL operation request passed from client to server.
31 | */
32 | export interface Operation {
33 | operationIdentifier?: string;
34 | operationName?: string;
35 | query: string;
36 | variables?: { [key: string]: any };
37 | }
38 |
39 | /**
40 | * State change event pushed from server to client.
41 | */
42 | export interface StateChange {
43 | dataWithOptimisticResults: { [key: string]: any };
44 | state: State;
45 | }
46 |
47 | export interface State {
48 | mutations: Mutation[];
49 | queries: Query[];
50 | }
51 |
52 | export interface Mutation {
53 | error?: ErrorLike;
54 | loading: boolean;
55 | mutation: string;
56 | variables?: { [key: string]: any };
57 | }
58 |
59 | /**
60 | * JavaScript error serialized to JSON.
61 | */
62 | export interface ErrorLike {
63 | columnNumber?: number;
64 | fileName?: string;
65 | lineNumber?: number;
66 | message: string;
67 | name: string;
68 | }
69 |
70 | export interface Query {
71 | document: string;
72 | graphQLErrors?: ErrorLike[];
73 | networkError?: ErrorLike;
74 | previousVariables?: { [key: string]: any };
75 | variables?: { [key: string]: any };
76 | }
77 |
--------------------------------------------------------------------------------
/src/types/apollo-client-devtools/index.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for apollo-client-devtools 2.2.5
2 | // Project: https://github.com/apollographql/apollo-client-devtools
3 | // Definitions by: Ryosuke Ito
4 | // TypeScript Version: 3.5.2
5 |
6 | import { Hook } from 'apollo-client-devtools/src/backend/hook'
7 | import { DocumentNode } from 'apollo-link'
8 |
9 | declare global {
10 | interface Window {
11 | __APOLLO_CLIENT__: unknown
12 | __APOLLO_DEVTOOLS_GLOBAL_HOOK__: Hook
13 | }
14 | }
15 |
16 | type Variables = Record
17 |
18 | type CacheStorage = Record
19 |
20 | type Query = {
21 | document: DocumentNode,
22 | variables?: Variables,
23 | previousVariables?: Variables,
24 | networkError?: Error,
25 | graphQLErrors?: Error[]
26 | }
27 |
28 | type Mutation = {
29 | mutation: DocumentNode,
30 | variables?: Variables,
31 | loading: boolean,
32 | error?: Error
33 | }
34 |
35 | export type StateChange = {
36 | state: {
37 | queries: Query[],
38 | mutations: Mutation[]
39 | },
40 | dataWithOptimisticResults: CacheStorage
41 | }
42 |
--------------------------------------------------------------------------------
/src/types/apollo-client-devtools/src/backend/broadcastQueries.d.ts:
--------------------------------------------------------------------------------
1 | import { Hook } from './hook'
2 | import Bridge from '../bridge'
3 |
4 | export const initBroadCastEvents: (hook: Hook, bridge: Bridge) => void
5 |
--------------------------------------------------------------------------------
/src/types/apollo-client-devtools/src/backend/hook.d.ts:
--------------------------------------------------------------------------------
1 | export function installHook(window: Window, devToolsVersion: string): void
2 |
3 | export interface Hook {
4 | ApolloClient: unknown
5 | actionLog: string[]
6 | devToolsVersion: string
7 | on(event: string, fn: (...args: unknown[]) => void): void
8 | once(event: string, fn: (...args: unknown[]) => void): void
9 | off(event: string, fn: (...args: unknown[]) => void): void
10 | emit(event: string): void
11 | }
12 |
--------------------------------------------------------------------------------
/src/types/apollo-client-devtools/src/backend/index.d.ts:
--------------------------------------------------------------------------------
1 | import { Hook } from './hook'
2 | import Bridge from '../bridge'
3 |
4 | export const sendBridgeReady: () => void
5 | export const initBackend: (bridge: Bridge, hook: Hook, storage: Storage) => void
6 |
--------------------------------------------------------------------------------
/src/types/apollo-client-devtools/src/backend/links.d.ts:
--------------------------------------------------------------------------------
1 | import { Hook } from './hook'
2 | import Bridge from '../bridge'
3 |
4 | export const initLinkEvents: (hook: Hook, bridge: Bridge) => void
5 |
--------------------------------------------------------------------------------
/src/types/apollo-client-devtools/src/backend/typeDefs.d.ts:
--------------------------------------------------------------------------------
1 | type TypeDefs = string | Record
2 |
3 | export interface Schema {
4 | definition: string,
5 | directives: string
6 | }
7 |
8 | export function buildSchemasFromTypeDefs(typeDefs: TypeDefs): [Schema]
9 |
--------------------------------------------------------------------------------
/src/types/apollo-client-devtools/src/bridge.d.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events'
2 |
3 | export type Message = string | { event: string, payload: unknown }
4 |
5 | export interface Wall {
6 | listen(fn: (message: Message) => void): void
7 | send(message: Message): void
8 | }
9 |
10 | export default class Bridge extends EventEmitter {
11 | constructor(wall: Wall)
12 | send(event: string, payload: unknown): void
13 | log(message: string): void
14 | }
15 |
--------------------------------------------------------------------------------
/src/types/eventsourcemock/index.d.ts:
--------------------------------------------------------------------------------
1 | import type { EventEmitter } from 'events'
2 |
3 | type EventSourceConfigurationType = {
4 | withCredentials: boolean
5 | }
6 |
7 | type ReadyStateType = 0 | 1 | 2
8 |
9 | declare const defaultOptions: {
10 | withCredentials: false
11 | }
12 |
13 | export const sources: Record
14 |
15 | export default class EventSource {
16 | static CONNECTING: ReadyStateType
17 | static OPEN: ReadyStateType
18 | static CLOSED: ReadyStateType
19 |
20 | CONNECTING: ReadyStateType
21 | OPEN: ReadyStateType
22 | CLOSED: ReadyStateType
23 |
24 | __emitter: EventEmitter
25 | onerror: ((this: EventSource, ev: Event) => unknown) | null
26 | onmessage: ((this: EventSource, ev: MessageEvent) => unknown) | null
27 | onopen: ((this: EventSource, ev: Event) => unknown) | null
28 | readyState: ReadyStateType
29 | url: string
30 | withCredentials: boolean
31 |
32 | constructor(
33 | url: string,
34 | configuration?: EventSourceInit
35 | )
36 |
37 | addEventListener(eventName: string, listener: (ev: Event) => void): void
38 | removeEventListener(eventName: string, listener: (ev: Event) => void): void
39 | close(): void
40 | emit(eventName: string, messageEvent?: MessageEvent): void
41 | emitError(error: Event): void
42 | emitOpen(): void
43 | emitMessage(message: MessageEvent): void
44 |
45 | // Actually missing
46 | dispatchEvent(event: Event): boolean
47 | }
48 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "esModuleInterop": true,
5 | "module": "es2015",
6 | "target": "es2015",
7 | "moduleResolution": "node",
8 | "strict": true,
9 | "baseUrl": "src/types"
10 | },
11 | "files": [
12 | "src/index.ts",
13 | "src/__tests__/IntegrationTests.test.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | module.exports = {
4 | mode: 'production',
5 | module: {
6 | rules: [
7 | {
8 | test: /\.ts$/,
9 | use: 'ts-loader',
10 | exclude: /node_modules/
11 | }
12 | ]
13 | },
14 | resolve: {
15 | extensions: ['.ts', '.js']
16 | },
17 | output: {
18 | filename: 'bundle.js',
19 | path: path.resolve(__dirname, 'Sources/ApolloDeveloperKit/Assets')
20 | }
21 | }
22 |
--------------------------------------------------------------------------------