// Copyright (c) 2020, 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:convert'; import 'package:dwds/src/debugging/dart_runtime_debugger.dart'; import 'package:dwds/src/debugging/metadata/provider.dart'; import 'package:dwds/src/loaders/strategy.dart'; import 'package:dwds/src/readers/asset_reader.dart'; import 'package:dwds/src/services/expression_compiler.dart'; import 'package:path/path.dart' as p; import 'package:shelf/shelf.dart'; String removeJsExtension(String path) => path.endsWith('.js') ? p.withoutExtension(path) : path; String addJsExtension(String path) => '$path.js'; // web/main.ddc.js -> main.ddc.js // packages/test/test.dart.js -> packages/test/test.dart.js String stripTopLevelDirectory(String path) => path.startsWith('packages') ? path : path.split('/').skip(1).join('/'); /// JavaScript snippet to determine the base URL of the current path. const _baseUrlScript = ''' var baseUrl = (function () { // Attempt to detect --precompiled mode for tests, and set the base url // appropriately, otherwise set it to '/'. var pathParts = location.pathname.split("/"); if (pathParts[0] == "") { pathParts.shift(); } if (pathParts.length > 1 && pathParts[1] == "test") { return "/" + pathParts.slice(0, 2).join("/") + "/"; } // Attempt to detect base url using html tag // base href should start and end with "/" if (typeof document !== 'undefined') { var el = document.getElementsByTagName('base'); if (el && el[0] && el[0].getAttribute("href") && el[0].getAttribute ("href").startsWith("/") && el[0].getAttribute("href").endsWith("/")){ return el[0].getAttribute("href"); } } // return default value return "/"; }()); '''; /// A load strategy for the require-js module system. class RequireStrategy extends LoadStrategy { @override final ReloadConfiguration reloadConfiguration; final String _requireDigestsPath = r'$requireDigestsPath'; /// Returns a map of module name to corresponding server path (excluding .js) /// for the provided Dart application entrypoint. /// /// For example: /// /// web/main -> main.ddc /// packages/path/path -> packages/path/path.ddc /// final Future> Function(MetadataProvider metadataProvider) _moduleProvider; /// Returns a map of module name to corresponding digest value. /// /// For example: /// /// web/main -> 8363b363f74b41cac955024ab8b94a3f /// packages/path/path -> d348c2a4647e998011fe305f74f22961 /// final Future> Function(MetadataProvider metadataProvider) _digestsProvider; /// Returns the module for the corresponding server path. /// /// For example: /// /// /packages/path/path.ddc.js -> packages/path/path /// final Future Function(MetadataProvider provider, String sourcePath) _moduleForServerPath; /// Returns the server path for the provided module. /// /// For example: /// /// web/main -> main.ddc.js /// final Future Function(MetadataProvider provider, String module) _serverPathForModule; /// Returns the source map path for the provided module. /// /// For example: /// /// web/main -> main.ddc.js.map /// final Future Function(MetadataProvider provider, String module) _sourceMapPathForModule; /// Returns the server path for the app uri. /// /// For example: /// /// org-dartlang-app://web/main.dart -> main.dart /// /// Will return `null` if the provided uri is not /// an app URI. final String? Function(String appUri) _serverPathForAppUri; /// Returns a map from module id to module info. /// /// For example: /// /// web/main -> {main.ddc.full.dill, main.ddc.dill} /// final Future> Function( MetadataProvider metadataProvider, ) _moduleInfoForProvider; @override BuildSettings get buildSettings => _buildSettings; final BuildSettings _buildSettings; RequireStrategy( this.reloadConfiguration, this._moduleProvider, this._digestsProvider, this._moduleForServerPath, this._serverPathForModule, this._sourceMapPathForModule, this._serverPathForAppUri, this._moduleInfoForProvider, AssetReader assetReader, this._buildSettings, { String? packageConfigPath, }) : super(assetReader, packageConfigPath: packageConfigPath); @override Handler get handler => (request) async { if (request.url.path.endsWith(_requireDigestsPath)) { final entrypoint = request.url.queryParameters['entrypoint']; if (entrypoint == null) return Response.notFound('${request.url}'); final metadataProvider = metadataProviderFor( request.url.queryParameters['entrypoint']!, ); final digests = await _digestsProvider(metadataProvider); return Response.ok(json.encode(digests)); } return Response.notFound('${request.url}'); }; @override String get id => 'require-js'; @override String get moduleFormat => 'amd'; @override String get loadLibrariesModule => 'require.js'; @override String get loadModuleSnippet => 'require'; @override late final DartRuntimeDebugger dartRuntimeDebugger = DartRuntimeDebugger( loadStrategy: this, useLibraryBundleExpression: false, ); /// Require JS config for ddc. /// /// Sets the base url to `/` so that all modules can be loaded using absolute /// paths which simplifies a lot of scenarios. /// /// Sets the timeout for loading modules to infinity (0). /// /// Sets up the custom module paths. /// /// Adds error handler code for require.js which requests a `.errors` file for /// any failed module, and logs it to the console. String get _requireJsConfig => ''' $_baseUrlScript; require.config({ baseUrl: baseUrl, waitSeconds: 0, paths: modulePaths }); const modulesGraph = new Map(); requirejs.onResourceLoad = function (context, map, depArray) { const name = map.name; const depNameArray = depArray.map((dep) => dep.name); if (modulesGraph.has(name)) { var previousDeps = modulesGraph.get(name); var changed = previousDeps.length != depNameArray.length; changed = changed || depNameArray.some(function(depName) { return !previousDeps.includes(depName); }); if (changed) { console.warn("Dependencies graph change for module '" + name + "' detected. " + "Dependencies was [" + previousDeps + "], now [" + depNameArray.map((depName) => depName) +"]. " + "Page can't be hot-reloaded, firing full page reload."); window.location.reload(); } } else { modulesGraph.set(name, []); for (const depName of depNameArray) { if (!\$requireLoader.moduleParentsGraph.has(depName)) { \$requireLoader.moduleParentsGraph.set(depName, []); } \$requireLoader.moduleParentsGraph.get(depName).push(name); modulesGraph.get(name).push(depName); } } }; '''; @override Future bootstrapFor(String entrypoint) async => await _requireLoaderSetup(entrypoint) + _requireJsConfig; @override String loadClientSnippet(String clientScript) => 'window.\$requireLoader.forceLoadModule("$clientScript");\n'; Future _requireLoaderSetup(String entrypoint) async { final metadataProvider = metadataProviderFor(entrypoint); final modulePaths = await _moduleProvider(metadataProvider); final moduleNames = modulePaths.map( (key, value) => MapEntry(value, key), ); return ''' $_baseUrlScript let modulePaths = ${const JsonEncoder.withIndent(" ").convert(modulePaths)}; let moduleNames = ${const JsonEncoder.withIndent(" ").convert(moduleNames)}; if(!window.\$requireLoader) { window.\$requireLoader = { digestsPath: '$_requireDigestsPath?entrypoint=$entrypoint', // Used in package:build_runner/src/server/build_updates_client/hot_reload_client.dart moduleParentsGraph: new Map(), forceLoadModule: function (modulePath, callback, onError) { let moduleName = moduleNames[modulePath]; if (moduleName == null) { moduleName = modulePath; } requirejs.undef(moduleName); try{ requirejs([moduleName], function() { if (typeof callback != 'undefined') { callback(); } }); } catch (error) { if (typeof onError != 'undefined') { onError(error); }else{ throw(error); } } }, getModuleLibraries: null, // set up by _initializeTools }; } '''; } @override Future moduleForServerPath(String entrypoint, String serverPath) { final metadataProvider = metadataProviderFor(entrypoint); return _moduleForServerPath(metadataProvider, serverPath); } @override Future serverPathForModule(String entrypoint, String module) { final metadataProvider = metadataProviderFor(entrypoint); return _serverPathForModule(metadataProvider, module); } @override Future sourceMapPathForModule(String entrypoint, String module) { final metadataProvider = metadataProviderFor(entrypoint); return _sourceMapPathForModule(metadataProvider, module); } @override String? serverPathForAppUri(String appUri) => _serverPathForAppUri(appUri); @override Future> moduleInfoForEntrypoint(String entrypoint) => _moduleInfoForProvider(metadataProviderFor(entrypoint)); @override String? g3RelativePath(String absolutePath) => null; }