Proposals for file system implementations #485
-
ProblemInlang needs to define a filesystem API that works in server and browser contexts. Inlang used two browser filesystem implementations already. Lightning fs and memfs. Both come with downsides that lead to this discussion. Lightning fs limitations
memfs limitations
|
Beta Was this translation helpful? Give feedback.
Replies: 7 comments 29 replies
-
RequirementsUse this thread to discuss requirements. This post will be updated as the discussion evolves. |
Beta Was this translation helpful? Give feedback.
-
Idea: Plugin based filesystemThe requirements are so diverse that a filesystem with plugins might be the way to go. Just a sketch: // browser fs
const fs = createFs({ storage: localStorage, plugins: [
gitLazyLoading(),
]}) // server fs
import * as nodefs from "node:fs/promises"
const fs = createFs({ storage: localFilesystem(nodefs), plugins: [
gitLazyLoading()
]}) // vscode fs
import * as vscodefs from "vsce"
const fs = createFs({ storage: vscodefs, plugins: [
gitLazyLoading()
]}) Pros and Cons✅ flexible |
Beta Was this translation helpful? Give feedback.
-
Idea: Define a API schema and provide mappingsThis is kind of what we are doing now but with wrappers instead of trying to force everything into the correct types. Defining a minimal filesystem API. Here is the current implementation https://github.com/inlang/inlang/blob/main/source-code/core/src/config/environment-functions/%24fs.ts. Note how it uses memfs which leads to type issues. // minimal fs that is required for inlang and the git-sdk
type FsApi = {
readFile: (uri: string) => string
writeFile: (uri: string, data: string) => string
// ...
} Example wrapper for filesystems. Here is one in production https://github.com/inlang/inlang/blob/main/source-code/ide-extension/src/utils/createFileSystemMapper.ts. // wrapper for node (or vscode) fs
function nodeFs(fs: fs): FsApi {
return {
readFile: (uri) => {
fs.readFile(uri, { encoding: "utf-8"} )
},
writeFile: (uri) => {
fs.writeFile(uri, { encoding: "utf-8"} )
}
}
} We can export filesystems suited for different contexts // example on the server
import fs from "node:fs/promises"
import { nodeFs } from "@inlang/fs"
// wrapping the API around node's fs.
clone({ fs: nodeFs(fs) }) // example for vscode
import fs from "vsce"
import { vscodeFs } from "@inlang/fs"
// wrapping the API around node's fs.
clone({ fs: vscodeFs(fs) }) // in the browser (localStorageFs is a custom fs implementation by us).
import { localStorageFs } from "@inlang/fs"
clone({ fs: localStorageFs }) // for testing purposes
import { memoryFs } from "@inlang/fs"
clone({ fs: memoryFs }) ✅ Obeys to the "filesystem contract". Other apps can use the environment's filesystem as expected. |
Beta Was this translation helpful? Give feedback.
-
Moving a discussion from Discord (link) @araknast wrote: Definitely agree with the api approach opposed to plugins, then fs support for vscode and node environments is trivial. That leaves the browser environment, for which I see 3 main approaches:
The approach we take depends on the needs of the editor, curious what you think |
Beta Was this translation helpful? Give feedback.
-
Should we go for OOP or a functional/object-based implementation?The discussion in #540 (comment) revealed that an open question is whether an OOP or functional approach leads to better ergonomics.
Benefits of a functional approachOne type defines the entire interfaceUsing a functional approach allows us to define a schema like: type Filesystem = {
readFile: (path: string) => Promise<string>
fromJson: (json: string) => Filesystem
// ...
} An OOP approach with static methods would prevent async function loadConfig(fs: Fs) {
const config = await fs.readFile("./inlang.config.js")
// copying the filesystem with the fromJson method.
const copy = fs.fromJson(fs.toJson())
await copy.writeFile("./inlang/metadata/config.js", config)
return copy
} The consuming function Functional approach can be tree-shaken/use dynamic imports to reduce bundle sizeSince the filesystem API will be async only, we can use dynamic imports to reduce the bundling size of the filesystem. While this argument doesn't matter with a few functions, a complicated filesystem implementation with heavy caching capabilities (for example) and lots of functions likely benefits from a functional approach. A functional approach should ease bundle size reductions and performance optimizations. For example, bundlers can tree-shake functions. The function createMemoryFs(): Filesystem {
const files = []
// other state
// all methods are lazy loaded
return {
readFile: async (...args) => (await import("./readFile.js")).default(args)
fromJson: async (...args) => (await import("./fromJson.js")).default(args)
// ...
}
} Simpler composabilityComposability seems simpler if a functional approach is chosen. For example, I imagine following code patterns to eventually merge that share code between implementations. While yes, OOP can achieve a similar effect, defining a well, and independently, tested function createNodeFs(): Filesystem {
return {
readFile: lazyLoad(fs.readFile)
// ...
}
} function createMemoryFs(): Filesystem {
return {
readFile: lazyLoad(readFile)
// ...
}
} Writing reactivity wrappers likely becomes easy too: function withSolid(fs: Filesystem): Filesystem {
const retrievedFiles = createStore({})
return {
readFile: async (path: string) => {
const file = await fs.readFile(path)
setStore(path, file)
return retrievedFiles[path]
}
}
} (The rest of the code base uses the functional approach)Minor but the rest of inlang uses the functional approach. |
Beta Was this translation helpful? Give feedback.
-
I had tested an OOP approach with an ORM-like implementation for My advice would be to go with a functional approach. |
Beta Was this translation helpful? Give feedback.
-
We decided on a function approach that is a subset of |
Beta Was this translation helpful? Give feedback.
We decided on a function approach that is a subset of
node:fs/promises
.