diff --git a/source/main.ts b/source/main.ts index 332b19c7..230b389c 100755 --- a/source/main.ts +++ b/source/main.ts @@ -76,14 +76,21 @@ if (configError || !config) { // If the user wants all the URLs rewritten to `/index.html`, make it happen. if (args['--single']) { + const singleFile = args['--single-file']; + let destination = '/index.html'; + + if (singleFile && singleFile.length > 0) { + destination = singleFile.startsWith('/') ? singleFile : `/${singleFile}`; + } + const { rewrites } = config; const existingRewrites = Array.isArray(rewrites) ? rewrites : []; // Ensure this is the first rewrite rule so it gets priority. config.rewrites = [ { - source: '**', - destination: '/index.html', + source: '/**/:path([^/.]+)', + destination, }, ...existingRewrites, ]; diff --git a/source/types.ts b/source/types.ts index 9294d7c3..d1565257 100644 --- a/source/types.ts +++ b/source/types.ts @@ -73,6 +73,7 @@ export declare interface Options { '--version': boolean; '--listen': ParsedEndpoint[]; '--single': boolean; + '--single-file': string; '--debug': boolean; '--config': Path; '--no-request-logging': boolean; diff --git a/source/utilities/cli.ts b/source/utilities/cli.ts index 726c5268..7c3eb9e0 100644 --- a/source/utilities/cli.ts +++ b/source/utilities/cli.ts @@ -40,6 +40,8 @@ const helpText = chalkTemplate` -s, --single Rewrite all not-found requests to \`index.html\` + --single-file Rewrite all not-found requests to custom page (default: index.html) + -d, --debug Show debugging information -c, --config Specify custom path to \`serve.json\` @@ -148,6 +150,7 @@ const options = { '--version': Boolean, '--listen': [parseEndpoint] as [typeof parseEndpoint], '--single': Boolean, + '--single-file': String, '--debug': Boolean, '--config': String, '--no-clipboard': Boolean, diff --git a/tests/__fixtures__/server/app/bundle.js b/tests/__fixtures__/server/app/bundle.js new file mode 100644 index 00000000..a1983a84 --- /dev/null +++ b/tests/__fixtures__/server/app/bundle.js @@ -0,0 +1 @@ +console.log('bundle'); diff --git a/tests/__snapshots__/cli.test.ts.snap b/tests/__snapshots__/cli.test.ts.snap index 9cddf0e0..a470d5f6 100644 --- a/tests/__snapshots__/cli.test.ts.snap +++ b/tests/__snapshots__/cli.test.ts.snap @@ -29,6 +29,8 @@ exports[`utilities/cli > render help text 1`] = ` -s, --single Rewrite all not-found requests to \`index.html\` + --single-file Rewrite all not-found requests to custom page (default: index.html) + -d, --debug Show debugging information -c, --config Specify custom path to \`serve.json\` diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 21ba189a..aa78200a 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -9,6 +9,7 @@ import { getHelpText, parseEndpoint, checkForUpdates, + parseArguments, } from '../source/utilities/cli.js'; import { logger } from '../source/utilities/logger.js'; import { ParsedEndpoint } from '../source/types.js'; @@ -42,6 +43,22 @@ describe('utilities/cli', () => { // message, then make sure to run `vitest` with the `--update-snapshot` flag. test('render help text', () => expect(getHelpText()).toMatchSnapshot()); + // Make sure we can set a custom fallback file when using single-page mode. + test('parse single-file option', () => { + const originalArguments = process.argv; + + try { + process.argv = ['node', 'test', '-s', '--single-file', '_shell.html']; + + const args = parseArguments(); + + expect(args['--single']).toBe(true); + expect(args['--single-file']).toBe('_shell.html'); + } finally { + process.argv = originalArguments; + } + }); + // Make sure the `parseEndpoint` function parses valid endpoints correctly. test.each(validEndpoints)( 'parse %s as endpoint', diff --git a/tests/server.test.ts b/tests/server.test.ts index 3656bd31..e85af5db 100644 --- a/tests/server.test.ts +++ b/tests/server.test.ts @@ -97,4 +97,34 @@ describe('utilities/server', () => { expect(consoleSpy).not.toHaveBeenCalled(); }); + + test('rewrite extensionless routes to index when single-page rewrites are set', async () => { + const configWithSingle = { + ...config, + rewrites: [{ source: '/**/:path([^/.]+)', destination: '/index.html' }], + }; + + const address = await startServer({ port: 3005 }, configWithSingle, {}); + const response = await fetch(`${address.local!}/users/settings`); + + expect(response.statusCode).toBe(200); + expect(response.body).toContain('Hello there!'); + }); + + test('do not rewrite dotted request paths when single-page rewrites are set', async () => { + const configWithSingle = { + ...config, + rewrites: [{ source: '/**/:path([^/.]+)', destination: '/index.html' }], + }; + + const address = await startServer({ port: 3006 }, configWithSingle, {}); + const extensionResponse = await fetch(`${address.local!}/bundle.js`); + const missingExtensionResponse = await fetch( + `${address.local!}/does-not-exist.js`, + ); + + expect(extensionResponse.statusCode).toBe(200); + expect(extensionResponse.body.trimEnd()).toBe("console.log('bundle');"); + expect(missingExtensionResponse.statusCode).toBe(404); + }); });