diff --git a/samples/kotlin-js-store/yarn.lock b/samples/kotlin-js-store/yarn.lock index e45aa1dd40..064c2b84b7 100644 --- a/samples/kotlin-js-store/yarn.lock +++ b/samples/kotlin-js-store/yarn.lock @@ -52,6 +52,11 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@js-joda/core@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" + integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" @@ -851,6 +856,14 @@ dom-serialize@^2.2.1: extend "^3.0.0" void-elements "^2.0.0" +dukat@0.5.8-rc.4: + version "0.5.8-rc.4" + resolved "https://registry.yarnpkg.com/dukat/-/dukat-0.5.8-rc.4.tgz#90384dcb50b14c26f0e99dae92b2dea44f5fce21" + integrity sha512-ZnMt6DGBjlVgK2uQamXfd7uP/AxH7RqI0BL9GLrrJb2gKdDxvJChWy+M9AQEaL+7/6TmxzJxFOsRiInY9oGWTA== + dependencies: + google-protobuf "3.12.2" + typescript "3.9.5" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -1212,6 +1225,11 @@ glob@^7.1.3, glob@^7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" +google-protobuf@3.12.2: + version "3.12.2" + resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.12.2.tgz#50ce9f9b6281235724eb243d6a83e969a2176e53" + integrity sha512-4CZhpuRr1d6HjlyrxoXoocoGFnRYgKULgMtikMddA9ztRyYR59Aondv2FioyxWVamRo0rF2XpYawkTCBEQOSkA== + graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" @@ -2437,6 +2455,11 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typescript@3.9.5: + version "3.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36" + integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ== + ua-parser-js@^0.7.30: version "0.7.31" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" diff --git a/samples/world-clock/presenters/build.gradle.kts b/samples/world-clock/presenters/build.gradle.kts index 0129c24957..775bfb9366 100644 --- a/samples/world-clock/presenters/build.gradle.kts +++ b/samples/world-clock/presenters/build.gradle.kts @@ -23,6 +23,7 @@ kotlin { val commonMain by getting { dependencies { api("app.cash.zipline:zipline") + api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") // TODO gradle/libs.versions.toml } } val hostMain by creating { diff --git a/samples/world-clock/presenters/src/commonMain/kotlin/app/cash/zipline/samples/worldclock/common.kt b/samples/world-clock/presenters/src/commonMain/kotlin/app/cash/zipline/samples/worldclock/common.kt index 94299792a3..ae72c1407c 100644 --- a/samples/world-clock/presenters/src/commonMain/kotlin/app/cash/zipline/samples/worldclock/common.kt +++ b/samples/world-clock/presenters/src/commonMain/kotlin/app/cash/zipline/samples/worldclock/common.kt @@ -17,6 +17,7 @@ package app.cash.zipline.samples.worldclock import app.cash.zipline.ZiplineService import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.TimeZone import kotlinx.serialization.Serializable @Serializable @@ -29,10 +30,16 @@ data class WorldClockModel( val label: String, ) +@Serializable +data class TimeZoneModel( + val name: String, + val zone: TimeZone, +) + interface WorldClockPresenter : ZiplineService { fun models(events: Flow): Flow } interface WorldClockHost : ZiplineService { - fun timeZones(): List + fun timeZones(): List } diff --git a/samples/world-clock/presenters/src/hostMain/kotlin/app/cash/zipline/samples/worldclock/RealWorldClockHost.kt b/samples/world-clock/presenters/src/hostMain/kotlin/app/cash/zipline/samples/worldclock/RealWorldClockHost.kt index d7afe0cc47..28d548c879 100644 --- a/samples/world-clock/presenters/src/hostMain/kotlin/app/cash/zipline/samples/worldclock/RealWorldClockHost.kt +++ b/samples/world-clock/presenters/src/hostMain/kotlin/app/cash/zipline/samples/worldclock/RealWorldClockHost.kt @@ -15,8 +15,14 @@ */ package app.cash.zipline.samples.worldclock +import kotlinx.datetime.TimeZone + class RealWorldClockHost : WorldClockHost { - override fun timeZones(): List { - TODO() + override fun timeZones(): List { + return listOf( + TimeZoneModel("Barcelona", TimeZone.of("CEST")), + TimeZoneModel("NYC", TimeZone.of("EST")), + TimeZoneModel("SF", TimeZone.of("PST")), + ) } } diff --git a/samples/world-clock/presenters/src/jsMain/kotlin/app/cash/zipline/samples/worldclock/TimeFormatter.kt b/samples/world-clock/presenters/src/jsMain/kotlin/app/cash/zipline/samples/worldclock/TimeFormatter.kt index 2051d4f11a..d185251a49 100644 --- a/samples/world-clock/presenters/src/jsMain/kotlin/app/cash/zipline/samples/worldclock/TimeFormatter.kt +++ b/samples/world-clock/presenters/src/jsMain/kotlin/app/cash/zipline/samples/worldclock/TimeFormatter.kt @@ -16,8 +16,23 @@ package app.cash.zipline.samples.worldclock import kotlin.js.Date +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.datetime.toLocalDateTime class TimeFormatter { + fun format( + timestamp: Instant = Clock.System.now(), + timeZones: List, + ): String { + return timeZones.joinToString("\n\n") { + """ + |${it.name} + |${timestamp.toLocalDateTime(it.zone)} + """.trimMargin() + } + } + fun formatLocalTime( now: dynamic = Date(), millis: Boolean = false, diff --git a/samples/world-clock/presenters/src/jsMain/kotlin/app/cash/zipline/samples/worldclock/js.kt b/samples/world-clock/presenters/src/jsMain/kotlin/app/cash/zipline/samples/worldclock/js.kt index 569d0dde01..459babefb1 100644 --- a/samples/world-clock/presenters/src/jsMain/kotlin/app/cash/zipline/samples/worldclock/js.kt +++ b/samples/world-clock/presenters/src/jsMain/kotlin/app/cash/zipline/samples/worldclock/js.kt @@ -44,7 +44,7 @@ class RealWorldClockPresenter( while (true) { emit( WorldClockModel( - label = TimeFormatter().formatLocalTime() + label = TimeFormatter().format(timeZones = host.timeZones()) ) ) delay(16) diff --git a/zipline-gradle-plugin/build.gradle.kts b/zipline-gradle-plugin/build.gradle.kts index 88fc40f7c5..34ad8dc10b 100644 --- a/zipline-gradle-plugin/build.gradle.kts +++ b/zipline-gradle-plugin/build.gradle.kts @@ -5,6 +5,7 @@ import com.vanniktech.maven.publish.MavenPublishBaseExtension plugins { id("java-gradle-plugin") kotlin("jvm") + kotlin("plugin.serialization") id("com.github.gmazzo.buildconfig") id("org.jetbrains.dokka") id("com.vanniktech.maven.publish.base") diff --git a/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineCompileTask.kt b/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineCompileTask.kt index b14324f9f0..b454e0145f 100644 --- a/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineCompileTask.kt +++ b/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineCompileTask.kt @@ -46,6 +46,10 @@ abstract class ZiplineCompileTask : DefaultTask() { @get:OutputDirectory abstract val outputDir: DirectoryProperty + @get:Optional + @get:InputDirectory + abstract val nodeModuleDir: DirectoryProperty + @get:Optional @get:Input abstract val mainModuleId: Property @@ -65,6 +69,7 @@ abstract class ZiplineCompileTask : DefaultTask() { fun task(inputChanges: InputChanges) { val inputDirFile = inputDir.get().asFile val outputDirFile = outputDir.get().asFile + val nodeModuleDirFile = nodeModuleDir.orNull?.asFile val mainModuleId = mainModuleId.orNull val mainFunction = mainFunction.orNull val signingKeys = signingKeys.get() @@ -101,6 +106,7 @@ abstract class ZiplineCompileTask : DefaultTask() { ZiplineCompiler.compile( inputDir = inputDirFile, outputDir = outputDirFile, + nodeModulesDir = nodeModuleDirFile, mainFunction = mainFunction, mainModuleId = mainModuleId, manifestSigner = manifestSigner, diff --git a/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineCompiler.kt b/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineCompiler.kt index 075e990bb4..042d59b6f9 100644 --- a/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineCompiler.kt +++ b/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineCompiler.kt @@ -18,6 +18,7 @@ package app.cash.zipline.gradle import app.cash.zipline.QuickJs import app.cash.zipline.bytecode.applySourceMapToBytecode +import app.cash.zipline.gradle.internal.NpmPackage import app.cash.zipline.loader.CURRENT_ZIPLINE_VERSION import app.cash.zipline.loader.ManifestSigner import app.cash.zipline.loader.ZiplineFile @@ -40,16 +41,22 @@ internal object ZiplineCompiler { private const val MODULE_PATH_PREFIX = "./" private const val ZIPLINE_EXTENSION = ".zipline" + private val npmPackageJson = Json { ignoreUnknownKeys = true } + fun compile( inputDir: File, outputDir: File, + nodeModulesDir: File?, mainFunction: String?, mainModuleId: String?, manifestSigner: ManifestSigner?, version: String?, ) { val jsFiles = getJsFiles(inputDir.listFiles()!!.asList()) - val modules = compileFilesInParallel(jsFiles, outputDir) + var modules = compileFilesInParallel(jsFiles, outputDir) + if (nodeModulesDir != null) { + modules = compileNodeModules(modules, nodeModulesDir, outputDir) + } writeManifest( outputDir = outputDir, mainFunction = mainFunction, @@ -70,6 +77,7 @@ internal object ZiplineCompiler { manifestSigner: ManifestSigner?, version: String?, ) { + // TODO support node_modules resolution val modifiedFileNames = getJsFiles(modifiedFiles).map { it.name }.toSet() val removedFileNames = getJsFiles(removedFiles).map { it.name }.toSet() @@ -113,6 +121,29 @@ internal object ZiplineCompiler { .toMap() } + private fun compileNodeModules( + modules: Map, + nodeModulesDir: File, + outputDir: File, + ): Map { + val allModules = modules.toMutableMap() + val dependencies = ArrayDeque(allModules.values.flatMap { it.dependsOnIds }) + while (dependencies.isNotEmpty()) { + val dependency = dependencies.removeFirst() + if (dependency !in allModules) { + // TODO is `main` the right property to be reading? + val dependencyDir = nodeModulesDir.resolve(dependency) + val packageJson = dependencyDir.resolve("package.json") + val npmPackage = npmPackageJson.decodeFromString(packageJson.readText()) + val jsFile = dependencyDir.resolve(npmPackage.main ?: TODO()) + val (_, module) = compileSingleFile(jsFile, outputDir) + allModules[dependency] = module + dependencies.addAll(module.dependsOnIds) + } + } + return allModules + } + private fun compileSingleFile( jsFile: File, outputDir: File, @@ -189,10 +220,12 @@ fun main(vararg args: String) { val inputDir = File(argsList.removeFirst()) val outputDir = File(argsList.removeFirst()) + val nodeModulesDir = argsList.removeFirstOrNull()?.let { File(it) } // TODO new required argument? outputDir.mkdirs() ZiplineCompiler.compile( inputDir = inputDir, outputDir = outputDir, + nodeModulesDir = nodeModulesDir, mainFunction = argsList.removeFirstOrNull(), mainModuleId = argsList.removeFirstOrNull(), manifestSigner = null, diff --git a/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplinePlugin.kt b/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplinePlugin.kt index 34b33ca533..cdde8b7721 100644 --- a/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplinePlugin.kt +++ b/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplinePlugin.kt @@ -31,6 +31,7 @@ import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact import org.jetbrains.kotlin.gradle.plugin.SubpluginOption import org.jetbrains.kotlin.gradle.targets.js.ir.JsIrBinary import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget +import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask import org.slf4j.LoggerFactory @Suppress("unused") // Created reflectively by Gradle. @@ -67,6 +68,9 @@ class ZiplinePlugin : KotlinCompilerPluginSupportPlugin { kotlinBinary: JsIrBinary, configuration: ZiplineExtension, ) { + // TODO is this the best way to get the node_modules directory? + val npmInstallTask = project.rootProject.tasks.withType(KotlinNpmInstallTask::class.java).single() + // Like 'compileDevelopmentExecutableKotlinJsZipline'. val linkTaskName = kotlinBinary.linkTaskName val compileZiplineTaskName = "${linkTaskName}Zipline" @@ -81,6 +85,7 @@ class ZiplinePlugin : KotlinCompilerPluginSupportPlugin { createdTask.inputDir.fileProvider(linkOutputFolderProvider) createdTask.outputDir.fileProvider(linkOutputFolderProvider.map { it.parentFile.resolve("${it.name}Zipline") }) + createdTask.nodeModuleDir.fileValue(npmInstallTask.nodeModulesDir) createdTask.mainModuleId.set(configuration.mainModuleId) createdTask.mainFunction.set(configuration.mainFunction) createdTask.version.set(configuration.version) diff --git a/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/internal/NpmPackage.kt b/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/internal/NpmPackage.kt new file mode 100644 index 0000000000..41841f6ded --- /dev/null +++ b/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/internal/NpmPackage.kt @@ -0,0 +1,8 @@ +package app.cash.zipline.gradle.internal + +import kotlinx.serialization.Serializable + +@Serializable +internal data class NpmPackage( + val main: String?, +) diff --git a/zipline-gradle-plugin/src/test/kotlin/app/cash/zipline/gradle/ZiplineCompilerTest.kt b/zipline-gradle-plugin/src/test/kotlin/app/cash/zipline/gradle/ZiplineCompilerTest.kt index 384870daf4..e7d97ab8e7 100644 --- a/zipline-gradle-plugin/src/test/kotlin/app/cash/zipline/gradle/ZiplineCompilerTest.kt +++ b/zipline-gradle-plugin/src/test/kotlin/app/cash/zipline/gradle/ZiplineCompilerTest.kt @@ -145,6 +145,7 @@ class ZiplineCompilerTest { ZiplineCompiler.compile( inputDir = inputDir, outputDir = outputDir, + nodeModulesDir = null, // TODO add node_modules test case mainFunction = mainFunction, mainModuleId = mainModuleId, manifestSigner = null,