@quilted/rollup 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/source/app.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as path from 'path';
2
2
 
3
- import type {Plugin} from 'rollup';
3
+ import type {GetManualChunk, Plugin} from 'rollup';
4
4
 
5
5
  import {
6
6
  MAGIC_MODULE_ENTRY,
@@ -27,7 +27,7 @@ export interface AppOptions {
27
27
  *
28
28
  * @example './App.tsx'
29
29
  */
30
- entry?: string;
30
+ app?: string;
31
31
 
32
32
  /**
33
33
  * Whether to include GraphQL-related code transformations.
@@ -44,34 +44,6 @@ export interface AppOptions {
44
44
  env?: MagicModuleEnvOptions;
45
45
  }
46
46
 
47
- export function quiltApp({env, entry}: AppOptions = {}) {
48
- return {
49
- name: '@quilted/app',
50
- async options(originalOptions) {
51
- const newPlugins = rollupPluginsToArray(originalOptions.plugins);
52
- const newOptions = {...originalOptions, plugins: newPlugins};
53
-
54
- if (env) {
55
- const {magicModuleEnv, replaceProcessEnv} = await import('./env.ts');
56
-
57
- if (typeof env === 'boolean') {
58
- newPlugins.push(replaceProcessEnv({mode: 'production'}));
59
- newPlugins.push(magicModuleEnv({mode: 'production'}));
60
- } else {
61
- newPlugins.push(replaceProcessEnv({mode: env.mode ?? 'production'}));
62
- newPlugins.push(magicModuleEnv({mode: 'production', ...env}));
63
- }
64
- }
65
-
66
- if (entry) {
67
- newPlugins.push(magicModuleAppComponent({entry}));
68
- }
69
-
70
- return newOptions;
71
- },
72
- } satisfies Plugin;
73
- }
74
-
75
47
  export interface AppBrowserOptions extends AppOptions {
76
48
  /**
77
49
  * Customizes the magic `quilt:module/browser` entry module.
@@ -110,11 +82,11 @@ export interface AppBrowserAssetsOptions {
110
82
  }
111
83
 
112
84
  export function quiltAppBrowser({
113
- entry,
85
+ app,
114
86
  env,
115
- graphql,
116
87
  assets,
117
88
  module,
89
+ graphql = true,
118
90
  }: AppBrowserOptions = {}) {
119
91
  return {
120
92
  name: '@quilted/app/browser',
@@ -127,10 +99,33 @@ export function quiltAppBrowser({
127
99
  getNodePlugins(),
128
100
  ]);
129
101
 
130
- newPlugins.push(quiltApp({env, entry, graphql}));
131
102
  newPlugins.push(...nodePlugins);
103
+
104
+ if (env) {
105
+ const {magicModuleEnv, replaceProcessEnv} = await import('./env.ts');
106
+
107
+ if (typeof env === 'boolean') {
108
+ newPlugins.push(replaceProcessEnv({mode: 'production'}));
109
+ newPlugins.push(magicModuleEnv({mode: 'production'}));
110
+ } else {
111
+ newPlugins.push(replaceProcessEnv({mode: env.mode ?? 'production'}));
112
+ newPlugins.push(magicModuleEnv({mode: 'production', ...env}));
113
+ }
114
+ }
115
+
116
+ if (app) {
117
+ newPlugins.push(magicModuleAppComponent({entry: app}));
118
+ }
119
+
132
120
  newPlugins.push(magicModuleAppBrowserEntry(module));
133
121
 
122
+ if (graphql) {
123
+ const {graphql} = await import('./graphql.ts');
124
+ newPlugins.push(
125
+ graphql({manifest: path.resolve(`manifests/graphql.json`)}),
126
+ );
127
+ }
128
+
134
129
  const minify = assets?.minify ?? true;
135
130
 
136
131
  if (minify) {
@@ -143,16 +138,28 @@ export function quiltAppBrowser({
143
138
  template: 'treemap',
144
139
  open: false,
145
140
  brotliSize: true,
146
- filename: path.resolve('reports', `bundle-visualizer.html`),
141
+ filename: path.resolve(`reports/bundle-visualizer.html`),
147
142
  }),
148
143
  );
149
144
 
150
145
  return newOptions;
151
146
  },
147
+ outputOptions(originalOptions) {
148
+ return {
149
+ ...originalOptions,
150
+ // format: isESM ? 'esm' : 'systemjs',
151
+ format: 'esm',
152
+ dir: path.resolve(`build/assets`),
153
+ entryFileNames: `app.[hash].js`,
154
+ assetFileNames: `[name].[hash].[ext]`,
155
+ chunkFileNames: `[name].[hash].js`,
156
+ manualChunks: createManualChunksSorter(),
157
+ };
158
+ },
152
159
  } satisfies Plugin;
153
160
  }
154
161
 
155
- export interface AppServerOptions {
162
+ export interface AppServerOptions extends AppOptions {
156
163
  /**
157
164
  * The entry module for the server of this app. This module must export a
158
165
  * `RequestRouter` object as its default export, which will be wrapped in
@@ -161,22 +168,60 @@ export interface AppServerOptions {
161
168
  entry?: string;
162
169
  }
163
170
 
164
- export function quiltAppServer(options: AppServerOptions = {}) {
171
+ export function quiltAppServer({
172
+ app,
173
+ env,
174
+ graphql,
175
+ entry,
176
+ }: AppServerOptions = {}) {
165
177
  return {
166
178
  name: '@quilted/app/server',
167
179
  async options(originalOptions) {
168
180
  const newPlugins = rollupPluginsToArray(originalOptions.plugins);
169
181
  const newOptions = {...originalOptions, plugins: newPlugins};
170
182
 
171
- const [{magicModuleRequestRouterEntry}] = await Promise.all([
183
+ const [{magicModuleRequestRouterEntry}, nodePlugins] = await Promise.all([
172
184
  import('./request-router.ts'),
185
+ getNodePlugins(),
173
186
  ]);
174
187
 
188
+ newPlugins.push(...nodePlugins);
189
+
190
+ if (env) {
191
+ const {magicModuleEnv, replaceProcessEnv} = await import('./env.ts');
192
+
193
+ if (typeof env === 'boolean') {
194
+ newPlugins.push(replaceProcessEnv({mode: 'production'}));
195
+ newPlugins.push(magicModuleEnv({mode: 'production'}));
196
+ } else {
197
+ newPlugins.push(replaceProcessEnv({mode: env.mode ?? 'production'}));
198
+ newPlugins.push(magicModuleEnv({mode: 'production', ...env}));
199
+ }
200
+ }
201
+
202
+ if (app) {
203
+ newPlugins.push(magicModuleAppComponent({entry: app}));
204
+ }
205
+
175
206
  newPlugins.push(magicModuleRequestRouterEntry());
176
- newPlugins.push(magicModuleAppRequestRouter(options));
207
+ newPlugins.push(magicModuleAppRequestRouter({entry}));
208
+
209
+ if (graphql) {
210
+ const {graphql} = await import('./graphql.ts');
211
+ newPlugins.push(graphql({manifest: false}));
212
+ }
177
213
 
178
214
  return newOptions;
179
215
  },
216
+ outputOptions(originalOptions) {
217
+ return {
218
+ ...originalOptions,
219
+ // format,
220
+ format: 'esm',
221
+ dir: path.resolve(`build/server`),
222
+ entryFileNames: 'server.js',
223
+ };
224
+ },
180
225
  } satisfies Plugin;
181
226
  }
182
227
 
@@ -259,3 +304,103 @@ export function magicModuleAppBrowserEntry({
259
304
  },
260
305
  });
261
306
  }
307
+
308
+ const FRAMEWORK_CHUNK_NAME = 'framework';
309
+ const POLYFILLS_CHUNK_NAME = 'polyfills';
310
+ const VENDOR_CHUNK_NAME = 'vendor';
311
+ const INTERNALS_CHUNK_NAME = 'internals';
312
+ const SHARED_CHUNK_NAME = 'shared';
313
+ const PACKAGES_CHUNK_NAME = 'packages';
314
+ const GLOBAL_CHUNK_NAME = 'global';
315
+ const FRAMEWORK_TEST_STRINGS: (string | RegExp)[] = [
316
+ '/node_modules/preact/',
317
+ '/node_modules/react/',
318
+ '/node_modules/js-cookie/',
319
+ '/node_modules/@quilted/quilt/',
320
+ '/node_modules/@preact/signals/',
321
+ '/node_modules/@preact/signals-core/',
322
+ // TODO I should turn this into an allowlist
323
+ /node_modules[/]@quilted[/](?!react-query|swr)/,
324
+ ];
325
+
326
+ const POLYFILL_TEST_STRINGS = [
327
+ '/node_modules/@quilted/polyfills/',
328
+ '/node_modules/core-js/',
329
+ '/node_modules/whatwg-fetch/',
330
+ '/node_modules/regenerator-runtime/',
331
+ '/node_modules/abort-controller/',
332
+ ];
333
+
334
+ const INTERNALS_TEST_STRINGS = [
335
+ '\x00commonjsHelpers.js',
336
+ '/node_modules/@babel/runtime/',
337
+ ];
338
+
339
+ // When building from source, quilt packages are not in node_modules,
340
+ // so we instead add their repo paths to the list of framework test strings.
341
+ if (process.env.QUILT_FROM_SOURCE) {
342
+ FRAMEWORK_TEST_STRINGS.push('/quilt/packages/');
343
+ }
344
+
345
+ // Inspired by Vite: https://github.com/vitejs/vite/blob/c69f83615292953d40f07b1178d1ed1d72abe695/packages/vite/source/node/build.ts#L567
346
+ function createManualChunksSorter(): GetManualChunk {
347
+ // TODO: make this more configurable, and make it so that we bundle more intelligently
348
+ // for split entries
349
+ const packagesPath = path.resolve('packages') + path.sep;
350
+ const globalPath = path.resolve('global') + path.sep;
351
+ const sharedPath = path.resolve('shared') + path.sep;
352
+
353
+ return (id, {getModuleInfo}) => {
354
+ if (INTERNALS_TEST_STRINGS.some((test) => id.includes(test))) {
355
+ return INTERNALS_CHUNK_NAME;
356
+ }
357
+
358
+ if (
359
+ FRAMEWORK_TEST_STRINGS.some((test) =>
360
+ typeof test === 'string' ? id.includes(test) : test.test(id),
361
+ )
362
+ ) {
363
+ return FRAMEWORK_CHUNK_NAME;
364
+ }
365
+
366
+ if (POLYFILL_TEST_STRINGS.some((test) => id.includes(test))) {
367
+ return POLYFILLS_CHUNK_NAME;
368
+ }
369
+
370
+ let bundleBaseName: string | undefined;
371
+ let relativeId: string | undefined;
372
+
373
+ if (id.includes('/node_modules/')) {
374
+ const moduleInfo = getModuleInfo(id);
375
+
376
+ // If the only dependency is another vendor, let Rollup handle the naming
377
+ if (moduleInfo == null) return;
378
+ if (
379
+ moduleInfo.importers.length > 0 &&
380
+ moduleInfo.importers.every((importer) =>
381
+ importer.includes('/node_modules/'),
382
+ )
383
+ ) {
384
+ return;
385
+ }
386
+
387
+ bundleBaseName = VENDOR_CHUNK_NAME;
388
+ relativeId = id.replace(/^.*[/]node_modules[/]/, '');
389
+ } else if (id.startsWith(packagesPath)) {
390
+ bundleBaseName = PACKAGES_CHUNK_NAME;
391
+ relativeId = id.replace(packagesPath, '');
392
+ } else if (id.startsWith(globalPath)) {
393
+ bundleBaseName = GLOBAL_CHUNK_NAME;
394
+ relativeId = id.replace(globalPath, '');
395
+ } else if (id.startsWith(sharedPath)) {
396
+ bundleBaseName = SHARED_CHUNK_NAME;
397
+ relativeId = id.replace(sharedPath, '');
398
+ }
399
+
400
+ if (bundleBaseName == null || relativeId == null) {
401
+ return;
402
+ }
403
+
404
+ return `${bundleBaseName}-${relativeId.split(path.sep)[0]?.split('.')[0]}`;
405
+ };
406
+ }
@@ -0,0 +1,283 @@
1
+ import {createHash} from 'crypto';
2
+
3
+ import {print, parse} from 'graphql';
4
+ import type {
5
+ DocumentNode,
6
+ TypedQueryDocumentNode,
7
+ DefinitionNode,
8
+ SelectionSetNode,
9
+ ExecutableDefinitionNode,
10
+ OperationDefinitionNode,
11
+ SelectionNode,
12
+ Location,
13
+ } from 'graphql';
14
+ import type {GraphQLOperation} from '@quilted/graphql';
15
+
16
+ const IMPORT_REGEX = /^#import\s+['"]([^'"]*)['"];?[\s\n]*/gm;
17
+ const DEFAULT_NAME = 'Operation';
18
+
19
+ export interface EnhancedDocumentNode<
20
+ Data = Record<string, any>,
21
+ Variables = Record<string, any>,
22
+ > extends TypedQueryDocumentNode<Data, Variables> {
23
+ readonly id: string;
24
+ }
25
+
26
+ export function cleanGraphQLDocument<
27
+ Data = Record<string, any>,
28
+ Variables = Record<string, any>,
29
+ >(
30
+ document: DocumentNode | TypedQueryDocumentNode<Data, Variables>,
31
+ {removeUnused = true}: {removeUnused?: boolean | {exclude: Set<string>}} = {},
32
+ ): EnhancedDocumentNode<Data, Variables> {
33
+ if (removeUnused) {
34
+ removeUnusedDefinitions(document, {
35
+ exclude: removeUnused === true ? new Set() : removeUnused.exclude,
36
+ });
37
+ }
38
+
39
+ for (const definition of document.definitions) {
40
+ addTypename(definition);
41
+ }
42
+
43
+ const normalizedSource = minifyGraphQLSource(print(document));
44
+ const normalizedDocument = parse(normalizedSource);
45
+
46
+ for (const definition of normalizedDocument.definitions) {
47
+ stripLoc(definition);
48
+ }
49
+
50
+ // This ID is a hash of the full file contents that are part of the document,
51
+ // including other documents that are injected in, but excluding any unused
52
+ // fragments. This is useful for things like persisted queries.
53
+ const id = createHash('sha256').update(normalizedSource).digest('hex');
54
+
55
+ Reflect.defineProperty(normalizedDocument, 'id', {
56
+ value: id,
57
+ enumerable: true,
58
+ writable: false,
59
+ configurable: false,
60
+ });
61
+
62
+ Reflect.defineProperty(normalizedDocument, 'loc', {
63
+ value: stripDocumentLoc(normalizedDocument.loc),
64
+ enumerable: true,
65
+ writable: false,
66
+ configurable: false,
67
+ });
68
+
69
+ return normalizedDocument as any;
70
+ }
71
+
72
+ export function extractGraphQLImports(rawSource: string) {
73
+ const imports = new Set<string>();
74
+
75
+ const source = rawSource.replace(IMPORT_REGEX, (_, imported) => {
76
+ imports.add(imported);
77
+ return '';
78
+ });
79
+
80
+ return {imports: [...imports], source};
81
+ }
82
+
83
+ export function toGraphQLOperation<Data = unknown, Variables = unknown>(
84
+ documentOrSource: EnhancedDocumentNode<Data, Variables> | string,
85
+ ): GraphQLOperation<Data, Variables> {
86
+ const document =
87
+ typeof documentOrSource === 'string'
88
+ ? cleanGraphQLDocument(parse(documentOrSource))
89
+ : documentOrSource;
90
+
91
+ return {
92
+ id: document.id,
93
+ name: operationNameForDocument(document),
94
+ source: document.loc!.source.body,
95
+ };
96
+ }
97
+
98
+ function operationNameForDocument(document: DocumentNode) {
99
+ return document.definitions.find(
100
+ (definition): definition is OperationDefinitionNode =>
101
+ definition.kind === 'OperationDefinition',
102
+ )?.name?.value;
103
+ }
104
+
105
+ function removeUnusedDefinitions(
106
+ document: DocumentNode,
107
+ {exclude}: {exclude: Set<string>},
108
+ ) {
109
+ const usedDefinitions = new Set<DefinitionNode>();
110
+ const dependencies = definitionDependencies(document.definitions);
111
+
112
+ const markAsUsed = (definition: DefinitionNode) => {
113
+ if (usedDefinitions.has(definition)) {
114
+ return;
115
+ }
116
+
117
+ usedDefinitions.add(definition);
118
+
119
+ for (const dependency of dependencies.get(definition) || []) {
120
+ markAsUsed(dependency);
121
+ }
122
+ };
123
+
124
+ for (const definition of document.definitions) {
125
+ if (definition.kind === 'FragmentDefinition') {
126
+ if (exclude.has(definition.name.value)) {
127
+ markAsUsed(definition);
128
+ }
129
+ } else {
130
+ markAsUsed(definition);
131
+ }
132
+ }
133
+
134
+ (document as any).definitions = [...usedDefinitions];
135
+ }
136
+
137
+ function definitionDependencies(definitions: readonly DefinitionNode[]) {
138
+ const executableDefinitions: ExecutableDefinitionNode[] = definitions.filter(
139
+ (definition) =>
140
+ definition.kind === 'OperationDefinition' ||
141
+ definition.kind === 'FragmentDefinition',
142
+ ) as any[];
143
+
144
+ const definitionsByName = new Map(
145
+ executableDefinitions.map<[string, DefinitionNode]>((definition) => [
146
+ definition.name ? definition.name.value : DEFAULT_NAME,
147
+ definition,
148
+ ]),
149
+ );
150
+
151
+ return new Map(
152
+ executableDefinitions.map<[DefinitionNode, DefinitionNode[]]>(
153
+ (executableNode) => [
154
+ executableNode,
155
+ [...collectUsedFragmentSpreads(executableNode, new Set())].map(
156
+ (usedFragment) => {
157
+ const definition = definitionsByName.get(usedFragment);
158
+
159
+ if (definition == null) {
160
+ throw new Error(
161
+ `You attempted to use the fragment '${usedFragment}' (in '${
162
+ executableNode.name ? executableNode.name.value : DEFAULT_NAME
163
+ }'), but it does not exist. Maybe you forgot to import it from another document?`,
164
+ );
165
+ }
166
+
167
+ return definition;
168
+ },
169
+ ),
170
+ ],
171
+ ),
172
+ );
173
+ }
174
+
175
+ const TYPENAME_FIELD = {
176
+ kind: 'Field',
177
+ alias: null,
178
+ name: {kind: 'Name', value: '__typename'},
179
+ };
180
+
181
+ function addTypename(definition: DefinitionNode) {
182
+ for (const {selections} of selectionSetsForDefinition(definition)) {
183
+ const hasTypename = selections.some(
184
+ (selection) =>
185
+ selection.kind === 'Field' && selection.name.value === '__typename',
186
+ );
187
+
188
+ if (!hasTypename) {
189
+ (selections as any[]).push(TYPENAME_FIELD);
190
+ }
191
+ }
192
+ }
193
+
194
+ function collectUsedFragmentSpreads(
195
+ definition: DefinitionNode,
196
+ usedSpreads: Set<string>,
197
+ ) {
198
+ for (const selection of selectionsForDefinition(definition)) {
199
+ if (selection.kind === 'FragmentSpread') {
200
+ usedSpreads.add(selection.name.value);
201
+ }
202
+ }
203
+
204
+ return usedSpreads;
205
+ }
206
+
207
+ function selectionsForDefinition(
208
+ definition: DefinitionNode,
209
+ ): IterableIterator<SelectionNode> {
210
+ if (!('selectionSet' in definition) || definition.selectionSet == null) {
211
+ return [][Symbol.iterator]();
212
+ }
213
+
214
+ return selectionsForSelectionSet(definition.selectionSet);
215
+ }
216
+
217
+ function* selectionSetsForDefinition(
218
+ definition: DefinitionNode,
219
+ ): IterableIterator<SelectionSetNode> {
220
+ if (!('selectionSet' in definition) || definition.selectionSet == null) {
221
+ return [][Symbol.iterator]();
222
+ }
223
+
224
+ if (definition.kind !== 'OperationDefinition') {
225
+ yield definition.selectionSet;
226
+ }
227
+
228
+ for (const nestedSelection of selectionsForDefinition(definition)) {
229
+ if (
230
+ 'selectionSet' in nestedSelection &&
231
+ nestedSelection.selectionSet != null
232
+ ) {
233
+ yield nestedSelection.selectionSet;
234
+ }
235
+ }
236
+ }
237
+
238
+ function* selectionsForSelectionSet({
239
+ selections,
240
+ }: SelectionSetNode): IterableIterator<SelectionNode> {
241
+ for (const selection of selections) {
242
+ yield selection;
243
+
244
+ if ('selectionSet' in selection && selection.selectionSet != null) {
245
+ yield* selectionsForSelectionSet(selection.selectionSet);
246
+ }
247
+ }
248
+ }
249
+
250
+ type Writable<T> = {-readonly [K in keyof T]: T[K]};
251
+
252
+ function stripDocumentLoc(loc?: Location) {
253
+ const normalizedLoc: Partial<Writable<Location>> = {...loc};
254
+ delete normalizedLoc.endToken;
255
+ delete normalizedLoc.startToken;
256
+ return normalizedLoc;
257
+ }
258
+
259
+ function stripLoc(value: unknown) {
260
+ if (Array.isArray(value)) {
261
+ value.forEach(stripLoc);
262
+ } else if (typeof value === 'object') {
263
+ if (value == null) {
264
+ return;
265
+ }
266
+
267
+ if ('loc' in value) {
268
+ delete (value as {loc: unknown}).loc;
269
+ }
270
+
271
+ for (const key of Object.keys(value)) {
272
+ stripLoc((value as any)[key]);
273
+ }
274
+ }
275
+ }
276
+
277
+ export function minifyGraphQLSource(source: string) {
278
+ return source
279
+ .replace(/#.*/g, '')
280
+ .replace(/\\n/g, ' ')
281
+ .replace(/\s\s+/g, ' ')
282
+ .replace(/\s*({|}|\(|\)|\.|:|,)\s*/g, '$1');
283
+ }
@@ -0,0 +1,139 @@
1
+ import {dirname} from 'path';
2
+ import {readFile, mkdir, writeFile} from 'fs/promises';
3
+
4
+ import {parse, type DocumentNode} from 'graphql';
5
+ import type {Plugin, TransformPluginContext} from 'rollup';
6
+
7
+ import {
8
+ toGraphQLOperation,
9
+ cleanGraphQLDocument,
10
+ extractGraphQLImports,
11
+ } from './graphql/transform.ts';
12
+
13
+ export interface Options {
14
+ manifest?: string | boolean;
15
+ }
16
+
17
+ export function graphql({manifest}: Options = {}): Plugin {
18
+ const shouldWriteManifest = Boolean(manifest);
19
+ const manifestPath =
20
+ typeof manifest === 'string' ? manifest : `manifests/graphql.json`;
21
+
22
+ return {
23
+ name: '@quilted/graphql',
24
+ async transform(code, id) {
25
+ if (!id.endsWith('.graphql') && !id.endsWith('.gql')) return null;
26
+
27
+ const topLevelDefinitions = new Set<string>();
28
+
29
+ const loadedDocument = await loadDocument(
30
+ code,
31
+ id,
32
+ this,
33
+ (document, level) => {
34
+ if (level !== 0) return;
35
+
36
+ for (const definition of document.definitions) {
37
+ if ('name' in definition && definition.name != null) {
38
+ topLevelDefinitions.add(definition.name.value);
39
+ }
40
+ }
41
+ },
42
+ );
43
+
44
+ const document = toGraphQLOperation(
45
+ cleanGraphQLDocument(loadedDocument, {
46
+ removeUnused: {exclude: topLevelDefinitions},
47
+ }),
48
+ );
49
+
50
+ return {
51
+ code: `export default JSON.parse(${JSON.stringify(
52
+ JSON.stringify(document),
53
+ )})`,
54
+ meta: shouldWriteManifest
55
+ ? {
56
+ quilt: {graphql: document},
57
+ }
58
+ : undefined,
59
+ };
60
+ },
61
+ async generateBundle() {
62
+ if (!shouldWriteManifest) return;
63
+
64
+ const operations: Record<string, string> = {};
65
+
66
+ for (const moduleId of this.getModuleIds()) {
67
+ const operation = this.getModuleInfo(moduleId)?.meta?.quilt?.graphql;
68
+
69
+ if (
70
+ operation != null &&
71
+ typeof operation.id === 'string' &&
72
+ typeof operation.source === 'string'
73
+ ) {
74
+ operations[operation.id] = operation.source;
75
+ }
76
+ }
77
+
78
+ await mkdir(dirname(manifestPath), {recursive: true});
79
+ await writeFile(manifestPath, JSON.stringify(operations, null, 2));
80
+ },
81
+ };
82
+ }
83
+
84
+ async function loadDocument(
85
+ code: string,
86
+ file: string,
87
+ plugin: TransformPluginContext,
88
+ add?: (document: DocumentNode, level: number) => void,
89
+ level = 0,
90
+ seen = new Set<string>(),
91
+ ) {
92
+ const {imports, source} = extractGraphQLImports(code);
93
+ const document = parse(source);
94
+
95
+ add?.(document, level);
96
+
97
+ if (imports.length === 0) {
98
+ return document;
99
+ }
100
+
101
+ const resolvedImports = await Promise.all(
102
+ imports.map(async (imported) => {
103
+ if (seen.has(imported)) return;
104
+
105
+ seen.add(imported);
106
+
107
+ const resolvedId = await plugin.resolve(imported, file);
108
+
109
+ if (resolvedId == null) {
110
+ throw new Error(
111
+ `Could not find ${JSON.stringify(imported)} from ${JSON.stringify(
112
+ file,
113
+ )}`,
114
+ );
115
+ }
116
+
117
+ plugin.addWatchFile(resolvedId.id);
118
+ const contents = await readFile(resolvedId.id, {
119
+ encoding: 'utf8',
120
+ });
121
+
122
+ return loadDocument(
123
+ contents,
124
+ resolvedId.id,
125
+ plugin,
126
+ add,
127
+ level + 1,
128
+ seen,
129
+ );
130
+ }),
131
+ );
132
+
133
+ for (const importedDocument of resolvedImports) {
134
+ if (importedDocument == null) continue;
135
+ (document.definitions as any[]).push(...importedDocument.definitions);
136
+ }
137
+
138
+ return document;
139
+ }