From 750edb35a540fe5b8d551b95f509cc174a4ab620 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Mon, 22 Sep 2025 19:25:10 +0100 Subject: [PATCH] feat: :sparkles: Static Assets standalone servers with fileRead Allow for combining multiple static asset manifests and serving or reading content. allow consuming project to provide tsconfig --- integration-tests/fastedge-build.test.js | 55 ++++++--- .../{asset-cli.ts.ts => asset-cli.ts} | 2 +- src/cli/fastedge-build/build.ts | 9 ++ src/cli/fastedge-build/config-build.ts | 8 +- src/cli/fastedge-build/print-info.ts | 1 + src/cli/fastedge-build/types.ts | 1 + src/cli/fastedge-init/create-config.ts | 6 +- .../create-static-assets-cache.test.ts | 4 + .../__tests__/inline-asset.test.ts | 106 ++++++++++++------ .../asset-loader/inline-asset/inline-asset.ts | 28 ++--- .../asset-loader/inline-asset/types.ts | 2 + .../__tests__/create-manifest.test.ts | 37 +++++- .../create-manifest-file-map.ts | 1 + .../asset-manifest/create-manifest.ts | 11 +- .../__tests__/static-server.test.ts | 76 ++++++++++++- .../static-server/create-static-server.ts | 1 + .../static-server/static-server.ts | 34 ++++-- .../static-assets/static-server/types.ts | 9 +- src/utils/__tests__/content-types.test.ts | 57 ++++++++++ src/utils/content-types.ts | 63 ++++++----- src/utils/input-path-verification.ts | 9 +- src/utils/syntax-checker.ts | 13 ++- tsconfig.json | 1 - .../asset-loader/inline-asset/types.d.ts | 4 +- .../static-server/static-server.d.ts | 2 +- .../static-assets/static-server/types.d.ts | 8 +- types/utils/content-types.d.ts | 2 + 27 files changed, 414 insertions(+), 136 deletions(-) rename src/cli/fastedge-assets/{asset-cli.ts.ts => asset-cli.ts} (97%) diff --git a/integration-tests/fastedge-build.test.js b/integration-tests/fastedge-build.test.js index 93fb256..dde82c4 100644 --- a/integration-tests/fastedge-build.test.js +++ b/integration-tests/fastedge-build.test.js @@ -45,6 +45,7 @@ describe('fastedge-build', () => { await cleanup(); }); }); + describe('validates files and paths exist', () => { it('should error if input file does not exist', async () => { expect.assertions(2); @@ -69,21 +70,44 @@ describe('fastedge-build', () => { expect(distFolderExists).toBe(true); await cleanup(); }); - it('should exit with an error if the input is not a ".js" or ".ts" file', async () => { - expect.assertions(2); - const { execute, cleanup, writeFile } = await prepareEnvironment(); - await writeFile('input.jsx', 'function() { console.log("Hello World"); }'); - await writeFile('./lib/fastedge-runtime.wasm', 'Some binary data'); - const { code, stderr } = await execute( - 'node', - './bin/fastedge-build.js input.jsx dist/output.wasm', - ); - expect(code).toBe(1); - expect(stderr[0]).toContain( - 'Error: "input.jsx" is not a valid file type - must be ".js" or ".ts"', - ); - await cleanup(); - }); + it.each(['js', 'jsx', 'cjs', 'mjs', 'ts', 'tsx'])( + 'should handle all valid file extensions ".%s"', + async (ext) => { + expect.assertions(3); + const { execute, cleanup, writeFile } = await prepareEnvironment(); + const filename = `input.${ext}`; + await writeFile(filename, 'function hello() { console.log("Hello World"); }'); + await writeFile('./lib/fastedge-runtime.wasm', 'Some binary data'); + const { code, stdout, stderr } = await execute( + 'node', + `./bin/fastedge-build.js ${filename} dist/output.wasm`, + ); + expect(code).toBe(0); + expect(stderr).toHaveLength(0); + expect(stdout[0]).toContain('Build success!!'); + await cleanup(); + }, + ); + it.each(['txt', 'wasm', 'pdf', 'xml', 'jpg'])( + 'should exit with an error if the input is not a Javascript file ".%s"', + async (ext) => { + expect.assertions(2); + const { execute, cleanup, writeFile } = await prepareEnvironment(); + const filename = `input.${ext}`; + await writeFile(filename, 'function() { console.log("Hello World"); }'); + await writeFile('./lib/fastedge-runtime.wasm', 'Some binary data'); + const { code, stderr } = await execute( + 'node', + `./bin/fastedge-build.js ${filename} dist/output.wasm`, + ); + expect(code).toBe(1); + expect(stderr[0]).toContain( + `Error: "${filename}" is not a valid file type - must be ".js" or ".ts"`, + ); + await cleanup(); + }, + ); + it('should exit with an error if the Javascript is not valid', async () => { expect.assertions(6); const { execute, cleanup, writeFile } = await prepareEnvironment(); @@ -101,6 +125,7 @@ describe('fastedge-build', () => { expect(stderr[1]).toContain('Error: "input.js" contains JS errors'); await cleanup(); }); + it('should exit with an error if the TypeScript is not valid', async () => { expect.assertions(4); const { execute, cleanup, writeFile, path } = await prepareEnvironment(); diff --git a/src/cli/fastedge-assets/asset-cli.ts.ts b/src/cli/fastedge-assets/asset-cli.ts similarity index 97% rename from src/cli/fastedge-assets/asset-cli.ts.ts rename to src/cli/fastedge-assets/asset-cli.ts index ab860d8..88e80e8 100644 --- a/src/cli/fastedge-assets/asset-cli.ts.ts +++ b/src/cli/fastedge-assets/asset-cli.ts @@ -52,7 +52,7 @@ try { process.exit(0); } -if (args['--help']) { +if (args['--help'] || (Object.keys(args).length === 1 && args._.length === 0)) { printHelp(); process.exit(0); } diff --git a/src/cli/fastedge-build/build.ts b/src/cli/fastedge-build/build.ts index adc8b3a..fc34af9 100644 --- a/src/cli/fastedge-build/build.ts +++ b/src/cli/fastedge-build/build.ts @@ -15,11 +15,13 @@ interface ParsedArgs { '--help'?: boolean; '--input'?: string; '--output'?: string; + '--tsconfig'?: string; '--config'?: string[]; } let inputFileName = ''; let outputFileName = ''; +let tsConfigPath = ''; let configFiles: string[] = []; let args: ParsedArgs; @@ -31,6 +33,7 @@ try { '--help': Boolean, '--input': String, '--output': String, + '--tsconfig': String, '--config': [String], // Aliases @@ -38,6 +41,7 @@ try { '-h': '--help', '-i': '--input', '-o': '--output', + '-t': '--tsconfig', '-c': '--config', }, { @@ -80,6 +84,10 @@ if (args['--output']) { outputFileName = args['--output']; } +if (args['--tsconfig']) { + tsConfigPath = args['--tsconfig']; +} + if (args._.length === 2) { [inputFileName, outputFileName] = args._; } @@ -96,6 +104,7 @@ if (inputFileName && outputFileName) { await buildWasm({ entryPoint: inputFileName, wasmOutput: outputFileName, + tsConfigPath, }); colorLog('success', `Build success!!`); colorLog('info', `"${inputFileName}" -> "${outputFileName}"`); diff --git a/src/cli/fastedge-build/config-build.ts b/src/cli/fastedge-build/config-build.ts index 8885133..0266fd7 100644 --- a/src/cli/fastedge-build/config-build.ts +++ b/src/cli/fastedge-build/config-build.ts @@ -11,8 +11,8 @@ import { loadConfig } from '~utils/load-config-file.ts'; * Builds a WebAssembly file from the provided input and output paths. * @param options - The input and output file paths. */ -async function buildWasm({ entryPoint, wasmOutput }: BuildConfig): Promise { - await validateFilePaths(entryPoint, wasmOutput); +async function buildWasm({ entryPoint, wasmOutput, tsConfigPath }: BuildConfig): Promise { + await validateFilePaths(entryPoint, wasmOutput, tsConfigPath); if (process.env.NODE_ENV !== 'test') { await componentize(entryPoint, wasmOutput); } @@ -29,12 +29,12 @@ async function buildFromConfig(config: BuildConfig | null): Promise { case 'static': { await createStaticAssetsManifest(config); await buildWasm(config); - colorLog('success', `Success: Built ${config.output}`); + colorLog('success', `Success: Built ${config.wasmOutput}`); break; } case 'http': { await buildWasm(config); - colorLog('success', `Success: Built ${config.output}`); + colorLog('success', `Success: Built ${config.wasmOutput}`); break; } default: { diff --git a/src/cli/fastedge-build/print-info.ts b/src/cli/fastedge-build/print-info.ts index a6cb1c8..1039a0b 100644 --- a/src/cli/fastedge-build/print-info.ts +++ b/src/cli/fastedge-build/print-info.ts @@ -11,6 +11,7 @@ const USAGE_TEXT = `\nUsage: fastedge-build [options] --version, -v Print the version number --input, -i Js filepath to build (e.g. ./src/index.js) --output, -o Output filepath for wasm (e.g. ./dist/main.wasm) + --tsconfig, -t Path to a TypeScript config file (default: ./tsconfig.json) --config, -c Path to a build config file (default: ./.fastedge/build-config.js) `; diff --git a/src/cli/fastedge-build/types.ts b/src/cli/fastedge-build/types.ts index 04cda18..8deaddd 100644 --- a/src/cli/fastedge-build/types.ts +++ b/src/cli/fastedge-build/types.ts @@ -9,6 +9,7 @@ interface BuildConfig extends Partial { type?: BuildType; entryPoint: string; wasmOutput: string; + tsConfigPath?: string; } export type { BuildConfig, BuildType }; diff --git a/src/cli/fastedge-init/create-config.ts b/src/cli/fastedge-init/create-config.ts index 6611757..db092ee 100644 --- a/src/cli/fastedge-init/create-config.ts +++ b/src/cli/fastedge-init/create-config.ts @@ -29,6 +29,7 @@ interface ConfigTypeObject { notFoundPage?: string; autoExt?: string[]; autoIndex?: string[]; + [key: string]: unknown; }; } @@ -40,12 +41,15 @@ type DefaultConfig = Record; /** Default configuration object */ const defaultConfig: DefaultConfig = { build: { - http: {}, + http: { + tsConfigPath: './tsconfig.json', + }, static: { entryPoint: '.fastedge/static-index.js', ignoreDotFiles: true, ignoreDirs: ['./node_modules'], ignoreWellKnown: false, + tsConfigPath: './tsconfig.json', }, }, server: { diff --git a/src/server/static-assets/asset-loader/__tests__/create-static-assets-cache.test.ts b/src/server/static-assets/asset-loader/__tests__/create-static-assets-cache.test.ts index 319e2a4..9451bfa 100644 --- a/src/server/static-assets/asset-loader/__tests__/create-static-assets-cache.test.ts +++ b/src/server/static-assets/asset-loader/__tests__/create-static-assets-cache.test.ts @@ -21,6 +21,7 @@ const createTestMetadata = (overrides?: Partial): StaticAss assetKey: 'test-asset', type: 'wasm-inline', contentType: 'text/html', + isText: true, fileInfo: { assetPath: '/test.html', hash: 'hash123', @@ -48,6 +49,7 @@ describe('createStaticAssetsCache', () => { type: 'wasm-inline', getMetadata: jest.fn(), getEmbeddedStoreEntry: jest.fn(), + getText: jest.fn(), } as jest.Mocked; beforeEach(() => { @@ -294,6 +296,7 @@ describe('createStaticAssetsCache', () => { assetKey: 'minimal', type: 'wasm-inline', contentType: 'text/plain', + isText: true, fileInfo: { assetPath: '/minimal.txt', hash: 'hash', @@ -318,6 +321,7 @@ describe('createStaticAssetsCache', () => { assetKey: 'maximal-asset-with-very-long-name-that-includes-many-details', type: 'wasm-inline', contentType: 'text/html; charset=utf-8; boundary=something', + isText: true, fileInfo: { assetPath: '/very/deep/nested/folder/structure/with/many/levels/file.html', hash: `sha256-${'a'.repeat(64)}`, diff --git a/src/server/static-assets/asset-loader/__tests__/inline-asset.test.ts b/src/server/static-assets/asset-loader/__tests__/inline-asset.test.ts index 11bad78..94ca2c2 100644 --- a/src/server/static-assets/asset-loader/__tests__/inline-asset.test.ts +++ b/src/server/static-assets/asset-loader/__tests__/inline-asset.test.ts @@ -21,6 +21,7 @@ const createTestMetadata = (overrides?: Partial): StaticAss assetKey: 'test-asset', type: 'wasm-inline', contentType: 'text/html; charset=utf-8', + isText: true, fileInfo: { assetPath: '/path/to/test.html', hash: 'sha256-abcdef123456', @@ -347,6 +348,7 @@ describe('inline-asset', () => { assetKey: 'minimal', type: 'wasm-inline', contentType: 'text/plain', + isText: true, fileInfo: { assetPath: '/minimal.txt', hash: 'hash', @@ -430,50 +432,80 @@ describe('inline-asset', () => { } }); }); - }); - describe('error handling', () => { - it('should propagate file system errors', () => { - expect.assertions(1); - const metadata = { - assetKey: 'error-file', - type: 'wasm-inline', - contentType: 'text/plain', - fileInfo: { - assetPath: '/nonexistent/file.txt', - hash: 'hash', - lastModifiedTime: testLastModified(), - size: 100, - }, - }; - - mockReadFileSync.mockImplementation(() => { - throw new Error('ENOENT: no such file or directory'); + describe('error handling', () => { + it('should propagate file system errors', () => { + expect.assertions(1); + const metadata = { + assetKey: 'error-file', + type: 'wasm-inline', + contentType: 'text/plain', + isText: true, + fileInfo: { + assetPath: '/nonexistent/file.txt', + hash: 'hash', + lastModifiedTime: testLastModified(), + size: 100, + }, + }; + + mockReadFileSync.mockImplementation(() => { + throw new Error('ENOENT: no such file or directory'); + }); + + expect(() => createWasmInlineAsset(metadata)).toThrow('ENOENT: no such file or directory'); }); - expect(() => createWasmInlineAsset(metadata)).toThrow('ENOENT: no such file or directory'); + it('should handle permission errors', () => { + expect.assertions(1); + const metadata = { + assetKey: 'permission-denied', + type: 'wasm-inline', + contentType: 'text/plain', + isText: true, + fileInfo: { + assetPath: '/restricted/file.txt', + hash: 'hash', + lastModifiedTime: testLastModified(), + size: 100, + }, + }; + + mockReadFileSync.mockImplementation(() => { + throw new Error('EACCES: permission denied'); + }); + + expect(() => createWasmInlineAsset(metadata)).toThrow('EACCES: permission denied'); + }); }); - it('should handle permission errors', () => { - expect.assertions(1); - const metadata = { - assetKey: 'permission-denied', - type: 'wasm-inline', - contentType: 'text/plain', - text: true, - fileInfo: { - assetPath: '/restricted/file.txt', - hash: 'hash', - lastModifiedTime: testLastModified(), - size: 100, - }, - }; - - mockReadFileSync.mockImplementation(() => { - throw new Error('EACCES: permission denied'); + describe('text content handling', () => { + it('should decode text content with getText()', () => { + expect.assertions(2); + const testString = 'Hello, WASM!'; + const metadata = createTestMetadata({ + isText: true, + contentType: 'text/plain', + }); + // Encode string as Uint8Array + const encoded = new TextEncoder().encode(testString); + mockReadFileSync.mockReturnValue(encoded); + + const asset = createWasmInlineAsset(metadata); + expect(asset.getText()).toBe(testString); + expect(() => asset.getText()).not.toThrow(); }); - expect(() => createWasmInlineAsset(metadata)).toThrow('EACCES: permission denied'); + it('should throw error for getText() on non-text asset', () => { + expect.assertions(1); + const metadata = createTestMetadata({ + isText: false, + contentType: 'application/octet-stream', + }); + mockReadFileSync.mockReturnValue(new Uint8Array([1, 2, 3])); + const asset = createWasmInlineAsset(metadata); + expect(() => asset.getText()).toThrow("Can't getText() for non-text content"); + }); }); }); }); diff --git a/src/server/static-assets/asset-loader/inline-asset/inline-asset.ts b/src/server/static-assets/asset-loader/inline-asset/inline-asset.ts index 115321c..062bb7e 100644 --- a/src/server/static-assets/asset-loader/inline-asset/inline-asset.ts +++ b/src/server/static-assets/asset-loader/inline-asset/inline-asset.ts @@ -82,30 +82,20 @@ const createWasmInlineAsset = (metadata: StaticAssetMetadata): StaticAsset => { }; // eslint-disable-next-line capitalized-comments - // const getText = () => { - // if (!_metadata.text) { - // throw new Error("Can't getText() for non-text content"); - // } - // return decoder.decode(_sourceAndInfo.source); - // }; - - // const getJson = () => { - // const text = getText(); - // return JSON.parse(text); - // }; + const getText = (): string => { + if (!_metadata.isText) { + throw new Error("Can't getText() for non-text content"); + } + const sourceArr = _sourceAndInfo.source; + const decoder = new TextDecoder(); + return decoder.decode(sourceArr); + }; return { assetKey: _metadata.assetKey, - // eslint-disable-next-line capitalized-comments - // assetKey: () => _metadata.assetKey, getMetadata: () => _metadata, getEmbeddedStoreEntry, - // Farq: I think we can remove these, everything is being inlined at present. - // text/json/bytes etc comes from kvStore implementation - // isLocal: () => true, - // getBytes: () => _sourceAndInfo.source, - // getText, - // getJson, + getText, type: _metadata.type, }; }; diff --git a/src/server/static-assets/asset-loader/inline-asset/types.ts b/src/server/static-assets/asset-loader/inline-asset/types.ts index 8b8801f..cb544d1 100644 --- a/src/server/static-assets/asset-loader/inline-asset/types.ts +++ b/src/server/static-assets/asset-loader/inline-asset/types.ts @@ -29,6 +29,7 @@ interface StaticAssetMetadata { assetKey: string; // Key of the asset contentType: string; // Content type of the asset fileInfo: FileInfo; // Information about the file + isText: boolean; // Indicates if the asset is text } /** @@ -39,6 +40,7 @@ interface StaticAsset { assetKey: string; // Key of the asset getMetadata(): StaticAssetMetadata; // Gets the metadata of the asset getEmbeddedStoreEntry(arg: unknown): Promise; // Gets the store entry of the asset + getText(): string; // Gets the text content of the asset } export type { ContentCompressionTypes, SourceAndInfo, StaticAsset, StaticAssetMetadata }; diff --git a/src/server/static-assets/asset-manifest/__tests__/create-manifest.test.ts b/src/server/static-assets/asset-manifest/__tests__/create-manifest.test.ts index 9a7d57c..3a9b0ca 100644 --- a/src/server/static-assets/asset-manifest/__tests__/create-manifest.test.ts +++ b/src/server/static-assets/asset-manifest/__tests__/create-manifest.test.ts @@ -126,11 +126,46 @@ describe('createStaticAssetsManifest', () => { expect(result).toBe(manifest); }); + it('should ensure assetManifestPath has a js extension if it is not provided', async () => { + expect.assertions(4); + const config: Partial = { + publicDir, + assetManifestPath: '/custom/path/manifest', + }; + const normalizedConfig: AssetCacheConfig = { + publicDir, + ignoreDotFiles: false, + ignoreWellKnown: false, + ignorePaths: [], + contentTypes: [], + assetManifestPath: '/custom/path/manifest', + }; + const manifest: StaticAssetManifest = { + '/foo.txt': { assetKey: '/foo.txt', contentType: 'text/plain' } as any, + }; + + mockNormalizeConfig.mockReturnValue(normalizedConfig); + mockCreateManifestFileMap.mockResolvedValue(manifest); + mockIsFile.mockResolvedValue(true); + + const expectedManifestBuildOutput = path.resolve(`.${config.assetManifestPath}.js`); + + const result = await createStaticAssetsManifest(config); + + expect(mockIsFile).toHaveBeenCalledWith(`${config.assetManifestPath}.js`, true); + expect(mockCreateOutputDirectory).toHaveBeenCalledWith(expectedManifestBuildOutput); + expect(writeFileSync).toHaveBeenCalledWith( + expectedManifestBuildOutput, + expect.stringContaining('const staticAssetManifest = {'), + ); + expect(result).toBe(manifest); + }); + it('should fallback to default assetManifestPath if provided assetManifestPath is not a file', async () => { expect.assertions(5); const config: Partial = { publicDir, - assetManifestPath: '/not/a/file', + assetManifestPath: '/not/a/file.js', }; const normalizedConfig: AssetCacheConfig = { publicDir, diff --git a/src/server/static-assets/asset-manifest/create-manifest-file-map.ts b/src/server/static-assets/asset-manifest/create-manifest-file-map.ts index 20bba27..8216f7c 100644 --- a/src/server/static-assets/asset-manifest/create-manifest-file-map.ts +++ b/src/server/static-assets/asset-manifest/create-manifest-file-map.ts @@ -83,6 +83,7 @@ function createManifestFileMap(asssetCacheConfig: AssetCacheConfig): StaticAsset ); contentTypeInfo = { contentType: 'application/octet-stream', + isText: false, }; } diff --git a/src/server/static-assets/asset-manifest/create-manifest.ts b/src/server/static-assets/asset-manifest/create-manifest.ts index a107aca..842f6c9 100644 --- a/src/server/static-assets/asset-manifest/create-manifest.ts +++ b/src/server/static-assets/asset-manifest/create-manifest.ts @@ -26,7 +26,16 @@ async function createStaticAssetsManifest( const { assetManifestPath: providedOutputPath } = asssetCacheConfig; if (providedOutputPath?.length) { const allowNonExistentFile = true; - const { assetManifestPath: normalizedOutputPath } = config; + let { assetManifestPath: normalizedOutputPath } = config; + // Ensure that normalizedOutputPath ends with .js/.ts/.cjs/.mjs else add .js + if (!/\.(js|ts|cjs|mjs)$/u.test(normalizedOutputPath)) { + colorLog( + 'warning', + `The provided assetManifestPath '${normalizedOutputPath}' does not end with .js, .ts, .cjs, or .mjs. Adding .js extension.`, + ); + normalizedOutputPath += '.js'; + } + // Check if the provided path is a file const outputIsAFile = await isFile(normalizedOutputPath, allowNonExistentFile); if (!outputIsAFile) { colorLog( diff --git a/src/server/static-assets/static-server/__tests__/static-server.test.ts b/src/server/static-assets/static-server/__tests__/static-server.test.ts index ce7ab21..df00ec8 100644 --- a/src/server/static-assets/static-server/__tests__/static-server.test.ts +++ b/src/server/static-assets/static-server/__tests__/static-server.test.ts @@ -1,10 +1,12 @@ import { getStaticServer } from '../static-server.ts'; -import type { ServerConfig } from '../types.ts'; +import type { InternalStaticServer, ServerConfig } from '../types.ts'; import type { AssetCache } from '~static-assets/asset-loader/asset-cache/asset-cache.ts'; import type { StaticAsset } from '~static-assets/asset-loader/inline-asset/inline-asset.ts'; +const MOCK_TEXT_CONTENT = 'mock text content'; + function mockAsset(metadata: any, storeEntry?: any): StaticAsset { return { getMetadata: () => metadata, @@ -14,6 +16,12 @@ function mockAsset(metadata: any, storeEntry?: any): StaticAsset { contentEncoding: () => null, hash: () => 'mockhash', }, + getText: () => { + if (metadata.contentType?.startsWith('text/')) { + return MOCK_TEXT_CONTENT; + } + throw new Error("Can't getText() for non-text content"); + }, } as unknown as StaticAsset; } @@ -30,7 +38,7 @@ function makeRequest(url: string, method = 'GET', headers: Record { let serverConfig: ServerConfig; let assetCache: AssetCache; - let server: ReturnType; + let server: InternalStaticServer; beforeEach(() => { serverConfig = { @@ -46,7 +54,6 @@ describe('getStaticServer', () => { assetCache = mockAssetCache({}); server = getStaticServer(serverConfig, assetCache); }); - describe('getMatchingAsset', () => { it('should match asset directly', () => { expect.assertions(1); @@ -137,6 +144,65 @@ describe('getStaticServer', () => { }); }); + describe('readFileString', () => { + it('should return text content for existing text asset', async () => { + expect.assertions(1); + const asset = mockAsset({ contentType: 'text/plain', fileInfo: { lastModifiedTime: 123 } }); + assetCache = mockAssetCache({ '/public/test.txt': asset }); + server = getStaticServer(serverConfig, assetCache); + + const result = await server.readFileString('/public/test.txt'); + expect(result).toBe(MOCK_TEXT_CONTENT); + }); + + it('should return text content for paths without leading slash', async () => { + expect.assertions(1); + const asset = mockAsset({ contentType: 'text/plain', fileInfo: { lastModifiedTime: 123 } }); + assetCache = mockAssetCache({ '/public/test.txt': asset }); + server = getStaticServer(serverConfig, assetCache); + + const result = await server.readFileString('public/test.txt'); + expect(result).toBe(MOCK_TEXT_CONTENT); + }); + + it('should throw error when asset not found', async () => { + expect.assertions(1); + assetCache = mockAssetCache({}); + server = getStaticServer(serverConfig, assetCache); + + await expect(server.readFileString('/public/nonexistent.txt')).rejects.toThrow( + 'Asset not found', + ); + }); + + it('should propagate getText() error for non-text content', async () => { + expect.assertions(1); + const asset = mockAsset({ + contentType: 'image/png', + fileInfo: { lastModifiedTime: 123 }, + }); + assetCache = mockAssetCache({ '/public/image.png': asset }); + server = getStaticServer(serverConfig, assetCache); + + await expect(server.readFileString('/public/image.png')).rejects.toThrow( + "Can't getText() for non-text content", + ); + }); + + it('should handle publicDirPrefix configuration', async () => { + expect.assertions(1); + const asset = mockAsset({ contentType: 'text/plain', fileInfo: { lastModifiedTime: 123 } }); + + // Update server config to include publicDirPrefix + const configWithPrefix = { ...serverConfig, publicDirPrefix: '/static' }; + assetCache = mockAssetCache({ '/static/test.txt': asset }); + server = getStaticServer(configWithPrefix, assetCache); + + const result = await server.readFileString('/test.txt'); + expect(result).toBe(MOCK_TEXT_CONTENT); + }); + }); + describe('handlePreconditions', () => { const asset = mockAsset({ contentType: 'text/html', @@ -299,7 +365,7 @@ describe('getStaticServer', () => { expect.assertions(1); const req = makeRequest('http://localhost/public/test.html', 'POST'); const resp = await server.serveRequest(req); - expect(resp).toBeNull(); + expect(resp).toBeUndefined(); }); it('should return null if no asset and no fallback', async () => { @@ -313,7 +379,7 @@ describe('getStaticServer', () => { Accept: 'text/html', }); const resp = await server.serveRequest(req); - expect(resp).toBeNull(); + expect(resp).toBeUndefined(); }); }); }); diff --git a/src/server/static-assets/static-server/create-static-server.ts b/src/server/static-assets/static-server/create-static-server.ts index eb79ad4..045d00f 100644 --- a/src/server/static-assets/static-server/create-static-server.ts +++ b/src/server/static-assets/static-server/create-static-server.ts @@ -25,6 +25,7 @@ const createStaticServer = ( function normalizeServerConfig(config: Partial): ServerConfig { return normalizeConfig(config, { publicDirPrefix: 'string', + routePrefix: 'path', extendedCache: 'pathsOrRegexArray', compression: 'stringArray', notFoundPage: 'path', diff --git a/src/server/static-assets/static-server/static-server.ts b/src/server/static-assets/static-server/static-server.ts index 23ac473..920e26b 100644 --- a/src/server/static-assets/static-server/static-server.ts +++ b/src/server/static-assets/static-server/static-server.ts @@ -108,10 +108,10 @@ const handlePreconditions = ( * @param assetCache - The asset cache. * @returns A `StaticServer` instance. */ -const getStaticServer = ( +const getStaticServer = ( serverConfig: ServerConfig, assetCache: AssetCache, -): StaticServer => { +): T => { const _serverConfig = serverConfig; const _assetCache = assetCache; const _extendedCache = serverConfig.extendedCache; @@ -123,7 +123,19 @@ const getStaticServer = ( * @returns A `StaticAsset` */ const getMatchingAsset = (path: string): StaticAsset | null => { - const assetKey = _serverConfig.publicDirPrefix + path; + let assetKey = path; + + if (_serverConfig.routePrefix) { + assetKey = assetKey.replace(_serverConfig.routePrefix, ''); + } + + if (_serverConfig.publicDirPrefix) { + assetKey = `${_serverConfig.publicDirPrefix}${assetKey}`; + } + + if (!assetKey.startsWith('/')) { + assetKey = `/${assetKey}`; + } if (!assetKey.endsWith('/')) { // A path that does not end in a slash can match an asset directly @@ -220,6 +232,14 @@ const getStaticServer = ( return x === pathname; }); + const readFileString = async (path: string): Promise => { + const asset = getMatchingAsset(path); + if (asset != null) { + return asset.getText(); + } + throw new Error('Asset not found'); + }; + const serveAsset = async ( request: Request, asset: StaticAsset, @@ -270,10 +290,10 @@ const getStaticServer = ( }); }; - const serveRequest = async (request: Request): Promise => { + const serveRequest = async (request: Request): Promise => { // Only handle GET and HEAD if (request.method !== 'GET' && request.method !== 'HEAD') { - return null; + return; } const url = new URL(request.url); @@ -310,17 +330,17 @@ const getStaticServer = ( } } } - return null; }; return { findAcceptEncodings, getMatchingAsset, handlePreconditions, + readFileString, serveAsset, serveRequest, testExtendedCache, - }; + } as T; }; export { getStaticServer }; diff --git a/src/server/static-assets/static-server/types.ts b/src/server/static-assets/static-server/types.ts index b62abf6..ddf5ac4 100644 --- a/src/server/static-assets/static-server/types.ts +++ b/src/server/static-assets/static-server/types.ts @@ -3,6 +3,7 @@ import { ContentCompressionTypes, StaticAsset } from '../asset-loader/inline-ass interface ServerConfig extends Record { extendedCache: Array; publicDirPrefix: string; + routePrefix?: string; compression: string[]; notFoundPage: string | null; autoExt: string[]; @@ -28,6 +29,11 @@ interface AssetInit { * Represents the static server. */ interface StaticServer { + serveRequest(request: Request): Promise; + readFileString(path: string): Promise; +} + +interface InternalStaticServer extends StaticServer { getMatchingAsset(path: string): StaticAsset | null; findAcceptEncodings(request: Request): Array; testExtendedCache(pathname: string): boolean; @@ -37,7 +43,6 @@ interface StaticServer { responseHeaders: HeadersType, ): Response | null; serveAsset(request: Request, asset: StaticAsset, init?: AssetInit): Promise; - serveRequest(request: Request): Promise; } -export type { AssetInit, HeadersType, ServerConfig, StaticServer }; +export type { AssetInit, HeadersType, InternalStaticServer, ServerConfig, StaticServer }; diff --git a/src/utils/__tests__/content-types.test.ts b/src/utils/__tests__/content-types.test.ts index 09b590b..e944554 100644 --- a/src/utils/__tests__/content-types.test.ts +++ b/src/utils/__tests__/content-types.test.ts @@ -9,9 +9,11 @@ describe('content-types', () => { expect.assertions(2); expect(testFileContentType(undefined, 'test.html')).toStrictEqual({ contentType: 'text/html', + isText: true, }); expect(testFileContentType(undefined, 'test.htm')).toStrictEqual({ contentType: 'text/html', + isText: true, }); }); @@ -19,6 +21,7 @@ describe('content-types', () => { expect.assertions(1); expect(testFileContentType(undefined, 'script.js')).toStrictEqual({ contentType: 'application/javascript', + isText: true, }); }); @@ -26,6 +29,7 @@ describe('content-types', () => { expect.assertions(1); expect(testFileContentType(undefined, 'style.css')).toStrictEqual({ contentType: 'text/css', + isText: true, }); }); @@ -33,9 +37,11 @@ describe('content-types', () => { expect.assertions(2); expect(testFileContentType(undefined, 'data.json')).toStrictEqual({ contentType: 'application/json', + isText: true, }); expect(testFileContentType(undefined, 'sourcemap.map')).toStrictEqual({ contentType: 'application/json', + isText: true, }); }); @@ -43,6 +49,7 @@ describe('content-types', () => { expect.assertions(1); expect(testFileContentType(undefined, 'config.xml')).toStrictEqual({ contentType: 'application/xml', + isText: true, }); }); @@ -50,6 +57,7 @@ describe('content-types', () => { expect.assertions(1); expect(testFileContentType(undefined, 'readme.txt')).toStrictEqual({ contentType: 'text/plain', + isText: true, }); }); @@ -57,6 +65,7 @@ describe('content-types', () => { expect.assertions(1); expect(testFileContentType(undefined, 'icon.svg')).toStrictEqual({ contentType: 'image/svg+xml', + isText: true, }); }); }); @@ -66,6 +75,7 @@ describe('content-types', () => { expect.assertions(1); expect(testFileContentType(undefined, 'image.png')).toStrictEqual({ contentType: 'image/png', + isText: false, }); }); @@ -73,9 +83,11 @@ describe('content-types', () => { expect.assertions(2); expect(testFileContentType(undefined, 'photo.jpg')).toStrictEqual({ contentType: 'image/jpeg', + isText: false, }); expect(testFileContentType(undefined, 'photo.jpeg')).toStrictEqual({ contentType: 'image/jpeg', + isText: false, }); }); @@ -83,6 +95,7 @@ describe('content-types', () => { expect.assertions(1); expect(testFileContentType(undefined, 'animation.gif')).toStrictEqual({ contentType: 'image/gif', + isText: false, }); }); @@ -90,6 +103,7 @@ describe('content-types', () => { expect.assertions(1); expect(testFileContentType(undefined, 'bitmap.bmp')).toStrictEqual({ contentType: 'image/bmp', + isText: false, }); }); @@ -97,6 +111,7 @@ describe('content-types', () => { expect.assertions(1); expect(testFileContentType(undefined, 'favicon.ico')).toStrictEqual({ contentType: 'image/vnd.microsoft.icon', + isText: false, }); }); @@ -104,9 +119,11 @@ describe('content-types', () => { expect.assertions(2); expect(testFileContentType(undefined, 'image.tif')).toStrictEqual({ contentType: 'image/png', // Note: your code maps TIFF to PNG + isText: false, }); expect(testFileContentType(undefined, 'image.tiff')).toStrictEqual({ contentType: 'image/png', + isText: false, }); }); }); @@ -116,9 +133,11 @@ describe('content-types', () => { expect.assertions(2); expect(testFileContentType(undefined, 'audio.aac')).toStrictEqual({ contentType: 'audio/aac', + isText: false, }); expect(testFileContentType(undefined, 'music.mp3')).toStrictEqual({ contentType: 'audio/mpeg', + isText: false, }); }); @@ -126,15 +145,19 @@ describe('content-types', () => { expect.assertions(4); expect(testFileContentType(undefined, 'video.avi')).toStrictEqual({ contentType: 'video/x-msvideo', + isText: false, }); expect(testFileContentType(undefined, 'video.mp4')).toStrictEqual({ contentType: 'video/mp4', + isText: false, }); expect(testFileContentType(undefined, 'video.mpeg')).toStrictEqual({ contentType: 'video/mpeg', + isText: false, }); expect(testFileContentType(undefined, 'video.webm')).toStrictEqual({ contentType: 'video/webm', + isText: false, }); }); }); @@ -144,18 +167,23 @@ describe('content-types', () => { expect.assertions(5); expect(testFileContentType(undefined, 'font.eot')).toStrictEqual({ contentType: 'application/vnd.ms-fontobject', + isText: false, }); expect(testFileContentType(undefined, 'font.otf')).toStrictEqual({ contentType: 'font/otf', + isText: false, }); expect(testFileContentType(undefined, 'font.ttf')).toStrictEqual({ contentType: 'font/ttf', + isText: false, }); expect(testFileContentType(undefined, 'font.woff')).toStrictEqual({ contentType: 'font/woff', + isText: false, }); expect(testFileContentType(undefined, 'font.woff2')).toStrictEqual({ contentType: 'font/woff2', + isText: false, }); }); }); @@ -165,12 +193,15 @@ describe('content-types', () => { expect.assertions(3); expect(testFileContentType(undefined, 'document.pdf')).toStrictEqual({ contentType: 'application/pdf', + isText: false, }); expect(testFileContentType(undefined, 'archive.tar')).toStrictEqual({ contentType: 'application/x-tar', + isText: false, }); expect(testFileContentType(undefined, 'package.zip')).toStrictEqual({ contentType: 'application/zip', + isText: false, }); }); }); @@ -189,9 +220,11 @@ describe('content-types', () => { expect.assertions(2); expect(testFileContentType(undefined, 'file.min.js')).toStrictEqual({ contentType: 'application/javascript', + isText: true, }); expect(testFileContentType(undefined, 'styles.responsive.css')).toStrictEqual({ contentType: 'text/css', + isText: true, }); }); @@ -199,9 +232,11 @@ describe('content-types', () => { expect.assertions(2); expect(testFileContentType(undefined, 'FILE.JS')).toStrictEqual({ contentType: 'application/javascript', + isText: true, }); expect(testFileContentType(undefined, 'IMAGE.PNG')).toStrictEqual({ contentType: 'image/png', + isText: false, }); }); @@ -223,6 +258,7 @@ describe('content-types', () => { expect(testFileContentType(undefined, '.htaccess')).toBeNull(); expect(testFileContentType(undefined, '.env.js')).toStrictEqual({ contentType: 'application/javascript', + isText: true, }); }); }); @@ -234,12 +270,14 @@ describe('content-types', () => { { test: /\.custom$/u, contentType: 'application/custom', + isText: false, }, ]; const knownTypes = getKnownContentTypes(customTypes); expect(testFileContentType(knownTypes, 'file.custom')).toStrictEqual({ contentType: 'application/custom', + isText: false, }); }); @@ -249,12 +287,14 @@ describe('content-types', () => { { test: /\.js$/u, contentType: 'text/custom-javascript', + isText: true, }, ]; const knownTypes = getKnownContentTypes(customTypes); expect(testFileContentType(knownTypes, 'script.js')).toStrictEqual({ contentType: 'text/custom-javascript', + isText: true, }); }); @@ -264,15 +304,18 @@ describe('content-types', () => { { test: (assetKey: string) => assetKey.includes('special'), contentType: 'application/special', + isText: false, }, ]; const knownTypes = getKnownContentTypes(customTypes); expect(testFileContentType(knownTypes, 'special-file.txt')).toStrictEqual({ contentType: 'application/special', + isText: false, }); expect(testFileContentType(knownTypes, 'normal-file.txt')).toStrictEqual({ contentType: 'text/plain', + isText: true, }); }); @@ -282,22 +325,27 @@ describe('content-types', () => { { test: (assetKey: string) => assetKey.endsWith('.config.js'), contentType: 'application/config+javascript', + isText: true, }, { test: (assetKey: string) => assetKey.startsWith('api/') && assetKey.endsWith('.json'), contentType: 'application/api+json', + isText: false, }, ]; const knownTypes = getKnownContentTypes(customTypes); expect(testFileContentType(knownTypes, 'webpack.config.js')).toStrictEqual({ contentType: 'application/config+javascript', + isText: true, }); expect(testFileContentType(knownTypes, 'api/users.json')).toStrictEqual({ contentType: 'application/api+json', + isText: false, }); expect(testFileContentType(knownTypes, 'regular.js')).toStrictEqual({ contentType: 'application/javascript', + isText: true, }); }); @@ -307,16 +355,19 @@ describe('content-types', () => { { test: /\.js$/u, contentType: 'application/first-js', + isText: true, }, { test: /\.js$/u, contentType: 'application/second-js', + isText: true, }, ]; const knownTypes = getKnownContentTypes(customTypes); expect(testFileContentType(knownTypes, 'script.js')).toStrictEqual({ contentType: 'application/first-js', + isText: true, }); }); }); @@ -352,6 +403,7 @@ describe('content-types', () => { { test: /\.custom$/u, contentType: 'application/custom', + isText: false, }, ]; @@ -372,14 +424,17 @@ describe('content-types', () => { { test: /\.custom1$/u, contentType: 'application/custom1', + isText: false, }, { test: /\.custom2$/u, contentType: 'application/custom2', + isText: false, }, { test: (key: string) => key.includes('special'), contentType: 'application/special', + isText: true, }, ]; @@ -498,10 +553,12 @@ describe('content-types', () => { { test: /\.custom$/u, contentType: 'application/custom', + isText: false, }, { test: /\.special$/u, contentType: 'application/special', + isText: false, }, ]; diff --git a/src/utils/content-types.ts b/src/utils/content-types.ts index 51b86d1..b6b37d9 100644 --- a/src/utils/content-types.ts +++ b/src/utils/content-types.ts @@ -6,46 +6,48 @@ import { colorLog } from '~utils/color-log.ts'; interface ContentTypeDefinition { test: RegExp | ((assetKey: string) => boolean); contentType: string; + isText: boolean; } /** * Text-based content types. */ const textFormats: ContentTypeDefinition[] = [ - { test: /.txt$/u, contentType: 'text/plain' }, - { test: /.htm(l)?$/u, contentType: 'text/html' }, - { test: /.xml$/u, contentType: 'application/xml' }, - { test: /.json$/u, contentType: 'application/json' }, - { test: /.map$/u, contentType: 'application/json' }, - { test: /.js$/u, contentType: 'application/javascript' }, - { test: /.css$/u, contentType: 'text/css' }, - { test: /.svg$/u, contentType: 'image/svg+xml' }, + { test: /.txt$/u, contentType: 'text/plain', isText: true }, + { test: /.htm(l)?$/u, contentType: 'text/html', isText: true }, + { test: /.xml$/u, contentType: 'application/xml', isText: true }, + { test: /.json$/u, contentType: 'application/json', isText: true }, + { test: /.map$/u, contentType: 'application/json', isText: true }, + { test: /.js$/u, contentType: 'application/javascript', isText: true }, + { test: /.ts$/u, contentType: 'application/typescript', isText: true }, + { test: /.css$/u, contentType: 'text/css', isText: true }, + { test: /.svg$/u, contentType: 'image/svg+xml', isText: true }, ]; /** * Binary-based content types. */ const binaryFormats: ContentTypeDefinition[] = [ - { test: /.bmp$/u, contentType: 'image/bmp' }, - { test: /.png$/u, contentType: 'image/png' }, - { test: /.gif$/u, contentType: 'image/gif' }, - { test: /.jp(e)?g$/u, contentType: 'image/jpeg' }, - { test: /.ico$/u, contentType: 'image/vnd.microsoft.icon' }, - { test: /.tif(f)?$/u, contentType: 'image/png' }, - { test: /.aac$/u, contentType: 'audio/aac' }, - { test: /.mp3$/u, contentType: 'audio/mpeg' }, - { test: /.avi$/u, contentType: 'video/x-msvideo' }, - { test: /.mp4$/u, contentType: 'video/mp4' }, - { test: /.mpeg$/u, contentType: 'video/mpeg' }, - { test: /.webm$/u, contentType: 'video/webm' }, - { test: /.pdf$/u, contentType: 'application/pdf' }, - { test: /.tar$/u, contentType: 'application/x-tar' }, - { test: /.zip$/u, contentType: 'application/zip' }, - { test: /.eot$/u, contentType: 'application/vnd.ms-fontobject' }, - { test: /.otf$/u, contentType: 'font/otf' }, - { test: /.ttf$/u, contentType: 'font/ttf' }, - { test: /.woff$/u, contentType: 'font/woff' }, - { test: /.woff2$/u, contentType: 'font/woff2' }, + { test: /.bmp$/u, contentType: 'image/bmp', isText: false }, + { test: /.png$/u, contentType: 'image/png', isText: false }, + { test: /.gif$/u, contentType: 'image/gif', isText: false }, + { test: /.jp(e)?g$/u, contentType: 'image/jpeg', isText: false }, + { test: /.ico$/u, contentType: 'image/vnd.microsoft.icon', isText: false }, + { test: /.tif(f)?$/u, contentType: 'image/png', isText: false }, + { test: /.aac$/u, contentType: 'audio/aac', isText: false }, + { test: /.mp3$/u, contentType: 'audio/mpeg', isText: false }, + { test: /.avi$/u, contentType: 'video/x-msvideo', isText: false }, + { test: /.mp4$/u, contentType: 'video/mp4', isText: false }, + { test: /.mpeg$/u, contentType: 'video/mpeg', isText: false }, + { test: /.webm$/u, contentType: 'video/webm', isText: false }, + { test: /.pdf$/u, contentType: 'application/pdf', isText: false }, + { test: /.tar$/u, contentType: 'application/x-tar', isText: false }, + { test: /.zip$/u, contentType: 'application/zip', isText: false }, + { test: /.eot$/u, contentType: 'application/vnd.ms-fontobject', isText: false }, + { test: /.otf$/u, contentType: 'font/otf', isText: false }, + { test: /.ttf$/u, contentType: 'font/ttf', isText: false }, + { test: /.woff$/u, contentType: 'font/woff', isText: false }, + { test: /.woff2$/u, contentType: 'font/woff2', isText: false }, ]; /** @@ -100,6 +102,7 @@ function getKnownContentTypes( finalContentTypes.push({ test: contentType.test, contentType: contentType.contentType, + isText: Boolean(contentType.isText), }); } } @@ -126,7 +129,7 @@ function getKnownContentTypes( function testFileContentType( contentTypes: ContentTypeDefinition[] | undefined, assetKey: string, -): { contentType: string } | null { +): { contentType: string; isText: boolean } | null { for (const contentType of contentTypes ?? defaultContentTypes) { const _assetKey = assetKey.toLowerCase(); let matched = false; @@ -135,7 +138,7 @@ function testFileContentType( : (matched = contentType.test(_assetKey)); if (matched) { - return { contentType: contentType.contentType }; + return { contentType: contentType.contentType, isText: contentType.isText }; } } return null; diff --git a/src/utils/input-path-verification.ts b/src/utils/input-path-verification.ts index afaef5d..c774c2e 100644 --- a/src/utils/input-path-verification.ts +++ b/src/utils/input-path-verification.ts @@ -18,9 +18,14 @@ async function validateFileExists(filePath: string): Promise { * Validates the input and output file paths. * @param input - The path to the input file. * @param output - The path to the output file. + * @param tsConfigPath - The path to the TypeScript configuration file (optional). * @throws An error if the paths are invalid. */ -async function validateFilePaths(input: string, output: string): Promise { +async function validateFilePaths( + input: string, + output: string, + tsConfigPath?: string, +): Promise { if (!(await isFile(input))) { colorLog('error', `Error: Input "${input}" is not a file`); process.exit(1); @@ -40,7 +45,7 @@ async function validateFilePaths(input: string, output: string): Promise { } } - if (containsSyntaxErrors(input)) { + if (containsSyntaxErrors(input, tsConfigPath)) { process.exit(1); } } diff --git a/src/utils/syntax-checker.ts b/src/utils/syntax-checker.ts index 67e6268..66de3f8 100644 --- a/src/utils/syntax-checker.ts +++ b/src/utils/syntax-checker.ts @@ -54,7 +54,7 @@ function isTypeScriptInstalled(): boolean { * @param tsconfigPath - The path to the TypeScript configuration file (optional). * @returns `true` if the file contains syntax errors, otherwise `false`. */ -function containsTypeScriptSyntaxErrors(tsInput: string, tsconfigPath?: string): boolean { +function containsTypeScriptSyntaxErrors(tsInput: string, tsConfigPath?: string): boolean { if (isTypeScriptInstalled()) { const includeFastEdgeTypes = process.env.NODE_ENV === 'test' @@ -64,6 +64,7 @@ function containsTypeScriptSyntaxErrors(tsInput: string, tsconfigPath?: string): const defaultTscBuildFlags = [ '--noEmit', '--skipLibCheck', + '--allowJs', '--strict', '--target', 'esnext', @@ -73,7 +74,7 @@ function containsTypeScriptSyntaxErrors(tsInput: string, tsconfigPath?: string): tsInput, ]; - const tscBuildFlags = tsconfigPath ? ['--project', tsconfigPath] : defaultTscBuildFlags; + const tscBuildFlags = tsConfigPath ? ['--project', tsConfigPath] : defaultTscBuildFlags; const nodeProcess: SpawnSyncReturns = spawnSync('npx', ['tsc', ...tscBuildFlags], { stdio: [null, null, null], @@ -100,13 +101,13 @@ function containsTypeScriptSyntaxErrors(tsInput: string, tsconfigPath?: string): * @param tsconfigPath - The path to the TypeScript configuration file (optional). * @returns `true` if the file contains syntax errors, otherwise `false`. */ -function containsSyntaxErrors(jsInput: string, tsconfigPath?: string): boolean { - if (jsInput.endsWith('.js')) { +function containsSyntaxErrors(jsInput: string, tsConfigPath?: string): boolean { + if (/\.(js|cjs|mjs)$/u.test(jsInput)) { return containsJavascriptSyntaxErrors(jsInput); } - if (jsInput.endsWith('.ts')) { - return containsTypeScriptSyntaxErrors(jsInput, tsconfigPath); + if (/\.(ts|tsx|jsx)$/u.test(jsInput)) { + return containsTypeScriptSyntaxErrors(jsInput, tsConfigPath); } colorLog('error', `Error: "${jsInput}" is not a valid file type - must be ".js" or ".ts"`); diff --git a/tsconfig.json b/tsconfig.json index 4771e07..055c116 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,6 @@ "isolatedModules": true, "esModuleInterop": true, "rootDir": "./src", - // "types": ["jest", "node"], "skipLibCheck": true, "baseUrl": ".", "paths": { diff --git a/types/server/static-assets/asset-loader/inline-asset/types.d.ts b/types/server/static-assets/asset-loader/inline-asset/types.d.ts index 704260d..c9e4682 100644 --- a/types/server/static-assets/asset-loader/inline-asset/types.d.ts +++ b/types/server/static-assets/asset-loader/inline-asset/types.d.ts @@ -25,6 +25,7 @@ interface StaticAssetMetadata { assetKey: string; contentType: string; fileInfo: FileInfo; + isText: boolean; } /** * Represents an inlined static asset. @@ -33,6 +34,7 @@ interface StaticAsset { type: string; assetKey: string; getMetadata(): StaticAssetMetadata; - getStoreEntry(arg: unknown): Promise; + getEmbeddedStoreEntry(arg: unknown): Promise; + getText(): string; } export type { ContentCompressionTypes, SourceAndInfo, StaticAsset, StaticAssetMetadata }; diff --git a/types/server/static-assets/static-server/static-server.d.ts b/types/server/static-assets/static-server/static-server.d.ts index 7dd0bb4..86f32a8 100644 --- a/types/server/static-assets/static-server/static-server.d.ts +++ b/types/server/static-assets/static-server/static-server.d.ts @@ -8,5 +8,5 @@ import type { StaticAsset } from '~static-assets/asset-loader/inline-asset/inlin * @param assetCache - The asset cache. * @returns A `StaticServer` instance. */ -declare const getStaticServer: (serverConfig: ServerConfig, assetCache: AssetCache) => StaticServer; +declare const getStaticServer: (serverConfig: ServerConfig, assetCache: AssetCache) => T; export { getStaticServer }; diff --git a/types/server/static-assets/static-server/types.d.ts b/types/server/static-assets/static-server/types.d.ts index 4457c88..ce1d1a9 100644 --- a/types/server/static-assets/static-server/types.d.ts +++ b/types/server/static-assets/static-server/types.d.ts @@ -2,6 +2,7 @@ import { ContentCompressionTypes, StaticAsset } from '../asset-loader/inline-ass interface ServerConfig extends Record { extendedCache: Array; publicDirPrefix: string; + routePrefix?: string; compression: string[]; notFoundPage: string | null; autoExt: string[]; @@ -24,11 +25,14 @@ interface AssetInit { * Represents the static server. */ interface StaticServer { + serveRequest(request: Request): Promise; + readFileString(path: string): Promise; +} +interface InternalStaticServer extends StaticServer { getMatchingAsset(path: string): StaticAsset | null; findAcceptEncodings(request: Request): Array; testExtendedCache(pathname: string): boolean; handlePreconditions(request: Request, asset: StaticAsset, responseHeaders: HeadersType): Response | null; serveAsset(request: Request, asset: StaticAsset, init?: AssetInit): Promise; - serveRequest(request: Request): Promise; } -export type { AssetInit, HeadersType, ServerConfig, StaticServer }; +export type { AssetInit, HeadersType, InternalStaticServer, ServerConfig, StaticServer }; diff --git a/types/utils/content-types.d.ts b/types/utils/content-types.d.ts index 0e2e79e..9e99064 100644 --- a/types/utils/content-types.d.ts +++ b/types/utils/content-types.d.ts @@ -4,6 +4,7 @@ interface ContentTypeDefinition { test: RegExp | ((assetKey: string) => boolean); contentType: string; + isText: boolean; } /** * Retrieves the default content types. @@ -23,6 +24,7 @@ declare function getKnownContentTypes(customContentTypes: ContentTypeDefinition[ */ declare function testFileContentType(contentTypes: ContentTypeDefinition[] | undefined, assetKey: string): { contentType: string; + isText: boolean; } | null; export { getKnownContentTypes, testFileContentType }; export type { ContentTypeDefinition };