diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index cca808f6788..47a3ca7d22f 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -102,6 +102,7 @@ ** xref:extending/meta-build.adoc[] ** xref:extending/example-typescript-support.adoc[] ** xref:extending/example-python-support.adoc[] +** xref:large/multi-language-builds.adoc[] // This section focuses on diving into deeper, more advanced topics for Mill. // These are things that most Mill developers would not encounter day to day, // but people developing Mill plugins or working on particularly large or diff --git a/docs/modules/ROOT/pages/large/multi-language-builds.adoc b/docs/modules/ROOT/pages/large/multi-language-builds.adoc new file mode 100644 index 00000000000..606fe3dc68f --- /dev/null +++ b/docs/modules/ROOT/pages/large/multi-language-builds.adoc @@ -0,0 +1,4 @@ += Multi-Language Builds +:page-aliases: Multi_Language_Builds.adoc + +include::partial$example/large/multi/14-multi-language.adoc[] diff --git a/example/large/multi/14-multi-language/build.mill b/example/large/multi/14-multi-language/build.mill new file mode 100644 index 00000000000..375d51905f8 --- /dev/null +++ b/example/large/multi/14-multi-language/build.mill @@ -0,0 +1,76 @@ +package build +import mill._, javascriptlib._, pythonlib._, javalib._ + +object client extends ReactScriptsModule + +object `sentiment-analysis` extends PythonModule { + def mainScript = Task.Source { millSourcePath / "src" / "foo.py" } + + def pythonDeps = Seq("textblob==0.19.0") + + object test extends PythonTests with pythonlib.TestModule.Unittest +} + +object server extends JavaModule { + def ivyDeps = Agg( + ivy"org.springframework.boot:spring-boot-starter-web:2.5.6", + ivy"org.springframework.boot:spring-boot-starter-actuator:2.5.6" + ) + + /** Bundle client & sentiment-analysis as resource */ + def resources = Task.Sources { + os.copy(client.bundle().path, Task.dest / "static") + os.makeDir.all(Task.dest / "analysis") + os.copy(`sentiment-analysis`.bundle().path, Task.dest / "analysis" / "analysis.pex") + super.resources() ++ Seq(PathRef(Task.dest)) + } + + object test extends JavaTests with javalib.TestModule.Junit5 { + def ivyDeps = super.ivyDeps() ++ Agg( + ivy"org.springframework.boot:spring-boot-starter-test:2.5.6" + ) + } +} + +// This example demonstrates a simple multi-langauge project, +// running a `spring boot webserver` serving a `react client` and interacting with a `python binary` +// through the web-server api. + +/** Usage + +> mill client.test +PASS src/test/App.test.tsx +...Text Analysis Tool +...renders the app with initial UI... +...displays sentiment result... +... +Test Suites:...1 passed, 1 total +Tests:...2 passed, 2 total +... + +> mill sentiment-analysis.test +... +test_negative_sentiment... ok +test_neutral_sentiment... ok +test_positive_sentiment... ok +... +Ran 3 tests... +... +OK +... + +> mill server.test +...com.example.ServerTest#shouldReturnStaticPage() finished... +...com.example.ServerTest#shouldReturnPositiveAnalysis() finished... +...com.example.ServerTest#shouldReturnNegativeAnalysis() finished... + +> mill server.runBackground + +> curl http://localhost:8086 +...Sentiment Analysis Tool... + +> curl -X POST http://localhost:8086/api/analysis -H "Content-Type: text/plain" --data "This is awesome!" # Make request to the analysis api +Positive sentiment (polarity: 1.0) + +> mill clean server.runBackground +*/ diff --git a/example/large/multi/14-multi-language/client/public/index.html b/example/large/multi/14-multi-language/client/public/index.html new file mode 100644 index 00000000000..0901c54eb79 --- /dev/null +++ b/example/large/multi/14-multi-language/client/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + Sentiment Analysis Tool + + + +
+ + + diff --git a/example/large/multi/14-multi-language/client/src/App.css b/example/large/multi/14-multi-language/client/src/App.css new file mode 100644 index 00000000000..76ea8e4f8cc --- /dev/null +++ b/example/large/multi/14-multi-language/client/src/App.css @@ -0,0 +1,90 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Arial', sans-serif; +} + +body { + background: #f4f4f4; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +} + +.app-container { + background: #fff; + border-radius: 12px; + padding: 40px; + width: 400px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + text-align: center; +} + +h1 { + margin-bottom: 20px; + font-size: 1.8rem; + color: #333; +} + +.analysis-form { + display: flex; + flex-direction: column; + gap: 10px; +} + +textarea { + width: 100%; + height: 120px; + padding: 10px; + font-size: 1rem; + border: 1px solid #ccc; + border-radius: 8px; + resize: none; + outline: none; +} + +textarea:focus { + border-color: #007bff; +} + +button { + padding: 10px; + background: #007bff; + color: #fff; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 1rem; +} + +button:hover { + background: #0056b3; +} + +button:disabled { + background: #b0c4de; + cursor: not-allowed; +} + +.result-container { + margin-top: 20px; + padding: 15px; + border-radius: 8px; + color: #fff; + font-weight: bold; +} + +/* Sentiment-based styles */ +.result-container.positive { + background-color: #28a745; /* Green for positive */ +} + +.result-container.negative { + background-color: #dc3545; /* Red for negative */ +} + +.result-container.neutral { + background-color: #007bff; /* Blue for neutral */ +} \ No newline at end of file diff --git a/example/large/multi/14-multi-language/client/src/app/App.tsx b/example/large/multi/14-multi-language/client/src/app/App.tsx new file mode 100644 index 00000000000..ece1582abb8 --- /dev/null +++ b/example/large/multi/14-multi-language/client/src/app/App.tsx @@ -0,0 +1,76 @@ +import React, {useState} from 'react'; +import 'src/App.css'; + +const App = () => { + const [inputText, setInputText] = useState(''); + const [result, setResult] = useState(''); + const [loading, setLoading] = useState(false); + const [sentiment, setSentiment] = useState('neutral'); + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + setResult(''); + setSentiment('neutral'); + + try { + const response = await fetch('http://localhost:8086/api/analysis', { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + body: inputText, + }); + + if (response.ok) { + const responseData = await response.text(); + setResult(responseData); + + // Determine sentiment from the response + const polarityMatch = responseData.match(/polarity: ([+-]?[0-9]*\.?[0-9]+)/); + if (polarityMatch) { + const polarity = parseFloat(polarityMatch[1]); + + if (polarity > 0) { + setSentiment('positive'); + } else if (polarity < 0) { + setSentiment('negative'); + } else { + setSentiment('neutral'); + } + } + } else { + setResult('Error occurred during analysis.'); + } + } catch (error) { + setResult('Network error: Could not connect to the server.'); + } finally { + setLoading(false); + } + }; + + return ( +
+

Text Analysis Tool

+
+