diff --git a/.gitignore b/.gitignore
index 1e702d229e3cfd..bc8e45b6aafa54 100644
--- a/.gitignore
+++ b/.gitignore
@@ -96,6 +96,7 @@ templates/**/android/app/build
templates/**/Pods/**
templates/**/Podfile.lock
templates/**/yarn.lock
+templates/**/*.tgz
# EYW template
/packages/expo-yarn-workspaces/workspace-template/yarn.lock
diff --git a/packages/@expo/cli/CHANGELOG.md b/packages/@expo/cli/CHANGELOG.md
index 4a926966b4f827..f4c29eb4f0add9 100644
--- a/packages/@expo/cli/CHANGELOG.md
+++ b/packages/@expo/cli/CHANGELOG.md
@@ -15,6 +15,7 @@
- Reduce install prompt. ([#21264](https://github.com/expo/expo/pull/21264) by [@EvanBacon](https://github.com/EvanBacon))
- Improve multi-target iOS scheme resolution for `expo run:ios`. ([#21240](https://github.com/expo/expo/pull/21240) by [@EvanBacon](https://github.com/EvanBacon))
- Added experimental react-devtools integration. ([#21462](https://github.com/expo/expo/pull/21462) by [@kudo](https://github.com/kudo))
+- Add experimental static rendering for Metro web in Expo Router. ([#21572](https://github.com/expo/expo/pull/21572) by [@EvanBacon](https://github.com/EvanBacon))
- Add experimental inspector proxy to handle more CDP requests. ([#21449](https://github.com/expo/expo/pull/21449) by [@byCedric](https://github.com/byCedric))
- Add inspector proxy workarounds for known issues with vscode debugger and Hermes CDP messages. ([#21560](https://github.com/expo/expo/pull/21560) by [@byCedric](https://github.com/byCedric))
diff --git a/packages/@expo/cli/e2e/__tests__/__snapshots__/export-test.ts.snap b/packages/@expo/cli/e2e/__tests__/__snapshots__/export-test.ts.snap
new file mode 100644
index 00000000000000..2647f1bdf0301a
--- /dev/null
+++ b/packages/@expo/cli/e2e/__tests__/__snapshots__/export-test.ts.snap
@@ -0,0 +1,261 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`server runs \`npx expo export -p web\` for static rendering 1`] = `
+"
About | Website"
+`;
diff --git a/packages/@expo/cli/e2e/__tests__/config-test.ts b/packages/@expo/cli/e2e/__tests__/config-test.ts
index b73e53241e2f38..bf7a0a64a2c35a 100644
--- a/packages/@expo/cli/e2e/__tests__/config-test.ts
+++ b/packages/@expo/cli/e2e/__tests__/config-test.ts
@@ -49,26 +49,30 @@ it('runs `npx expo config --help`', async () => {
`);
});
-it('runs `npx expo config --json`', async () => {
- const projectName = 'basic-config';
- const projectRoot = getRoot(projectName);
- // Create the project root aot
- await fs.mkdir(projectRoot, { recursive: true });
- // Create a fake package.json -- this is a terminal file that cannot be overwritten.
- await fs.writeFile(path.join(projectRoot, 'package.json'), '{ "version": "1.0.0" }');
- await fs.writeFile(path.join(projectRoot, 'app.json'), '{ "expo": { "name": "foobar" } }');
+it(
+ 'runs `npx expo config --json`',
+ async () => {
+ const projectName = 'basic-config';
+ const projectRoot = getRoot(projectName);
+ // Create the project root aot
+ await fs.mkdir(projectRoot, { recursive: true });
+ // Create a fake package.json -- this is a terminal file that cannot be overwritten.
+ await fs.writeFile(path.join(projectRoot, 'package.json'), '{ "version": "1.0.0" }');
+ await fs.writeFile(path.join(projectRoot, 'app.json'), '{ "expo": { "name": "foobar" } }');
- const results = await execute('config', projectName, '--json');
- // @ts-ignore
- const exp = JSON.parse(results.stdout);
+ const results = await execute('config', projectName, '--json');
+ // @ts-ignore
+ const exp = JSON.parse(results.stdout);
- expect(exp.name).toEqual('foobar');
- expect(exp.slug).toEqual('foobar');
- expect(exp.platforms).toStrictEqual([]);
- expect(exp.version).toBe('1.0.0');
- expect(exp._internal.dynamicConfigPath).toBe(null);
- expect(exp._internal.staticConfigPath).toMatch(/\/basic-config\/app\.json$/);
-});
+ expect(exp.name).toEqual('foobar');
+ expect(exp.slug).toEqual('foobar');
+ expect(exp.platforms).toStrictEqual([]);
+ expect(exp.version).toBe('1.0.0');
+ expect(exp._internal.dynamicConfigPath).toBe(null);
+ expect(exp._internal.staticConfigPath).toMatch(/\/basic-config\/app\.json$/);
+ }, // Could take 45s depending on how fast npm installs
+ 120 * 1000
+);
it('throws on invalid project root', async () => {
expect.assertions(1);
diff --git a/packages/@expo/cli/e2e/__tests__/export-test.ts b/packages/@expo/cli/e2e/__tests__/export-test.ts
index 768af7b9e8d0b7..0a1e6cd332db20 100644
--- a/packages/@expo/cli/e2e/__tests__/export-test.ts
+++ b/packages/@expo/cli/e2e/__tests__/export-test.ts
@@ -5,7 +5,14 @@ import fs from 'fs-extra';
import klawSync from 'klaw-sync';
import path from 'path';
-import { execute, projectRoot, getLoadedModulesAsync, setupTestProjectAsync, bin } from './utils';
+import {
+ execute,
+ projectRoot,
+ getLoadedModulesAsync,
+ setupTestProjectAsync,
+ bin,
+ ensurePortFreeAsync,
+} from './utils';
const originalForceColor = process.env.FORCE_COLOR;
const originalCI = process.env.CI;
@@ -15,6 +22,7 @@ beforeAll(async () => {
process.env.FORCE_COLOR = '0';
process.env.CI = '1';
process.env.EXPO_USE_PATH_ALIASES = '1';
+ delete process.env.EXPO_USE_STATIC;
});
afterAll(() => {
@@ -63,157 +71,242 @@ it('runs `npx expo export --help`', async () => {
`);
});
-it(
- 'runs `npx expo export`',
- async () => {
- const projectRoot = await setupTestProjectAsync('basic-export', 'with-assets');
- // `npx expo export`
- await execa('node', [bin, 'export', '--dump-sourcemap', '--dump-assetmap'], {
- cwd: projectRoot,
- });
-
- const outputDir = path.join(projectRoot, 'dist');
- // List output files with sizes for snapshotting.
- // This is to make sure that any changes to the output are intentional.
- // Posix path formatting is used to make paths the same across OSes.
- const files = klawSync(outputDir)
- .map((entry) => {
- if (entry.path.includes('node_modules') || !entry.stats.isFile()) {
- return null;
- }
- return path.posix.relative(outputDir, entry.path);
- })
- .filter(Boolean);
-
- const metadata = await JsonFile.readAsync(path.resolve(outputDir, 'metadata.json'));
-
- expect(metadata).toEqual({
- bundler: 'metro',
- fileMetadata: {
- android: {
- assets: [
- {
- ext: 'png',
- path: 'assets/fb960eb5e4eb49ec8786c7f6c4a57ce2',
- },
- {
- ext: 'png',
- path: 'assets/9ce7db807e4147e00df372d053c154c2',
- },
- {
- ext: 'ttf',
- path: 'assets/3858f62230ac3c915f300c664312c63f',
- },
- ],
- bundle: expect.stringMatching(/bundles\/android-.*\.js/),
+describe('server', () => {
+ beforeEach(() => ensurePortFreeAsync(19000));
+ it(
+ 'runs `npx expo export`',
+ async () => {
+ const projectRoot = await setupTestProjectAsync('basic-export', 'with-assets');
+ // `npx expo export`
+ await execa('node', [bin, 'export', '--dump-sourcemap', '--dump-assetmap'], {
+ cwd: projectRoot,
+ });
+
+ const outputDir = path.join(projectRoot, 'dist');
+ // List output files with sizes for snapshotting.
+ // This is to make sure that any changes to the output are intentional.
+ // Posix path formatting is used to make paths the same across OSes.
+ const files = klawSync(outputDir)
+ .map((entry) => {
+ if (entry.path.includes('node_modules') || !entry.stats.isFile()) {
+ return null;
+ }
+ return path.posix.relative(outputDir, entry.path);
+ })
+ .filter(Boolean);
+
+ const metadata = await JsonFile.readAsync(path.resolve(outputDir, 'metadata.json'));
+
+ expect(metadata).toEqual({
+ bundler: 'metro',
+ fileMetadata: {
+ android: {
+ assets: [
+ {
+ ext: 'png',
+ path: 'assets/fb960eb5e4eb49ec8786c7f6c4a57ce2',
+ },
+ {
+ ext: 'png',
+ path: 'assets/9ce7db807e4147e00df372d053c154c2',
+ },
+ {
+ ext: 'ttf',
+ path: 'assets/3858f62230ac3c915f300c664312c63f',
+ },
+ ],
+ bundle: expect.stringMatching(/bundles\/android-.*\.js/),
+ },
+ ios: {
+ assets: [
+ {
+ ext: 'png',
+ path: 'assets/fb960eb5e4eb49ec8786c7f6c4a57ce2',
+ },
+ {
+ ext: 'png',
+ path: 'assets/9ce7db807e4147e00df372d053c154c2',
+ },
+ {
+ ext: 'ttf',
+ path: 'assets/2f334f6c7ca5b2a504bdf8acdee104f3',
+ },
+ ],
+ bundle: expect.stringMatching(/bundles\/ios-.*\.js/),
+ },
+ web: {
+ assets: [
+ {
+ ext: 'png',
+ path: 'assets/fb960eb5e4eb49ec8786c7f6c4a57ce2',
+ },
+ {
+ ext: 'png',
+ path: 'assets/9ce7db807e4147e00df372d053c154c2',
+ },
+ {
+ ext: 'ttf',
+ path: 'assets/3858f62230ac3c915f300c664312c63f',
+ },
+ ],
+ bundle: expect.stringMatching(/bundles\/web-.*\.js/),
+ },
},
- ios: {
- assets: [
- {
- ext: 'png',
- path: 'assets/fb960eb5e4eb49ec8786c7f6c4a57ce2',
- },
- {
- ext: 'png',
- path: 'assets/9ce7db807e4147e00df372d053c154c2',
- },
- {
- ext: 'ttf',
- path: 'assets/2f334f6c7ca5b2a504bdf8acdee104f3',
- },
- ],
- bundle: expect.stringMatching(/bundles\/ios-.*\.js/),
+ version: 0,
+ });
+
+ const assetmap = await JsonFile.readAsync(path.resolve(outputDir, 'assetmap.json'));
+ expect(assetmap).toEqual({
+ '2f334f6c7ca5b2a504bdf8acdee104f3': {
+ __packager_asset: true,
+ fileHashes: ['2f334f6c7ca5b2a504bdf8acdee104f3'],
+ fileSystemLocation: expect.stringMatching(/\/.*\/basic-export\/assets/),
+ files: [expect.stringMatching(/\/.*\/basic-export\/assets\/font\.ios\.ttf/)],
+ hash: '2f334f6c7ca5b2a504bdf8acdee104f3',
+ httpServerLocation: '/assets/assets',
+ name: 'font',
+ scales: [1],
+ type: 'ttf',
},
- web: {
- assets: [
- {
- ext: 'png',
- path: 'assets/fb960eb5e4eb49ec8786c7f6c4a57ce2',
- },
- {
- ext: 'png',
- path: 'assets/9ce7db807e4147e00df372d053c154c2',
- },
- {
- ext: 'ttf',
- path: 'assets/3858f62230ac3c915f300c664312c63f',
- },
+
+ '3858f62230ac3c915f300c664312c63f': {
+ __packager_asset: true,
+ fileHashes: ['3858f62230ac3c915f300c664312c63f'],
+ fileSystemLocation: expect.stringMatching(/\/.*\/basic-export\/assets/),
+ files: [expect.stringMatching(/\/.*\/basic-export\/assets\/font\.ttf/)],
+ hash: '3858f62230ac3c915f300c664312c63f',
+ httpServerLocation: '/assets/assets',
+ name: 'font',
+ scales: [1],
+ type: 'ttf',
+ },
+ d48d481475a80809fcf9253a765193d1: {
+ __packager_asset: true,
+ fileHashes: ['fb960eb5e4eb49ec8786c7f6c4a57ce2', '9ce7db807e4147e00df372d053c154c2'],
+ fileSystemLocation: expect.stringMatching(/\/.*\/basic-export\/assets/),
+ files: [
+ expect.stringMatching(/\/.*\/basic-export\/assets\/icon\.png/),
+ expect.stringMatching(/\/.*\/basic-export\/assets\/icon@2x\.png/),
],
- bundle: expect.stringMatching(/bundles\/web-.*\.js/),
+ hash: 'd48d481475a80809fcf9253a765193d1',
+ height: 1,
+ httpServerLocation: '/assets/assets',
+ name: 'icon',
+ scales: [1, 2],
+ type: 'png',
+ width: 1,
+ },
+ });
+
+ // If this changes then everything else probably changed as well.
+ expect(files).toEqual([
+ 'assetmap.json',
+ 'assets/2f334f6c7ca5b2a504bdf8acdee104f3',
+ 'assets/3858f62230ac3c915f300c664312c63f',
+ 'assets/9ce7db807e4147e00df372d053c154c2',
+ 'assets/assets/font.ttf',
+ 'assets/assets/icon.png',
+ 'assets/assets/icon@2x.png',
+
+ 'assets/fb960eb5e4eb49ec8786c7f6c4a57ce2',
+ expect.stringMatching(/bundles\/android-[\w\d]+\.js/),
+ expect.stringMatching(/bundles\/android-[\w\d]+\.map/),
+ expect.stringMatching(/bundles\/ios-[\w\d]+\.js/),
+ expect.stringMatching(/bundles\/ios-[\w\d]+\.map/),
+ expect.stringMatching(/bundles\/web-[\w\d]+\.js/),
+ expect.stringMatching(/bundles\/web-[\w\d]+\.map/),
+ 'debug.html',
+ 'drawable-mdpi/assets_icon.png',
+ 'drawable-xhdpi/assets_icon.png',
+ 'favicon.ico',
+ 'index.html',
+ 'metadata.json',
+ 'raw/assets_font.ttf',
+ ]);
+ },
+ // Could take 45s depending on how fast npm installs
+ 120 * 1000
+ );
+
+ it(
+ 'runs `npx expo export -p web` for static rendering',
+ async () => {
+ const projectRoot = await setupTestProjectAsync('export-router', 'with-router', '48.0.0');
+ await execa('node', [bin, 'export', '-p', 'web'], {
+ cwd: projectRoot,
+ env: {
+ EXPO_USE_STATIC: '1',
},
- },
- version: 0,
- });
-
- const assetmap = await JsonFile.readAsync(path.resolve(outputDir, 'assetmap.json'));
- expect(assetmap).toEqual({
- '2f334f6c7ca5b2a504bdf8acdee104f3': {
- __packager_asset: true,
- fileHashes: ['2f334f6c7ca5b2a504bdf8acdee104f3'],
- fileSystemLocation: expect.stringMatching(/\/.*\/basic-export\/assets/),
- files: [expect.stringMatching(/\/.*\/basic-export\/assets\/font\.ios\.ttf/)],
- hash: '2f334f6c7ca5b2a504bdf8acdee104f3',
- httpServerLocation: '/assets/assets',
- name: 'font',
- scales: [1],
- type: 'ttf',
- },
-
- '3858f62230ac3c915f300c664312c63f': {
- __packager_asset: true,
- fileHashes: ['3858f62230ac3c915f300c664312c63f'],
- fileSystemLocation: expect.stringMatching(/\/.*\/basic-export\/assets/),
- files: [expect.stringMatching(/\/.*\/basic-export\/assets\/font\.ttf/)],
- hash: '3858f62230ac3c915f300c664312c63f',
- httpServerLocation: '/assets/assets',
- name: 'font',
- scales: [1],
- type: 'ttf',
- },
- d48d481475a80809fcf9253a765193d1: {
- __packager_asset: true,
- fileHashes: ['fb960eb5e4eb49ec8786c7f6c4a57ce2', '9ce7db807e4147e00df372d053c154c2'],
- fileSystemLocation: expect.stringMatching(/\/.*\/basic-export\/assets/),
- files: [
- expect.stringMatching(/\/.*\/basic-export\/assets\/icon\.png/),
- expect.stringMatching(/\/.*\/basic-export\/assets\/icon@2x\.png/),
- ],
- hash: 'd48d481475a80809fcf9253a765193d1',
- height: 1,
- httpServerLocation: '/assets/assets',
- name: 'icon',
- scales: [1, 2],
- type: 'png',
- width: 1,
- },
- });
-
- // If this changes then everything else probably changed as well.
- expect(files).toEqual([
- 'assetmap.json',
- 'assets/2f334f6c7ca5b2a504bdf8acdee104f3',
- 'assets/3858f62230ac3c915f300c664312c63f',
- 'assets/9ce7db807e4147e00df372d053c154c2',
- 'assets/assets/font.ttf',
- 'assets/assets/icon.png',
- 'assets/assets/icon@2x.png',
-
- 'assets/fb960eb5e4eb49ec8786c7f6c4a57ce2',
- expect.stringMatching(/bundles\/android-[\w\d]+\.js/),
- expect.stringMatching(/bundles\/android-[\w\d]+\.map/),
- expect.stringMatching(/bundles\/ios-[\w\d]+\.js/),
- expect.stringMatching(/bundles\/ios-[\w\d]+\.map/),
- expect.stringMatching(/bundles\/web-[\w\d]+\.js/),
- expect.stringMatching(/bundles\/web-[\w\d]+\.map/),
- 'debug.html',
- 'drawable-mdpi/assets_icon.png',
- 'drawable-xhdpi/assets_icon.png',
- 'favicon.ico',
- 'index.html',
- 'metadata.json',
- 'raw/assets_font.ttf',
- ]);
- },
- // Could take 45s depending on how fast npm installs
- 120 * 1000
-);
+ });
+
+ const outputDir = path.join(projectRoot, 'dist');
+ // List output files with sizes for snapshotting.
+ // This is to make sure that any changes to the output are intentional.
+ // Posix path formatting is used to make paths the same across OSes.
+ const files = klawSync(outputDir)
+ .map((entry) => {
+ if (entry.path.includes('node_modules') || !entry.stats.isFile()) {
+ return null;
+ }
+ return path.posix.relative(outputDir, entry.path);
+ })
+ .filter(Boolean);
+
+ const metadata = await JsonFile.readAsync(path.resolve(outputDir, 'metadata.json'));
+
+ expect(metadata).toEqual({
+ bundler: 'metro',
+ fileMetadata: {
+ web: {
+ assets: expect.anything(),
+ bundle: expect.stringMatching(/bundles\/web-.*\.js/),
+ },
+ },
+ version: 0,
+ });
+
+ // If this changes then everything else probably changed as well.
+ expect(files).toEqual([
+ '[...404].html',
+ '_sitemap.html',
+ 'about.html',
+ 'assets/35ba0eaec5a4f5ed12ca16fabeae451d',
+ 'assets/369745d4a4a6fa62fa0ed495f89aa964',
+ 'assets/4f355ba1efca4b9c0e7a6271af047f61',
+ 'assets/5223c8d9b0d08b82a5670fb5f71faf78',
+ 'assets/52dec48a970c0a4eed4119cd1252ab09',
+ 'assets/5b50965d3dfbc518fe50ce36c314a6ec',
+ 'assets/817aca47ff3cea63020753d336e628a4',
+ 'assets/b2de8e638d92e0f719fa92fa4085e02a',
+ 'assets/cbbeac683d803ac27cefb817787d2bfa',
+ 'assets/e62addcde857ebdb7342e6b9f1095e97',
+ expect.stringMatching(/bundles\/web-[\w\d]+\.js/),
+ 'favicon.ico',
+ 'index.html',
+ 'metadata.json',
+ ]);
+
+ const about = await fs.readFile(path.join(outputDir, 'about.html'), 'utf8');
+
+ // Route-specific head tags
+ expect(about).toContain(`About | Website`);
+
+ // Nested head tags from layout route
+ expect(about).toContain('');
+
+ // Root element
+ expect(about).toContain('');
+ // Content of the page
+ expect(about).toContain('data-testid="content">About
');
+
+ // `).join('') + '