From 7ecff4697afdf3248613545dcd5c6f049315fd7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Wed, 2 Sep 2026 18:51:33 +0200 Subject: [PATCH 1/2] Add a pathPrefix setting for services mounted under a prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generated client uses each operation's `@http` URI verbatim. That is right when the model describes the whole path, but a service is often mounted under a prefix the model does not mention — a server framework deriving one from a trait, or a reverse proxy — and then every generated request misses it. `pathPrefix` is prepended to each operation's URI. It reaches the Storybook mocks too, split into literal segments, since the mock router matches segment by segment and would otherwise stop matching the client it is standing in for. Normalization accepts what people will actually write: a leading slash is optional, a trailing one is ignored, and `"/"` or whitespace means no prefix. Without that, `"/internal/v1/"` would emit `v1//things`. Deliberately a plain string rather than anything trait-derived: which prefix a deployment sits behind is not a property of the model, and a trait-aware version would have to encode one framework's conventions. Note for release: `generate` gains a parameter. The default value keeps it source-compatible, but it is a binary break, so this wants a minor bump rather than a patch. MiMa cannot see it — the 0.4 baseline has no published artifacts yet. --- README.md | 15 ++- .../org/polyvariant/smithy/ts/cli/Main.scala | 24 ++-- .../smithy/ts/TsCodegenPlugin.scala | 91 ++++++++++++--- .../smithy/ts/TsCodegenPluginTest.scala | 106 +++++++++++++++++- .../smithy/ts/sbt/SmithyTsCodegenPlugin.scala | 11 +- .../smithy/ts/sbt/TsCodegenRunner.scala | 5 +- .../smithy-ts-codegen/basic/build.sbt | 8 ++ 7 files changed, 228 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 0ed6ba0..406f315 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,8 @@ Add the artifact to your smithy-build classpath and reference the plugin by name "plugins": { "ts-codegen": { "outFile": "generated.ts", - "excludeServices": ["myorg.auth#AuthService"] + "excludeServices": ["myorg.auth#AuthService"], + "pathPrefix": "/internal/v1" } } } @@ -51,6 +52,12 @@ Settings: - `excludeServices` — fully-qualified service shape ids (`namespace#Name`) to skip. Their referenced data shapes are still emitted; only the operations and the client class are dropped. Use this for services you hand-roll (streaming, custom routing, …). +- `pathPrefix` — path prepended to every operation's `@http` URI (default: none). For a service + mounted under a prefix that the model itself does not describe — a server framework that + derives one from a trait, or a reverse proxy. A leading slash is optional and a trailing one + is ignored, so `"internal/v1"`, `"/internal/v1"` and `"/internal/v1/"` are equivalent. The + prefix applies to the generated Storybook mocks too, so mocked routes keep matching the + client. ### From sbt (recommended) @@ -68,6 +75,7 @@ enablePlugins(SmithyTsCodegenPlugin) tsCodegenSmithyDirs := Seq(baseDirectory.value / "src" / "main" / "smithy") tsCodegenOutputFile := baseDirectory.value / "src" / "generated.ts" tsCodegenExcludeServices := Seq("myorg.auth#AuthService") +tsCodegenPathPrefix := "/internal/v1" ``` then run `tsCodegen`. The plugin resolves the `smithy-ts-codegen-cli` artifact (at the plugin's @@ -77,6 +85,7 @@ version affects the codegen. Settings: - `tsCodegenSmithyDirs` — dirs scanned for `*.smithy` / `*.json` (default `src/main/smithy`). - `tsCodegenOutputFile` — where to write the TypeScript (default `target/generated.ts`). - `tsCodegenExcludeServices` — service shape ids to skip (see above). +- `tsCodegenPathPrefix` — path prepended to every `@http` URI (see above; default none). - `tsCodegenVersion` — override the codegen version to resolve (defaults to the plugin's own). ### Programmatically (`TsCodegenPlugin.generate`) @@ -88,7 +97,7 @@ import org.polyvariant.smithy.ts.TsCodegenPlugin import software.amazon.smithy.model.Model val model: Model = ??? -val ts: String = TsCodegenPlugin.generate(model, excludeServices = Set.empty) +val ts: String = TsCodegenPlugin.generate(model, excludeServices = Set.empty, pathPrefix = "") ``` ### From any build (forked JVM) @@ -98,7 +107,7 @@ from `.smithy`/`.json` sources and writes the output file. Run it with the CLI ( the classpath: ``` -Main [] +Main [ []] ``` This is what the sbt plugin forks; the smithy-build plugin is discovered via the SPI. diff --git a/cli/src/main/scala/org/polyvariant/smithy/ts/cli/Main.scala b/cli/src/main/scala/org/polyvariant/smithy/ts/cli/Main.scala index c310f01..ca4599f 100644 --- a/cli/src/main/scala/org/polyvariant/smithy/ts/cli/Main.scala +++ b/cli/src/main/scala/org/polyvariant/smithy/ts/cli/Main.scala @@ -31,7 +31,7 @@ import scala.jdk.CollectionConverters.* * from a build via a forked `java` process so the plugin lives on the JVM's classpath (and is * discovered via the smithy-build SPI). * - * Usage: `Main []` + * Usage: `Main [ []]` * * - `smithyDirs` — `File.pathSeparator`-joined list of directories containing `.smithy` sources. * Files matching `*.smithy` or `*.json` are loaded into one model. @@ -39,7 +39,9 @@ import scala.jdk.CollectionConverters.* * necessary. * - `excludeServices` — optional `,`-joined list of fully-qualified service shape ids * (`namespace#Name`) that the plugin should not emit clients for. Their referenced data shapes - * are still emitted. + * are still emitted. Pass `""` to skip it while still supplying `pathPrefix`. + * - `pathPrefix` — optional path prepended to every operation's `@http` URI, for a service + * mounted under a prefix the model does not describe. */ object Main { @@ -48,21 +50,24 @@ object Main { case smithyDirsArg :: outFileArg :: rest => val dirs = smithyDirsArg.split(java.io.File.pathSeparatorChar).toList.map(Paths.get(_)) val excluded = - rest match { - case head :: Nil if head.nonEmpty => head.split(',').toList - case _ => Nil - } - run(dirs, Paths.get(outFileArg), excluded) + rest.headOption.filter(_.nonEmpty).map(_.split(',').toList).getOrElse(Nil) + val pathPrefix = rest.drop(1).headOption.getOrElse("") + run(dirs, Paths.get(outFileArg), excluded, pathPrefix) case _ => Console .err .println( - "usage: Main []" + "usage: Main [ []]" ) sys.exit(2) } - private def run(smithyDirs: List[Path], outFile: Path, excludeServices: List[String]): Unit = { + private def run( + smithyDirs: List[Path], + outFile: Path, + excludeServices: List[String], + pathPrefix: String, + ): Unit = { val tmp = Files.createTempDirectory("smithy-ts-codegen") try { val pluginSettings = Node @@ -71,6 +76,7 @@ object Main { "excludeServices", Node.fromStrings(excludeServices.asJava), ) + .withMember("pathPrefix", pathPrefix) .build() val configNode = Node .objectNodeBuilder() diff --git a/core/src/main/scala/org/polyvariant/smithy/ts/TsCodegenPlugin.scala b/core/src/main/scala/org/polyvariant/smithy/ts/TsCodegenPlugin.scala index 16cadf6..d64c0b7 100644 --- a/core/src/main/scala/org/polyvariant/smithy/ts/TsCodegenPlugin.scala +++ b/core/src/main/scala/org/polyvariant/smithy/ts/TsCodegenPlugin.scala @@ -75,7 +75,10 @@ class TsCodegenPlugin extends SmithyBuildPlugin { .map(_.getElementsAs(classOf[software.amazon.smithy.model.node.StringNode])) .map(_.asScala.iterator.map(_.getValue).toSet) .getOrElse(Set.empty[String]) - val rendered = TsCodegenPlugin.generate(ctx.getModel, excludeServices) + val pathPrefix = Option(settings.getStringMember("pathPrefix").orElse(null)) + .map(_.getValue) + .getOrElse("") + val rendered = TsCodegenPlugin.generate(ctx.getModel, excludeServices, pathPrefix) val _ = ctx.getFileManifest.writeFile(Paths.get(outFile), rendered) } @@ -251,7 +254,12 @@ object TsCodegenPlugin { } - def generate(model: Model, excludeServices: Set[String]): String = { + def generate( + model: Model, + excludeServices: Set[String], + pathPrefix: String = "", + ): String = { + val prefix = normalizePathPrefix(pathPrefix) // `model.shapes` iterates a hash map keyed by ShapeId, so its order varies // between environments (JVM, classpath, filesystem). Sort by shape id to give // the topological sort below a deterministic starting order — otherwise the @@ -299,12 +307,12 @@ object TsCodegenPlugin { w.line("") w.line("// --- Service clients ---") w.line("") - servicesToEmit.foreach(svc => writeClient(w, model, svc)) + servicesToEmit.foreach(svc => writeClient(w, model, svc, prefix)) w.line("") w.line("// --- Storybook mock server ---") w.line("") writeMockRuntime(w, servicesToEmit.exists(hasStreamingOperation(w, model, _))) - servicesToEmit.foreach(svc => writeMockService(w, model, svc)) + servicesToEmit.foreach(svc => writeMockService(w, model, svc, prefix)) } w.toString @@ -1048,7 +1056,12 @@ object TsCodegenPlugin { // Service clients // -------------------------------------------------------------------------- - private def writeClient(w: TsWriter, model: Model, service: ServiceShape): Unit = { + private def writeClient( + w: TsWriter, + model: Model, + service: ServiceShape, + pathPrefix: String, + ): Unit = { val svcName = service.getId.getName val ops = serviceOperations(model, service) // Each half of the transport is taken only when some operation actually @@ -1080,13 +1093,18 @@ object TsCodegenPlugin { if (streaming) w.line("this.streamTransport = streamTransport") } - ops.foreach(op => writeOperation(w, model, op, service)) + ops.foreach(op => writeOperation(w, model, op, service, pathPrefix)) } w.line("") } - private def writeOperation(w: TsWriter, model: Model, op: OperationShape, service: ServiceShape) - : Unit = { + private def writeOperation( + w: TsWriter, + model: Model, + op: OperationShape, + service: ServiceShape, + pathPrefix: String, + ): Unit = { val svcName = service.getId.getName val opName = op.getId.getName val methodName = lowerFirst(opName) @@ -1159,7 +1177,10 @@ object TsCodegenPlugin { // urlExpression returns a TS template literal containing `${...}`; route // through the $L (literal) formatter so smithy's `$`-parser doesn't try // to interpret it. - w.line("const url = $L", urlExpression(http.getUri, labelMembers.map(_._1).toSet)) + w.line( + "const url = $L", + urlExpression(http.getUri, labelMembers.map(_._1).toSet, pathPrefix), + ) if (queryMembers.nonEmpty) { w.line("const query: Record = {}") @@ -1264,9 +1285,24 @@ object TsCodegenPlugin { } } + /** Normalizes a configured `pathPrefix` to either `""` or a string with a leading and no trailing + * slash, so it composes with an `@http` URI (which always starts with `/`) by plain + * concatenation. `"/"` alone normalizes to `""`: it would otherwise double the URI's own slash. + */ + private[ts] def normalizePathPrefix(pathPrefix: String): String = { + val trimmed = pathPrefix.trim.stripSuffix("/") + if (trimmed.isEmpty) + "" + else if (trimmed.startsWith("/")) + trimmed + else + "/" + trimmed + } + private def urlExpression( uri: software.amazon.smithy.model.pattern.UriPattern, labelNames: Set[String], + pathPrefix: String, ): String = { val segments = uri.getSegments.asScala val rendered = @@ -1286,10 +1322,10 @@ object TsCodegenPlugin { } .mkString val final_ = - if (rendered.isEmpty) + if (rendered.isEmpty && pathPrefix.isEmpty) "/" else - rendered + pathPrefix + rendered "`" + final_ + "`" } @@ -1643,7 +1679,12 @@ object TsCodegenPlugin { w.line("") } - private def writeMockService(w: TsWriter, model: Model, service: ServiceShape): Unit = { + private def writeMockService( + w: TsWriter, + model: Model, + service: ServiceShape, + pathPrefix: String, + ): Unit = { val svcName = service.getId.getName val ops = serviceOperations(model, service) @@ -1679,13 +1720,18 @@ object TsCodegenPlugin { w.block(s"export const ${svcName}Mock: MockServiceDescriptor<${svcName}Handlers> = {", "}") { w.line(s"serviceName: ${jsString(svcName)},") w.block("operations: [", "],") { - ops.foreach(op => writeMockOperation(w, model, op)) + ops.foreach(op => writeMockOperation(w, model, op, pathPrefix)) } } w.line("") } - private def writeMockOperation(w: TsWriter, model: Model, op: OperationShape): Unit = { + private def writeMockOperation( + w: TsWriter, + model: Model, + op: OperationShape, + pathPrefix: String, + ): Unit = { val opName = op.getId.getName val methodName = lowerFirst(opName) val http = op @@ -1716,7 +1762,10 @@ object TsCodegenPlugin { w.block("{", "},") { w.line(s"key: ${jsString(methodName)},") w.line(s"method: ${jsString(http.getMethod)},") - w.line("segments: $L,", segmentsArrayExpr(http.getUri, labelMembers.map(_._1).toSet)) + w.line( + "segments: $L,", + segmentsArrayExpr(http.getUri, labelMembers.map(_._1).toSet, pathPrefix), + ) writeMockDecodeInput( w, model, @@ -1916,7 +1965,17 @@ object TsCodegenPlugin { private def segmentsArrayExpr( uri: software.amazon.smithy.model.pattern.UriPattern, labelNames: Set[String], + pathPrefix: String, ): String = { + // The mock router matches path segments one by one, so a prefix has to arrive + // as literal segments here rather than as one concatenated string. + val prefixSegs = + pathPrefix + .split('/') + .iterator + .filter(_.nonEmpty) + .map(seg => s"{ literal: ${jsString(seg)} }") + .toList val segs = uri .getSegments .asScala @@ -1931,7 +1990,7 @@ object TsCodegenPlugin { s"{ label: ${jsString(name)} }" } } - "[" + segs.mkString(", ") + "]" + "[" + (prefixSegs ++ segs).mkString(", ") + "]" } // -------------------------------------------------------------------------- diff --git a/core/src/test/scala/org/polyvariant/smithy/ts/TsCodegenPluginTest.scala b/core/src/test/scala/org/polyvariant/smithy/ts/TsCodegenPluginTest.scala index 14937da..0d067cd 100644 --- a/core/src/test/scala/org/polyvariant/smithy/ts/TsCodegenPluginTest.scala +++ b/core/src/test/scala/org/polyvariant/smithy/ts/TsCodegenPluginTest.scala @@ -30,8 +30,12 @@ class TsCodegenPluginTest extends munit.FunSuite { .assemble() .unwrap() - private def generate(smithy: String, exclude: Set[String] = Set.empty): String = - TsCodegenPlugin.generate(model(smithy), exclude) + private def generate( + smithy: String, + exclude: Set[String] = Set.empty, + pathPrefix: String = "", + ): String = + TsCodegenPlugin.generate(model(smithy), exclude, pathPrefix) /** Several model files at once — the only way to put two namespaces in one model, since a * `.smithy` file declares exactly one. @@ -1098,4 +1102,102 @@ class TsCodegenPluginTest extends munit.FunSuite { assert(out.contains("brand<'com_example_b_ProfileId'>()")) } + // --- pathPrefix ------------------------------------------------------------ + + private val prefixModel = + """|$version: "2" + |namespace com.example + | + |service Svc { + | version: "v1" + | operations: [GetThing] + |} + | + |@http(method: "GET", uri: "/things/{id}") + |operation GetThing { + | input := { + | @required + | @httpLabel + | id: String + | } + | output := { + | name: String + | } + |} + |""".stripMargin + + test("no pathPrefix leaves the @http uri untouched") { + val out = generate(prefixModel) + assert(clue(out).contains("const url = `/things/${encodeURIComponent(String(input.id))}`")) + } + + test("pathPrefix is prepended to the @http uri") { + val out = generate(prefixModel, pathPrefix = "/internal/v1") + assert( + clue(out).contains( + "const url = `/internal/v1/things/${encodeURIComponent(String(input.id))}`" + ) + ) + } + + test("pathPrefix reaches the mock router as literal segments, so mocks still match") { + val out = generate(prefixModel, pathPrefix = "/internal/v1") + assert( + clue(out).contains( + "segments: [{ literal: 'internal' }, { literal: 'v1' }, { literal: 'things' }, { label: 'id' }]," + ) + ) + } + + test("a pathPrefix without a leading slash is still prepended as a path") { + val out = generate(prefixModel, pathPrefix = "internal/v1") + assert(clue(out).contains("const url = `/internal/v1/things/")) + } + + test("a trailing slash on pathPrefix does not double the uri's own slash") { + val out = generate(prefixModel, pathPrefix = "/internal/v1/") + assert(clue(out).contains("const url = `/internal/v1/things/")) + assert(!out.contains("v1//things")) + } + + test("a bare slash pathPrefix is the same as none") { + assertEquals(generate(prefixModel, pathPrefix = "/"), generate(prefixModel)) + } + + test("whitespace-only pathPrefix is the same as none") { + assertEquals(generate(prefixModel, pathPrefix = " "), generate(prefixModel)) + } + + test("pathPrefix applies to an operation whose uri is just /") { + val out = generate( + """|$version: "2" + |namespace com.example + | + |service Svc { + | version: "v1" + | operations: [Ping] + |} + | + |@http(method: "GET", uri: "/") + |operation Ping { + | output := { + | ok: Boolean + | } + |} + |""".stripMargin, + pathPrefix = "/internal", + ) + assert(clue(out).contains("const url = `/internal`")) + } + + test("normalizePathPrefix") { + assertEquals(TsCodegenPlugin.normalizePathPrefix(""), "") + assertEquals(TsCodegenPlugin.normalizePathPrefix("/"), "") + assertEquals(TsCodegenPlugin.normalizePathPrefix(" "), "") + assertEquals(TsCodegenPlugin.normalizePathPrefix("/internal"), "/internal") + assertEquals(TsCodegenPlugin.normalizePathPrefix("internal"), "/internal") + assertEquals(TsCodegenPlugin.normalizePathPrefix("/internal/"), "/internal") + assertEquals(TsCodegenPlugin.normalizePathPrefix("internal/v1/"), "/internal/v1") + } + } diff --git a/sbt-plugin/src/main/scala/org/polyvariant/smithy/ts/sbt/SmithyTsCodegenPlugin.scala b/sbt-plugin/src/main/scala/org/polyvariant/smithy/ts/sbt/SmithyTsCodegenPlugin.scala index f5d0c71..bcd57d3 100644 --- a/sbt-plugin/src/main/scala/org/polyvariant/smithy/ts/sbt/SmithyTsCodegenPlugin.scala +++ b/sbt-plugin/src/main/scala/org/polyvariant/smithy/ts/sbt/SmithyTsCodegenPlugin.scala @@ -43,6 +43,7 @@ import scala.collection.JavaConverters._ * tsCodegenSmithyDirs := Seq(baseDirectory.value / "src" / "main" / "smithy") * tsCodegenOutputFile := baseDirectory.value / "generated.ts" * tsCodegenExcludeServices := Seq("myorg.auth#AuthService") + * tsCodegenPathPrefix := "/internal/v1" * }}} * * then run `tsCodegen`. @@ -68,6 +69,11 @@ object SmithyTsCodegenPlugin extends AutoPlugin { "their referenced data shapes are still emitted" ) + val tsCodegenPathPrefix = settingKey[String]( + "Path prepended to every operation's @http URI, for a service mounted under a prefix " + + "the model does not describe (e.g. \"/internal/v1\")" + ) + val tsCodegenVersion = settingKey[String]("Version of the smithy-ts-codegen-cli artifact to resolve and run") } @@ -78,6 +84,7 @@ object SmithyTsCodegenPlugin extends AutoPlugin { Seq( tsCodegenVersion := BuildInfo.smithyTsCodegenVersion, tsCodegenExcludeServices := Seq.empty, + tsCodegenPathPrefix := "", tsCodegenSmithyDirs := Seq((Compile / sourceDirectory).value / "smithy"), tsCodegenOutputFile := (Compile / target).value / "generated.ts", tsCodegen := tsCodegenTask.value, @@ -119,6 +126,7 @@ object SmithyTsCodegenPlugin extends AutoPlugin { val smithyDirs = tsCodegenSmithyDirs.value val outFile = tsCodegenOutputFile.value val excludeServices = tsCodegenExcludeServices.value + val pathPrefix = tsCodegenPathPrefix.value val version = tsCodegenVersion.value val cacheDir = streams.value.cacheDirectory / "smithy-ts-codegen" @@ -129,7 +137,7 @@ object SmithyTsCodegenPlugin extends AutoPlugin { .flatMap(d => (d ** ("*.smithy" || "*.json")).get) .toSet val configInput = cacheDir / "exclude-services.txt" - IO.write(configInput, excludeServices.mkString("\n")) + IO.write(configInput, (excludeServices :+ s"pathPrefix=$pathPrefix").mkString("\n")) val cached = FileFunction.cached( @@ -144,6 +152,7 @@ object SmithyTsCodegenPlugin extends AutoPlugin { classpath = classpath, outFile = outFile, excludeServices = excludeServices, + pathPrefix = pathPrefix, log = log, ) Set(outFile) diff --git a/sbt-plugin/src/main/scala/org/polyvariant/smithy/ts/sbt/TsCodegenRunner.scala b/sbt-plugin/src/main/scala/org/polyvariant/smithy/ts/sbt/TsCodegenRunner.scala index af845d7..a74a8ed 100644 --- a/sbt-plugin/src/main/scala/org/polyvariant/smithy/ts/sbt/TsCodegenRunner.scala +++ b/sbt-plugin/src/main/scala/org/polyvariant/smithy/ts/sbt/TsCodegenRunner.scala @@ -32,6 +32,7 @@ private[sbt] object TsCodegenRunner { classpath: Seq[File], outFile: File, excludeServices: Seq[String], + pathPrefix: String, log: Logger, ): Unit = { val cpString = classpath.map(_.getAbsolutePath).mkString(File.pathSeparator) @@ -45,7 +46,9 @@ private[sbt] object TsCodegenRunner { outFile.getAbsolutePath, ) val cmd = - if (excludeServices.nonEmpty) + if (pathPrefix.nonEmpty) + baseCmd ++ Seq(excludeServices.mkString(","), pathPrefix) + else if (excludeServices.nonEmpty) baseCmd :+ excludeServices.mkString(",") else baseCmd diff --git a/sbt-plugin/src/sbt-test/smithy-ts-codegen/basic/build.sbt b/sbt-plugin/src/sbt-test/smithy-ts-codegen/basic/build.sbt index 5226ab5..69b66c5 100644 --- a/sbt-plugin/src/sbt-test/smithy-ts-codegen/basic/build.sbt +++ b/sbt-plugin/src/sbt-test/smithy-ts-codegen/basic/build.sbt @@ -9,6 +9,7 @@ resolvers += Resolver.sonatypeCentralSnapshots tsCodegenSmithyDirs := Seq(baseDirectory.value / "src" / "main" / "smithy") tsCodegenOutputFile := baseDirectory.value / "target" / "generated.ts" tsCodegenExcludeServices := Seq("test#HiddenService") +tsCodegenPathPrefix := "/internal/v1" TaskKey[Unit]("checkOutput") := { val f = tsCodegenOutputFile.value @@ -17,6 +18,13 @@ TaskKey[Unit]("checkOutput") := { def require(sub: String): Unit = assert(contents.contains(sub), s"expected generated.ts to contain: $sub") + // pathPrefix reaches the client through the forked CLI, where it is passed + // positionally after excludeServices — so this covers that wiring too. + require("const url = `/internal/v1/greet/${encodeURIComponent(String(input.name))}`") + // ...and the mock router gets it as literal segments, so mocks still match. + require("segments: [{ literal: 'internal' }, { literal: 'v1' }, { literal: 'greet' }") + assert(!contents.contains("const url = `/greet/"), "the un-prefixed url must not survive") + require("export const PersonSchema = z.object({") require("export type Person = z.infer") require("export class GreeterClient {") From 242eb1de3f3d6cb42d9355a24fc8ccdbef7d0df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Wed, 2 Sep 2026 18:57:52 +0200 Subject: [PATCH 2/2] Bump the base version to 0.5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `generate` gained a `pathPrefix` parameter in the previous commit. The default value keeps it source-compatible, but a defaulted parameter still changes the JVM signature, so the two-argument overload disappears and MiMa reports it against the `0.4.0` baseline: * static method generate(Model,Set)String in class TsCodegenPlugin does not have a correspondent in current version That is a real binary break, not a false positive, so the next release is a minor one. sbt-typelevel checks the release tag against `tlBaseVersion`, so this has to move before `v0.5.0` can be tagged. A filter would have been the wrong tool here. `generate` is the documented programmatic entry point, so the break is genuinely user-visible — unlike the `TsWriter` filter dropped when 0.4 opened, which excused a change to a class no caller could reach. --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index cdf9026..5401e3c 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,4 @@ -ThisBuild / tlBaseVersion := "0.4" +ThisBuild / tlBaseVersion := "0.5" ThisBuild / organization := "org.polyvariant" ThisBuild / organizationName := "Polyvariant" ThisBuild / startYear := Some(2026)