153b4c0b0SEvan Bacon/* eslint-env jest */
253b4c0b0SEvan Baconimport JsonFile from '@expo/json-file';
3e1bb5bdfSKudo Chienimport assert from 'assert';
453b4c0b0SEvan Baconimport execa from 'execa';
553b4c0b0SEvan Baconimport fs from 'fs-extra';
653b4c0b0SEvan Baconimport klawSync from 'klaw-sync';
753b4c0b0SEvan Baconimport path from 'path';
853b4c0b0SEvan Bacon
953b4c0b0SEvan Baconimport { execute, projectRoot, getLoadedModulesAsync, setupTestProjectAsync, bin } from './utils';
1053b4c0b0SEvan Bacon
1153b4c0b0SEvan Baconconst originalForceColor = process.env.FORCE_COLOR;
1253b4c0b0SEvan Baconconst originalCI = process.env.CI;
1353b4c0b0SEvan Bacon
1453b4c0b0SEvan BaconbeforeAll(async () => {
1553b4c0b0SEvan Bacon  await fs.mkdir(projectRoot, { recursive: true });
1653b4c0b0SEvan Bacon  process.env.FORCE_COLOR = '0';
1753b4c0b0SEvan Bacon  process.env.CI = '1';
1853b4c0b0SEvan Bacon});
1953b4c0b0SEvan Bacon
2053b4c0b0SEvan BaconafterAll(() => {
2153b4c0b0SEvan Bacon  process.env.FORCE_COLOR = originalForceColor;
2253b4c0b0SEvan Bacon  process.env.CI = originalCI;
2353b4c0b0SEvan Bacon});
2453b4c0b0SEvan Bacon
2553b4c0b0SEvan Baconit('loads expected modules by default', async () => {
2653b4c0b0SEvan Bacon  const modules = await getLoadedModulesAsync(
2753b4c0b0SEvan Bacon    `require('../../build/src/export/web').expoExportWeb`
2853b4c0b0SEvan Bacon  );
2953b4c0b0SEvan Bacon  expect(modules).toStrictEqual([
30*4067174dSWill Schurman    '../node_modules/ansi-styles/index.js',
3153b4c0b0SEvan Bacon    '../node_modules/arg/index.js',
3253b4c0b0SEvan Bacon    '../node_modules/chalk/source/index.js',
3353b4c0b0SEvan Bacon    '../node_modules/chalk/source/util.js',
3453b4c0b0SEvan Bacon    '../node_modules/has-flag/index.js',
3553b4c0b0SEvan Bacon    '../node_modules/supports-color/index.js',
3653b4c0b0SEvan Bacon    '@expo/cli/build/src/export/web/index.js',
3753b4c0b0SEvan Bacon    '@expo/cli/build/src/log.js',
3853b4c0b0SEvan Bacon    '@expo/cli/build/src/utils/args.js',
3953b4c0b0SEvan Bacon    '@expo/cli/build/src/utils/errors.js',
4053b4c0b0SEvan Bacon  ]);
4153b4c0b0SEvan Bacon});
4253b4c0b0SEvan Bacon
4353b4c0b0SEvan Baconit('runs `npx expo export:web --help`', async () => {
4453b4c0b0SEvan Bacon  const results = await execute('export:web', '--help');
4553b4c0b0SEvan Bacon  expect(results.stdout).toMatchInlineSnapshot(`
4653b4c0b0SEvan Bacon    "
4753b4c0b0SEvan Bacon      Info
4853b4c0b0SEvan Bacon        Export the static files of the web app for hosting on a web server
4953b4c0b0SEvan Bacon
5053b4c0b0SEvan Bacon      Usage
5153b4c0b0SEvan Bacon        $ npx expo export:web <dir>
5253b4c0b0SEvan Bacon
5353b4c0b0SEvan Bacon      Options
5453b4c0b0SEvan Bacon        <dir>                         Directory of the Expo project. Default: Current working directory
5553b4c0b0SEvan Bacon        --dev                         Bundle in development mode
5653b4c0b0SEvan Bacon        -c, --clear                   Clear the bundler cache
5753b4c0b0SEvan Bacon        -h, --help                    Usage info
5853b4c0b0SEvan Bacon    "
5953b4c0b0SEvan Bacon  `);
6053b4c0b0SEvan Bacon});
6153b4c0b0SEvan Bacon
6253b4c0b0SEvan Baconit(
6353b4c0b0SEvan Bacon  'runs `npx expo export:web`',
6453b4c0b0SEvan Bacon  async () => {
6553b4c0b0SEvan Bacon    const projectRoot = await setupTestProjectAsync('basic-export-web', 'with-web');
6653b4c0b0SEvan Bacon    // `npx expo export:web`
6753b4c0b0SEvan Bacon    await execa('node', [bin, 'export:web'], {
6853b4c0b0SEvan Bacon      cwd: projectRoot,
6953b4c0b0SEvan Bacon    });
7053b4c0b0SEvan Bacon
7153b4c0b0SEvan Bacon    const outputDir = path.join(projectRoot, 'web-build');
7253b4c0b0SEvan Bacon    // List output files with sizes for snapshotting.
7353b4c0b0SEvan Bacon    // This is to make sure that any changes to the output are intentional.
7453b4c0b0SEvan Bacon    // Posix path formatting is used to make paths the same across OSes.
7553b4c0b0SEvan Bacon    const files = klawSync(outputDir)
7653b4c0b0SEvan Bacon      .map((entry) => {
7753b4c0b0SEvan Bacon        if (entry.path.includes('node_modules') || !entry.stats.isFile()) {
7853b4c0b0SEvan Bacon          return null;
7953b4c0b0SEvan Bacon        }
8053b4c0b0SEvan Bacon        return path.posix.relative(outputDir, entry.path);
8153b4c0b0SEvan Bacon      })
8253b4c0b0SEvan Bacon      .filter(Boolean);
8353b4c0b0SEvan Bacon
84dfe12d45SEvan Bacon    const assetsManifest = await JsonFile.readAsync(path.resolve(outputDir, 'asset-manifest.json'));
85dfe12d45SEvan Bacon    expect(assetsManifest.entrypoints).toEqual([
86425e7f7fSEvan Bacon      expect.stringMatching(/static\/js\/\d+\.[a-z\d]+\.js/),
87425e7f7fSEvan Bacon      expect.stringMatching(/static\/js\/main\.[a-z\d]+\.js/),
88dfe12d45SEvan Bacon    ]);
89dfe12d45SEvan Bacon
90dfe12d45SEvan Bacon    const knownFiles = [
91425e7f7fSEvan Bacon      ['main.js', expect.stringMatching(/static\/js\/main\.[a-z\d]+\.js/)],
92dfe12d45SEvan Bacon      ['index.html', '/index.html'],
93dfe12d45SEvan Bacon      ['manifest.json', '/manifest.json'],
94dfe12d45SEvan Bacon      ['serve.json', '/serve.json'],
95dfe12d45SEvan Bacon    ];
96dfe12d45SEvan Bacon
97e1bb5bdfSKudo Chien    assert(assetsManifest.files);
98425e7f7fSEvan Bacon    console.log(assetsManifest.files);
99dfe12d45SEvan Bacon    for (const [key, value] of knownFiles) {
100e1bb5bdfSKudo Chien      const files = assetsManifest.files as Record<string, string>;
101e1bb5bdfSKudo Chien      expect(files[key]).toEqual(value);
102e1bb5bdfSKudo Chien      delete files[key];
103dfe12d45SEvan Bacon    }
104dfe12d45SEvan Bacon
105e1bb5bdfSKudo Chien    for (const [key, value] of Object.entries(assetsManifest?.files ?? {})) {
106425e7f7fSEvan Bacon      expect(key).toMatch(/(static\/js\/)?(\d+|main)\.[a-z\d]+\.js(\.LICENSE\.txt|\.map)?/);
107425e7f7fSEvan Bacon      expect(value).toMatch(/(static\/js\/)?(\d+|main)\.[a-z\d]+\.js(\.LICENSE\.txt|\.map)?/);
108dfe12d45SEvan Bacon    }
109dfe12d45SEvan Bacon
11053b4c0b0SEvan Bacon    expect(await JsonFile.readAsync(path.resolve(outputDir, 'manifest.json'))).toEqual({
11153b4c0b0SEvan Bacon      display: 'standalone',
11253b4c0b0SEvan Bacon      lang: 'en',
11353b4c0b0SEvan Bacon      name: 'basic-export-web',
11453b4c0b0SEvan Bacon      prefer_related_applications: true,
11553b4c0b0SEvan Bacon      related_applications: [
11653b4c0b0SEvan Bacon        {
11753b4c0b0SEvan Bacon          id: 'com.example.minimal',
11853b4c0b0SEvan Bacon          platform: 'itunes',
11953b4c0b0SEvan Bacon        },
12053b4c0b0SEvan Bacon        {
12153b4c0b0SEvan Bacon          id: 'com.example.minimal',
12253b4c0b0SEvan Bacon          platform: 'play',
12353b4c0b0SEvan Bacon          url: 'http://play.google.com/store/apps/details?id=com.example.minimal',
12453b4c0b0SEvan Bacon        },
12553b4c0b0SEvan Bacon      ],
12653b4c0b0SEvan Bacon      short_name: 'basic-export-web',
12753b4c0b0SEvan Bacon      start_url: '/?utm_source=web_app_manifest',
12853b4c0b0SEvan Bacon    });
12953b4c0b0SEvan Bacon    expect(await JsonFile.readAsync(path.resolve(outputDir, 'serve.json'))).toEqual({
13053b4c0b0SEvan Bacon      headers: [
13153b4c0b0SEvan Bacon        {
13253b4c0b0SEvan Bacon          headers: [
13353b4c0b0SEvan Bacon            {
13453b4c0b0SEvan Bacon              key: 'Cache-Control',
13553b4c0b0SEvan Bacon              value: 'public, max-age=31536000, immutable',
13653b4c0b0SEvan Bacon            },
13753b4c0b0SEvan Bacon          ],
13853b4c0b0SEvan Bacon          source: 'static/**/*.js',
13953b4c0b0SEvan Bacon        },
14053b4c0b0SEvan Bacon      ],
14153b4c0b0SEvan Bacon    });
14253b4c0b0SEvan Bacon
14353b4c0b0SEvan Bacon    // If this changes then everything else probably changed as well.
14453b4c0b0SEvan Bacon    expect(files).toEqual([
14553b4c0b0SEvan Bacon      'asset-manifest.json',
14653b4c0b0SEvan Bacon      'index.html',
14753b4c0b0SEvan Bacon      'manifest.json',
14853b4c0b0SEvan Bacon      'serve.json',
149425e7f7fSEvan Bacon      expect.stringMatching(/static\/js\/\d+\.[a-z\d]+\.js/),
150425e7f7fSEvan Bacon      expect.stringMatching(/static\/js\/\d+\.[a-z\d]+\.js\.LICENSE\.txt/),
151425e7f7fSEvan Bacon      expect.stringMatching(/static\/js\/\d+\.[a-z\d]+\.js\.map/),
152425e7f7fSEvan Bacon      expect.stringMatching(/static\/js\/main\.[a-z\d]+\.js/),
153425e7f7fSEvan Bacon      expect.stringMatching(/static\/js\/main\.[a-z\d]+\.js\.map/),
15453b4c0b0SEvan Bacon    ]);
15553b4c0b0SEvan Bacon  },
15653b4c0b0SEvan Bacon  // Could take 45s depending on how fast npm installs
15753b4c0b0SEvan Bacon  120 * 1000
15853b4c0b0SEvan Bacon);
159