Skip to content

Commit e2d14f1

Browse files
authored
Adding initial support for DDC's Library Bundler module system to webdev. (#2724)
Adds support for DDC's new module system, which supports hot reload. This is currently locked behind the canary flag in webdev and requires uses-hot-reload flags to be enabled on downstream builders.
1 parent 234e44c commit e2d14f1

File tree

10 files changed

+207
-18
lines changed

10 files changed

+207
-18
lines changed

dwds/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
- Bump `build_web_compilers` to ^4.4.1.
44
- Remove unused `clientFuture` arg from `DwdsVmClient` methods.
55
- Fix pausing starting of `main` after the hot restart.
6+
- Updating bootstrapper for DDC library bundler module format + Frontend Server.
67

78
## 26.2.2
89

dwds/lib/src/loaders/ddc_library_bundle.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,15 +195,19 @@ class DdcLibraryBundleStrategy extends LoadStrategy {
195195
modulePaths.forEach((name, path) {
196196
scripts.add(<String, String>{'src': '$path.js', 'id': name});
197197
});
198-
return '''
199-
$baseUrlScript
198+
// canary-mode uses the Frontend Server, which begins script loads via a
199+
// separate pathway.
200+
final scriptLoader = buildSettings.canaryFeatures
201+
? '''
200202
var scripts = ${const JsonEncoder.withIndent(" ").convert(scripts)};
201203
window.\$dartLoader.loadConfig.loadScriptFn = function(loader) {
202204
loader.addScriptsToQueue(scripts, null);
203205
loader.loadEnqueuedModules();
204206
};
205207
window.\$dartLoader.loader.nextAttempt();
206-
''';
208+
'''
209+
: '';
210+
return '$baseUrlScript\n$scriptLoader';
207211
}
208212

209213
@override

webdev/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 3.8.1-wip
2+
3+
- Adding initial DDC library bundler support behind the `canary` flag.
4+
15
## 3.8.0-wip
26

37
- Bump minimum SDK constraint to 3.10.0

webdev/lib/src/command/build_command.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ class BuildCommand extends Command<int> {
9393
DefaultBuildTarget(
9494
(b) => b
9595
..target = configuration.outputInput
96-
..outputLocation = outputLocation?.toBuilder(),
96+
..outputLocation = outputLocation?.toBuilder()
97+
..reportChangedAssets = true,
9798
),
9899
);
99100
client.startBuild();

webdev/lib/src/command/shared.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,24 @@ List<String> buildRunnerArgs(Configuration configuration) {
126126
arguments.add('--enable-experiment=$experiment');
127127
}
128128

129+
if (configuration.canaryFeatures) {
130+
arguments
131+
..add('--define')
132+
..add('build_web_compilers|sdk_js=web-hot-reload=true');
133+
arguments
134+
..add('--define')
135+
..add('build_web_compilers|entrypoint=web-hot-reload=true');
136+
arguments
137+
..add('--define')
138+
..add('build_web_compilers|entrypoint_marker=web-hot-reload=true');
139+
arguments
140+
..add('--define')
141+
..add('build_web_compilers|ddc=web-hot-reload=true');
142+
arguments
143+
..add('--define')
144+
..add('build_web_compilers|ddc_modules=web-hot-reload=true');
145+
}
146+
129147
return arguments;
130148
}
131149

webdev/lib/src/serve/dev_workflow.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ void _registerBuildTargets(
144144
DefaultBuildTarget(
145145
(b) => b
146146
..target = target
147-
..outputLocation = outputLocation?.toBuilder(),
147+
..outputLocation = outputLocation?.toBuilder()
148+
..reportChangedAssets = true,
148149
),
149150
);
150151
}
@@ -161,7 +162,8 @@ void _registerBuildTargets(
161162
DefaultBuildTarget(
162163
(b) => b
163164
..target = ''
164-
..outputLocation = outputLocation.toBuilder(),
165+
..outputLocation = outputLocation.toBuilder()
166+
..reportChangedAssets = true,
165167
),
166168
);
167169
}

webdev/lib/src/serve/utils.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,27 @@ String? findPackageConfigFilePath() {
106106
candidateDir = parentDir;
107107
}
108108
}
109+
110+
/// Returns the absolute file path of the `package_config.json` file in the `.dart_tool`
111+
/// directory, searching recursively from the current directory hierarchy.
112+
Uri? findPackageConfigUri() {
113+
var candidateDir = Directory(p.current).absolute;
114+
115+
while (true) {
116+
final candidatePackageConfigFile = File(
117+
p.join(candidateDir.path, '.dart_tool', 'package_config.json'),
118+
);
119+
120+
if (candidatePackageConfigFile.existsSync()) {
121+
return candidatePackageConfigFile.uri;
122+
}
123+
124+
final parentDir = candidateDir.parent;
125+
if (parentDir.path == candidateDir.path) {
126+
// We've reached the root directory
127+
return null;
128+
}
129+
130+
candidateDir = parentDir;
131+
}
132+
}

webdev/lib/src/serve/webdev_server.dart

Lines changed: 143 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:convert';
67
import 'dart:io';
78

89
import 'package:build_daemon/data/build_status.dart' as daemon;
910
import 'package:dds/devtools_server.dart';
1011
import 'package:dwds/data/build_result.dart';
1112
import 'package:dwds/dwds.dart';
1213
import 'package:dwds/sdk_configuration.dart';
14+
import 'package:file/local.dart';
1315
import 'package:http/http.dart' as http;
1416
import 'package:http/io_client.dart';
1517
import 'package:http_multi_server/http_multi_server.dart';
@@ -21,10 +23,14 @@ import '../command/configuration.dart';
2123
import '../util.dart';
2224
import 'chrome.dart';
2325
import 'handlers/favicon_handler.dart';
24-
import 'utils.dart' show findPackageConfigFilePath;
26+
import 'utils.dart' show findPackageConfigFilePath, findPackageConfigUri;
2527

2628
Logger _logger = Logger('WebDevServer');
2729

30+
const reloadedSourcesFileName = 'reloaded_sources.json';
31+
const jsLibraryBundleExtension = '.ddc.js';
32+
const multiRootScheme = 'org-dartlang-app';
33+
2834
class ServerOptions {
2935
final Configuration configuration;
3036
final int port;
@@ -80,6 +86,7 @@ class WebDevServer {
8086
ServerOptions options,
8187
Stream<daemon.BuildResults> buildResults,
8288
) async {
89+
final basePath = 'http://localhost:${options.port}';
8390
var pipeline = const Pipeline();
8491

8592
if (options.configuration.logRequests) {
@@ -88,8 +95,47 @@ class WebDevServer {
8895

8996
pipeline = pipeline.addMiddleware(interceptFavicon);
9097

98+
/// JSON-ifiable list of sources that were reloaded in this restart and
99+
/// follows the following format:
100+
///
101+
/// `src`: A string that corresponds to the file path containing a DDC library
102+
/// bundle. To support embedded libraries, the path should include the
103+
/// `baseUri` of the web server.
104+
/// `module`: The name of the library bundle in `src`.
105+
/// `libraries`: An array of strings containing the libraries that were
106+
/// compiled in `src`.
107+
///
108+
/// For example:
109+
/// ```json
110+
/// [
111+
/// {
112+
/// "src": "<baseUri>/<file_name>",
113+
/// "module": "<module_name>",
114+
/// "libraries": ["<lib1>", "<lib2>"],
115+
/// },
116+
/// ]
117+
/// ```
118+
///
119+
/// The path of the output file should stay consistent across the lifetime of
120+
/// the app.
121+
final reloadedSources = <Map<String, dynamic>>[];
122+
91123
// Only provide relevant build results
92124
final filteredBuildResults = buildResults.asyncMap<BuildResult>((results) {
125+
if (options.configuration.canaryFeatures) {
126+
// Clear reloaded sources for the new build results.
127+
reloadedSources.clear();
128+
results.changedAssets?.forEach((uri) {
129+
if (uri.path.endsWith(jsLibraryBundleExtension)) {
130+
final reloadedSource = {
131+
'src': ddcUriToSourceUrl(basePath, options.target, uri),
132+
'module': ddcUriToLibraryId(uri),
133+
'libraries': [ddcUriToLibraryId(uri)],
134+
};
135+
reloadedSources.add(reloadedSource);
136+
}
137+
});
138+
}
93139
final result = results.results.firstWhere(
94140
(result) => result.target == options.target,
95141
);
@@ -132,20 +178,39 @@ class WebDevServer {
132178
// the load strategy?
133179
final buildSettings = BuildSettings(
134180
appEntrypoint: Uri.parse(
135-
'org-dartlang-app:///${options.target}/main.dart',
181+
'$multiRootScheme:///${options.target}/main.dart',
136182
),
137183
canaryFeatures: options.configuration.canaryFeatures,
138184
isFlutterApp: false,
139185
experiments: options.configuration.experiments,
140186
);
141187

142-
final loadStrategy = BuildRunnerRequireStrategyProvider(
143-
assetHandler,
144-
options.configuration.reload,
145-
assetReader,
146-
buildSettings,
147-
packageConfigPath: findPackageConfigFilePath(),
148-
).strategy;
188+
final LoadStrategy loadStrategy;
189+
if (options.configuration.canaryFeatures) {
190+
final frontendServerFileSystem = LocalFileSystem();
191+
final packageUriMapper = await PackageUriMapper.create(
192+
frontendServerFileSystem,
193+
findPackageConfigUri()!,
194+
useDebuggerModuleNames: false,
195+
);
196+
loadStrategy = FrontendServerDdcLibraryBundleStrategyProvider(
197+
options.configuration.reload,
198+
assetReader,
199+
packageUriMapper,
200+
() async => {},
201+
buildSettings,
202+
packageConfigPath: findPackageConfigFilePath(),
203+
reloadedSourcesUri: Uri.parse('$basePath/$reloadedSourcesFileName'),
204+
).strategy;
205+
} else {
206+
loadStrategy = BuildRunnerRequireStrategyProvider(
207+
assetHandler,
208+
options.configuration.reload,
209+
assetReader,
210+
buildSettings,
211+
packageConfigPath: findPackageConfigFilePath(),
212+
).strategy;
213+
}
149214

150215
if (options.configuration.enableExpressionEvaluation) {
151216
ddcService = ExpressionCompilerService(
@@ -161,8 +226,10 @@ class WebDevServer {
161226
final debugSettings = DebugSettings(
162227
enableDebugExtension: options.configuration.debugExtension,
163228
enableDebugging: options.configuration.debug,
229+
// ignore: deprecated_member_use
164230
spawnDds: !options.configuration.disableDds,
165231
expressionCompiler: ddcService,
232+
// ignore: deprecated_member_use
166233
devToolsLauncher: shouldServeDevTools
167234
? (String hostname) async {
168235
final server = await DevToolsServer().serveDevTools(
@@ -191,6 +258,18 @@ class WebDevServer {
191258
);
192259
pipeline = pipeline.addMiddleware(dwds.middleware);
193260
cascade = cascade.add(dwds.handler);
261+
if (options.configuration.canaryFeatures) {
262+
// Add a handler to serve reloaded sources.
263+
cascade = cascade.add((Request request) {
264+
if (request.url.path == reloadedSourcesFileName) {
265+
return Response.ok(
266+
jsonEncode(reloadedSources),
267+
headers: {'Content-Type': 'application/json'},
268+
);
269+
}
270+
return Response.notFound('');
271+
});
272+
}
194273
cascade = cascade.add(assetHandler);
195274
} else {
196275
cascade = cascade.add(assetHandler);
@@ -233,3 +312,58 @@ class WebDevServer {
233312
);
234313
}
235314
}
315+
316+
/// Transforms a package:build JS asset id [uri] into a source url compatible
317+
/// with DDC's bootstrapper.
318+
///
319+
/// [basePath] is the path from which JS files as served up to but not
320+
/// including the path.
321+
/// [target] is the path whose files will be served from [basePath].
322+
/// [uri] is the asset id's uri being transformed.
323+
///
324+
/// Example:
325+
/// basePath: http://localhost:8080
326+
/// target: web
327+
///
328+
/// uri: asset:some_package/web/main.ddc.js
329+
/// returns http://localhost:8080/main.ddc.js
330+
///
331+
/// uri: package:some_package/src/sub_dir/file.ddc.js
332+
/// returns http://localhost:8080/some_package/src/sub_dir/file.ddc.js
333+
String ddcUriToSourceUrl(String basePath, String target, Uri uri) {
334+
String jsPath;
335+
if (uri.isScheme('asset')) {
336+
// This indicates that this asset is the 'main' web asset. We directly
337+
// serve all files under the package's [target] directory.
338+
var pathParts = uri.pathSegments.skip(1);
339+
if (pathParts.first == target) {
340+
pathParts = pathParts.skip(1);
341+
}
342+
jsPath = pathParts.join('/');
343+
} else if (uri.isScheme('package')) {
344+
jsPath = 'packages/${uri.path}';
345+
} else {
346+
jsPath = uri.path;
347+
}
348+
return '$basePath/$jsPath';
349+
}
350+
351+
/// Transforms a package:build JS asset id [uri] into a library id compatible
352+
/// with DDC's bootstrapper.
353+
///
354+
/// Example:
355+
/// uri: asset:some_package/web/main.ddc.js
356+
/// returns org-dartlang-app:///web/main.dart
357+
///
358+
/// uri: package:some_package/src/sub_dir/file.ddc.js
359+
/// returns package:some_package/src/sub_dir/file.dart
360+
String ddcUriToLibraryId(Uri uri) {
361+
final jsPath = uri.isScheme('package')
362+
? 'package:${uri.path}'
363+
: '$multiRootScheme:///${uri.path}';
364+
final prefix = jsPath.substring(
365+
0,
366+
jsPath.length - jsLibraryBundleExtension.length,
367+
);
368+
return '$prefix.dart';
369+
}

webdev/lib/src/version.dart

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webdev/pubspec.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: webdev
22
# Every time this changes you need to run `dart run build_runner build`.
3-
version: 3.8.0-wip
3+
version: 3.8.1-wip
44
# We should not depend on a dev SDK before publishing.
55
# publish_to: none
66
description: >-
@@ -19,7 +19,8 @@ dependencies:
1919
crypto: ^3.0.2
2020
dds: ^4.1.0
2121
# Pin DWDS to avoid dependency conflicts with vm_service:
22-
dwds: 24.3.11
22+
dwds: ^25.0.0
23+
file: ^7.0.1
2324
http: ^1.0.0
2425
http_multi_server: ^3.2.0
2526
io: ^1.0.3

0 commit comments

Comments
 (0)