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