aboutsummaryrefslogtreecommitdiff
path: root/source/import.ts
blob: 41786b900c1832f9bdba04ca0b58118a1ce62a9d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/*** IMPORT ------------------------------------------- ***/

import { dirname, resolve } from "@std/path";

/*** UTILITY ------------------------------------------ ***/

const { readFileSync } = Deno;
const DYNAMIC_IMPORT_MARKER = "# DYNAMIC_IMPORTS";
const DYNAMIC_IMPORT_REGEX = /^#\s(DYNAMIC_IMPORTS)/gm;
const ENCODING = "utf-8";
const IMPORT_PATH_REGEX = /"([^"]+\.graphql)"/;
const IMPORT_REGEX = /^#\s(import)\s.*(.graphql")/gm;

/*** EXPORT ------------------------------------------- ***/

/**
 * Reads a `.graphql` file and resolves its imports into a single SDL string.
 *
 * Two mechanisms are supported, and both can appear in the same file:
 *
 * 1. **Explicit imports** — lines like `# import "./user.graphql"` are replaced
 *    inline with the contents of the referenced file, resolved relative to the
 *    entry file.
 * 2. **Dynamic imports** — if the file contains `# DYNAMIC_IMPORTS`, every
 *    `.graphql` file found one level below the entry file’s directory is
 *    inlined at that marker. Useful for feature-folder layouts.
 *
 * All paths are resolved relative to the entry file itself, so consumers are
 * free to place their schema wherever they like.
 *
 * Errors are logged and an empty string is returned so boot doesn’t crash.
 *
 * @param path - Path to the entry `.graphql` file. Absolute paths are used
 *   as-is; relative paths are resolved against `Deno.cwd()`.
 * @returns The fully-expanded schema string.
 */
export async function importQL(path: string): Promise<string> {
  try {
    const decoder = new TextDecoder(ENCODING);
    const entryPath = resolve(String(path));
    const entryDir = dirname(entryPath);
    const mainContent = decoder.decode(readFileSync(entryPath));
    const imports = mainContent.match(IMPORT_REGEX) || [];
    const shouldTryDynamicallyImporting = DYNAMIC_IMPORT_REGEX.test(mainContent);
    let parsedFile = mainContent;

    /*** `import` statements in the supplied schema file
         are parsed to dynamically bring in linked files. ***/

    for (const imp of imports) {
      const matched = imp.match(IMPORT_PATH_REGEX);

      if (!matched || !matched[1])
        continue;

      const importedFile = readFileSync(resolve(entryDir, matched[1]));
      const decodedFile = decoder.decode(importedFile);

      parsedFile = parsedFile.replace(imp, decodedFile);
    }

    /*** With dynamic importing, we look one level below the entry
         file’s directory to find `.graphql` files and automatically
         bring them in, if `# DYNAMIC_IMPORTS` exists in the entry. ***/

    if (shouldTryDynamicallyImporting) {
      const graphqlFiles: string[] = [];

      for await (const dirEntry of Deno.readDir(entryDir)) {
        if (!dirEntry.isDirectory)
          continue;

        const subDir = resolve(entryDir, dirEntry.name);

        for await (const sub of Deno.readDir(subDir)) {
          if (sub.isFile && sub.name.endsWith(".graphql"))
            graphqlFiles.push(resolve(subDir, sub.name));
        }
      }

      for (const file of graphqlFiles) {
        const decodedFile = decoder.decode(readFileSync(file));
        const insertPosition = parsedFile.indexOf(DYNAMIC_IMPORT_MARKER);

        if (insertPosition !== -1) {
          parsedFile =
            parsedFile.substring(0, insertPosition) +
            decodedFile +
            parsedFile.substring(insertPosition);
        }
      }
    }

    return parsedFile;
  } catch(parseError) {
    console.error(new Error(`error parsing file [${String(path)}]`), parseError);
    return "";
  }
}