xref: /expo/packages/@expo/cli/src/api/getExpoSchema.ts (revision 8a424beb)
18d307f52SEvan Baconimport { JSONObject } from '@expo/json-file';
28d307f52SEvan Baconimport fs from 'fs';
38d307f52SEvan Baconimport schemaDerefSync from 'json-schema-deref-sync';
48d307f52SEvan Baconimport path from 'path';
58d307f52SEvan Bacon
6*8a424bebSJames Ideimport { createCachedFetch } from './rest/client';
7814b6fafSEvan Baconimport { env } from '../utils/env';
88d307f52SEvan Baconimport { CommandError } from '../utils/errors';
98d307f52SEvan Bacon
108d307f52SEvan Baconexport type Schema = any;
118d307f52SEvan Bacon
128d307f52SEvan Baconexport type AssetSchema = {
138d307f52SEvan Bacon  fieldPath: string;
148d307f52SEvan Bacon};
158d307f52SEvan Bacon
168d307f52SEvan Baconconst schemaJson: { [sdkVersion: string]: Schema } = {};
178d307f52SEvan Bacon
188d307f52SEvan Bacon// TODO: Maybe move json-schema-deref-sync out of api (1.58MB -- lodash)
198d307f52SEvan Bacon// https://packagephobia.com/result?p=json-schema-deref-sync
208d307f52SEvan Baconasync function getSchemaAsync(sdkVersion: string): Promise<Schema> {
218d307f52SEvan Bacon  const json = await getSchemaJSONAsync(sdkVersion);
228d307f52SEvan Bacon  return schemaDerefSync(json.schema);
238d307f52SEvan Bacon}
248d307f52SEvan Bacon
258d307f52SEvan Bacon/**
268d307f52SEvan Bacon * Array of schema nodes that refer to assets along with their field path (eg. 'notification.icon')
278d307f52SEvan Bacon *
288d307f52SEvan Bacon * @param sdkVersion
298d307f52SEvan Bacon */
308d307f52SEvan Baconexport async function getAssetSchemasAsync(sdkVersion: string = 'UNVERSIONED'): Promise<string[]> {
318d307f52SEvan Bacon  // If no SDK version is available then fall back to unversioned
328d307f52SEvan Bacon  const schema = await getSchemaAsync(sdkVersion);
338d307f52SEvan Bacon  const assetSchemas: string[] = [];
348d307f52SEvan Bacon  const visit = (node: Schema, fieldPath: string) => {
358d307f52SEvan Bacon    if (node.meta && node.meta.asset) {
368d307f52SEvan Bacon      assetSchemas.push(fieldPath);
378d307f52SEvan Bacon    }
388d307f52SEvan Bacon    const properties = node.properties;
398d307f52SEvan Bacon    if (properties) {
408d307f52SEvan Bacon      Object.keys(properties).forEach((property) =>
418d307f52SEvan Bacon        visit(properties[property], `${fieldPath}${fieldPath.length > 0 ? '.' : ''}${property}`)
428d307f52SEvan Bacon      );
438d307f52SEvan Bacon    }
448d307f52SEvan Bacon  };
458d307f52SEvan Bacon  visit(schema, '');
468d307f52SEvan Bacon
478d307f52SEvan Bacon  return assetSchemas;
488d307f52SEvan Bacon}
498d307f52SEvan Bacon
508d307f52SEvan Baconasync function getSchemaJSONAsync(sdkVersion: string): Promise<{ schema: Schema }> {
51814b6fafSEvan Bacon  if (env.EXPO_UNIVERSE_DIR) {
528d307f52SEvan Bacon    return JSON.parse(
538d307f52SEvan Bacon      fs
548d307f52SEvan Bacon        .readFileSync(
55814b6fafSEvan Bacon          path.join(
56814b6fafSEvan Bacon            env.EXPO_UNIVERSE_DIR,
57814b6fafSEvan Bacon            'server',
58814b6fafSEvan Bacon            'www',
59814b6fafSEvan Bacon            'xdl-schemas',
60814b6fafSEvan Bacon            'UNVERSIONED-schema.json'
61814b6fafSEvan Bacon          )
628d307f52SEvan Bacon        )
638d307f52SEvan Bacon        .toString()
648d307f52SEvan Bacon    );
658d307f52SEvan Bacon  }
668d307f52SEvan Bacon
678d307f52SEvan Bacon  if (!schemaJson[sdkVersion]) {
688d307f52SEvan Bacon    try {
698d307f52SEvan Bacon      schemaJson[sdkVersion] = await getConfigurationSchemaAsync(sdkVersion);
708d307f52SEvan Bacon    } catch (e: any) {
718d307f52SEvan Bacon      if (e.code === 'INVALID_JSON') {
728d307f52SEvan Bacon        throw new CommandError('INVALID_JSON', `Couldn't read schema from server`);
738d307f52SEvan Bacon      }
748d307f52SEvan Bacon
758d307f52SEvan Bacon      throw e;
768d307f52SEvan Bacon    }
778d307f52SEvan Bacon  }
788d307f52SEvan Bacon
798d307f52SEvan Bacon  return schemaJson[sdkVersion];
808d307f52SEvan Bacon}
818d307f52SEvan Bacon
828d307f52SEvan Baconasync function getConfigurationSchemaAsync(sdkVersion: string): Promise<JSONObject> {
838d307f52SEvan Bacon  // Reconstruct the cached fetch since caching could be disabled.
848d307f52SEvan Bacon  const fetchAsync = createCachedFetch({
858d307f52SEvan Bacon    cacheDirectory: 'schema-cache',
868d307f52SEvan Bacon    // We'll use a 1 week cache for versions so older versions get flushed out eventually.
878d307f52SEvan Bacon    ttl: 1000 * 60 * 60 * 24 * 7,
888d307f52SEvan Bacon  });
898d307f52SEvan Bacon  const response = await fetchAsync(`project/configuration/schema/${sdkVersion}`);
908d307f52SEvan Bacon  const { data } = await response.json();
918d307f52SEvan Bacon  return data;
928d307f52SEvan Bacon}
93