├── .circleci └── config.yml ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── build.sbt ├── package-lock.json ├── package.json ├── project ├── build.properties └── plugins.sbt ├── src ├── main │ ├── resources │ │ └── sbt-js-plugin.js │ └── scala │ │ └── tanin │ │ └── play │ │ └── svelte │ │ ├── Compiler.scala │ │ └── SbtSvelte.scala └── test │ └── scala │ ├── helpers │ └── BaseSpec.scala │ └── tanin │ └── play │ └── svelte │ ├── CompilerIntegrationSpec.scala │ ├── CompilerSpec.scala │ └── assets │ ├── dummy.ts │ ├── svelte │ ├── component-a.svelte │ ├── component-d.svelte │ └── dependencies │ │ ├── _component-b.svelte │ │ ├── _component-c.svelte │ │ └── style.scss │ ├── tsconfig.json │ └── webpack.config.js ├── test-play-project ├── .scalafmt.conf ├── .vscode │ └── settings.json ├── README.md ├── app │ ├── assets │ │ └── svelte │ │ │ └── components │ │ │ ├── common │ │ │ ├── _our-button.svelte │ │ │ ├── _our-js-button.svelte │ │ │ └── button.scss │ │ │ ├── greeting-form.svelte │ │ │ └── test-form.svelte │ ├── controllers │ │ ├── AssetsController.scala │ │ └── HomeController.scala │ ├── libraries │ │ └── Renderer.scala │ └── views │ │ ├── index.scala.html │ │ └── test.scala.html ├── build.sbt ├── conf │ ├── application.conf │ └── routes ├── hmr.js ├── package.json ├── postcss.config.js ├── project │ ├── build.properties │ └── plugin.sbt ├── public │ └── stylesheets │ │ └── tailwindbase.css ├── tailwind.config.js ├── test │ └── browsers │ │ ├── Base.scala │ │ └── BrowserSpec.scala ├── tsconfig.json └── webpack.config.js └── version.sbt /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | orbs: 4 | browser-tools: circleci/browser-tools@1.4.6 5 | 6 | jobs: 7 | build: 8 | docker: 9 | - image: cimg/openjdk:11.0.21-browsers 10 | 11 | working_directory: ~/repo 12 | 13 | environment: 14 | JVM_OPTS: -Xmx16G -XX:+UseG1GC 15 | TERM: dumb 16 | 17 | steps: 18 | - checkout 19 | - browser-tools/install-chrome 20 | - browser-tools/install-chromedriver 21 | - run: 22 | name: Check install 23 | command: | 24 | google-chrome --version 25 | chromedriver --version 26 | 27 | - restore_cache: 28 | name: Restore Node app dependencies 29 | keys: 30 | - node-v1-{{ checksum "package.json" }} 31 | - node-v1 32 | - restore_cache: 33 | name: Restore Scala dependencies 34 | keys: 35 | - scala-v1-{{ checksum "project/plugins.sbt" }}-{{ checksum "build.sbt" }} 36 | - scala-v1- 37 | 38 | - run: npm install 39 | - run: sbt test 40 | 41 | - save_cache: 42 | paths: 43 | - node_modules 44 | key: node-v1-{{ checksum "package.json" }} 45 | 46 | - save_cache: 47 | paths: 48 | - ~/.m2 49 | - ~/.ivy2/cache 50 | - ~/.sbt 51 | - ~/.cache 52 | - .bloop 53 | - .metals 54 | - target 55 | key: scala-v1-{{ checksum "project/plugins.sbt" }}-{{ checksum "build.sbt" }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .bloop 3 | .bsp 4 | .classpath 5 | .g8 6 | .gradle 7 | .idea 8 | .idea_modules 9 | .metals 10 | .project 11 | .settings 12 | RUNNING_PID 13 | logs 14 | node_modules 15 | package-lock.json 16 | target 17 | yarn-error.log 18 | yarn.lock 19 | metals.sbt -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.watcherExclude": { 3 | "**/target": true 4 | } 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sbt-svelte 2 | =========== 3 | 4 | [![CircleCI](https://circleci.com/gh/tanin47/sbt-svelte.svg?style=svg)](https://circleci.com/gh/tanin47/sbt-svelte) 5 | 6 | sbt-svelte integrates Webpack + Svelte into Playframework's asset generation. 7 | 8 | It eliminates the need to run a separate node process to hot-reload your Svelte code. Moreover, it enables you to mix between SPA and non-SPA pages in your application; it's your choice whether to use SPA or not. 9 | 10 | It works with both `sbt run` (which hot-reloads the code changes) and `sbt stage`. 11 | 12 | Please see the example project in the folder `test-play-project`. 13 | 14 | Requirements 15 | ------------- 16 | 17 | * __[Webpack 5.x](https://webpack.js.org/):__ you'll need to specify the webpack binary location and webpack's configuration localtion. This enables you to choose your own version of Webpack and your own Webpack's configuration. You can see an example in the folder `test-play-project`. 18 | * __Playframework 2.8.x__ 19 | * __Scala >= 2.12.x and SBT 1.x:__ Because the artifact is only published this setting. If you would like other combinations of Scala and SBT versions, please open an issue. 20 | 21 | How to use 22 | ----------- 23 | 24 | ### 1. Install the plugin 25 | 26 | Add the below line to `project/plugins.sbt`: 27 | 28 | ``` 29 | lazy val root = 30 | Project("plugins", file(".")).aggregate(SbtSvelte).dependsOn(SbtSvelte) 31 | lazy val SbtSvelte = RootProject(uri("https://github.com/tanin47/sbt-svelte.git#")) 32 | ``` 33 | 34 | ### 2. Configure Webpack config file. 35 | 36 | Create `webpack.config.js by copying from `test-play-project/webpack.config.js` 37 | 38 | You should NOT specify `module.exports.output` because sbt-svelte will automatically set the field. 39 | 40 | Your config file will be copied and added with some required additional code. Then, it will used by sbt-svelte when compiling the components. 41 | 42 | When running sbt-svelte, we print the webpack command with the modified `webpack.config.js`, so you can inspect the config that we use. 43 | 44 | ### 3. Configure `build.sbt` 45 | 46 | Specifying necessary configurations: 47 | 48 | ``` 49 | lazy val root = (project in file(".")).enablePlugins(PlayScala, SbtWeb, SbtSvelte) // Enable the plugin 50 | 51 | // The location of the webpack binary. For windows, it might be `webpack.cmd`. 52 | Assets / SvelteKeys.svelte / SvelteKeys.webpackBinary := "./node_modules/.bin/webpack" 53 | 54 | // The location of the webpack configuration. 55 | Assets / SvelteKeys.svelte / SvelteKeys.webpackConfig := "./webpack.config.js" 56 | ``` 57 | 58 | ### 4. Find out where the output JS file is and how to use it 59 | 60 | The plugin compiles `*.svelte` within `app/assets`. 61 | 62 | For the path `app/assets/svelte/components/some-component.svelte`, the output JS should be at `http://.../assets/svelte/components/some-component.js`. 63 | It should also work with `@routes.Assets.versioned("svelte/components/some-component.js")`. 64 | 65 | The exported module name is the camel case of the file name. In the above example, the module name is `SomeComponent`. 66 | 67 | Therefore, we can use the component as shown below: 68 | 69 | ``` 70 | 71 | 72 | 73 |
74 | 82 | ``` 83 | 84 | Please see the folder `test-play-project` for a complete example. 85 | 86 | Use the Hot Module Reload (HMR) 87 | -------------------------------- 88 | 89 | The setup for HMR is complex but completely worth it because it auto-reloads the JS code changes without reloading the page or triggering the recompilation of Play Framework. 90 | It's 10x faster for development. If you have issues with setting it up, please open an issue. 91 | 92 | ### 1. Make hmr.js and set up the command. 93 | 94 | Please copy `test-play-project/hmr.js` to your project and set up the hmr command in `package.json` as shown below: 95 | 96 | ``` 97 | "scripts": { 98 | "hmr": "NODE_PATH=./node_modules ENABLE_HMR=true node hmr.js" 99 | }, 100 | ``` 101 | 102 | ### 2. Configure the webpack config 103 | 104 | Detect `ENABLE_HMR` and reconfigure the `svelte-loader` as shown below: 105 | 106 | ``` 107 | if (process.env.ENABLE_HMR) { 108 | console.log('Enable HMR') 109 | for (const rule of config.module.rules) { 110 | if (rule.use.loader === 'svelte-loader') { 111 | rule.use.options.emitCss = false 112 | rule.use.options.compilerOptions.dev = true 113 | rule.use.options.hotReload = true 114 | } 115 | } 116 | config.plugins.push(new webpack.HotModuleReplacementPlugin()) 117 | } 118 | ``` 119 | 120 | ### 3. Configure the Play Framework to redirect assets to the HMR server 121 | 122 | In `hmr.js`, the HMR server will listen to the port 9001. We will need to redirect the assets to the HMR server by making the Assets controller as follows: 123 | 124 | ``` 125 | @Singleton 126 | class AssetsController @Inject()( 127 | errorHandler: HttpErrorHandler, 128 | meta: AssetsMetadata, 129 | env: Environment 130 | )(implicit ec: ExecutionContext) 131 | extends Assets(errorHandler, meta, env) { 132 | 133 | override def versioned(path: String, file: Assets.Asset): Action[AnyContent] = Action.async { req => 134 | if ( 135 | env.mode == Mode.Dev && 136 | ( 137 | file.name.startsWith("svelte_") || 138 | file.name.startsWith("svelte/") || 139 | file.name.startsWith("stylesheets/tailwindbase.css") 140 | ) 141 | ) { 142 | Future(Redirect(s"http://localhost:9001/assets/${file.name}")) 143 | } else { 144 | super.versioned(path, file)(req) 145 | } 146 | } 147 | } 148 | ``` 149 | 150 | Then, you configure `conf/routes` as follows: 151 | 152 | ``` 153 | GET /assets/*file controllers.AssetsController.versioned(path="/public", file: Asset) 154 | ``` 155 | 156 | ### 4. Try it out 157 | 158 | Now, you can run `sbt run` in one terminal and `npm run hmr` in another terminal. 159 | 160 | Go to `http://localhost:9000` and you should see the page. Now, you can make changes to the svelte components and see the changes immediately. 161 | 162 | See a working example in the `test-play-project` folder. 163 | 164 | Interested in using the plugin? 165 | -------------------------------- 166 | 167 | Please feel free to open an issue to ask questions. Let us know how you want to use the plugin. We want to help you use the plugin successfully. 168 | 169 | 170 | Contributing 171 | --------------- 172 | 173 | The project welcomes any contribution. Here are the steps for testing when developing locally: 174 | 175 | 1. Run `npm install` in order to install packages needed for the integration tests. 176 | 2. Run `sbt test` to run all tests. 177 | 3. To test the plugin on an actual Playframework project, go to `test-play-project`, run `npm install`, and run `sbt run`. 178 | 179 | Publish 180 | -------- 181 | We are not publishing a jar file anymore. You can use it by referencing a github URL with a specific commit directly. 182 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "sbt-svelte" 2 | 3 | lazy val `sbt-svelte` = (project in file(".")) 4 | .enablePlugins(SbtWebBase) 5 | .settings( 6 | scalaVersion := "2.12.20", 7 | libraryDependencies ++= Seq( 8 | "com.typesafe.play" %% "play-json" % "2.8.1", 9 | "org.mockito" % "mockito-core" % "3.0.0" % Test, 10 | "com.lihaoyi" %% "utest" % "0.7.1" % Test 11 | ), 12 | testFrameworks += new TestFramework("utest.runner.Framework") 13 | ) 14 | 15 | addSbtJsEngine("1.3.9") 16 | 17 | addCommandAlias( 18 | "fmt", 19 | "all scalafmtSbt scalafmt test:scalafmt" 20 | ) 21 | addCommandAlias( 22 | "check", 23 | "all scalafmtSbtCheck scalafmtCheck test:scalafmtCheck" 24 | ) 25 | 26 | organization := "io.github.tanin47" 27 | organizationName := "tanin47" 28 | 29 | Test / publishArtifact := false 30 | 31 | homepage := Some(url("https://github.com/tanin47/sbt-svelte")) 32 | 33 | publishMavenStyle := true 34 | publishTo := { 35 | val nexus = "https://s01.oss.sonatype.org/" 36 | if (isSnapshot.value) Some("snapshots" at nexus + "content/repositories/snapshots") 37 | else Some("releases" at nexus + "service/local/staging/deploy/maven2") 38 | } 39 | pomIncludeRepository := { _ => 40 | false 41 | } 42 | licenses := Seq(("MIT", url("http://opensource.org/licenses/MIT"))) 43 | scmInfo := Some( 44 | ScmInfo( 45 | url("https://github.com/tanin47/sbt-svelte"), 46 | "scm:git@github.com:tanin47/sbt-svelte.git" 47 | ) 48 | ) 49 | 50 | developers := List( 51 | Developer( 52 | id = "tanin", 53 | name = "Tanin Na Nakorn", 54 | email = "@tanin", 55 | url = url("https://github.com/tanin47") 56 | ) 57 | ) 58 | 59 | versionScheme := Some("semver-spec") 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "browserslist": [ 3 | "> 1%", 4 | "last 2 versions", 5 | "not ie <= 8" 6 | ], 7 | "devDependencies": { 8 | "@babel/core": "^7.12.10", 9 | "@types/node": "^14.14.16", 10 | "babel-loader": "^8.2.1", 11 | "css-loader": "^5.0.1", 12 | "mini-css-extract-plugin": "^2.7.6", 13 | "sass": "1.86.3", 14 | "sass-loader": "16.0.5", 15 | "style-loader": "^2.0.0", 16 | "svelte": "4.2.15", 17 | "svelte-check": "^3.4.6", 18 | "svelte-loader": "^3.1.9", 19 | "svelte-preprocess": "^5.0.4", 20 | "ts-loader": "^8.0.12", 21 | "tsconfig-paths-webpack-plugin": "^3.3.0", 22 | "typescript": "^4.1.3", 23 | "webpack": "5.98.0", 24 | "webpack-cli": "6.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.1 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.sbt" % "sbt-web-build-base" % "2.0.2") 2 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.0") 3 | -------------------------------------------------------------------------------- /src/main/resources/sbt-js-plugin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const pathModule = require('path') 4 | 5 | const replacePathVariables = (path, data) => { 6 | const REGEXP_CAMEL_CASE_NAME = /\[camel-case-name\]/gi; 7 | if (typeof path === "function") { 8 | path = path(data); 9 | } 10 | 11 | if (data && data.chunk && data.chunk.name) { 12 | let tokens = data.chunk.name.split(pathModule.sep); 13 | return path.replace( 14 | REGEXP_CAMEL_CASE_NAME, 15 | tokens[tokens.length - 1] 16 | .replace(/(\-\w)/g, (matches) => { return matches[1].toUpperCase(); }) 17 | .replace(/(^\w)/, (matches) => { return matches[0].toUpperCase(); }) 18 | ); 19 | } else { 20 | return path; 21 | } 22 | }; 23 | 24 | const writeStats = (compilation, assets) => { 25 | const processedModules = new Set() 26 | const ms = []; 27 | 28 | const modules = compilation 29 | .getStats() 30 | .toJson({ 31 | assets: false, 32 | chunks: false, 33 | chunkGroups: false, 34 | entrypoints: false, 35 | module: true, 36 | errors: false, 37 | warnings: false 38 | }) 39 | .modules 40 | for (let module of modules) { 41 | if (processedModules.has(module.name)) { 42 | continue; 43 | } 44 | 45 | let reasons = new Set(); 46 | for (let reason of module.reasons) { 47 | reasons.add(reason.moduleName); 48 | } 49 | ms.push({ 50 | name: module.name, 51 | reasons: Array.from(reasons) 52 | }) 53 | processedModules.add(module.name) 54 | } 55 | 56 | const s = JSON.stringify(ms); 57 | assets['sbt-js-tree.json'] = { 58 | source() { 59 | return s; 60 | }, 61 | size() { 62 | return s.length; 63 | } 64 | }; 65 | }; 66 | 67 | class SbtJsPlugin { 68 | apply(compiler) { 69 | compiler.hooks.compilation.tap("sbt-js-compilation", (compilation) => { 70 | compilation.hooks.assetPath.tap('sbt-js-asset-path', replacePathVariables); 71 | compilation.hooks.processAssets.tap( 72 | { 73 | name: "sbt-js-emit", 74 | stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT 75 | }, 76 | (assets) => { 77 | writeStats(compilation, assets); 78 | }); 79 | }); 80 | } 81 | } 82 | 83 | module.exports = SbtJsPlugin; 84 | -------------------------------------------------------------------------------- /src/main/scala/tanin/play/svelte/Compiler.scala: -------------------------------------------------------------------------------- 1 | package tanin.play.svelte 2 | 3 | import java.io.{ File, FileOutputStream, PrintWriter } 4 | import java.nio.file.{ Files, Path } 5 | 6 | import play.api.libs.json._ 7 | import sbt.internal.util.ManagedLogger 8 | 9 | import scala.io.Source 10 | 11 | case class CompilationResult(success: Boolean, entries: Seq[CompilationEntry]) 12 | case class CompilationEntry(inputFile: File, filesRead: Set[Path], filesWritten: Set[Path]) 13 | case class Input(name: String, path: Path) 14 | 15 | class Shell { 16 | def execute(logger: ManagedLogger, cmd: String, cwd: File, envs: (String, String)*): Int = { 17 | import scala.sys.process._ 18 | 19 | val envString = if (envs.nonEmpty) { 20 | envs.toList.map { case (k, v) => s"$k=$v"}.mkString(" ") + " " 21 | } else { 22 | "" 23 | } 24 | 25 | logger.info(s"Executing: ${envString}${cmd}") 26 | 27 | val exitCode = Process(cmd, cwd, envs: _*).! 28 | logger.info(s"Exited with $exitCode") 29 | 30 | exitCode 31 | } 32 | 33 | def fileExists(file: File): Boolean = file.exists() 34 | } 35 | 36 | class ComputeDependencyTree { 37 | 38 | val LOCAL_PATH_PREFIX_REGEX = "^\\./".r 39 | 40 | def sanitize(s: String): String = 41 | LOCAL_PATH_PREFIX_REGEX.replaceAllIn(s, "").replaceAllLiterally("/", sbt.Path.sep.toString) 42 | 43 | def apply(file: File): Map[String, Set[String]] = 44 | apply(scala.io.Source.fromFile(file).mkString) 45 | 46 | def apply(content: String): Map[String, Set[String]] = { 47 | val json = Json.parse(content) 48 | 49 | val deps = json 50 | .as[JsArray] 51 | .value 52 | .flatMap { obj => 53 | // For some reason, webpack or svelte-loader includes the string ` + 4 modules` in `name`. 54 | val name = obj("name").as[String].split(" \\+ ").head 55 | val relations = obj("reasons") 56 | .as[Seq[JsValue]] 57 | .flatMap { 58 | case JsNull => None 59 | // For some reason, webpack or svelte-loader includes the string ` + 4 modules` in `moduleName`. 60 | // See: https://github.com/webpack/webpack/issues/8507 61 | case JsString(v) => Some(v.split(" \\+ ").head) 62 | case _ => throw new IllegalArgumentException() 63 | } 64 | .map { reason => 65 | reason -> name 66 | } 67 | 68 | relations ++ Seq(name -> name) // the file also depends on itself. 69 | } 70 | .groupBy { case (key, _) => key } 71 | .mapValues(_.map(_._2).toSet) 72 | 73 | flatten(deps) 74 | // We only care about our directories. 75 | // The path separator here is always `/`, even on windows. 76 | .filter { case (key, _) => key.startsWith("./") }.mapValues { vs => 77 | vs.filter { v => 78 | // There are some dependencies that we don't care about. 79 | // An example: ./vue/component-a.vue?vue&type=style&index=0&id=f8aaa26e&scoped=true&lang=scss& 80 | // Another example: /home/tanin/projects/sbt-svelte/node_modules/vue-style-loader!/home/ta... 81 | v.startsWith("./") && !v.contains("?") 82 | } 83 | }.map { 84 | case (key, values) => 85 | sanitize(key) -> values.map(sanitize) 86 | } 87 | } 88 | 89 | private[this] def flatten(deps: Map[String, Set[String]]): Map[String, Set[String]] = { 90 | var changed = false 91 | val newDeps = deps.map { 92 | case (key, children) => 93 | val newChildren = children ++ children.flatMap { v => 94 | deps.getOrElse(v, Set.empty) 95 | } 96 | if (newChildren.size != children.size) { 97 | changed = true 98 | } 99 | key -> newChildren 100 | } 101 | 102 | if (changed) { 103 | flatten(newDeps) 104 | } else { 105 | newDeps 106 | } 107 | } 108 | } 109 | 110 | class PrepareWebpackConfig { 111 | def apply(originalWebpackConfig: File, inputs: Seq[Input]) = { 112 | import sbt._ 113 | 114 | val tmpDir = Files.createTempDirectory("sbt-svelte") 115 | val targetFile = tmpDir.toFile / "webpack.config.js" 116 | 117 | Files.copy(originalWebpackConfig.toPath, targetFile.toPath) 118 | 119 | val webpackConfigFile = new PrintWriter(new FileOutputStream(targetFile, true)) 120 | try { 121 | val entries = inputs.map { input => 122 | input.name -> JsString(input.path.toAbsolutePath.toString) 123 | } 124 | 125 | webpackConfigFile.write("\n") 126 | webpackConfigFile.write(s""" 127 | |const userDefinedModuleExports = module.exports; 128 | | 129 | |module.exports = (env, argv) => { 130 | | let config; 131 | | if (typeof userDefinedModuleExports === 'function') { 132 | | config = userDefinedModuleExports(env, argv); 133 | | } else { 134 | | config = userDefinedModuleExports; 135 | | } 136 | | config.entry = ${Json.prettyPrint(JsObject(entries))}; 137 | | config.output = config.output || {}; 138 | | config.output.publicPath = config.output.publicPath || '/assets'; 139 | | config.output.library = config.output.library || '[camel-case-name]'; 140 | | config.output.filename = config.output.filename || '[name].js'; 141 | | 142 | | const SbtJsPlugin = require('./sbt-js-plugin.js'); 143 | | config.plugins = config.plugins || []; 144 | | config.plugins.push(new SbtJsPlugin()); 145 | | return config; 146 | |} 147 | | 148 | """.stripMargin) 149 | } finally { 150 | webpackConfigFile.close() 151 | } 152 | 153 | val sbtJsPluginFile = new PrintWriter(tmpDir.toFile / "sbt-js-plugin.js") 154 | try { 155 | sbtJsPluginFile.write(Source.fromInputStream(getClass.getResourceAsStream("/sbt-js-plugin.js")).mkString) 156 | } finally { 157 | sbtJsPluginFile.close() 158 | } 159 | 160 | targetFile.getAbsolutePath 161 | } 162 | } 163 | 164 | class Compiler( 165 | webpackBinary: File, 166 | webpackConfig: File, 167 | sourceDir: File, 168 | targetDir: File, 169 | isProd: Boolean, 170 | logger: ManagedLogger, 171 | nodeModules: File, 172 | shell: Shell = new Shell, 173 | dependencyComputer: ComputeDependencyTree = new ComputeDependencyTree, 174 | prepareWebpackConfig: PrepareWebpackConfig = new PrepareWebpackConfig 175 | ) { 176 | 177 | def compile(inputFiles: Seq[Path]): CompilationResult = { 178 | import sbt._ 179 | 180 | if (inputFiles.isEmpty) { 181 | return CompilationResult(success = true, entries = Seq.empty) 182 | } 183 | 184 | val inputs = inputFiles.map { inputFile => 185 | val name = sourceDir.toPath.relativize((inputFile.getParent.toFile / inputFile.toFile.base).toPath).toString 186 | Input(name, inputFile) 187 | } 188 | 189 | val cmd = Seq( 190 | webpackBinary.getCanonicalPath, 191 | "--config", 192 | prepareWebpackConfig.apply(webpackConfig, inputs), 193 | "--output-path", 194 | targetDir.getCanonicalPath, 195 | "--mode", 196 | if (isProd) { 197 | "production" 198 | } else { 199 | "development" 200 | } 201 | ).mkString(" ") 202 | 203 | val exitCode = shell.execute(logger, cmd, sourceDir, "NODE_PATH" -> nodeModules.getCanonicalPath, "ENABLE_SVELTE_CHECK" -> "true") 204 | val success = exitCode == 0 205 | 206 | CompilationResult( 207 | success = success, 208 | entries = if (success) { 209 | val dependencyMap = dependencyComputer.apply(targetDir / "sbt-js-tree.json") 210 | 211 | inputs.map { input => 212 | val outputJsRelativePath = 213 | sourceDir.toPath.relativize((input.path.getParent.toFile / s"${input.path.toFile.base}.js").toPath).toString 214 | val outputJsFile = targetDir / outputJsRelativePath 215 | val outputCssRelativePath = 216 | sourceDir.toPath.relativize((input.path.getParent.toFile / s"${input.path.toFile.base}.css").toPath).toString 217 | val outputCssFile = targetDir / outputCssRelativePath 218 | 219 | val dependencies = dependencyMap 220 | .getOrElse(s"${input.name}.svelte", Set.empty) 221 | .map { relativePath => 222 | (sourceDir / relativePath).toPath 223 | } 224 | 225 | CompilationEntry( 226 | inputFile = input.path.toFile, 227 | filesRead = dependencies, 228 | filesWritten = Set(outputJsFile, outputCssFile).filter(shell.fileExists).map(_.toPath) 229 | ) 230 | } 231 | } else { 232 | Seq.empty 233 | } 234 | ) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/main/scala/tanin/play/svelte/SbtSvelte.scala: -------------------------------------------------------------------------------- 1 | package tanin.play.svelte 2 | 3 | import com.typesafe.sbt.web.Import.WebKeys.* 4 | import com.typesafe.sbt.web.SbtWeb.autoImport.* 5 | import com.typesafe.sbt.web.* 6 | import com.typesafe.sbt.web.incremental.* 7 | import com.typesafe.sbt.web.pipeline.Pipeline 8 | import sbt.Keys.* 9 | import sbt.* 10 | import xsbti.{Position, Problem, Severity} 11 | 12 | import scala.io.Source 13 | 14 | object SbtSvelte extends AutoPlugin { 15 | override def requires: Plugins = SbtWeb 16 | override def trigger: PluginTrigger = AllRequirements 17 | 18 | object autoImport { 19 | object SvelteKeys { 20 | val svelte = TaskKey[Pipeline.Stage]("svelte", "Generate compiled Javascripts files from Svelte components.") 21 | val webpackBinary = TaskKey[String]("svelteWebpackBinary", "The binary location for webpack.") 22 | val webpackConfig = TaskKey[String]("svelteWebpackConfig", "The location for webpack config.") 23 | val nodeModulesPath = TaskKey[String]("svelteNodeModules", "The location of the node_modules.") 24 | val prodCommands = TaskKey[Set[String]]( 25 | "svelteProdCommands", 26 | "A set of SBT commands that triggers production build. The default is `stage`. In other words, use -p (as opposed to -d) with webpack." 27 | ) 28 | } 29 | } 30 | 31 | import autoImport.SvelteKeys._ 32 | 33 | override def projectSettings: Seq[Setting[_]] = Seq( 34 | svelte := svelteStage.value, 35 | svelte / excludeFilter := HiddenFileFilter || "_*", 36 | svelte / includeFilter := "*.svelte", 37 | prodCommands := Set("stage", "docker:publish", "docker:stage", "docker:publishLocal"), 38 | nodeModulesPath := "./node_modules", 39 | webpackBinary := "please-define-the-binary", 40 | webpackConfig := "please-define-the-config-location.js", 41 | ) 42 | 43 | def svelteStage: Def.Initialize[Task[Pipeline.Stage]] = Def.task { 44 | val sourceDir = (Assets / sourceDirectory).value 45 | val targetDir = webTarget.value 46 | val logger = streams.value.log 47 | val webpackBinaryLocation = (svelte / webpackBinary).value 48 | val webpackConfigLocation = (svelte / webpackConfig).value 49 | val nodeModulesLocation = (svelte / nodeModulesPath).value 50 | val svelteReporter = (Assets / reporter).value 51 | val isProd = state.value.currentCommand.exists { exec => 52 | val v = Set("stage", "docker:publish", "docker:stage", "docker:publishLocal").contains(exec.commandLine) 53 | logger.info(s"Detected the command: ${exec.commandLine}. Use the production mode: ${v}.") 54 | v 55 | } 56 | 57 | val sources = (sourceDir ** ((svelte / includeFilter).value -- (svelte / excludeFilter).value)).get 58 | val cacheDirectory = (Assets / streams).value.cacheDirectory 59 | 60 | implicit val fileHasherIncludingOptions = OpInputHasher[File] { f => 61 | OpInputHash.hashString( 62 | Seq( 63 | f.getCanonicalPath, 64 | isProd, 65 | sourceDir.getAbsolutePath 66 | ).mkString("--") 67 | ) 68 | } 69 | 70 | { mappings => 71 | 72 | val results = incremental.syncIncremental(cacheDirectory / "run", sources) { modifiedSources => 73 | val startInstant = System.currentTimeMillis 74 | 75 | if (modifiedSources.nonEmpty) { 76 | logger.info(s"[Svelte] Compile on ${modifiedSources.size} changed files") 77 | } else { 78 | logger.info(s"[Svelte] No changes to compile") 79 | } 80 | 81 | val compiler = new Compiler( 82 | new File(webpackBinaryLocation), 83 | new File(webpackConfigLocation), 84 | sourceDir, 85 | targetDir, 86 | isProd, 87 | logger, 88 | new File(nodeModulesLocation) 89 | ) 90 | 91 | // Compile all modified sources at once 92 | val result = compiler.compile(modifiedSources.map(_.toPath)) 93 | 94 | // Report compilation problems 95 | CompileProblems.report( 96 | reporter = svelteReporter, 97 | problems = if (!result.success) { 98 | Seq(new Problem { 99 | override def category() = "" 100 | 101 | override def severity() = Severity.Error 102 | 103 | override def message() = "" 104 | 105 | override def position() = new Position { 106 | override def line() = java.util.Optional.empty() 107 | 108 | override def lineContent() = "" 109 | 110 | override def offset() = java.util.Optional.empty() 111 | 112 | override def pointer() = java.util.Optional.empty() 113 | 114 | override def pointerSpace() = java.util.Optional.empty() 115 | 116 | override def sourcePath() = java.util.Optional.empty() 117 | 118 | override def sourceFile() = java.util.Optional.empty() 119 | } 120 | }) 121 | } else { 122 | Seq.empty 123 | } 124 | ) 125 | 126 | // Collect OpResults 127 | val opResults: Map[File, OpResult] = result.entries.map { entry => 128 | entry.inputFile -> OpSuccess(entry.filesRead.map(_.toFile), entry.filesWritten.map(_.toFile)) 129 | }.toMap 130 | 131 | // Collect the created files 132 | val createdFiles = result.entries.flatMap(_.filesWritten.map(_.toFile)) 133 | 134 | val endInstant = System.currentTimeMillis 135 | 136 | if (createdFiles.nonEmpty) { 137 | logger.info(s"[Svelte] finished compilation in ${endInstant - startInstant} ms and generated ${createdFiles.size} files") 138 | } 139 | 140 | (opResults, createdFiles) 141 | 142 | }(fileHasherIncludingOptions) 143 | 144 | val newMappings = (results._1 ++ results._2.toSet).toSeq.map { file => 145 | file -> targetDir.relativize(file).get.toString 146 | } 147 | 148 | val newKeys = newMappings.map(_._2).toSet 149 | 150 | newMappings ++ mappings.filterNot { case (_, key) => newKeys.contains(key) } // prevent duplicates 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/test/scala/helpers/BaseSpec.scala: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import org.mockito.ArgumentMatcher 4 | import org.mockito.internal.progress.ThreadSafeMockingProgress 5 | import org.mockito.verification.VerificationMode 6 | import utest._ 7 | 8 | import scala.collection.mutable 9 | import scala.reflect.ClassTag 10 | 11 | abstract class BaseSpec extends TestSuite { 12 | 13 | def mock[T](implicit m: ClassTag[T]): T = org.mockito.Mockito.mock(m.runtimeClass.asInstanceOf[Class[T]]) 14 | 15 | def any[T]() = org.mockito.ArgumentMatchers.any[T]() 16 | def argThat[T](fn: T => Boolean) = 17 | org.mockito.ArgumentMatchers.argThat[T](new ArgumentMatcher[T] { 18 | override def matches(argument: T) = fn(argument) 19 | }) 20 | def varArgsThat[T](fn: Seq[T] => Boolean): T = { 21 | ThreadSafeMockingProgress 22 | .mockingProgress() 23 | .getArgumentMatcherStorage 24 | .reportMatcher(new ArgumentMatcher[mutable.WrappedArray[T]] { 25 | override def matches(argument: mutable.WrappedArray[T]) = fn(argument) 26 | }) 27 | null.asInstanceOf[T] 28 | } 29 | 30 | def times(n: Int) = org.mockito.Mockito.times(n) 31 | 32 | def eq[T](v: T) = org.mockito.ArgumentMatchers.eq(v) 33 | 34 | def verify[T](mock: T) = org.mockito.Mockito.verify(mock) 35 | def verify[T](mock: T, mode: VerificationMode) = org.mockito.Mockito.verify(mock, mode) 36 | def verifyNoMoreInteractions(mocks: AnyRef*) = org.mockito.Mockito.verifyNoMoreInteractions(mocks: _*) 37 | def verifyZeroInteractions(mocks: AnyRef*) = org.mockito.Mockito.verifyZeroInteractions(mocks: _*) 38 | def when[T](methodCall: T) = org.mockito.Mockito.when(methodCall) 39 | } 40 | -------------------------------------------------------------------------------- /src/test/scala/tanin/play/svelte/CompilerIntegrationSpec.scala: -------------------------------------------------------------------------------- 1 | package tanin.play.svelte 2 | 3 | import java.io.File 4 | import java.nio.file.Files 5 | 6 | import helpers.BaseSpec 7 | import sbt.internal.util.ManagedLogger 8 | import sbt.{ Tests => _, _ } 9 | import utest._ 10 | 11 | object CompilerIntegrationSpec extends BaseSpec { 12 | 13 | def runTest(webpackConfigFilename: String) = { 14 | val targetDir = Files.createTempDirectory("sbt-svelte-compiler-integration-spec").toFile 15 | println(targetDir) 16 | val compiler = new Compiler( 17 | webpackBinary = if (sys.props.getOrElse("os.name", "").toLowerCase.contains("win")) { 18 | new File("node_modules") / ".bin" / "webpack.cmd" // Detect Windows 19 | } else { 20 | new File("node_modules") / ".bin" / "webpack" 21 | }, 22 | webpackConfig = new File("src") / "test" / "scala" / "tanin" / "play" / "svelte" / "assets" / webpackConfigFilename, 23 | sourceDir = new File("src") / "test" / "scala" / "tanin" / "play" / "svelte" / "assets", 24 | targetDir = targetDir, 25 | isProd = true, 26 | logger = mock[ManagedLogger], 27 | nodeModules = new File("node_modules") 28 | ) 29 | 30 | val baseInputDir = new File("src") / "test" / "scala" / "tanin" / "play" / "svelte" / "assets" / "svelte" 31 | val componentA = baseInputDir / "component-a.svelte" 32 | val componentD = baseInputDir / "component-d.svelte" 33 | val componentB = baseInputDir / "dependencies/_component-b.svelte" 34 | val componentC = baseInputDir / "dependencies/_component-c.svelte" 35 | val inputs = Seq(componentA, componentD) 36 | val result = compiler.compile(inputs.map(_.toPath)) 37 | 38 | result.success ==> true 39 | result.entries.size ==> 2 40 | 41 | result.entries.head.inputFile ==> componentA 42 | result.entries.head.filesWritten.size ==> 2 43 | result.entries.head.filesWritten.foreach { fileWritten => 44 | Files.exists(fileWritten) ==> true 45 | } 46 | result.entries.head.filesWritten ==> Set( 47 | (targetDir / "svelte" / "component-a.js").toPath, 48 | (targetDir / "svelte" / "component-a.css").toPath 49 | ) 50 | result.entries.head.filesRead ==> Set(componentA.toPath, componentB.toPath, componentC.toPath) 51 | 52 | result.entries(1).inputFile ==> componentD 53 | result.entries(1).filesWritten.size ==> 1 54 | result.entries(1).filesWritten.foreach { fileWritten => 55 | Files.exists(fileWritten) ==> true 56 | } 57 | result.entries(1).filesWritten ==> Set((targetDir / "svelte" / "component-d.js").toPath) 58 | result.entries(1).filesRead ==> Set(componentD.toPath, componentC.toPath) 59 | } 60 | 61 | val tests = Tests { 62 | 'compile - { 63 | "run webpack and get result correctly (webpack.config.js)" - { 64 | runTest("webpack.config.js") 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/scala/tanin/play/svelte/CompilerSpec.scala: -------------------------------------------------------------------------------- 1 | package tanin.play.svelte 2 | 3 | import java.io.File 4 | import java.nio.file.{ Files, Paths } 5 | 6 | import helpers.BaseSpec 7 | import play.api.libs.json.{ JsArray, Json } 8 | import sbt.internal.util.ManagedLogger 9 | import sbt.{ Tests => _, _ } 10 | import utest._ 11 | 12 | import scala.io.Source 13 | 14 | object CompilerSpec extends BaseSpec { 15 | 16 | val tests = Tests { 17 | 'compile - { 18 | val logger = mock[ManagedLogger] 19 | val shell = mock[Shell] 20 | val computeDependencyTree = mock[ComputeDependencyTree] 21 | val prepareWebpackConfig = mock[PrepareWebpackConfig] 22 | val sourceDir = new File("sourceDir") / "somepath" 23 | val targetDir = new File("targetDir") / "anotherpath" 24 | val nodeModulesDir = new File("node_modules") 25 | val originalConfigFile = sourceDir / "config" / "webpack.config.js" 26 | val webpackBinaryFile = sourceDir / "binary" / "webpack.binary" 27 | val compiler = new Compiler( 28 | webpackBinaryFile, 29 | originalConfigFile, 30 | sourceDir, 31 | targetDir, 32 | true, 33 | logger, 34 | nodeModulesDir, 35 | shell, 36 | computeDependencyTree, 37 | prepareWebpackConfig 38 | ) 39 | 40 | val preparedConfigFile = new File("new") / "webpack" / "prepared-config.js" 41 | when(prepareWebpackConfig.apply(any(), any())).thenReturn(preparedConfigFile.getAbsolutePath) 42 | 43 | "handles empty" - { 44 | compiler.compile(Seq.empty) ==> CompilationResult(true, Seq.empty) 45 | verifyZeroInteractions(shell, logger, computeDependencyTree, prepareWebpackConfig) 46 | } 47 | 48 | "fails" - { 49 | when(shell.execute(any(), any(), any(), any())).thenReturn(1) 50 | 51 | val (module1, file1) = Seq("a", "b", "c").mkString(Path.sep.toString) -> (sourceDir / "a" / "b" / "c.svelte") 52 | val (module2, file2) = Seq("a", "b").mkString(Path.sep.toString) -> (sourceDir / "a" / "b.svelte") 53 | val inputPaths = Seq(file1.toPath, file2.toPath) 54 | val result = compiler.compile(inputPaths) 55 | result.success ==> false 56 | result.entries.isEmpty ==> true 57 | 58 | verify(prepareWebpackConfig) 59 | .apply(originalWebpackConfig = originalConfigFile, inputs = Seq(Input(module1, file1.toPath), Input(module2, file2.toPath))) 60 | verifyZeroInteractions(computeDependencyTree) 61 | verify(shell).execute( 62 | any[ManagedLogger](), 63 | eq( 64 | Seq( 65 | webpackBinaryFile.getCanonicalPath, 66 | "--config", 67 | preparedConfigFile.getAbsolutePath, 68 | "--output-path", 69 | targetDir.getCanonicalPath, 70 | "--mode", 71 | "production" 72 | ).mkString(" ") 73 | ), 74 | eq(sourceDir), 75 | varArgsThat[(String, String)] { varargs => 76 | varargs.toSet == Set("NODE_PATH" -> nodeModulesDir.getCanonicalPath, "ENABLE_SVELTE_CHECK" -> "true") 77 | } 78 | ) 79 | } 80 | 81 | "compiles successfully" - { 82 | val (module1, file1) = Seq("a", "b", "c").mkString(Path.sep.toString) -> (sourceDir / "a" / "b" / "c.svelte") 83 | val (module2, file2) = Seq("a", "b").mkString(Path.sep.toString) -> (sourceDir / "a" / "b.svelte") 84 | 85 | when(shell.fileExists(any())).thenReturn(false) 86 | when(shell.fileExists(argThat[File] { f => Set("c.js", "b.js", "b.css").contains(f.getName) })).thenReturn(true) 87 | 88 | when(shell.execute(any(), any(), any(), any())).thenReturn(0) 89 | when(computeDependencyTree.apply(any[File]())).thenReturn( 90 | Map( 91 | Seq("a", "b", "c.svelte").mkString(Path.sep.toString) -> Set(Seq("a", "b", "c.svelte").mkString(Path.sep.toString)), 92 | Seq("a", "b.svelte").mkString(Path.sep.toString) -> Set( 93 | Seq("a", "b.svelte").mkString(Path.sep.toString), 94 | Seq("a", "b", "c.svelte").mkString(Path.sep.toString) 95 | ) 96 | ) 97 | ) 98 | 99 | val inputPaths = Seq(file1.toPath, file2.toPath) 100 | val result = compiler.compile(inputPaths) 101 | result.success ==> true 102 | result.entries.size ==> 2 103 | 104 | Files.isSameFile(result.entries.head.inputFile.toPath, inputPaths.head) ==> true 105 | result.entries.head.filesRead ==> Set((sourceDir / "a" / "b" / "c.svelte").toPath) 106 | result.entries.head.filesWritten ==> Set((targetDir / "a" / "b" / "c.js").toPath) 107 | 108 | Files.isSameFile(result.entries(1).inputFile.toPath, inputPaths(1)) ==> true 109 | result.entries(1).filesRead ==> Set((sourceDir / "a" / "b.svelte").toPath, (sourceDir / "a" / "b" / "c.svelte").toPath) 110 | result.entries(1).filesWritten ==> Set((targetDir / "a" / "b.js").toPath, (targetDir / "a" / "b.css").toPath) 111 | 112 | verify(prepareWebpackConfig) 113 | .apply(originalWebpackConfig = originalConfigFile, inputs = Seq(Input(module1, file1.toPath), Input(module2, file2.toPath))) 114 | verify(computeDependencyTree).apply(targetDir / "sbt-js-tree.json") 115 | verify(shell).execute( 116 | any[ManagedLogger](), 117 | eq( 118 | Seq( 119 | webpackBinaryFile.getCanonicalPath, 120 | "--config", 121 | preparedConfigFile.getAbsolutePath, 122 | "--output-path", 123 | targetDir.getCanonicalPath, 124 | "--mode", 125 | "production" 126 | ).mkString(" ") 127 | ), 128 | eq(sourceDir), 129 | varArgsThat[(String, String)] { varargs => 130 | varargs.toSet == Set("NODE_PATH" -> nodeModulesDir.getCanonicalPath, "ENABLE_SVELTE_CHECK" -> "true") 131 | } 132 | ) 133 | } 134 | } 135 | 136 | 'getWebpackConfig - { 137 | val originalWebpackConfig = Files.createTempFile("test", "test") 138 | val sourceDir = new File("sourceDir") / "somepath" 139 | val (module1, file1) = Seq("a", "b", "c").mkString(Path.sep.toString) -> (sourceDir / "a" / "b" / "c.svelte") 140 | 141 | val webpackConfig = 142 | (new PrepareWebpackConfig).apply(originalWebpackConfig = originalWebpackConfig.toFile, inputs = Seq(Input(module1, file1.toPath))) 143 | val sbtJsFile = new File(webpackConfig).getParentFile / "sbt-js-plugin.js" 144 | 145 | Files.exists(Paths.get(webpackConfig)) ==> true 146 | Files.exists(sbtJsFile.toPath) ==> true 147 | 148 | val src = Source.fromFile(sbtJsFile) 149 | src.mkString ==> Source.fromInputStream(getClass.getResourceAsStream("/sbt-js-plugin.js")).mkString 150 | src.close() 151 | 152 | Files.deleteIfExists(originalWebpackConfig) 153 | Files.deleteIfExists(sbtJsFile.toPath) 154 | } 155 | 156 | 'buildDependencies - { 157 | val compute = new ComputeDependencyTree 158 | def make(s: String) = s"vue${Path.sep}$s" 159 | val a = make("a") 160 | val b = make("b") 161 | val c = make("c") 162 | val d = make("d") 163 | val nonVue = "non-vue" 164 | 165 | "builds correctly with flatten" - { 166 | // Even on window, the path separator from webpack's command is still `/`. 167 | val jsonStr = JsArray( 168 | Seq( 169 | Json.obj( 170 | "name" -> "./vue/a", 171 | "reasons" -> Seq.empty[String] 172 | ), 173 | Json.obj( 174 | "name" -> "./vue/b", 175 | "reasons" -> Seq("./vue/a + 4 modules") 176 | ), 177 | Json.obj( 178 | "name" -> "./vue/c", 179 | "reasons" -> Seq("./vue/b + 4 modules") 180 | ), 181 | Json.obj( 182 | "name" -> "./vue/d", 183 | "reasons" -> Seq("./vue/a + 4 modules") 184 | ) 185 | ) 186 | ).toString 187 | 188 | compute(jsonStr) ==> Map( 189 | a -> Set(a, b, c, d), 190 | b -> Set(b, c), 191 | c -> Set(c), 192 | d -> Set(d) 193 | ) 194 | } 195 | 196 | "handles non ./vue correctly" - { 197 | val jsonStr = JsArray( 198 | Seq( 199 | Json.obj( 200 | "name" -> "./vue/a", 201 | "reasons" -> Seq.empty[String] 202 | ), 203 | Json.obj( 204 | "name" -> nonVue, 205 | "reasons" -> Seq("./vue/a + 4 modules") 206 | ), 207 | Json.obj( 208 | "name" -> "./vue/c + 4 modules", 209 | "reasons" -> Seq(nonVue) 210 | ) 211 | ) 212 | ).toString 213 | 214 | compute(jsonStr) ==> Map( 215 | a -> Set(a, c), 216 | c -> Set(c) 217 | ) 218 | } 219 | 220 | "handles cyclic dependencies" - { 221 | val jsonStr = JsArray( 222 | Seq( 223 | Json.obj( 224 | "name" -> "./vue/a", 225 | "reasons" -> Seq("./vue/c + 4 modules") 226 | ), 227 | Json.obj( 228 | "name" -> "./vue/b", 229 | "reasons" -> Seq("./vue/a + 4 modules") 230 | ), 231 | Json.obj( 232 | "name" -> "./vue/c", 233 | "reasons" -> Seq("./vue/b + 4 modules") 234 | ) 235 | ) 236 | ).toString() 237 | 238 | compute(jsonStr) ==> Map( 239 | a -> Set(a, b, c), 240 | b -> Set(a, b, c), 241 | c -> Set(a, b, c) 242 | ) 243 | } 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/test/scala/tanin/play/svelte/assets/dummy.ts: -------------------------------------------------------------------------------- 1 | // This file is needed. Otherwise, Typescript compiler would refuse to run. Who knows why? 2 | // See: https://stackoverflow.com/questions/41211566/tsconfig-json-buildno-inputs-were-found-in-config-file 3 | -------------------------------------------------------------------------------- /src/test/scala/tanin/play/svelte/assets/svelte/component-a.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
A
6 | 7 | 10 | -------------------------------------------------------------------------------- /src/test/scala/tanin/play/svelte/assets/svelte/component-d.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
D
6 | -------------------------------------------------------------------------------- /src/test/scala/tanin/play/svelte/assets/svelte/dependencies/_component-b.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
B
6 | 7 | 9 | -------------------------------------------------------------------------------- /src/test/scala/tanin/play/svelte/assets/svelte/dependencies/_component-c.svelte: -------------------------------------------------------------------------------- 1 | 3 | 4 |
C
5 | 6 | 8 | -------------------------------------------------------------------------------- /src/test/scala/tanin/play/svelte/assets/svelte/dependencies/style.scss: -------------------------------------------------------------------------------- 1 | .beautiful-box { 2 | font-size: 16px; 3 | } -------------------------------------------------------------------------------- /src/test/scala/tanin/play/svelte/assets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "allowJs": true, 6 | "jsx": "preserve", 7 | "moduleResolution": "node" 8 | }, 9 | "exclude": [ 10 | "../../../../../../node_modules" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/test/scala/tanin/play/svelte/assets/webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const sveltePreprocess = require("svelte-preprocess"); 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | 7 | module.exports = { 8 | resolve: { 9 | alias: { 10 | svelte: path.join(process.env.NODE_PATH, 'svelte/src/runtime') 11 | }, 12 | extensions: ['.mjs', '.js', '.svelte'], 13 | mainFields: ['svelte', 'browser', 'module', 'main'], 14 | conditionNames: ['svelte', 'browser'] 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.svelte$/, 20 | use: { 21 | loader: 'svelte-loader', 22 | options: { 23 | emitCss: true, 24 | preprocess: sveltePreprocess({}), 25 | } 26 | } 27 | }, 28 | { 29 | test: /\.css$/, 30 | exclude: /node_modules/, 31 | use: [ 32 | MiniCssExtractPlugin.loader, 33 | 'css-loader', 34 | ], 35 | }, 36 | ] 37 | }, 38 | plugins: [ 39 | new MiniCssExtractPlugin(), 40 | ], 41 | performance: { 42 | hints: 'error', 43 | }, 44 | stats: 'minimal' 45 | }; 46 | -------------------------------------------------------------------------------- /test-play-project/.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "2.0.0" 2 | 3 | maxColumn = 140 4 | align = most 5 | continuationIndent.defnSite = 2 6 | assumeStandardLibraryStripMargin = true 7 | docstrings = JavaDoc 8 | lineEndings = preserve 9 | includeCurlyBraceInSelectChains = false 10 | danglingParentheses = true 11 | spaces { 12 | inImportCurlyBraces = true 13 | } 14 | optIn.annotationNewlines = true 15 | 16 | rewrite.rules = [SortImports, RedundantBraces] 17 | -------------------------------------------------------------------------------- /test-play-project/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.watcherExclude": { 3 | "**/target": true 4 | } 5 | } -------------------------------------------------------------------------------- /test-play-project/README.md: -------------------------------------------------------------------------------- 1 | Test project for sbt-svelte 2 | ============================ 3 | 4 | Run `npm install` in order to install all the necessary packages. 5 | 6 | `sbt run` to run locally. Try modify *.vue to see that the changes are recompiled. 7 | 8 | `sbt test` to run the browser test. It's important that the change is re-compiled when we run the test. 9 | 10 | `sbt stage` to package app for production deployment, and run ` ./target/universal/stage/bin/test-play-project -Dplay.http.secret.key=abcdefghijk`. 11 | -------------------------------------------------------------------------------- /test-play-project/app/assets/svelte/components/common/_our-button.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /test-play-project/app/assets/svelte/components/common/_our-js-button.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | 19 | 21 | -------------------------------------------------------------------------------- /test-play-project/app/assets/svelte/components/common/button.scss: -------------------------------------------------------------------------------- 1 | .our-button { 2 | font-size: 18px; 3 | } 4 | -------------------------------------------------------------------------------- /test-play-project/app/assets/svelte/components/greeting-form.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 |

{greeting}

24 |

Value: {toggleText}

25 | Toggle Typescript button 26 | Toggle Javascript button 27 |
28 | 29 | 36 | -------------------------------------------------------------------------------- /test-play-project/app/assets/svelte/components/test-form.svelte: -------------------------------------------------------------------------------- 1 | 3 | 4 |
5 | Hello World 6 |
7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /test-play-project/app/controllers/AssetsController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api.{Environment, Mode} 4 | import play.api.http.HttpErrorHandler 5 | import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents} 6 | 7 | import javax.inject.{Inject, Singleton} 8 | import scala.concurrent.{ExecutionContext, Future} 9 | 10 | @Singleton 11 | class AssetsController @Inject()( 12 | errorHandler: HttpErrorHandler, 13 | meta: AssetsMetadata, 14 | env: Environment 15 | )(implicit ec: ExecutionContext) 16 | extends Assets(errorHandler, meta, env) { 17 | 18 | override def versioned(path: String, file: Assets.Asset): Action[AnyContent] = Action.async { req => 19 | if ( 20 | env.mode == Mode.Dev && 21 | ( 22 | file.name.startsWith("svelte_") || 23 | file.name.startsWith("svelte/") || 24 | file.name.startsWith("stylesheets/tailwindbase.css") 25 | ) 26 | ) { 27 | Future(Redirect(s"http://localhost:9001/assets/${file.name}")) 28 | } else { 29 | super.versioned(path, file)(req) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test-play-project/app/controllers/HomeController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.{ Inject, Singleton } 4 | import play.api.mvc.{ AbstractController, ControllerComponents } 5 | 6 | import scala.concurrent.{ ExecutionContext, Future } 7 | 8 | @Singleton 9 | class HomeController @Inject()( 10 | controllerComponents: ControllerComponents 11 | )(implicit ec: ExecutionContext) 12 | extends AbstractController(controllerComponents) { 13 | 14 | def index = Action.async { 15 | Future(Ok(views.html.index("Welcome to sbt-svelte"))) 16 | } 17 | 18 | def test = Action.async { 19 | Future(Ok(views.html.test())) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test-play-project/app/libraries/Renderer.scala: -------------------------------------------------------------------------------- 1 | package libraries 2 | 3 | import play.api.libs.json.{JsArray, JsObject, JsString, JsValue, Json} 4 | 5 | object Renderer { 6 | def apply(value: String): String = { 7 | sanitize(Json.toJson(value).toString) 8 | } 9 | 10 | def sanitize(value: String): String = { 11 | value.replaceAll("<", "\\\\u003C") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test-play-project/app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(greeting: String) 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 17 | -------------------------------------------------------------------------------- /test-play-project/app/views/test.scala.html: -------------------------------------------------------------------------------- 1 | @() 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 16 | -------------------------------------------------------------------------------- /test-play-project/build.sbt: -------------------------------------------------------------------------------- 1 | import tanin.play.svelte.SbtSvelte.autoImport.SvelteKeys.svelte 2 | 3 | name := """test-play-project""" 4 | organization := "tanin.play.svelte" 5 | version := "1.0-SNAPSHOT" 6 | 7 | val isWin = sys.props.get("os.name").exists(_.toLowerCase.contains("win")) 8 | 9 | lazy val root = (project in file(".")) 10 | .enablePlugins(PlayScala, SbtWeb, SbtSvelte, SbtPostcss) 11 | .settings( 12 | scalaVersion := "2.13.16", 13 | libraryDependencies ++= Seq( 14 | guice, 15 | "org.scalatestplus.play" %% "scalatestplus-play" % "5.1.0" % Test, 16 | ), 17 | svelte / SvelteKeys.webpackBinary := { 18 | if (isWin) { 19 | (new File(".") / "node_modules" / ".bin" / "webpack.cmd").getAbsolutePath 20 | } else { 21 | (new File(".") / "node_modules" / ".bin" / "webpack").getAbsolutePath 22 | } 23 | }, 24 | svelte / SvelteKeys.webpackConfig := (new File(".") / "webpack.config.js").getAbsolutePath, 25 | // All non-entry-points components, which are not included directly in HTML, should have the prefix `_`. 26 | // Webpack shouldn't compile non-entry-components directly. It's wasteful. 27 | svelte / excludeFilter := "_*", 28 | postcss / PostcssKeys.binaryFile := { 29 | if (isWin) { 30 | (new File(".") / "node_modules" / ".bin" / "postcss.cmd").getAbsolutePath 31 | } else { 32 | (new File(".") / "node_modules" / ".bin" / "postcss").getAbsolutePath 33 | } 34 | }, 35 | postcss / PostcssKeys.inputFile := "./public/stylesheets/tailwindbase.css", 36 | pipelineStages ++= Seq(postcss, svelte, digest), 37 | TestAssets / pipelineStages ++= Seq(postcss, svelte), 38 | ) 39 | 40 | addCommandAlias( 41 | "fmt", 42 | "all scalafmtSbt scalafmt test:scalafmt" 43 | ) 44 | addCommandAlias( 45 | "check", 46 | "all scalafmtSbtCheck scalafmtCheck test:scalafmtCheck" 47 | ) 48 | -------------------------------------------------------------------------------- /test-play-project/conf/application.conf: -------------------------------------------------------------------------------- 1 | play.filters.enabled=[] 2 | play.http.secret.key="QCY?tAnfk?aZ?iwrNwnxIlRwerew6CTf:G3gf:90Latabg@5241AB`R5W:1uDFN];Ik@n" 3 | -------------------------------------------------------------------------------- /test-play-project/conf/routes: -------------------------------------------------------------------------------- 1 | GET / controllers.HomeController.index 2 | GET /test controllers.HomeController.test 3 | 4 | GET /assets/*file controllers.AssetsController.versioned(path="/public", file: Asset) 5 | -------------------------------------------------------------------------------- /test-play-project/hmr.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var express = require('express'); 3 | var cors = require('cors') 4 | var app = express(); 5 | 6 | // Step 1: Create & configure a webpack compiler 7 | var webpack = require('webpack'); 8 | var webpackConfig = require('./webpack.config.js')({}, {mode: 'development'}); 9 | var compiler = webpack(webpackConfig); 10 | 11 | app.use(cors()) 12 | // Step 2: Attach the dev middleware to the compiler & the server 13 | app.use( 14 | require('webpack-dev-middleware')(compiler, { 15 | publicPath: webpackConfig.output.publicPath, 16 | }) 17 | ); 18 | 19 | // Step 3: Attach the hot middleware to the compiler & the server 20 | app.use( 21 | require('webpack-hot-middleware')(compiler, { 22 | log: console.log, 23 | path: '/__webpack_hmr', 24 | heartbeat: 10 * 1000, 25 | }) 26 | ); 27 | 28 | // Do anything you like with the rest of your express application. 29 | 30 | var hmr = http.createServer(app); 31 | hmr.listen(9001, "localhost", function () { 32 | console.log('Listening on %j', hmr.address()); 33 | }); 34 | -------------------------------------------------------------------------------- /test-play-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "browserslist": [ 3 | "> 1%", 4 | "last 2 versions", 5 | "not ie <= 8" 6 | ], 7 | "devDependencies": { 8 | "@babel/core": "^7.12.10", 9 | "@types/node": "^14.14.16", 10 | "babel-loader": "^8.2.1", 11 | "css-loader": "^5.0.1", 12 | "express": "^4.21.1", 13 | "glob": "^11.0.0", 14 | "html-entities": "2.5.2", 15 | "mini-css-extract-plugin": "^2.7.6", 16 | "postcss": "^8.4.38", 17 | "postcss-cli": "^11.0.0", 18 | "postcss-loader": "^8.1.1", 19 | "sass": "1.86.3", 20 | "sass-loader": "16.0.5", 21 | "style-loader": "^2.0.0", 22 | "svelte": "5.34.7", 23 | "svelte-check": "4.2.2", 24 | "svelte-loader": "3.2.4", 25 | "svelte-preprocess": "6.0.3", 26 | "tailwindcss": "^3.4.3", 27 | "ts-loader": "^8.0.12", 28 | "tsconfig-paths-webpack-plugin": "^3.3.0", 29 | "typescript": "5.8.3", 30 | "webpack": "5.98.0", 31 | "webpack-cli": "6.0.1", 32 | "webpack-dev-middleware": "7.4.2", 33 | "webpack-dev-server": "5.2.1", 34 | "webpack-hot-middleware": "2.26.1" 35 | }, 36 | "scripts": { 37 | "hmr": "NODE_PATH=./node_modules ENABLE_HMR=true node hmr.js" 38 | }, 39 | "dependencies": { 40 | "cors": "2.8.5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test-play-project/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('tailwindcss') 4 | ] 5 | } -------------------------------------------------------------------------------- /test-play-project/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.2 2 | -------------------------------------------------------------------------------- /test-play-project/project/plugin.sbt: -------------------------------------------------------------------------------- 1 | lazy val root = 2 | Project("plugins", file(".")).aggregate(sbtSvelte, sbtPostcss).dependsOn(sbtSvelte, sbtPostcss) 3 | lazy val sbtSvelte = RootProject(file("./..").getCanonicalFile.toURI) 4 | lazy val sbtPostcss = RootProject(uri("https://github.com/tanin47/sbt-postcss.git#7df3d113993f31523381c63103091e96636f8684")) 5 | 6 | addSbtPlugin("com.github.sbt" % "sbt-digest" % "2.1.0") 7 | addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.7") 8 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.0") 9 | -------------------------------------------------------------------------------- /test-play-project/public/stylesheets/tailwindbase.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /test-play-project/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | 3 | module.exports = { 4 | content: [ 5 | './public/**/*.{html,js,css}', 6 | './app/**/*.{html,ts,js,svelte,css,scss}' 7 | ], 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [], 12 | } 13 | -------------------------------------------------------------------------------- /test-play-project/test/browsers/Base.scala: -------------------------------------------------------------------------------- 1 | package browsers 2 | 3 | import org.openqa.selenium.chrome.{ChromeDriver, ChromeOptions} 4 | import org.openqa.selenium.{Cookie, WebElement} 5 | import org.scalatest.funspec.AnyFunSpecLike 6 | import org.scalatest.matchers.should.Matchers 7 | import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach} 8 | import play.api.test.TestServer 9 | 10 | import java.net.URLEncoder 11 | import java.nio.charset.StandardCharsets 12 | import play.api.Configuration 13 | import play.api.Application 14 | import play.api.inject.guice.GuiceApplicationBuilder 15 | import play.api.Mode 16 | import scala.concurrent.ExecutionContext 17 | import scala.concurrent.Await 18 | import scala.concurrent.Future 19 | import scala.concurrent.duration.Duration 20 | import java.util.concurrent.TimeUnit 21 | 22 | object Base { 23 | val PORT = 9001 24 | 25 | lazy val app: Application = new GuiceApplicationBuilder() 26 | .configure(Configuration.from(Map.empty)) 27 | .in(Mode.Test) 28 | .build() 29 | 30 | lazy val testServer: TestServer = { 31 | val s = TestServer( 32 | port = PORT, 33 | application = app 34 | ) 35 | s.start() 36 | 37 | s 38 | } 39 | } 40 | 41 | trait Base 42 | extends AnyFunSpecLike 43 | with BeforeAndAfterEach 44 | with BeforeAndAfterAll 45 | with Matchers { 46 | 47 | implicit val ec: ExecutionContext = ExecutionContext.Implicits.global 48 | 49 | def await[T](future: Future[T]): T = Await.result(future, Duration.apply(5, TimeUnit.MINUTES)) 50 | 51 | lazy val webDriver: ChromeDriver = { 52 | 53 | def init(retryCount: Int = 4): ChromeDriver = { 54 | val options = new ChromeOptions() 55 | // TIP: Comment the below line to see the browser in the headful mode. 56 | // Or, in the SBT console, you can set no-headless to true with `set Test / javaOptions += "-Dno-headless=false"`. 57 | // Add Thread.sleep(10000) in your test in order to see the current state of the browser. 58 | if (Option(sys.props("no-headless")).contains("true")) { 59 | // Show the browser 60 | } else { 61 | // options.addArguments("--headless") 62 | } 63 | 64 | options.addArguments("--disable-extensions") 65 | options.addArguments("--disable-gpu") 66 | options.addArguments("--disable-web-security") 67 | options.addArguments("--window-size=800,640") 68 | options.addArguments("--disable-dev-shm-usage") 69 | options.addArguments("--disable-smooth-scrolling") 70 | 71 | try { 72 | val driver = new ChromeDriver(options) 73 | 74 | driver 75 | } catch { 76 | case e: Exception => 77 | Thread.sleep(250) 78 | if (retryCount > 0) { 79 | init(retryCount - 1) 80 | } else { 81 | throw e 82 | } 83 | } 84 | } 85 | 86 | init() 87 | } 88 | 89 | override protected def beforeAll(): Unit = { 90 | val _testServer = Base.testServer // init the test server 91 | 92 | super.beforeAll() 93 | webDriver.getWindowHandle() // initialize web driver 94 | } 95 | 96 | override protected def afterAll(): Unit = { 97 | webDriver.quit() 98 | super.afterAll() 99 | } 100 | } -------------------------------------------------------------------------------- /test-play-project/test/browsers/BrowserSpec.scala: -------------------------------------------------------------------------------- 1 | package browsers 2 | 3 | import org.scalatest.SequentialNestedSuiteExecution 4 | 5 | class BrowserSpec extends Base with SequentialNestedSuiteExecution { 6 | it("clicks on a button") { 7 | webDriver.get(s"http://localhost:${Base.PORT}") 8 | 9 | webDriver.findElementByCssSelector("[data-test-id='text']").getText() should be("Value: off") 10 | 11 | webDriver.findElementByCssSelector(".our-button").click() 12 | webDriver.findElementByCssSelector("[data-test-id='text']").getText() should be("Value: on") 13 | 14 | webDriver.findElementByCssSelector(".our-js-button").click() 15 | webDriver.findElementByCssSelector("[data-test-id='text']").getText() should be("Value: off") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test-play-project/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "types": ["svelte"], 10 | "verbatimModuleSyntax": true 11 | }, 12 | "exclude": ["node_modules", "target"] 13 | } -------------------------------------------------------------------------------- /test-play-project/webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const webpack = require('webpack'); 4 | const sveltePreprocess = require("svelte-preprocess"); 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | const pathModule = require("path"); 7 | const glob = require('glob'); 8 | 9 | let entry = {} 10 | for (const relativePath of glob.globSync('./app/assets/svelte/**/*.svelte')) { 11 | if (pathModule.basename(relativePath).startsWith('_')) { 12 | continue 13 | } 14 | 15 | const key = relativePath.substring('app/assets/'.length, relativePath.length - '.svelte'.length) 16 | 17 | entry[key] = [ 18 | // See why we need reload=true for Svelte 5: https://github.com/sveltejs/svelte-loader/issues/250 19 | "webpack-hot-middleware/client?path=http://localhost:9001/__webpack_hmr&timeout=5000&reload=true", 20 | "./public/stylesheets/tailwindbase.css", 21 | `./${relativePath}` 22 | ] 23 | } 24 | 25 | const replacePathVariables = (path, data) => { 26 | const REGEXP_CAMEL_CASE_NAME = /\[camel-case-name\]/gi; 27 | if (typeof path === "function") { 28 | path = path(data); 29 | } 30 | 31 | if (data && data.chunk && data.chunk.name) { 32 | let tokens = data.chunk.name.split(pathModule.sep); 33 | return path.replace( 34 | REGEXP_CAMEL_CASE_NAME, 35 | tokens[tokens.length - 1] 36 | .replace(/(\-\w)/g, (matches) => { return matches[1].toUpperCase(); }) 37 | .replace(/(^\w)/, (matches) => { return matches[0].toUpperCase(); }) 38 | ); 39 | } else { 40 | return path; 41 | } 42 | }; 43 | 44 | class CamelCaseNamePlugin { 45 | apply(compiler) { 46 | compiler.hooks.compilation.tap("sbt-js-compilation", (compilation) => { 47 | compilation.hooks.assetPath.tap('sbt-js-asset-path', replacePathVariables); 48 | }); 49 | } 50 | } 51 | 52 | 53 | const config = { 54 | mode: 'development', 55 | cache: true, 56 | stats: 'minimal', 57 | entry, 58 | resolve: { 59 | extensions: ['.mjs', '.js', '.svelte'], 60 | mainFields: ['svelte', 'browser', 'module', 'main'], 61 | conditionNames: ['svelte', 'browser'] 62 | }, 63 | module: { 64 | rules: [ 65 | { 66 | test: /\.svelte(\.ts)?$/, 67 | use: { 68 | loader: 'svelte-loader', 69 | options: { 70 | emitCss: true, 71 | preprocess: sveltePreprocess({}), 72 | compilerOptions: { 73 | dev: false, 74 | compatibility: { 75 | componentApi: 4 76 | } 77 | }, 78 | hotReload: false 79 | } 80 | } 81 | }, 82 | { 83 | test: /\.css$/, 84 | exclude: /node_modules/, 85 | use: [ 86 | MiniCssExtractPlugin.loader, 87 | { 88 | loader: 'css-loader', 89 | options: { 90 | importLoaders: 1 91 | } 92 | }, 93 | 'postcss-loader' 94 | ], 95 | }, 96 | ] 97 | }, 98 | plugins: [ 99 | new MiniCssExtractPlugin(), 100 | new CamelCaseNamePlugin() 101 | ], 102 | output: { 103 | publicPath: '/assets/', 104 | library: '[camel-case-name]', 105 | filename: '[name].js', 106 | }, 107 | performance: { 108 | hints: 'error', 109 | maxAssetSize: 2000000, 110 | maxEntrypointSize: 2000000, 111 | assetFilter: function(assetFilename) { 112 | return assetFilename.endsWith('.js'); 113 | } 114 | }, 115 | devtool: 'eval-cheap-source-map', 116 | }; 117 | 118 | module.exports = (env, argv) => { 119 | if (argv.mode === 'production') { 120 | console.log('Webpack for production'); 121 | config.devtool = false; 122 | config.performance.maxAssetSize = 250000; 123 | config.performance.maxEntrypointSize = 250000; 124 | config.optimization = (config.optimization || {}); 125 | } else if (argv.mode === 'development') { 126 | console.log('Webpack for development') 127 | 128 | if (process.env.ENABLE_HMR) { 129 | console.log('Enable HMR') 130 | for (const rule of config.module.rules) { 131 | if (rule.use.loader === 'svelte-loader') { 132 | rule.use.options.emitCss = false 133 | rule.use.options.compilerOptions.dev = true 134 | rule.use.options.hotReload = true 135 | } 136 | } 137 | config.plugins.push(new webpack.HotModuleReplacementPlugin()) 138 | } 139 | } else if (argv.mode === 'none') { 140 | 141 | } else { 142 | throw new Error('argv.mode must be either development, none, or production.') 143 | } 144 | 145 | return config; 146 | }; 147 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / version := "0.2.0" 2 | --------------------------------------------------------------------------------