blob: 7e114a5d46003be167e690dccc3770d890ff03f8 [file] [edit]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:dartpad/src/dartpad_config.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as p;
import 'package:tar/tar.dart';
Future<void> main() async {
final dartSdkRoot = p.dirname(p.dirname(Platform.resolvedExecutable));
// Locate Flutter
var flutterRoot = Platform.environment['FLUTTER_ROOT'];
if (flutterRoot == null) {
try {
final flutterExecutable = await _resolveFlutterExecutable();
flutterRoot = Directory(flutterExecutable).parent.parent.path;
} catch (e) {
print('Error: FLUTTER_ROOT not set and flutter not found in PATH.');
exit(1);
}
}
flutterRoot = p.canonicalize(flutterRoot);
if (!Directory(flutterRoot).existsSync()) {
print('Flutter SDK not found at $flutterRoot.');
exit(1);
}
final flutterBin = p.join(flutterRoot, 'bin', 'flutter');
// Find output folder
final workerPkgUri = await Isolate.resolvePackageUri(
Uri.parse('package:dartpad_worker/'),
);
if (workerPkgUri == null) {
print('Error: Could not resolve package:dartpad_worker/');
exit(1);
}
final projectRoot = p.dirname(workerPkgUri.toFilePath());
final flutterAssetDir = p.join(
projectRoot,
'.dart_tool',
'dartpad_worker',
'asset',
'flutter',
);
final packageDir = p.join(
projectRoot,
'.dart_tool',
'dartpad_worker',
'packages',
);
// Create empty output folders
final flutterAssetDirectory = Directory(flutterAssetDir);
if (flutterAssetDirectory.existsSync()) {
flutterAssetDirectory.deleteSync(recursive: true);
}
flutterAssetDirectory.createSync(recursive: true);
final packageDirectory = Directory(packageDir);
if (packageDirectory.existsSync()) {
packageDirectory.deleteSync(recursive: true);
}
packageDirectory.createSync(recursive: true);
print('Using Flutter SDK at: $flutterRoot');
print('Target asset directory: $flutterAssetDir');
final tempDir = Directory.systemTemp.createTempSync('dartpad_flutter_setup_');
try {
await _setupLocalFlutter(
_BuildContext(
dartSdkRoot: dartSdkRoot,
dartBin: p.join(dartSdkRoot, 'bin', 'dart'),
dartAotRuntimeBin: p.join(dartSdkRoot, 'bin', 'dartaotruntime'),
flutterRoot: flutterRoot,
tempDir: tempDir.path,
flutterBin: flutterBin,
projectRoot: projectRoot,
flutterAssetDir: flutterAssetDir,
packageDir: packageDir,
),
);
} finally {
tempDir.deleteSync(recursive: true);
}
}
final class _BuildContext {
final String dartSdkRoot;
final String dartBin;
final String dartAotRuntimeBin;
final String flutterRoot;
final String tempDir;
final String flutterBin;
final String projectRoot;
final String flutterAssetDir;
final String packageDir;
_BuildContext({
required this.dartSdkRoot,
required this.dartBin,
required this.dartAotRuntimeBin,
required this.flutterRoot,
required this.tempDir,
required this.flutterBin,
required this.projectRoot,
required this.flutterAssetDir,
required this.packageDir,
});
}
Future<void> _setupLocalFlutter(_BuildContext ctx) async {
// 1. Create & Build Dummy App
print('Creating dummy app...');
_runSync(ctx.flutterBin, [
'create',
'myapp',
'--empty',
'--platforms',
'web',
], ctx.tempDir);
final myappDir = p.join(ctx.tempDir, 'myapp');
print('Pruning pubspec.yaml...');
_runSync(ctx.flutterBin, [
'pub',
'remove',
'flutter_test',
'flutter_lints',
], myappDir);
print('Running flutter pub get...');
_runSync(ctx.flutterBin, ['pub', 'get'], myappDir);
print('Building dummy app for web (to harvest assets)...');
_runSync(ctx.flutterBin, ['build', 'web', '--debug'], myappDir);
// 2. Scrape Assets (CanvasKit, Fonts)
print('Scraping assets...');
final sourceAssetsDir = p.join(myappDir, 'build', 'web', 'assets');
_copyDir(sourceAssetsDir, p.join(ctx.flutterAssetDir, 'assets'));
print('Copying flutter.js');
_copyFile(
p.join(myappDir, 'build', 'web', 'flutter.js'),
p.join(ctx.flutterAssetDir, 'flutter.js'),
);
print('Scraping CanvasKit...');
final sourceCanvasKitDir = p.join(myappDir, 'build', 'web', 'canvaskit');
_copyDir(sourceCanvasKitDir, p.join(ctx.flutterAssetDir, 'canvaskit'));
// 3. Compile flutter_web.js and flutter_web.dill
print('Compiling flutter_web.js and flutter_web.dill...');
final pkgConfigPath = p.join(myappDir, '.dart_tool', 'package_config.json');
final pkgConfig =
jsonDecode(File(pkgConfigPath).readAsStringSync())
as Map<String, dynamic>;
final compileSources = <String>[];
for (final pkgEntry in pkgConfig['packages'] as List<dynamic>) {
final pkg = pkgEntry as Map<String, dynamic>;
final name = pkg['name'] as String;
if (name == 'sky_engine' || name == 'myapp') continue;
var rootUriStr = pkg['rootUri'] as String;
final rootUri = Uri.parse(rootUriStr);
final rootPath = rootUri.scheme == 'file'
? rootUri.toFilePath()
: p.normalize(p.join(myappDir, '.dart_tool', rootUriStr));
final libDir = Directory(p.join(rootPath, pkg['packageUri'] as String));
if (libDir.existsSync()) {
final topLevelFiles = libDir
.listSync(recursive: false)
.whereType<File>()
.where((f) => f.path.endsWith('.dart'));
for (final file in topLevelFiles) {
final relative = p.relative(file.path, from: libDir.path);
compileSources.add('package:$name/${p.toUri(relative).path}');
}
}
}
final snapshotPath = p.join(
ctx.dartSdkRoot,
'bin',
'snapshots',
'dartdevc.dart.snapshot',
);
final outlinePath = p.join(
ctx.flutterRoot,
'bin',
'cache',
'flutter_web_sdk',
'kernel',
'ddc_outline.dill',
);
final outputJsPath = p.join(ctx.flutterAssetDir, 'flutter_web.js');
final outputDillPath = p.join(ctx.tempDir, 'flutter_web.dill');
_runSync(ctx.dartBin, [
snapshotPath,
'-s',
outlinePath,
'--modules=ddc',
'--canary',
'--module-name=flutter_web',
'--packages=$pkgConfigPath',
'-o',
outputJsPath,
...compileSources,
], myappDir);
// We don't want the full dill generated by DDC.
final fullDillPath = p.setExtension(outputJsPath, '.dill');
if (File(fullDillPath).existsSync()) {
File(fullDillPath).deleteSync();
}
final kernelWorkerPath = p.join(
ctx.dartSdkRoot,
'bin',
'snapshots',
'kernel_worker_aot.dart.snapshot',
);
_runSync(ctx.dartAotRuntimeBin, [
kernelWorkerPath,
'--target',
'ddc',
'--summary-only',
'--packages-file',
pkgConfigPath,
'--dart-sdk-summary',
outlinePath,
'--output',
outputDillPath,
...compileSources.expand((s) => ['--source', s]),
], myappDir);
// Scrape JS from Flutter Cache
print('Scraping pre-compiled JS from cache...');
final webSdkKernel = p.join(
ctx.flutterRoot,
'bin',
'cache',
'flutter_web_sdk',
'kernel',
);
final canaryJsDir = p.join(webSdkKernel, 'ddcLibraryBundle-canvaskit');
_copyFile(
p.join(canaryJsDir, 'dart_sdk.js'),
p.join(ctx.flutterAssetDir, 'dart_sdk.js'),
);
// Synthesize combined sdk.js
print('Synthesizing sdk.js...');
File(p.join(ctx.flutterAssetDir, 'sdk.js')).writeAsStringSync(r'''
const scriptUrl = document.currentScript?.src || self.location.href;
// Tell the Flutter engine where to find CanvasKit and assets
self.dartpadFlutterConfiguration = {
canvasKitBaseUrl: new URL('./canvaskit/', scriptUrl).href,
assetBase: new URL('./', scriptUrl).href,
};
const flutterJs = new URL('./flutter.js', scriptUrl);
const dartSdkJs = new URL('./dart_sdk.js', scriptUrl);
const flutterWebJs = new URL('./flutter_web.js', scriptUrl);
self.$dartLoader.forceLoadScript(flutterJs, () => null);
self.$dartLoader.forceLoadScript(dartSdkJs, () => null);
self.$dartLoader.forceLoadScript(flutterWebJs, () => null);
''');
// Download Dependencies from Pub.dev
print('Downloading hosted dependencies...');
final depsJson = _runSync(ctx.flutterBin, [
'pub',
'deps',
'--json',
], myappDir);
await _downloadHostedPackages(depsJson, ctx.packageDir);
// Build sdk.tar
print('Building sdk.tar...');
final tar = tarWritingSink(
File(p.join(ctx.flutterAssetDir, 'sdk.tar')).openWrite(),
);
print('Adding Dart SDK lib...');
tar.addDirectory(
target: '/sdk/bin/cache/dart-sdk/lib',
source: p.join(ctx.flutterRoot, 'bin/cache/dart-sdk/lib'),
where: (f) =>
(f.endsWith('.dart') ||
f.endsWith('.json') ||
f.contains('${p.separator}_internal${p.separator}')) &&
!f.endsWith('.dill'),
);
print('Adding version and libraries');
tar.addFile(
target: '/sdk/bin/cache/flutter.version.json',
source: p.join(ctx.flutterRoot, 'bin/cache/flutter.version.json'),
);
tar.addFile(
// TODO(jonasfj): Is it weird that we're taking flutters libraries.json and
// sticking it into the Dart SDK? I don't think we have a config
// option for getting the LSP to pick libraries.json from a path!
// But maybe we should have two library.json files, the one we compile
// with (which we can configure) and the one we feed to analyzer!
// TODO(jonasfj): Is this file even needed for anything?
target: '/sdk/bin/cache/libraries.json',
source: p.join(ctx.flutterRoot, 'bin/cache/dart-sdk/lib/libraries.json'),
);
tar.addFile(
target: '/sdk/bin/cache/dart-sdk/version',
source: p.join(ctx.flutterRoot, 'bin', 'cache', 'dart-sdk', 'version'),
);
// Add the ddc_outline.dill from Flutter which contains dart:ui
tar.addFile(
target: '/sdk/bin/cache/dart-sdk/lib/_internal/ddc_outline.dill',
source: p.join(
ctx.flutterRoot,
'bin',
'cache',
'flutter_web_sdk',
'kernel',
'ddc_outline.dill',
),
);
// Add the framework outline dill we just built
tar.addFile(
target: '/sdk/bin/cache/flutter_web_sdk/kernel/flutter_web.dill',
source: outputDillPath,
);
// Create dartpad-config.json
tar.addJsonFile(
target: DartPadConfig.defaultDartPadConfigPath,
json: DartPadConfig(
dartSdkPath: '/sdk/bin/cache/dart-sdk',
summaryModules: {
'/sdk/bin/cache/flutter_web_sdk/kernel/flutter_web.dill': 'flutter_web',
},
bootstrapCode: kBootstrapFlutterCode,
flutterSdkPath: '/sdk',
),
);
// Add SDK packages for analyzer
print('Adding package:flutter for analysis...');
tar.addDirectory(
target: '/sdk/packages/flutter',
source: p.join(ctx.flutterRoot, 'packages', 'flutter'),
where: (f) =>
!f.startsWith('test/') &&
(f.endsWith('pubspec.yaml') || f.startsWith('lib/')),
);
tar.addDirectory(
target: '/sdk/bin/cache/pkg/sky_engine',
source: p.join(ctx.flutterRoot, 'bin', 'cache', 'pkg', 'sky_engine'),
where: (f) =>
!f.startsWith('test') &&
(f.endsWith('pubspec.yaml') || f.startsWith('lib/')),
);
await tar.close();
print('\nSuccessfully set up local Flutter assets!');
print('Run your tests with PubTestServer reporting hasFlutter: true.');
}
String _runSync(String command, List<String> args, String workingDir) {
final result = Process.runSync(command, args, workingDirectory: workingDir);
if (result.exitCode != 0) {
print('Command failed: $command ${args.join(' ')}');
print('stdout: ${result.stdout}');
print('stderr: ${result.stderr}');
throw Exception('Command failed');
}
return result.stdout.toString();
}
void _copyFile(String source, String dest) => File(source).copySync(dest);
void _copyDir(String source, String dest) {
final s = Directory(source);
if (!s.existsSync()) {
throw Exception('Expected $source to exist!');
}
Directory(dest).createSync(recursive: true);
for (final entity in s.listSync(recursive: true)) {
if (entity is File) {
final relative = p.relative(entity.path, from: source);
final destFile = File(p.join(dest, relative));
destFile.parent.createSync(recursive: true);
entity.copySync(destFile.path);
}
}
}
Future<void> _downloadHostedPackages(String depsJson, String dest) async {
final data = jsonDecode(depsJson) as Map<String, Object?>;
final packages = data['packages'] as List<Object?>;
final client = http.Client();
try {
for (final pkg in packages) {
if (pkg is Map && pkg['source'] == 'hosted') {
final name = pkg['name'] as String;
final version = pkg['version'] as String;
final tarballName = '$name-$version.tar.gz';
final tarballFile = File(p.join(dest, tarballName));
if (tarballFile.existsSync()) continue;
print('Downloading $name $version...');
final url = 'https://pub.dev/api/archives/$tarballName';
final response = await client.get(Uri.parse(url));
if (response.statusCode == 200) {
tarballFile.writeAsBytesSync(response.bodyBytes);
} else {
print('Failed to download $name: ${response.statusCode}');
}
}
}
} finally {
client.close();
}
}
extension on StreamSink<TarEntry> {
void addFile({required String target, required String source}) => add(
TarEntry.data(
TarHeader(name: target, mode: 420),
File(source).readAsBytesSync(),
),
);
void addTextFile({required String target, required String text}) =>
add(TarEntry.data(TarHeader(name: target, mode: 420), utf8.encode(text)));
void addJsonFile({required String target, required Object? json}) =>
addTextFile(target: target, text: jsonEncode(json));
void addDirectory({
required String source,
required String target,
bool Function(String path)? where,
}) {
final s = Directory(source);
if (!s.existsSync()) return;
for (final f in s.listSync(recursive: true).whereType<File>()) {
final relative = p.relative(f.path, from: source);
if (where != null && !where(relative)) continue;
add(
TarEntry.data(
TarHeader(name: p.join(target, relative), mode: 420),
f.readAsBytesSync(),
),
);
}
}
}
Future<String> _resolveFlutterExecutable() async {
final command = Platform.isWindows ? 'where' : 'which';
final result = await Process.run(command, ['flutter']);
if (result.exitCode != 0 || result.stdout.toString().trim().isEmpty) {
throw Exception('Flutter not found in PATH');
}
return result.stdout.toString().split('\n').first.trim();
}
const kBootstrapFlutterCode = r'''
import 'dart:ui_web' as ui_web;
import 'dart:js_interop';
import 'dart:js_interop_unsafe';
import 'package:flutter/foundation.dart';
//import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import '{{entrypoint}}' as entrypoint;
@JS('window')
external JSObject get _window;
@JS('console.error')
external void _consoleError(JSString message);
Future<void> main() async {
// Disable URL strategy to prevent SecurityError in srcdoc iframes
ui_web.urlStrategy = null;
// Capture errors and pipe to console.error
FlutterError.onError = (details) {
_consoleError(details.toString().toJS);
};
// Mock DWDS indicators to allow Flutter to register hot reload 'reassemble'
// extension.
_window[r'$dwdsVersion'] = true.toJS;
_window[r'$emitRegisterEvent'] = ((String _) {}).toJS;
await ui_web.bootstrapEngine(
runApp: () {
entrypoint.main();
},
registerPlugins: () {
// pluginRegistrant.registerPlugins();
},
);
}
''';