Skip to content

Commit 54914dc

Browse files
authored
Basic filesystems support for Wasm: reuse JS implementation for wasmJs target (#256)
* Split Wasm source sets * Supported filesystem for wasmJs target (`node:fs` along with a few related packages is required).
1 parent d960b3b commit 54914dc

File tree

20 files changed

+384
-197
lines changed

20 files changed

+384
-197
lines changed

build-logic/src/main/kotlin/kotlinx/io/conventions/kotlinx-io-multiplatform.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ kotlin {
9494
createSourceSet("linuxTest", parent = unixTest, children = linuxTargets())
9595
createSourceSet("androidMain", parent = unixMain, children = androidTargets())
9696
createSourceSet("androidTest", parent = unixTest, children = androidTargets())
97+
createSourceSet("nodeFilesystemSharedMain", parent = commonMain.get(), children = nodeTargets())
98+
createSourceSet("nodeFilesystemSharedTest", parent = commonTest.get(), children = nodeTargets())
9799
createSourceSet("wasmMain", parent = commonMain.get(), children = wasmTargets())
98100
createSourceSet("wasmTest", parent = commonTest.get(), children = wasmTargets())
99101
}
@@ -200,6 +202,8 @@ fun androidTargets() = listOf(
200202

201203
fun wasmTargets() = listOf("wasmJs", "wasmWasi")
202204

205+
fun nodeTargets() = listOf("js", "wasmJs")
206+
203207
rootProject.the<NodeJsRootExtension>().apply {
204208
nodeVersion = "21.0.0-v8-canary202310177990572111"
205209
nodeDownloadBaseUrl = "https://nodejs.org/download/v8-canary"

core/build.gradle.kts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,6 @@ kotlin {
4343
}
4444
}
4545

46-
@OptIn(org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl::class)
47-
wasmJs {
48-
filterSmokeTests()
49-
}
50-
5146
@OptIn(org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl::class)
5247
wasmWasi {
5348
filterSmokeTests()

core/js/src/-PlatfromJs.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,18 @@ public actual open class IOException actual constructor(
2121
public actual open class EOFException actual constructor(message: String?) : IOException(message) {
2222
public actual constructor() : this(null)
2323
}
24+
25+
@Suppress("ACTUAL_WITHOUT_EXPECT")
26+
internal actual typealias CommonJsModule = JsModule
27+
28+
@Suppress("ACTUAL_WITHOUT_EXPECT")
29+
internal actual typealias CommonJsNonModule = JsNonModule
30+
31+
internal actual fun withCaughtException(block: () -> Unit): Throwable? {
32+
try {
33+
block()
34+
return null
35+
} catch (t: Throwable) {
36+
return t
37+
}
38+
}

core/js/src/imports.kt

Lines changed: 0 additions & 42 deletions
This file was deleted.

core/js/test/utils.kt

Lines changed: 0 additions & 49 deletions
This file was deleted.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.io
7+
8+
// The source set is shared by js and wasmJs targets that don't have a single common JS-source set.
9+
// As a result, JsModule and JsNonModule annotations are unavailable. To overcome that issue,
10+
// the following expects are declared in this module and actualized in the leaf source sets.
11+
12+
/**
13+
* Actualized with JsModule on both JS and WasmJs.
14+
*/
15+
@Target(AnnotationTarget.FILE)
16+
internal expect annotation class CommonJsModule(val import: String)
17+
18+
/**
19+
* Actualized with JsNonModule on JS, left "empty" on WasmJs.
20+
*/
21+
@Target(AnnotationTarget.FILE)
22+
internal expect annotation class CommonJsNonModule()

core/js/src/files/FileSystemJs.kt renamed to core/nodeFilesystemShared/src/files/FileSystemNodeJs.kt

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,35 @@
55

66
package kotlinx.io.files
77

8-
import kotlinx.io.*
8+
import kotlinx.io.IOException
9+
import kotlinx.io.RawSink
10+
import kotlinx.io.RawSource
11+
import kotlinx.io.node.fs.*
12+
import kotlinx.io.node.os.platform
13+
import kotlinx.io.node.os.tmpdir
14+
import kotlinx.io.withCaughtException
915

1016
public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl() {
1117
override fun exists(path: Path): Boolean {
12-
check(fs !== null) { "Module 'fs' was not found" }
13-
return fs.existsSync(path.path) as Boolean
18+
return existsSync(path.path)
1419
}
1520

1621
override fun delete(path: Path, mustExist: Boolean) {
17-
check(fs !== null) { "Module 'fs' was not found" }
1822
if (!exists(path)) {
1923
if (mustExist) {
20-
throw FileNotFoundException("File does not exist: ${path.path}")
24+
throw FileNotFoundException("File does not exist: $path")
2125
}
2226
return
2327
}
24-
try {
25-
val stats = fs.statSync(path.path)
26-
if (stats.isDirectory() as Boolean) {
27-
fs.rmdirSync(path.path)
28+
withCaughtException {
29+
val stats = statSync(path.path) ?: throw FileNotFoundException("File does not exist: $path")
30+
if (stats.isDirectory()) {
31+
rmdirSync(path.path)
2832
} else {
29-
fs.rmSync(path.path)
33+
rmSync(path.path)
3034
}
31-
} catch (t: Throwable) {
32-
throw IOException("Delete failed for $path", t)
35+
}?.also {
36+
throw IOException("Delete failed for $path", it)
3337
}
3438
}
3539

@@ -52,65 +56,60 @@ public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl()
5256
p = p.parent
5357
}
5458
parts.asReversed().forEach {
55-
fs.mkdirSync(it)
59+
mkdirSync(it)
5660
}
5761
}
5862

5963
override fun atomicMove(source: Path, destination: Path) {
60-
check(fs !== null) { "Module 'fs' was not found" }
6164
if (!exists(source)) {
6265
throw FileNotFoundException("Source does not exist: ${source.path}")
6366
}
64-
try {
65-
fs.renameSync(source.path, destination.path)
66-
} catch (t: Throwable) {
67-
throw IOException("Move failed from $source to $destination", t)
67+
withCaughtException {
68+
renameSync(source.path, destination.path)
69+
}?.also {
70+
throw IOException("Move failed from $source to $destination", it)
6871
}
6972
}
7073

7174
override fun metadataOrNull(path: Path): FileMetadata? {
72-
check(fs !== null) { "Module 'fs' was not found" }
73-
return try {
74-
val stat = fs.statSync(path.path)
75-
val mode = stat.mode as Int
76-
val isFile = (mode and fs.constants.S_IFMT as Int) == fs.constants.S_IFREG
77-
FileMetadata(
75+
if (!exists(path)) return null
76+
var metadata: FileMetadata? = null
77+
withCaughtException {
78+
val stat = statSync(path.path) ?: return@withCaughtException
79+
val mode = stat.mode
80+
val isFile = (mode and constants.S_IFMT) == constants.S_IFREG
81+
metadata = FileMetadata(
7882
isRegularFile = isFile,
79-
isDirectory = (mode and fs.constants.S_IFMT as Int) == fs.constants.S_IFDIR,
80-
if (isFile) (stat.size as Int).toLong() else -1L
83+
isDirectory = (mode and constants.S_IFMT) == constants.S_IFDIR,
84+
if (isFile) stat.size.toLong() else -1L
8185
)
82-
} catch (t: Throwable) {
83-
if (exists(path)) throw IOException("Stat failed for $path", t)
84-
return null
86+
}?.also {
87+
throw IOException("Stat failed for $path", it)
8588
}
89+
return metadata
8690
}
8791

8892
override fun source(path: Path): RawSource {
89-
check(fs !== null) { "Module 'fs' was not found" }
9093
return FileSource(path)
9194
}
9295

9396
override fun sink(path: Path, append: Boolean): RawSink {
94-
check(fs !== null) { "Module 'fs' was not found" }
95-
check(buffer !== null) { "Module 'buffer' was not found" }
9697
return FileSink(path, append)
9798
}
9899

99100
override fun resolve(path: Path): Path {
100-
check(fs !== null) { "Module 'fs' was not found" }
101101
if (!exists(path)) throw FileNotFoundException(path.path)
102-
return Path(fs.realpathSync.native(path.path) as String)
102+
return Path(realpathSync.native(path.path))
103103
}
104104
}
105105

106106
public actual val SystemTemporaryDirectory: Path
107107
get() {
108-
check(os !== null) { "Module 'os' was not found" }
109-
return Path(os.tmpdir() as? String ?: "")
108+
return Path(tmpdir() ?: "")
110109
}
111110

112111
public actual open class FileNotFoundException actual constructor(
113112
message: String?,
114113
) : IOException(message)
115114

116-
internal actual val isWindows = os.platform() == "win32"
115+
internal actual val isWindows = platform() == "win32"

0 commit comments

Comments
 (0)