xref: /expo/packages/@expo/cli/src/api/getExpoSchema.ts (revision 8a424beb)
1import { JSONObject } from '@expo/json-file';
2import fs from 'fs';
3import schemaDerefSync from 'json-schema-deref-sync';
4import path from 'path';
5
6import { createCachedFetch } from './rest/client';
7import { env } from '../utils/env';
8import { CommandError } from '../utils/errors';
9
10export type Schema = any;
11
12export type AssetSchema = {
13  fieldPath: string;
14};
15
16const schemaJson: { [sdkVersion: string]: Schema } = {};
17
18// TODO: Maybe move json-schema-deref-sync out of api (1.58MB -- lodash)
19// https://packagephobia.com/result?p=json-schema-deref-sync
20async function getSchemaAsync(sdkVersion: string): Promise<Schema> {
21  const json = await getSchemaJSONAsync(sdkVersion);
22  return schemaDerefSync(json.schema);
23}
24
25/**
26 * Array of schema nodes that refer to assets along with their field path (eg. 'notification.icon')
27 *
28 * @param sdkVersion
29 */
30export async function getAssetSchemasAsync(sdkVersion: string = 'UNVERSIONED'): Promise<string[]> {
31  // If no SDK version is available then fall back to unversioned
32  const schema = await getSchemaAsync(sdkVersion);
33  const assetSchemas: string[] = [];
34  const visit = (node: Schema, fieldPath: string) => {
35    if (node.meta && node.meta.asset) {
36      assetSchemas.push(fieldPath);
37    }
38    const properties = node.properties;
39    if (properties) {
40      Object.keys(properties).forEach((property) =>
41        visit(properties[property], `${fieldPath}${fieldPath.length > 0 ? '.' : ''}${property}`)
42      );
43    }
44  };
45  visit(schema, '');
46
47  return assetSchemas;
48}
49
50async function getSchemaJSONAsync(sdkVersion: string): Promise<{ schema: Schema }> {
51  if (env.EXPO_UNIVERSE_DIR) {
52    return JSON.parse(
53      fs
54        .readFileSync(
55          path.join(
56            env.EXPO_UNIVERSE_DIR,
57            'server',
58            'www',
59            'xdl-schemas',
60            'UNVERSIONED-schema.json'
61          )
62        )
63        .toString()
64    );
65  }
66
67  if (!schemaJson[sdkVersion]) {
68    try {
69      schemaJson[sdkVersion] = await getConfigurationSchemaAsync(sdkVersion);
70    } catch (e: any) {
71      if (e.code === 'INVALID_JSON') {
72        throw new CommandError('INVALID_JSON', `Couldn't read schema from server`);
73      }
74
75      throw e;
76    }
77  }
78
79  return schemaJson[sdkVersion];
80}
81
82async function getConfigurationSchemaAsync(sdkVersion: string): Promise<JSONObject> {
83  // Reconstruct the cached fetch since caching could be disabled.
84  const fetchAsync = createCachedFetch({
85    cacheDirectory: 'schema-cache',
86    // We'll use a 1 week cache for versions so older versions get flushed out eventually.
87    ttl: 1000 * 60 * 60 * 24 * 7,
88  });
89  const response = await fetchAsync(`project/configuration/schema/${sdkVersion}`);
90  const { data } = await response.json();
91  return data;
92}
93