diff --git a/README.md b/README.md index df5adff08..3c7c9844d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A [feature-rich](https://livecodes.io/docs/features/), open-source, **client-sid [![LiveCodes: npm version](https://img.shields.io/npm/v/livecodes)](https://www.npmjs.com/package/livecodes) [![LiveCodes: npm downloads](https://img.shields.io/npm/dm/livecodes)](https://www.npmjs.com/package/livecodes) [![LiveCodes: jsdelivr downloads](https://data.jsdelivr.com/v1/package/npm/livecodes/badge?style=rounded)](https://www.jsdelivr.com/package/npm/livecodes) -[![LiveCodes: languages](https://img.shields.io/badge/languages-119-blue)](https://livecodes.io/docs/languages/) +[![LiveCodes: languages](https://img.shields.io/badge/languages-121-blue)](https://livecodes.io/docs/languages/) [![LiveCodes: docs](https://img.shields.io/badge/Documentation-575757?logo=gitbook&logoColor=white)](https://livecodes.io/docs/) [![LiveCodes: llms.txt](https://img.shields.io/badge/llms.txt-575757?logo=googledocs&logoColor=white)](https://livecodes.io/docs/llms.txt) [![LiveCodes: llms-full.txt](https://img.shields.io/badge/llms--full.txt-575757?logo=googledocs&logoColor=white)](https://livecodes.io/docs/llms-full.txt) diff --git a/docs/docs/languages/dart.mdx b/docs/docs/languages/dart.mdx new file mode 100644 index 000000000..dd342ee48 --- /dev/null +++ b/docs/docs/languages/dart.mdx @@ -0,0 +1,115 @@ +# Dart + +[Dart](https://dart.dev/) is a client-optimized, statically typed programming language developed by Google, with a focus on building fast apps on any platform. + +Dart in LiveCodes is compiled to JavaScript in the browser, with no server. It uses the Dart team's own client-side toolchain — DDC, the analyzer and a subset of `pub`, the same one that powers [DartPad](https://dartpad.dev/) — packaged as [`@live-codes/dart-wasm`](https://www.npmjs.com/package/@live-codes/dart-wasm). + +:::info + +For Flutter support in LiveCodes, see [Flutter](./flutter.mdx). + +::: + +## Usage + +Demo: + +import LiveCodes from '../../src/components/LiveCodes.tsx'; + +export const dartConfig = { + activeEditor: 'script', + script: { + language: 'dart', + content: `void main() {\n print('Hello, World!');\n}`, + }, + mode: 'simple', + editor: 'auto', + tools: { + status: 'full', + active: 'console', + }, +}; + + + +The compiled JavaScript runs in the context of the [result page](../features/result.mdx), so a Dart program has the page, its DOM and the markup and styles from the other editors. + +### Accessing the DOM + +`dart:js_interop` ships with the SDK, so the program can reach the page directly — no packages and no network needed: + +```ts +import 'dart:js_interop'; + +@JS('document') +external Document get document; + +extension type Document(JSObject _) implements JSObject { + external Element? querySelector(String selectors); +} + +extension type Element(JSObject _) implements JSObject { + external set textContent(String value); +} + +void main() { + document.querySelector('#name')?.textContent = 'Dart'; +} +``` + +What the program prints goes to the console. + +Templates, collections and pattern matching behave as expected: + +```ts +void main() { + final fibonacci = [0, 1]; + while (fibonacci.length < 10) { + fibonacci.add(fibonacci[fibonacci.length - 1] + fibonacci[fibonacci.length - 2]); + } + print('Fibonacci: $fibonacci'); + + final squares = {for (var i = 1; i <= 5; i++) i: i * i}; + for (final entry in squares.entries) { + print('${entry.key}² = ${entry.value}'); + } +} +``` + +## Language Info + +### Name + +`dart` + +### Aliases / Extensions + +`dartlang`, `dart` + +### Editor + +`script` + +## Compiler + +The Dart team's client-side toolchain: DDC (the Dart development compiler), using the [`dartpad`](https://pub.dev/packages/dartpad) package. + +### Version + +DartPad SDK 0.0.9 + +## Code Formatting + +Dart's official formatter (the [`dart_style`](https://pub.dev/packages/dart_style) package behind `dart format`). + +## Starter Template + +https://livecodes.io/?template=dart + +## Links + +- [Dart official website](https://dart.dev/) +- [Dart documentation](https://dart.dev/guides) +- [DartPad](https://dartpad.dev/) +- [@live-codes/dart-wasm](https://www.npmjs.com/package/@live-codes/dart-wasm) +- [Flutter](./flutter.mdx) in LiveCodes diff --git a/docs/docs/languages/flutter.mdx b/docs/docs/languages/flutter.mdx new file mode 100644 index 000000000..8c0492d65 --- /dev/null +++ b/docs/docs/languages/flutter.mdx @@ -0,0 +1,111 @@ +# Flutter + +[Flutter](https://flutter.dev/) is Google's open-source framework for building natively compiled, multi-platform applications from a single codebase. + +Flutter in LiveCodes is compiled in the browser, with no server, using the same client-side toolchain as [Dart](./dart.mdx) — packaged as [`@live-codes/dart-wasm`](https://www.npmjs.com/package/@live-codes/dart-wasm). The compiled app then boots the Flutter engine in the result page. + +:::info + +For Dart language support in LiveCodes, see [Dart](./dart.mdx). + +::: + +## Usage + +Demo: + +import LiveCodes from '../../src/components/LiveCodes.tsx'; + +export const flutterConfig = { + activeEditor: 'script', + script: { + language: 'flutter', + content: `import 'package:flutter/material.dart';\n\nvoid main() {\n runApp(\n const Center(\n child: Text('Hello, World!', textDirection: TextDirection.ltr),\n ),\n );\n}`, + }, + mode: 'simple', + editor: 'auto', +}; + + + +The app renders in the context of the [result page](../features/result.mdx), so it fills the result pane and responds to user interaction, just like any other Flutter web app. + +A stateful counter is the usual way to see a rebuild: + +```ts +import 'package:flutter/material.dart'; + +void main() { + runApp(const CounterApp()); +} + +class CounterApp extends StatefulWidget { + const CounterApp({super.key}); + + @override + State createState() => _CounterAppState(); +} + +class _CounterAppState extends State { + int _count = 0; + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('$_count', style: const TextStyle(fontSize: 56)), + const SizedBox(height: 12), + ElevatedButton( + onPressed: () => setState(() => _count++), + child: const Text('Increment'), + ), + ], + ), + ), + ), + ); + } +} +``` + +## Language Info + +### Name + +`flutter` + +### Extensions + +`flutter` + +### Editor + +`script` + +## Compiler + +The Dart team's client-side toolchain (DDC), using the [`dartpad`](https://pub.dev/packages/dartpad) package and the [CanvasKit](https://pub.dev/packages/canvas_kit) renderer. + +### Version + +DartPad SDK 0.0.9 + +## Code Formatting + +Dart's official formatter (the [`dart_style`](https://pub.dev/packages/dart_style) package behind `dart format`). + +## Starter Template + +https://livecodes.io/?template=flutter + +## Links + +- [Flutter official website](https://flutter.dev/) +- [Flutter documentation](https://docs.flutter.dev/) +- [Introduction to widgets](https://docs.flutter.dev/ui) +- [@live-codes/dart-wasm](https://www.npmjs.com/package/@live-codes/dart-wasm) +- [Dart](./dart.mdx) in LiveCodes diff --git a/docs/src/components/LanguageSliders.tsx b/docs/src/components/LanguageSliders.tsx index 6116778a2..a6857f45b 100644 --- a/docs/src/components/LanguageSliders.tsx +++ b/docs/src/components/LanguageSliders.tsx @@ -98,6 +98,8 @@ export default function Sliders() { { name: 'cobol-wasm', title: 'COBOL (Wasm)' }, { name: 'fortran', title: 'Fortran' }, { name: 'd-wasm', title: 'D (Wasm)' }, + { name: 'dart', title: 'Dart' }, + { name: 'flutter', title: 'Flutter' }, { name: 'v-wasm', title: 'V (Wasm)' }, { name: 'rust-wasm', title: 'Rust (Wasm)' }, { name: 'swift-wasm', title: 'Swift (Wasm)' }, diff --git a/docs/src/components/TemplateList.tsx b/docs/src/components/TemplateList.tsx index 9b7916baf..5ee78f5f2 100644 --- a/docs/src/components/TemplateList.tsx +++ b/docs/src/components/TemplateList.tsx @@ -93,6 +93,8 @@ const templates = [ { name: 'minizinc', title: 'MiniZinc Starter', thumbnail: 'minizinc.png' }, { name: 'blockly', title: 'Blockly Starter', thumbnail: 'blockly.svg' }, { name: 'diagrams', title: 'Diagrams Starter', thumbnail: 'diagrams.svg' }, + { name: 'dart', title: 'Dart Starter', thumbnail: 'dart.svg' }, + { name: 'flutter', title: 'Flutter Starter', thumbnail: 'flutter.svg' }, ]; export default function TemplateList() { diff --git a/e2e/specs/starter.spec.ts b/e2e/specs/starter.spec.ts index e284e5bec..8cb8f858d 100644 --- a/e2e/specs/starter.spec.ts +++ b/e2e/specs/starter.spec.ts @@ -1837,4 +1837,58 @@ test.describe('Starter Templates from URL', () => { const counterText = await getResult().innerText('text=You clicked'); expect(counterText).toBe('You clicked 3 times.'); }); + + test('Dart Starter', async ({ page, getTestUrl }) => { + // the Dart toolchain (DDC, the analyzer and a subset of pub) is downloaded on the first run + test.slow(); + + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=New'); + await app.click('text=Dart Starter'); + + await waitForEditorFocus(app); + await waitForResultUpdate(); + + // the button stays disabled until the compiled program has run and wired it up + await expect(getResult().locator('#counter-button')).toBeEnabled({ timeout: 280_000 }); + + await getResult().click('text=Click me'); + await getResult().click('text=Click me'); + await getResult().click('text=Click me'); + + await expect(getResult().locator('h1')).toHaveText('Hello, Dart!'); + await expect(getResult().locator('text=You clicked')).toHaveText('You clicked 3 times.'); + }); + + test('Flutter Starter', async ({ page, getTestUrl }) => { + // the Flutter toolchain and the engine are downloaded on the first run + test.slow(); + + await page.goto(getTestUrl()); + + const { app, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=New'); + await app.click('text=Flutter Starter'); + + await waitForEditorFocus(app); + await waitForResultUpdate(); + + // the app renders to a canvas, so the engine booting is asserted through the line its main() + // prints + await app.click('#tools-pane-titles > .console'); + await app.waitForFunction( + () => + [...document.querySelectorAll('#console-container .luna-console-log-item')].some((item) => + item.textContent?.includes('Hello from Flutter!'), + ), + undefined, + { timeout: 280_000 }, + ); + }); }); diff --git a/functions/vendors/templates.js b/functions/vendors/templates.js index 739552b50..f0ed0b6c1 100644 --- a/functions/vendors/templates.js +++ b/functions/vendors/templates.js @@ -91,5 +91,7 @@ export const starterTemplates = { "prolog": "Prolog Starter", "minizinc": "MiniZinc Starter", "blockly": "Blockly Starter", - "diagrams": "Diagrams Starter" + "diagrams": "Diagrams Starter", + "dart": "Dart Starter", + "flutter": "Flutter Starter" } \ No newline at end of file diff --git a/scripts/build.js b/scripts/build.js index 60cf2fc2d..ba90d931b 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -239,6 +239,9 @@ const iifeBuild = () => 'languages/fanak/lang-fanak-script.ts', 'languages/vb-wasm/lang-vb-wasm-script.ts', 'languages/zig-wasm/lang-zig-wasm-script.ts', + 'languages/dart/lang-dart-compiler.ts', + 'languages/dart/lang-dart-script.ts', + 'languages/flutter/lang-flutter-script.ts', 'languages/dot/lang-dot-compiler.ts', 'languages/ejs/lang-ejs-compiler.ts', 'languages/elm/lang-elm-compiler.ts', diff --git a/server/php/inc/starter-templates.json b/server/php/inc/starter-templates.json index bc6c15a79..8b7ff493e 100644 --- a/server/php/inc/starter-templates.json +++ b/server/php/inc/starter-templates.json @@ -91,5 +91,7 @@ "prolog": "Prolog Starter", "minizinc": "MiniZinc Starter", "blockly": "Blockly Starter", - "diagrams": "Diagrams Starter" + "diagrams": "Diagrams Starter", + "dart": "Dart Starter", + "flutter": "Flutter Starter" } \ No newline at end of file diff --git a/src/livecodes/UI/command-menu-actions.ts b/src/livecodes/UI/command-menu-actions.ts index 544e84566..7f4985c64 100644 --- a/src/livecodes/UI/command-menu-actions.ts +++ b/src/livecodes/UI/command-menu-actions.ts @@ -348,6 +348,8 @@ export const getCommandMenuActions = ({ 'minizinc', 'blockly', 'diagrams', + 'dart', + 'flutter', ).map((template) => ({ id: 'Starter template: ' + template, title: window.deps.translateString('commandMenu.template', 'Template') + ': ' + template, diff --git a/src/livecodes/assets/templates/dart.svg b/src/livecodes/assets/templates/dart.svg new file mode 100644 index 000000000..39594efac --- /dev/null +++ b/src/livecodes/assets/templates/dart.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/livecodes/assets/templates/flutter.svg b/src/livecodes/assets/templates/flutter.svg new file mode 100644 index 000000000..efe70706b --- /dev/null +++ b/src/livecodes/assets/templates/flutter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/livecodes/html/language-info.html b/src/livecodes/html/language-info.html index e1fa33fbc..6001d4241 100644 --- a/src/livecodes/html/language-info.html +++ b/src/livecodes/html/language-info.html @@ -638,6 +638,35 @@

D (Wasm)

+
+

Dart

+
+ Dart is compiled to JavaScript by the Dart team's client-side toolchain (DDC) — the same one + that powers DartPad — running entirely in the browser. The compiled bundle then runs in the + result page. No server is involved. +
+ +

Diagrams

(Experimental)
@@ -840,6 +869,36 @@

Flow

+
+

Flutter

+
+ Flutter is compiled to JavaScript with the same client-side toolchain as Dart, and the Flutter + engine is then booted in the result page, where the app renders. No server is involved. +
+ +

Fortran (Wasm)

diff --git a/src/livecodes/i18n/locales/en/language-info.lokalise.json b/src/livecodes/i18n/locales/en/language-info.lokalise.json index 4712dac76..801bb3959 100644 --- a/src/livecodes/i18n/locales/en/language-info.lokalise.json +++ b/src/livecodes/i18n/locales/en/language-info.lokalise.json @@ -268,6 +268,18 @@ "notes": "", "translation": "D (Wasm)" }, + "dart.desc": { + "notes": "", + "translation": "Dart is compiled to JavaScript by the Dart team's client-side toolchain (DDC) — the same one that powers DartPad — running entirely in the browser. The compiled bundle then runs in the result page. No server is involved." + }, + "dart.link": { + "notes": "### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n", + "translation": " Dart official website Dart documentation DartPad LiveCodes Documentation Load starter template " + }, + "dart.name": { + "notes": "", + "translation": "Dart" + }, "diagrams.desc1": { "notes": "", "translation": "(Experimental)" @@ -372,6 +384,18 @@ "notes": "", "translation": "Flow" }, + "flutter.desc": { + "notes": "", + "translation": "Flutter is compiled to JavaScript with the same client-side toolchain as Dart, and the Flutter engine is then booted in the result page, where the app renders. No server is involved." + }, + "flutter.link": { + "notes": "### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n", + "translation": " Flutter official website Flutter documentation Introduction to widgets LiveCodes Documentation Load starter template " + }, + "flutter.name": { + "notes": "", + "translation": "Flutter" + }, "fortran-wasm.desc": { "notes": "### ###\n\n\n", "translation": "Modern Fortran compiled to WebAssembly by LFortran, running entirely in the browser." diff --git a/src/livecodes/i18n/locales/en/language-info.ts b/src/livecodes/i18n/locales/en/language-info.ts index 44e984bb7..43bded72d 100644 --- a/src/livecodes/i18n/locales/en/language-info.ts +++ b/src/livecodes/i18n/locales/en/language-info.ts @@ -124,6 +124,11 @@ const languageInfo = { link: '<1> <2>D official website <3> <4>D language specification <5> <6>D language tour <7> <8>Learn X in Y minutes, where X=D <9> <10>LiveCodes Documentation <11> <12>Load starter template ', name: 'D (Wasm)', }, + dart: { + desc: "Dart is compiled to JavaScript by the Dart team's client-side toolchain (DDC) — the same one that powers DartPad — running entirely in the browser. The compiled bundle then runs in the result page. No server is involved.", + link: '<1> <2>Dart official website <3> <4>Dart documentation <5> <6>DartPad <7> <8>LiveCodes Documentation <9> <10>Load starter template ', + name: 'Dart', + }, diagrams: { desc1: '(Experimental)', desc2: 'Diagrams-as-code. Supports:', @@ -167,6 +172,11 @@ const languageInfo = { link: '<1> <2>Flow official website <3> <4>Flow documentation ', name: 'Flow', }, + flutter: { + desc: 'Flutter is compiled to JavaScript with the same client-side toolchain as Dart, and the Flutter engine is then booted in the result page, where the app renders. No server is involved.', + link: '<1> <2>Flutter official website <3> <4>Flutter documentation <5> <6>Introduction to widgets <7> <8>LiveCodes Documentation <9> <10>Load starter template ', + name: 'Flutter', + }, 'fortran-wasm': { desc: 'Modern Fortran compiled to WebAssembly by <1>LFortran, running entirely in the browser.', link: '<1> <2>Learn Fortran <3> <4>LFortran official website <5> <6>Fortran language website <7> <8>Learn X in Y minutes, where X=Fortran <9> <10>LiveCodes Documentation <11> <12>Load starter template ', diff --git a/src/livecodes/i18n/locales/en/translation.lokalise.json b/src/livecodes/i18n/locales/en/translation.lokalise.json index deeb09852..9518caf0b 100644 --- a/src/livecodes/i18n/locales/en/translation.lokalise.json +++ b/src/livecodes/i18n/locales/en/translation.lokalise.json @@ -2640,6 +2640,10 @@ "notes": "", "translation": "daisyUI Starter" }, + "templates.starter.dart": { + "notes": "", + "translation": "Dart Starter" + }, "templates.starter.diagrams": { "notes": "", "translation": "Diagrams Starter" @@ -2656,6 +2660,10 @@ "notes": "", "translation": "Fennel Starter" }, + "templates.starter.flutter": { + "notes": "", + "translation": "Flutter Starter" + }, "templates.starter.fortran-wasm": { "notes": "", "translation": "Fortran Starter" diff --git a/src/livecodes/i18n/locales/en/translation.ts b/src/livecodes/i18n/locales/en/translation.ts index 890397c76..30269ed0f 100644 --- a/src/livecodes/i18n/locales/en/translation.ts +++ b/src/livecodes/i18n/locales/en/translation.ts @@ -1006,10 +1006,12 @@ const translation = { 'd-wasm': 'D Starter', d3: 'D3 Starter', daisyui: 'daisyUI Starter', + dart: 'Dart Starter', diagrams: 'Diagrams Starter', elm: 'Elm Starter', fanak: 'Fanak Starter', fennel: 'Fennel Starter', + flutter: 'Flutter Starter', 'fortran-wasm': 'Fortran Starter', fsharp: 'F# Starter', 'fsharp-wasm': 'F# (Wasm) Starter', diff --git a/src/livecodes/languages/dart/index.ts b/src/livecodes/languages/dart/index.ts new file mode 100644 index 000000000..09b26498c --- /dev/null +++ b/src/livecodes/languages/dart/index.ts @@ -0,0 +1 @@ +export * from './lang-dart'; diff --git a/src/livecodes/languages/dart/lang-dart-compiler.ts b/src/livecodes/languages/dart/lang-dart-compiler.ts new file mode 100644 index 000000000..58ca60a50 --- /dev/null +++ b/src/livecodes/languages/dart/lang-dart-compiler.ts @@ -0,0 +1,65 @@ +import { getCompileResult } from '../../compiler/utils'; +import type { CompilerFunction } from '../../models'; +import { getErrorMessage } from '../../utils/utils'; +import { getLanguageCustomSettings } from '../utils'; + +declare const self: any; + +(self as any).createDartCompiler = (options: { + engine: 'dart' | 'flutter'; + assetBaseUrl: string; +}): CompilerFunction => { + let pending: Promise | undefined; + // Resolved packages survive between compiles, so `pub get` is paid once. + const compiler = () => { + pending ??= (self as any).DartWasm.createCompiler({ + engine: options.engine, + baseUrl: options.assetBaseUrl, + }); + return pending; + }; + + return async (code, { config }) => { + if (!code.trim()) return getCompileResult(''); + + try { + const instance = await compiler(); + const settings = getLanguageCustomSettings(options.engine, config) as { packages?: string[] }; + const program = await instance.compile(code, { + mode: options.engine === 'flutter' ? 'flutter' : 'console', + ...(Array.isArray(settings?.packages) ? { dependencies: settings.packages } : {}), + }); + + // The compiled bundle is data: the result page loads the runtime and then the modules, in + // order, because DDC bundles call `defineLibrary` as they evaluate. Passing it as JSON keeps + // that ordering in one place instead of relying on script load order. + const asJson = JSON.stringify({ + libraryUri: program.libraryUri, + mode: program.mode, + modules: program.modules.map((module: any) => ({ name: module.name, code: module.code })), + }).replace(/<\//g, '<\\/'); + + const jsCode = ` +(function () { + var program = ${asJson}; + var attempts = 0; + var start = function () { + var livecodes = window.livecodes; + if (!livecodes || !livecodes.${options.engine} || !livecodes.${options.engine}.run) { + if (attempts++ < 400) return setTimeout(start, 25); + return console.error('Dart runtime did not load.'); + } + livecodes.${options.engine}.run(program); + }; + start(); +})(); +`; + + return getCompileResult(jsCode); + } catch (error) { + // eslint-disable-next-line no-console + console.error(getErrorMessage(error)); + return { code: '', info: { errors: [getErrorMessage(error)] } }; + } + }; +}; diff --git a/src/livecodes/languages/dart/lang-dart-runtime.ts b/src/livecodes/languages/dart/lang-dart-runtime.ts new file mode 100644 index 000000000..7694d8dd6 --- /dev/null +++ b/src/livecodes/languages/dart/lang-dart-runtime.ts @@ -0,0 +1,34 @@ +import { dartWasmBaseUrl } from '../../vendors'; + +declare const window: any; +declare const parent: any; + +export const setupDartRuntime = (engine: 'dart' | 'flutter') => { + window.livecodes = window.livecodes || {}; + const ns = (window.livecodes[engine] = window.livecodes[engine] || {}); + + const postLoading = (payload: boolean) => parent.postMessage({ type: 'loading', payload }, '*'); // NOSONAR - the parent validates the source. + + ns.ready ??= (async () => { + ns.runtime = await window.DartWasm.loadRuntime({ engine, baseUrl: dartWasmBaseUrl }); + return ns.runtime; + })(); + + ns.run ??= async (program: { + libraryUri: string; + modules: Array<{ name: string; code: string }>; + }) => { + postLoading(true); + try { + const runtime = await ns.ready; + await runtime.run(program); + } catch (error) { + // eslint-disable-next-line no-console + console.error((error as Error)?.message ?? String(error)); + } finally { + postLoading(false); + } + }; + + return ns; +}; diff --git a/src/livecodes/languages/dart/lang-dart-script.ts b/src/livecodes/languages/dart/lang-dart-script.ts new file mode 100644 index 000000000..9cd73dd58 --- /dev/null +++ b/src/livecodes/languages/dart/lang-dart-script.ts @@ -0,0 +1,3 @@ +import { setupDartRuntime } from './lang-dart-runtime'; + +setupDartRuntime('dart'); diff --git a/src/livecodes/languages/dart/lang-dart.ts b/src/livecodes/languages/dart/lang-dart.ts new file mode 100644 index 000000000..572609180 --- /dev/null +++ b/src/livecodes/languages/dart/lang-dart.ts @@ -0,0 +1,39 @@ +import { codemirrorLegacy } from '../../editor/codemirror/utils'; +import type { LanguageSpecs } from '../../models'; +import { codeMirrorBaseUrl, dartWasmBaseUrl, monacoLanguagesBaseUrl } from '../../vendors'; + +export const dart: LanguageSpecs = { + name: 'dart', + title: 'Dart', + formatter: { + factory: async () => { + (self as any).importScripts(dartWasmBaseUrl + 'dart-wasm.iife.js'); + const formatter = await (self as any).DartWasm.createCompiler({ + baseUrl: dartWasmBaseUrl, + }); + return async (code) => ({ formatted: await formatter?.format(code) }); + }, + }, + compiler: { + url: dartWasmBaseUrl + 'dart-wasm.iife.js', + factory: (_config, baseUrl) => { + (self as any).importScripts(baseUrl + '{{hash:lang-dart-compiler.js}}'); + return (self as any).createDartCompiler({ engine: 'dart', assetBaseUrl: dartWasmBaseUrl }); + }, + scripts: ({ baseUrl }) => [ + dartWasmBaseUrl + 'dart-wasm.iife.js', + baseUrl + '{{hash:lang-dart-script.js}}', + ], + }, + extensions: ['dart', 'dartlang'], + editor: 'script', + editorSupport: { + monaco: { languageSupport: monacoLanguagesBaseUrl + 'dart.js', language: 'dart' }, + codemirror: { + languageSupport: async () => + codemirrorLegacy((await import(codeMirrorBaseUrl + 'codemirror-lang-clike.js')).dart), + }, + codejar: { language: 'dart' }, + }, + largeDownload: true, +}; diff --git a/src/livecodes/languages/flutter/index.ts b/src/livecodes/languages/flutter/index.ts new file mode 100644 index 000000000..e34426f9e --- /dev/null +++ b/src/livecodes/languages/flutter/index.ts @@ -0,0 +1 @@ +export * from './lang-flutter'; diff --git a/src/livecodes/languages/flutter/lang-flutter-script.ts b/src/livecodes/languages/flutter/lang-flutter-script.ts new file mode 100644 index 000000000..edc52057d --- /dev/null +++ b/src/livecodes/languages/flutter/lang-flutter-script.ts @@ -0,0 +1,4 @@ +import { setupDartRuntime } from '../dart/lang-dart-runtime'; + +// Flutter renders into the result page, which is also where the compiled Dart runs. +setupDartRuntime('flutter'); diff --git a/src/livecodes/languages/flutter/lang-flutter.ts b/src/livecodes/languages/flutter/lang-flutter.ts new file mode 100644 index 000000000..c79e12e0d --- /dev/null +++ b/src/livecodes/languages/flutter/lang-flutter.ts @@ -0,0 +1,25 @@ +import type { LanguageSpecs } from '../../models'; +import { dartWasmBaseUrl } from '../../vendors'; +import { dart } from '../dart'; + +export const flutter: LanguageSpecs = { + ...dart, + name: 'flutter', + title: 'Flutter', + compiler: { + url: dartWasmBaseUrl + 'dart-wasm.iife.js', + factory: (_config, baseUrl) => { + (self as any).importScripts(baseUrl + '{{hash:lang-dart-compiler.js}}'); + return (self as any).createDartCompiler({ + engine: 'flutter', + assetBaseUrl: dartWasmBaseUrl, + }); + }, + scripts: ({ baseUrl }) => [ + dartWasmBaseUrl + 'dart-wasm.iife.js', + baseUrl + '{{hash:lang-flutter-script.js}}', + ], + }, + extensions: ['flutter'], + editorLanguage: 'dart', +}; diff --git a/src/livecodes/languages/languages.ts b/src/livecodes/languages/languages.ts index cd1425972..a82060716 100644 --- a/src/livecodes/languages/languages.ts +++ b/src/livecodes/languages/languages.ts @@ -21,6 +21,7 @@ import { cpp } from './cpp'; import { csharpWasm } from './csharp-wasm'; import { css } from './css'; import { dWasm } from './d-wasm'; +import { dart } from './dart'; import { diagrams } from './diagrams'; import { dot } from './dot'; import { ejs } from './ejs'; @@ -29,6 +30,7 @@ import { eta } from './eta'; import { fanak } from './fanak'; import { fennel } from './fennel'; import { flow } from './flow'; +import { flutter } from './flutter'; import { fortranWasm } from './fortran-wasm'; import { fsharp } from './fsharp'; import { fsharpWasm } from './fsharp-wasm'; @@ -180,6 +182,8 @@ export const languages: LanguageSpecs[] = [ objcppWasm, fortranWasm, dWasm, + dart, + flutter, cobolWasm, rustWasm, swiftWasm, diff --git a/src/livecodes/models.ts b/src/livecodes/models.ts index 6d9a6995b..693e3a4a1 100644 --- a/src/livecodes/models.ts +++ b/src/livecodes/models.ts @@ -294,7 +294,8 @@ export type TemplateAlias = | 'pg' | 'pgsql' | 'janet' - | 'mzn'; + | 'mzn' + | 'dartlang'; export type Template = Pick & Partial & { diff --git a/src/livecodes/templates/starter/dart-starter.ts b/src/livecodes/templates/starter/dart-starter.ts new file mode 100644 index 000000000..77799dcab --- /dev/null +++ b/src/livecodes/templates/starter/dart-starter.ts @@ -0,0 +1,68 @@ +import type { Template } from '../../models'; + +export const dartStarter: Template = { + name: 'dart', + aliases: ['dartlang'], + title: window.deps.translateString('templates.starter.dart', 'Dart Starter'), + thumbnail: 'assets/templates/dart.svg', + activeEditor: 'script', + markup: { + language: 'html', + content: ` +
    +

    Hello, World!

    + +

    You clicked 0 times.

    + +
    +`.trimStart(), + }, + style: { + language: 'css', + content: ` +.container, +.container button { + text-align: center; + font: 1em sans-serif; +} +.logo { + width: 150px; +} +`.trimStart(), + }, + script: { + language: 'dart', + content: ` +import 'dart:js_interop'; + +@JS('document') +external Document get document; + +extension type Document(JSObject _) implements JSObject { + external Element? querySelector(String selectors); +} + +extension type Element(JSObject _) implements JSObject { + external set textContent(String value); + external set disabled(bool value); + external set onclick(JSFunction? value); +} + +void main() { + document.querySelector('#name')?.textContent = 'Dart'; + + final counter = document.querySelector('#counter'); + final button = document.querySelector('#counter-button'); + if (counter == null || button == null) return; + + var count = 0; + button.disabled = false; + button.textContent = 'Click me'; + button.onclick = (JSAny? _) { + count++; + counter.textContent = '\$count'; + }.toJS; +} +`.trimStart(), + }, +}; diff --git a/src/livecodes/templates/starter/flutter-starter.ts b/src/livecodes/templates/starter/flutter-starter.ts new file mode 100644 index 000000000..0ce3de8e6 --- /dev/null +++ b/src/livecodes/templates/starter/flutter-starter.ts @@ -0,0 +1,68 @@ +import type { Template } from '../../models'; + +/** + * The Flutter starter, matching the other starters: the app renders the heading, the logo and the + * counter itself, since Flutter draws its own UI in the result page. + */ +export const flutterStarter: Template = { + name: 'flutter', + title: window.deps.translateString('templates.starter.flutter', 'Flutter Starter'), + thumbnail: 'assets/templates/flutter.svg', + activeEditor: 'script', + markup: { + language: 'html', + content: '
    Loading...
    ', + }, + style: { + language: 'css', + content: '', + }, + script: { + language: 'flutter', + content: ` +import 'package:flutter/material.dart'; + +void main() { + print('Hello from Flutter!'); + runApp(const CounterApp()); +} + +class CounterApp extends StatefulWidget { + const CounterApp({super.key}); + + @override + State createState() => _CounterAppState(); +} + +class _CounterAppState extends State { + int _count = 0; + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Hello, Flutter!', style: TextStyle(fontSize: 24)), + const SizedBox(height: 16), + const FlutterLogo(size: 120), + const SizedBox(height: 16), + Text('You clicked \$_count times.', style: const TextStyle(fontSize: 16)), + const SizedBox(height: 12), + ElevatedButton( + onPressed: () => setState(() => _count++), + child: const Text('Click me'), + ), + ], + ), + ), + ), + ); + } +} +`.trimStart(), + }, +}; diff --git a/src/livecodes/templates/starter/index.ts b/src/livecodes/templates/starter/index.ts index b9ecdae2e..c4df74ebf 100644 --- a/src/livecodes/templates/starter/index.ts +++ b/src/livecodes/templates/starter/index.ts @@ -23,10 +23,12 @@ import { csharpWasmStarter } from './csharp-wasm-starter'; import { dWasmStarter } from './d-wasm-starter'; import { d3Starter } from './d3-starter'; import { daisyuiStarter } from './daisyui-starter'; +import { dartStarter } from './dart-starter'; import { diagramsStarter } from './diagrams-starter'; import { elmStarter } from './elm-starter'; import { fanakStarter } from './fanak-starter'; import { fennelStarter } from './fennel-starter'; +import { flutterStarter } from './flutter-starter'; import { fortranWasmStarter } from './fortran-wasm-starter'; import { fsharpStarter } from './fsharp-starter'; import { fsharpWasmStarter } from './fsharp-wasm-starter'; @@ -186,4 +188,6 @@ export const starterTemplates = [ minizincStarter, blocklyStarter, diagramsStarter, + dartStarter, + flutterStarter, ]; diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index 3d1a8a63c..8539685ac 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -132,6 +132,8 @@ export const cytoscapeSvgUrl = /* @__PURE__ */ getUrl('cytoscape-svg@0.4.0/cytos export const cytoscapeUrl = /* @__PURE__ */ getUrl('cytoscape@3.25.0/dist/cytoscape.min.js'); +export const dartWasmBaseUrl = /* @__PURE__ */ getUrl('@live-codes/dart-wasm@0.6.0/dist/'); + export const ddietrCmThemesBaseUrl = /* @__PURE__ */ getUrl( '@ddietr/codemirror-themes@1.4.2/dist/theme/', ); diff --git a/src/sdk/models.ts b/src/sdk/models.ts index 73b695146..b4868c5ab 100644 --- a/src/sdk/models.ts +++ b/src/sdk/models.ts @@ -197,6 +197,9 @@ export type Language = | 'dlang' | 'dmd' | 'd-wasm' + | 'dart' + | 'dartlang' + | 'flutter' | 'v' | 'vlang' | 'v-wasm' @@ -554,7 +557,9 @@ export type TemplateName = | 'prolog' | 'minizinc' | 'blockly' - | 'diagrams'; + | 'diagrams' + | 'dart' + | 'flutter'; /** * Tools in the tools pane. diff --git a/storybook/_stories/EmbedOptions/template.ts b/storybook/_stories/EmbedOptions/template.ts index d515bfb6a..f3e58846b 100644 --- a/storybook/_stories/EmbedOptions/template.ts +++ b/storybook/_stories/EmbedOptions/template.ts @@ -97,6 +97,8 @@ const storyDef: StoryDef = { Minizinc: { props: { template: 'minizinc' } }, Blockly: { props: { template: 'blockly' } }, Diagrams: { props: { template: 'diagrams' } }, + Dart: { props: { template: 'dart' } }, + Flutter: { props: { template: 'flutter' } }, }; export default storyDef; diff --git a/storybook/preact/stories/EmbedOptions/template.stories.ts b/storybook/preact/stories/EmbedOptions/template.stories.ts index 529d6d0fc..bf24eef65 100644 --- a/storybook/preact/stories/EmbedOptions/template.stories.ts +++ b/storybook/preact/stories/EmbedOptions/template.stories.ts @@ -113,3 +113,5 @@ export const Prolog = livecodesStory({ template: 'prolog' }); export const Minizinc = livecodesStory({ template: 'minizinc' }); export const Blockly = livecodesStory({ template: 'blockly' }); export const Diagrams = livecodesStory({ template: 'diagrams' }); +export const Dart = livecodesStory({ template: 'dart' }); +export const Flutter = livecodesStory({ template: 'flutter' }); diff --git a/storybook/react/stories/EmbedOptions/template.stories.ts b/storybook/react/stories/EmbedOptions/template.stories.ts index 529d6d0fc..bf24eef65 100644 --- a/storybook/react/stories/EmbedOptions/template.stories.ts +++ b/storybook/react/stories/EmbedOptions/template.stories.ts @@ -113,3 +113,5 @@ export const Prolog = livecodesStory({ template: 'prolog' }); export const Minizinc = livecodesStory({ template: 'minizinc' }); export const Blockly = livecodesStory({ template: 'blockly' }); export const Diagrams = livecodesStory({ template: 'diagrams' }); +export const Dart = livecodesStory({ template: 'dart' }); +export const Flutter = livecodesStory({ template: 'flutter' }); diff --git a/storybook/solid/stories/EmbedOptions/template.stories.ts b/storybook/solid/stories/EmbedOptions/template.stories.ts index 529d6d0fc..bf24eef65 100644 --- a/storybook/solid/stories/EmbedOptions/template.stories.ts +++ b/storybook/solid/stories/EmbedOptions/template.stories.ts @@ -113,3 +113,5 @@ export const Prolog = livecodesStory({ template: 'prolog' }); export const Minizinc = livecodesStory({ template: 'minizinc' }); export const Blockly = livecodesStory({ template: 'blockly' }); export const Diagrams = livecodesStory({ template: 'diagrams' }); +export const Dart = livecodesStory({ template: 'dart' }); +export const Flutter = livecodesStory({ template: 'flutter' }); diff --git a/storybook/svelte/stories/EmbedOptions/template.stories.ts b/storybook/svelte/stories/EmbedOptions/template.stories.ts index 529d6d0fc..bf24eef65 100644 --- a/storybook/svelte/stories/EmbedOptions/template.stories.ts +++ b/storybook/svelte/stories/EmbedOptions/template.stories.ts @@ -113,3 +113,5 @@ export const Prolog = livecodesStory({ template: 'prolog' }); export const Minizinc = livecodesStory({ template: 'minizinc' }); export const Blockly = livecodesStory({ template: 'blockly' }); export const Diagrams = livecodesStory({ template: 'diagrams' }); +export const Dart = livecodesStory({ template: 'dart' }); +export const Flutter = livecodesStory({ template: 'flutter' }); diff --git a/storybook/vue/stories/EmbedOptions/template.stories.ts b/storybook/vue/stories/EmbedOptions/template.stories.ts index 529d6d0fc..bf24eef65 100644 --- a/storybook/vue/stories/EmbedOptions/template.stories.ts +++ b/storybook/vue/stories/EmbedOptions/template.stories.ts @@ -113,3 +113,5 @@ export const Prolog = livecodesStory({ template: 'prolog' }); export const Minizinc = livecodesStory({ template: 'minizinc' }); export const Blockly = livecodesStory({ template: 'blockly' }); export const Diagrams = livecodesStory({ template: 'diagrams' }); +export const Dart = livecodesStory({ template: 'dart' }); +export const Flutter = livecodesStory({ template: 'flutter' }); diff --git a/storybook/web-components/stories/EmbedOptions/template.stories.ts b/storybook/web-components/stories/EmbedOptions/template.stories.ts index 529d6d0fc..bf24eef65 100644 --- a/storybook/web-components/stories/EmbedOptions/template.stories.ts +++ b/storybook/web-components/stories/EmbedOptions/template.stories.ts @@ -113,3 +113,5 @@ export const Prolog = livecodesStory({ template: 'prolog' }); export const Minizinc = livecodesStory({ template: 'minizinc' }); export const Blockly = livecodesStory({ template: 'blockly' }); export const Diagrams = livecodesStory({ template: 'diagrams' }); +export const Dart = livecodesStory({ template: 'dart' }); +export const Flutter = livecodesStory({ template: 'flutter' }); diff --git a/vendor-licenses.md b/vendor-licenses.md index 0cc46b5d6..c4d46c55a 100644 --- a/vendor-licenses.md +++ b/vendor-licenses.md @@ -70,6 +70,8 @@ dot: [MIT License](https://github.com/olado/doT/blob/031d3bb7520eed6b93886df2b65 dart-sass: [MIT License](https://github.com/sass/dart-sass/blob/e3bf3eb3a3a8708877a86a08c7e3bee92160ac1f/LICENSE) +Dart: [BSD-3-Clause License](https://github.com/dart-lang/sdk/blob/e6f4207adef69106d7519a79144efdb5c1acc4d0/LICENSE) + DoppioJVM: [MIT License](https://github.com/plasma-umass/doppio/blob/9a8bcf705c74f91301a542abfe8b7175ca757259/LICENSE) dts-bundle: [MIT License](https://github.com/TypeStrong/dts-bundle/blob/2ca1591e890dc4276efc4bb0893367e6ff32a039/LICENSE-MIT) @@ -104,6 +106,8 @@ FlexSearch: [Apache License 2.0](https://github.com/nextapps-de/flexsearch/blob/ flow-remove-types: [MIT License](https://github.com/facebook/flow/blob/3ebee9f08106eb8aefbf98111fee696d333b8456/packages/flow-remove-types/LICENSE) +Flutter: [BSD-3-Clause License](https://github.com/flutter/flutter/blob/9b5759043457c99e316507474dea424e15cfb0c0/LICENSE) + Fscreen: [MIT License](https://github.com/rafgraph/fscreen/blob/04244204efff724253df24f78336c9a2b7bc6505/LICENSE) GHC: [BSD-3-Clause and bundled component licenses](https://gitlab.haskell.org/ghc/ghc/-/blob/master/LICENSE) @@ -346,6 +350,8 @@ wabt.js: [Apache-2.0 license](https://github.com/AssemblyScript/wabt.js/blob/182 wacl: [BSD 3-Clause License](https://github.com/ecky-l/wacl/blob/9daacabb0102a9986f33263261350edfeebdd83b/LICENSE) +@live-codes/dart-wasm: [MIT License](https://github.com/live-codes/browser-dart/blob/main/LICENSE) + @live-codes/dlang-wasm: [MIT License](https://github.com/live-codes/browser-d/blob/main/LICENSE) DMD / druntime / Phobos (Wasm): [BSL-1.0 License](https://github.com/dlang/dmd/blob/master/LICENSE.txt)