Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 40 additions & 15 deletions integration-tests/fastedge-build.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
9 changes: 9 additions & 0 deletions src/cli/fastedge-build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -31,13 +33,15 @@ try {
'--help': Boolean,
'--input': String,
'--output': String,
'--tsconfig': String,
'--config': [String],

// Aliases
'-v': '--version',
'-h': '--help',
'-i': '--input',
'-o': '--output',
'-t': '--tsconfig',
'-c': '--config',
},
{
Expand Down Expand Up @@ -80,6 +84,10 @@ if (args['--output']) {
outputFileName = args['--output'];
}

if (args['--tsconfig']) {
tsConfigPath = args['--tsconfig'];
}

if (args._.length === 2) {
[inputFileName, outputFileName] = args._;
}
Expand All @@ -96,6 +104,7 @@ if (inputFileName && outputFileName) {
await buildWasm({
entryPoint: inputFileName,
wasmOutput: outputFileName,
tsConfigPath,
});
colorLog('success', `Build success!!`);
colorLog('info', `"${inputFileName}" -> "${outputFileName}"`);
Expand Down
8 changes: 4 additions & 4 deletions src/cli/fastedge-build/config-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
await validateFilePaths(entryPoint, wasmOutput);
async function buildWasm({ entryPoint, wasmOutput, tsConfigPath }: BuildConfig): Promise<void> {
await validateFilePaths(entryPoint, wasmOutput, tsConfigPath);
if (process.env.NODE_ENV !== 'test') {
await componentize(entryPoint, wasmOutput);
}
Expand All @@ -29,12 +29,12 @@ async function buildFromConfig(config: BuildConfig | null): Promise<void> {
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: {
Expand Down
1 change: 1 addition & 0 deletions src/cli/fastedge-build/print-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const USAGE_TEXT = `\nUsage: fastedge-build [options]
--version, -v Print the version number
--input, -i <input-file> Js filepath to build (e.g. ./src/index.js)
--output, -o <output-file> Output filepath for wasm (e.g. ./dist/main.wasm)
--tsconfig, -t <tsconfig-file> Path to a TypeScript config file (default: ./tsconfig.json)
--config, -c <config-file> Path to a build config file (default: ./.fastedge/build-config.js)
`;

Expand Down
1 change: 1 addition & 0 deletions src/cli/fastedge-build/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface BuildConfig extends Partial<AssetCacheConfig> {
type?: BuildType;
entryPoint: string;
wasmOutput: string;
tsConfigPath?: string;
}

export type { BuildConfig, BuildType };
6 changes: 5 additions & 1 deletion src/cli/fastedge-init/create-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface ConfigTypeObject {
notFoundPage?: string;
autoExt?: string[];
autoIndex?: string[];
[key: string]: unknown;
};
}

Expand All @@ -40,12 +41,15 @@ type DefaultConfig = Record<ConfigObjType, ConfigTypeObject>;
/** 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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const createTestMetadata = (overrides?: Partial<StaticAssetMetadata>): StaticAss
assetKey: 'test-asset',
type: 'wasm-inline',
contentType: 'text/html',
isText: true,
fileInfo: {
assetPath: '/test.html',
hash: 'hash123',
Expand Down Expand Up @@ -48,6 +49,7 @@ describe('createStaticAssetsCache', () => {
type: 'wasm-inline',
getMetadata: jest.fn(),
getEmbeddedStoreEntry: jest.fn(),
getText: jest.fn(),
} as jest.Mocked<StaticAsset>;

beforeEach(() => {
Expand Down Expand Up @@ -294,6 +296,7 @@ describe('createStaticAssetsCache', () => {
assetKey: 'minimal',
type: 'wasm-inline',
contentType: 'text/plain',
isText: true,
fileInfo: {
assetPath: '/minimal.txt',
hash: 'hash',
Expand All @@ -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)}`,
Expand Down
106 changes: 69 additions & 37 deletions src/server/static-assets/asset-loader/__tests__/inline-asset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const createTestMetadata = (overrides?: Partial<StaticAssetMetadata>): StaticAss
assetKey: 'test-asset',
type: 'wasm-inline',
contentType: 'text/html; charset=utf-8',
isText: true,
fileInfo: {
assetPath: '/path/to/test.html',
hash: 'sha256-abcdef123456',
Expand Down Expand Up @@ -347,6 +348,7 @@ describe('inline-asset', () => {
assetKey: 'minimal',
type: 'wasm-inline',
contentType: 'text/plain',
isText: true,
fileInfo: {
assetPath: '/minimal.txt',
hash: 'hash',
Expand Down Expand Up @@ -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");
});
});
});
});
Loading