1/* eslint-env jest */
2import JsonFile from '@expo/json-file';
3import execa from 'execa';
4import fs from 'fs-extra';
5import klawSync from 'klaw-sync';
6import path from 'path';
7
8import { execute, projectRoot, getLoadedModulesAsync, setupTestProjectAsync, bin } from './utils';
9
10const originalForceColor = process.env.FORCE_COLOR;
11const originalCI = process.env.CI;
12
13beforeAll(async () => {
14  await fs.mkdir(projectRoot, { recursive: true });
15  process.env.FORCE_COLOR = '0';
16  process.env.CI = '1';
17});
18
19afterAll(() => {
20  process.env.FORCE_COLOR = originalForceColor;
21  process.env.CI = originalCI;
22});
23
24it('loads expected modules by default', async () => {
25  const modules = await getLoadedModulesAsync(
26    `require('../../build/src/export/web').expoExportWeb`
27  );
28  expect(modules).toStrictEqual([
29    '../node_modules/ansi-styles/index.js',
30    '../node_modules/arg/index.js',
31    '../node_modules/chalk/source/index.js',
32    '../node_modules/chalk/source/util.js',
33    '../node_modules/has-flag/index.js',
34    '../node_modules/supports-color/index.js',
35    '@expo/cli/build/src/export/web/index.js',
36    '@expo/cli/build/src/log.js',
37    '@expo/cli/build/src/utils/args.js',
38    '@expo/cli/build/src/utils/errors.js',
39  ]);
40});
41
42it('runs `npx expo export:web --help`', async () => {
43  const results = await execute('export:web', '--help');
44  expect(results.stdout).toMatchInlineSnapshot(`
45    "
46      Info
47        Export the static files of the web app for hosting on a web server
48
49      Usage
50        $ npx expo export:web <dir>
51
52      Options
53        <dir>                         Directory of the Expo project. Default: Current working directory
54        --dev                         Bundle in development mode
55        -c, --clear                   Clear the bundler cache
56        -h, --help                    Usage info
57    "
58  `);
59});
60
61it(
62  'runs `npx expo export:web`',
63  async () => {
64    const projectRoot = await setupTestProjectAsync('basic-export-web', 'with-web');
65    // `npx expo export:web`
66    await execa('node', [bin, 'export:web'], {
67      cwd: projectRoot,
68    });
69
70    const outputDir = path.join(projectRoot, 'web-build');
71    // List output files with sizes for snapshotting.
72    // This is to make sure that any changes to the output are intentional.
73    // Posix path formatting is used to make paths the same across OSes.
74    const files = klawSync(outputDir)
75      .map((entry) => {
76        if (entry.path.includes('node_modules') || !entry.stats.isFile()) {
77          return null;
78        }
79        return path.posix.relative(outputDir, entry.path);
80      })
81      .filter(Boolean);
82
83    const assetsManifest = await JsonFile.readAsync(path.resolve(outputDir, 'asset-manifest.json'));
84    expect(assetsManifest.entrypoints).toEqual([
85      expect.stringMatching(/static\/js\/runtime~app\.[a-z\d]+\.js/),
86      expect.stringMatching(/static\/js\/\d\.[a-z\d]+\.chunk\.js/),
87      expect.stringMatching(/static\/js\/app\.[a-z\d]+\.chunk\.js/),
88    ]);
89
90    const knownFiles = [
91      ['app.js', expect.stringMatching(/static\/js\/app\.[a-z\d]+\.chunk\.js/)],
92      ['app.js.map', expect.stringMatching(/static\/js\/app\.[a-z\d]+\.chunk\.js\.map/)],
93      ['index.html', '/index.html'],
94      ['manifest.json', '/manifest.json'],
95      ['serve.json', '/serve.json'],
96      ['runtime~app.js', expect.stringMatching(/static\/js\/runtime~app\.[a-z\d]+\.js/)],
97      ['runtime~app.js.map', expect.stringMatching(/static\/js\/runtime~app\.[a-z\d]+\.js\.map/)],
98    ];
99
100    for (const [key, value] of knownFiles) {
101      expect(assetsManifest.files[key]).toEqual(value);
102      delete assetsManifest.files[key];
103    }
104
105    for (const [key, value] of Object.entries(assetsManifest.files)) {
106      expect(key).toMatch(/static\/js\/\d\.[a-z\d]+\.chunk\.js(\.LICENSE\.txt|\.map)?/);
107      expect(value).toMatch(/static\/js\/\d\.[a-z\d]+\.chunk\.js(\.LICENSE\.txt|\.map)?/);
108    }
109
110    expect(await JsonFile.readAsync(path.resolve(outputDir, 'manifest.json'))).toEqual({
111      display: 'standalone',
112      lang: 'en',
113      name: 'basic-export-web',
114      prefer_related_applications: true,
115      related_applications: [
116        {
117          id: 'com.example.minimal',
118          platform: 'itunes',
119        },
120        {
121          id: 'com.example.minimal',
122          platform: 'play',
123          url: 'http://play.google.com/store/apps/details?id=com.example.minimal',
124        },
125      ],
126      short_name: 'basic-export-web',
127      start_url: '/?utm_source=web_app_manifest',
128    });
129    expect(await JsonFile.readAsync(path.resolve(outputDir, 'serve.json'))).toEqual({
130      headers: [
131        {
132          headers: [
133            {
134              key: 'Cache-Control',
135              value: 'public, max-age=31536000, immutable',
136            },
137          ],
138          source: 'static/**/*.js',
139        },
140      ],
141    });
142
143    // If this changes then everything else probably changed as well.
144    expect(files).toEqual([
145      'asset-manifest.json',
146      'index.html',
147      'manifest.json',
148      'serve.json',
149      expect.stringMatching(/static\/js\/\d\.[a-z\d]+\.chunk\.js/),
150      expect.stringMatching(/static\/js\/\d\.[a-z\d]+\.chunk\.js\.LICENSE\.txt/),
151      expect.stringMatching(/static\/js\/\d\.[a-z\d]+\.chunk\.js\.map/),
152      expect.stringMatching(/static\/js\/app\.[a-z\d]+\.chunk\.js/),
153      expect.stringMatching(/static\/js\/app\.[a-z\d]+\.chunk\.js\.map/),
154      expect.stringMatching(/static\/js\/runtime~app\.[a-z\d]+\.js/),
155      expect.stringMatching(/static\/js\/runtime~app\.[a-z\d]+\.js\.map/),
156    ]);
157  },
158  // Could take 45s depending on how fast npm installs
159  120 * 1000
160);
161