Skip to content

Commit

Permalink
Overhaul filesystem operations
Browse files Browse the repository at this point in the history
- Added `Switch.mkdirSync()`, `Switch.removeSync()`, `Switch.statSync()`
- Read operations return `null` for `ENOENT`, instead of throwing an error
- `Switch.remove()` and `Switch.removeSync()` work with directories, and delete recursively
- `Switch.writeFileSync()` creates parent directories recursively as needed
  • Loading branch information
TooTallNate committed Jan 13, 2024
1 parent 0f84ee1 commit 14657f0
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 53 deletions.
10 changes: 10 additions & 0 deletions .changeset/perfect-pugs-smell.md
@@ -0,0 +1,10 @@
---
'nxjs-runtime': patch
---

Overhaul filesystem operations:

* Added `Switch.mkdirSync()`, `Switch.removeSync()`, `Switch.statSync()`
* Read operations return `null` for `ENOENT`, instead of throwing an error
* `Switch.remove()` and `Switch.removeSync()` work with directories, and delete recursively
* `Switch.writeFileSync()` creates parent directories recursively as needed
18 changes: 10 additions & 8 deletions apps/tests/src/switch.ts
Expand Up @@ -41,14 +41,16 @@ test('`Switch.readDirSync()` works on `romfs:/` path', () => {
assert.ok(files.length > 0);
});

test('`Switch.readDirSync()` throws error when directory does not exist', () => {
let err: Error | undefined;
try {
Switch.readDirSync('romfs:/__does_not_exist__');
} catch (_err) {
err = _err as Error;
}
assert.ok(err);
test('`Switch.readFileSync()` returns `null` when file does not exist', () => {
assert.equal(Switch.readFileSync('romfs:/__does_not_exist__'), null);
});

test('`Switch.readDirSync()` returns `null` when directory does not exist', () => {
assert.equal(Switch.readDirSync('romfs:/__does_not_exist__'), null);
});

test('`Switch.statSync()` returns `null` when file does not exist', () => {
assert.equal(Switch.statSync('romfs:/__does_not_exist__'), null);
});

test('`Switch.resolveDns()` works', async () => {
Expand Down
13 changes: 8 additions & 5 deletions packages/runtime/src/$.ts
Expand Up @@ -109,12 +109,15 @@ export interface Init {
getSystemFont(): ArrayBuffer;

// fs.c
readFile(cb: Callback<ArrayBuffer>, path: string): void;
readDirSync(path: string): string[];
readFileSync(path: string): ArrayBuffer;
writeFileSync(path: string, data: ArrayBuffer): void;
mkdirSync(path: string, mode: number): number;
readDirSync(path: string): string[] | null;
readFile(cb: Callback<ArrayBuffer | null>, path: string): void;
readFileSync(path: string): ArrayBuffer | null;
remove(cb: Callback<void>, path: string): void;
stat(cb: Callback<Stats>, path: string): void;
removeSync(path: string): void;
stat(cb: Callback<Stats | null>, path: string): void;
statSync(path: string): Stats | null;
writeFileSync(path: string, data: ArrayBuffer): void;

// image.c
imageInit(c: ClassOf<Image | ImageBitmap>): void;
Expand Down
13 changes: 8 additions & 5 deletions packages/runtime/src/fetch/fetch.ts
Expand Up @@ -230,11 +230,14 @@ async function fetchFile(req: Request, url: URL) {
const path = url.protocol === 'file:' ? `sdmc:${url.pathname}` : url.href;
// TODO: Use streaming FS interface
const data = await readFile(path);
return new Response(data, {
headers: {
'content-length': String(data.byteLength),
},
});
const headers = new Headers();
let status = 200;
if (data) {
headers.set('content-length', String(data.byteLength));
} else {
status = 404;
}
return new Response(data, { status, headers });
}

const fetchers = new Map<string, (req: Request, url: URL) => Promise<Response>>(
Expand Down
45 changes: 44 additions & 1 deletion packages/runtime/src/fs.ts
Expand Up @@ -3,6 +3,25 @@ import { bufferSourceToArrayBuffer, pathToString, toPromise } from './utils';
import { encoder } from './polyfills/text-encoder';
import type { PathLike } from './switch';

/**
* Creates the directory at the provided `path`, as well as any necessary parent directories.
*
* @example
*
* ```typescript
* const count = Switch.mkdirSync('sdmc:/foo/bar/baz');
* console.log(`Created ${count} directories`);
* // Created 3 directories
* ```
*
* @param path Path of the directory to create.
* @param mode The file mode to set for the directories. Default: `0o777`.
* @returns The number of directories created. If the directory already exists, returns `0`.
*/
export function mkdirSync(path: PathLike, mode = 0o777) {
return $.mkdirSync(pathToString(path), mode);
}

/**
* Returns a Promise which resolves to an `ArrayBuffer` containing
* the contents of the file at `path`.
Expand Down Expand Up @@ -51,6 +70,8 @@ export function readFileSync(path: PathLike) {
/**
* Synchronously writes the contents of `data` to the file at `path`.
*
* @example
*
* ```typescript
* const appStateJson = JSON.stringify(appState);
* Switch.writeFileSync('sdmc:/switch/awesome-app/state.json', appStateJson);
Expand All @@ -63,15 +84,37 @@ export function writeFileSync(path: PathLike, data: string | BufferSource) {
}

/**
* Removes the file or directory specified by `path`.
* Synchronously removes the file or directory recursively specified by `path`.
*
* @param path File path to remove.
*/
export function removeSync(path: PathLike) {
return $.removeSync(pathToString(path));
}

/**
* Removes the file or directory recursively specified by `path`.
*
* @param path File path to remove.
*/
export function remove(path: PathLike) {
return toPromise($.remove, pathToString(path));
}

/**
*
* @param path File path to retrieve file stats for.
* @returns Object containing the file stat information of `path`, or `null` if the file does not exist.
*/
export function statSync(path: PathLike) {
return $.statSync(pathToString(path));
}

/**
* Returns a Promise which resolves to an object containing
* information about the file pointed to by `path`.
*
* @param path File path to retrieve file stats for.
*/
export function stat(path: PathLike) {
return toPromise($.stat, pathToString(path));
Expand Down
40 changes: 23 additions & 17 deletions packages/runtime/src/source-map.ts
Expand Up @@ -51,25 +51,31 @@ function filenameToTracer(filename: string) {
tracer = null;

const contentsBuffer = readFileSync(filename);
const contents = new TextDecoder().decode(contentsBuffer).trimEnd();
const lastNewline = contents.lastIndexOf('\n');
const lastLine = contents.slice(lastNewline + 1);
if (lastLine.startsWith(SOURCE_MAPPING_URL_PREFIX)) {
const sourceMappingURL = lastLine.slice(
SOURCE_MAPPING_URL_PREFIX.length
);
let sourceMapBuffer: ArrayBuffer;
if (sourceMappingURL.startsWith('data:')) {
sourceMapBuffer = dataUriToBuffer(sourceMappingURL).buffer;
} else {
sourceMapBuffer = readFileSync(new URL(sourceMappingURL, filename));
if (contentsBuffer) {
const contents = new TextDecoder().decode(contentsBuffer).trimEnd();
const lastNewline = contents.lastIndexOf('\n');
const lastLine = contents.slice(lastNewline + 1);
if (lastLine.startsWith(SOURCE_MAPPING_URL_PREFIX)) {
const sourceMappingURL = lastLine.slice(
SOURCE_MAPPING_URL_PREFIX.length
);
let sourceMapBuffer: ArrayBuffer | null;
if (sourceMappingURL.startsWith('data:')) {
sourceMapBuffer = dataUriToBuffer(sourceMappingURL).buffer;
} else {
sourceMapBuffer = readFileSync(
new URL(sourceMappingURL, filename)
);
}
if (sourceMapBuffer) {
const sourceMap: EncodedSourceMap = JSON.parse(
new TextDecoder().decode(sourceMapBuffer)
);
tracer = new TraceMap(sourceMap);
}
}
const sourceMap: EncodedSourceMap = JSON.parse(
new TextDecoder().decode(sourceMapBuffer)
);
tracer = new TraceMap(sourceMap);
sourceMapCache.set(filename, tracer);
}
sourceMapCache.set(filename, tracer);
return tracer;
}

Expand Down

0 comments on commit 14657f0

Please sign in to comment.