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 {
9  execute,
10  projectRoot,
11  getLoadedModulesAsync,
12  setupTestProjectAsync,
13  bin,
14  ensurePortFreeAsync,
15} from './utils';
16
17const originalForceColor = process.env.FORCE_COLOR;
18const originalCI = process.env.CI;
19
20beforeAll(async () => {
21  await fs.mkdir(projectRoot, { recursive: true });
22  process.env.FORCE_COLOR = '0';
23  process.env.CI = '1';
24  process.env._EXPO_E2E_USE_PATH_ALIASES = '1';
25  delete process.env.EXPO_WEB_OUTPUT_MODE;
26});
27
28afterAll(() => {
29  process.env.FORCE_COLOR = originalForceColor;
30  process.env.CI = originalCI;
31  delete process.env._EXPO_E2E_USE_PATH_ALIASES;
32});
33
34it('loads expected modules by default', async () => {
35  const modules = await getLoadedModulesAsync(`require('../../build/src/export').expoExport`);
36  expect(modules).toStrictEqual([
37    '../node_modules/ansi-styles/index.js',
38    '../node_modules/arg/index.js',
39    '../node_modules/chalk/source/index.js',
40    '../node_modules/chalk/source/util.js',
41    '../node_modules/has-flag/index.js',
42    '../node_modules/supports-color/index.js',
43    '@expo/cli/build/src/export/index.js',
44    '@expo/cli/build/src/log.js',
45    '@expo/cli/build/src/utils/args.js',
46    '@expo/cli/build/src/utils/errors.js',
47  ]);
48});
49
50it('runs `npx expo export --help`', async () => {
51  const results = await execute('export', '--help');
52  expect(results.stdout).toMatchInlineSnapshot(`
53    "
54      Info
55        Export the static files of the app for hosting it on a web server
56
57      Usage
58        $ npx expo export <dir>
59
60      Options
61        <dir>                      Directory of the Expo project. Default: Current working directory
62        --dev                      Configure static files for developing locally using a non-https server
63        --output-dir <dir>         The directory to export the static files to. Default: dist
64        --max-workers <number>     Maximum number of tasks to allow the bundler to spawn
65        --dump-assetmap            Dump the asset map for further processing
66        --dump-sourcemap           Dump the source map for debugging the JS bundle
67        -p, --platform <platform>  Options: android, ios, web, all. Default: all
68        -c, --clear                Clear the bundler cache
69        -h, --help                 Usage info
70    "
71  `);
72});
73
74describe('server', () => {
75  beforeEach(() => ensurePortFreeAsync(19000));
76  it(
77    'runs `npx expo export`',
78    async () => {
79      const projectRoot = await setupTestProjectAsync('basic-export', 'with-assets');
80      // `npx expo export`
81      await execa('node', [bin, 'export', '--dump-sourcemap', '--dump-assetmap'], {
82        cwd: projectRoot,
83      });
84
85      const outputDir = path.join(projectRoot, 'dist');
86      // List output files with sizes for snapshotting.
87      // This is to make sure that any changes to the output are intentional.
88      // Posix path formatting is used to make paths the same across OSes.
89      const files = klawSync(outputDir)
90        .map((entry) => {
91          if (entry.path.includes('node_modules') || !entry.stats.isFile()) {
92            return null;
93          }
94          return path.posix.relative(outputDir, entry.path);
95        })
96        .filter(Boolean);
97
98      const metadata = await JsonFile.readAsync(path.resolve(outputDir, 'metadata.json'));
99
100      expect(metadata).toEqual({
101        bundler: 'metro',
102        fileMetadata: {
103          android: {
104            assets: [
105              {
106                ext: 'png',
107                path: 'assets/fb960eb5e4eb49ec8786c7f6c4a57ce2',
108              },
109              {
110                ext: 'png',
111                path: 'assets/9ce7db807e4147e00df372d053c154c2',
112              },
113              {
114                ext: 'ttf',
115                path: 'assets/3858f62230ac3c915f300c664312c63f',
116              },
117            ],
118            bundle: expect.stringMatching(/bundles\/android-.*\.js/),
119          },
120          ios: {
121            assets: [
122              {
123                ext: 'png',
124                path: 'assets/fb960eb5e4eb49ec8786c7f6c4a57ce2',
125              },
126              {
127                ext: 'png',
128                path: 'assets/9ce7db807e4147e00df372d053c154c2',
129              },
130              {
131                ext: 'ttf',
132                path: 'assets/2f334f6c7ca5b2a504bdf8acdee104f3',
133              },
134            ],
135            bundle: expect.stringMatching(/bundles\/ios-.*\.js/),
136          },
137          web: {
138            assets: [
139              {
140                ext: 'png',
141                path: 'assets/fb960eb5e4eb49ec8786c7f6c4a57ce2',
142              },
143              {
144                ext: 'png',
145                path: 'assets/9ce7db807e4147e00df372d053c154c2',
146              },
147              {
148                ext: 'ttf',
149                path: 'assets/3858f62230ac3c915f300c664312c63f',
150              },
151            ],
152            bundle: expect.stringMatching(/bundles\/web-.*\.js/),
153          },
154        },
155        version: 0,
156      });
157
158      const assetmap = await JsonFile.readAsync(path.resolve(outputDir, 'assetmap.json'));
159      expect(assetmap).toEqual({
160        '2f334f6c7ca5b2a504bdf8acdee104f3': {
161          __packager_asset: true,
162          fileHashes: ['2f334f6c7ca5b2a504bdf8acdee104f3'],
163          fileSystemLocation: expect.stringMatching(/\/.*\/basic-export\/assets/),
164          files: [expect.stringMatching(/\/.*\/basic-export\/assets\/font\.ios\.ttf/)],
165          hash: '2f334f6c7ca5b2a504bdf8acdee104f3',
166          httpServerLocation: '/assets/assets',
167          name: 'font',
168          scales: [1],
169          type: 'ttf',
170        },
171
172        '3858f62230ac3c915f300c664312c63f': {
173          __packager_asset: true,
174          fileHashes: ['3858f62230ac3c915f300c664312c63f'],
175          fileSystemLocation: expect.stringMatching(/\/.*\/basic-export\/assets/),
176          files: [expect.stringMatching(/\/.*\/basic-export\/assets\/font\.ttf/)],
177          hash: '3858f62230ac3c915f300c664312c63f',
178          httpServerLocation: '/assets/assets',
179          name: 'font',
180          scales: [1],
181          type: 'ttf',
182        },
183        d48d481475a80809fcf9253a765193d1: {
184          __packager_asset: true,
185          fileHashes: ['fb960eb5e4eb49ec8786c7f6c4a57ce2', '9ce7db807e4147e00df372d053c154c2'],
186          fileSystemLocation: expect.stringMatching(/\/.*\/basic-export\/assets/),
187          files: [
188            expect.stringMatching(/\/.*\/basic-export\/assets\/icon\.png/),
189            expect.stringMatching(/\/.*\/basic-export\/assets\/icon@2x\.png/),
190          ],
191          hash: 'd48d481475a80809fcf9253a765193d1',
192          height: 1,
193          httpServerLocation: '/assets/assets',
194          name: 'icon',
195          scales: [1, 2],
196          type: 'png',
197          width: 1,
198        },
199      });
200
201      // If this changes then everything else probably changed as well.
202      expect(files).toEqual([
203        'assetmap.json',
204        'assets/2f334f6c7ca5b2a504bdf8acdee104f3',
205        'assets/3858f62230ac3c915f300c664312c63f',
206        'assets/9ce7db807e4147e00df372d053c154c2',
207        'assets/assets/font.ttf',
208        'assets/assets/icon.png',
209        'assets/assets/icon@2x.png',
210
211        'assets/fb960eb5e4eb49ec8786c7f6c4a57ce2',
212        expect.stringMatching(/bundles\/android-[\w\d]+\.js/),
213        expect.stringMatching(/bundles\/android-[\w\d]+\.map/),
214        expect.stringMatching(/bundles\/ios-[\w\d]+\.js/),
215        expect.stringMatching(/bundles\/ios-[\w\d]+\.map/),
216        expect.stringMatching(/bundles\/web-[\w\d]+\.js/),
217        expect.stringMatching(/bundles\/web-[\w\d]+\.map/),
218        'debug.html',
219        'drawable-mdpi/assets_icon.png',
220        'drawable-xhdpi/assets_icon.png',
221        'favicon.ico',
222        'index.html',
223        'metadata.json',
224        'raw/assets_font.ttf',
225      ]);
226    },
227    // Could take 45s depending on how fast npm installs
228    120 * 1000
229  );
230
231  xit(
232    'runs `npx expo export -p web` for static rendering',
233    async () => {
234      const projectRoot = await setupTestProjectAsync('export-router', 'with-router', '48.0.0');
235      await execa('node', [bin, 'export', '-p', 'web'], {
236        cwd: projectRoot,
237        env: {
238          EXPO_WEB_OUTPUT_MODE: 'static',
239        },
240      });
241
242      const outputDir = path.join(projectRoot, 'dist');
243      // List output files with sizes for snapshotting.
244      // This is to make sure that any changes to the output are intentional.
245      // Posix path formatting is used to make paths the same across OSes.
246      const files = klawSync(outputDir)
247        .map((entry) => {
248          if (entry.path.includes('node_modules') || !entry.stats.isFile()) {
249            return null;
250          }
251          return path.posix.relative(outputDir, entry.path);
252        })
253        .filter(Boolean);
254
255      const metadata = await JsonFile.readAsync(path.resolve(outputDir, 'metadata.json'));
256
257      expect(metadata).toEqual({
258        bundler: 'metro',
259        fileMetadata: {
260          web: {
261            assets: expect.anything(),
262            bundle: expect.stringMatching(/bundles\/web-.*\.js/),
263          },
264        },
265        version: 0,
266      });
267
268      // If this changes then everything else probably changed as well.
269      expect(files).toEqual([
270        '[...404].html',
271        '_sitemap.html',
272        'about.html',
273        'assets/35ba0eaec5a4f5ed12ca16fabeae451d',
274        'assets/369745d4a4a6fa62fa0ed495f89aa964',
275        'assets/4f355ba1efca4b9c0e7a6271af047f61',
276        'assets/5223c8d9b0d08b82a5670fb5f71faf78',
277        'assets/52dec48a970c0a4eed4119cd1252ab09',
278        'assets/5b50965d3dfbc518fe50ce36c314a6ec',
279        'assets/817aca47ff3cea63020753d336e628a4',
280        'assets/b2de8e638d92e0f719fa92fa4085e02a',
281        'assets/cbbeac683d803ac27cefb817787d2bfa',
282        'assets/e62addcde857ebdb7342e6b9f1095e97',
283        expect.stringMatching(/bundles\/web-[\w\d]+\.js/),
284        'favicon.ico',
285        'index.html',
286        'metadata.json',
287      ]);
288
289      const about = await fs.readFile(path.join(outputDir, 'about.html'), 'utf8');
290
291      // Route-specific head tags
292      expect(about).toContain(`<title data-rh="true">About | Website</title>`);
293
294      // Nested head tags from layout route
295      expect(about).toContain('<meta data-rh="true" name="fake" content="bar"/>');
296
297      // Root element
298      expect(about).toContain('<div id="root">');
299      // Content of the page
300      expect(about).toContain('data-testid="content">About</div>');
301
302      // <script src="/bundles/web-c91ecb663cfce9b9e90e28d253e72e0a.js" defer>
303      const sanitizedAbout = about.replace(
304        /<script src="\/bundles\/.*" defer>/g,
305        '<script src="/bundles/[mock].js" defer>'
306      );
307      expect(sanitizedAbout).toMatchSnapshot();
308    },
309    // Could take 45s depending on how fast npm installs
310    240 * 1000
311  );
312});
313