web.dart 24.7 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:math';

7
import 'package:crypto/crypto.dart';
8
import 'package:package_config/package_config.dart';
9

10 11 12 13
import '../../artifacts.dart';
import '../../base/file_system.dart';
import '../../base/io.dart';
import '../../build_info.dart';
14
import '../../cache.dart';
15
import '../../convert.dart';
16
import '../../dart/language_version.dart';
17
import '../../dart/package_map.dart';
18
import '../../globals.dart' as globals;
19
import '../../project.dart';
20
import '../build_system.dart';
21
import '../depfile.dart';
22
import '../exceptions.dart';
23
import 'assets.dart';
24
import 'localizations.dart';
25 26 27 28 29 30 31 32 33

/// Whether the application has web plugins.
const String kHasWebPlugins = 'HasWebPlugins';

/// An override for the dart2js build mode.
///
/// Valid values are O1 (lowest, profile default) to O4 (highest, release default).
const String kDart2jsOptimization = 'Dart2jsOptimization';

34 35 36
/// Whether to disable dynamic generation code to satisfy csp policies.
const String kCspMode = 'cspMode';

37 38 39 40 41 42
/// Base href to set in index.html in flutter build command
const String kBaseHref = 'baseHref';

/// Placeholder for base href
const String kBaseHrefPlaceholder = r'$FLUTTER_BASE_HREF';

43
/// The caching strategy to use for service worker generation.
44
const String kServiceWorkerStrategy = 'ServiceWorkerStrategy';
45

46 47 48
/// Whether the dart2js build should output source maps.
const String kSourceMapsEnabled = 'SourceMaps';

49 50 51
/// Whether the dart2js native null assertions are enabled.
const String kNativeNullAssertions = 'NativeNullAssertions';

52 53 54 55 56 57 58 59 60 61 62 63 64
/// The caching strategy for the generated service worker.
enum ServiceWorkerStrategy {
  /// Download the app shell eagerly and all other assets lazily.
  /// Prefer the offline cached version.
  offlineFirst,
  /// Do not generate a service worker,
  none,
}

const String kOfflineFirst = 'offline-first';
const String kNoneWorker = 'none';

/// Convert a [value] into a [ServiceWorkerStrategy].
65
ServiceWorkerStrategy _serviceWorkerStrategyFromString(String? value) {
66 67 68 69 70 71 72 73 74
  switch (value) {
    case kNoneWorker:
      return ServiceWorkerStrategy.none;
    // offline-first is the default value for any invalid requests.
    default:
      return ServiceWorkerStrategy.offlineFirst;
  }
}

75
/// Generates an entry point for a web target.
Dan Field's avatar
Dan Field committed
76
// Keep this in sync with build_runner/resident_web_runner.dart
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
class WebEntrypointTarget extends Target {
  const WebEntrypointTarget();

  @override
  String get name => 'web_entrypoint';

  @override
  List<Target> get dependencies => const <Target>[];

  @override
  List<Source> get inputs => const <Source>[
    Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/web.dart'),
  ];

  @override
  List<Source> get outputs => const <Source>[
    Source.pattern('{BUILD_DIR}/main.dart'),
  ];

  @override
  Future<void> build(Environment environment) async {
98
    final String? targetFile = environment.defines[kTargetFile];
99
    final bool hasPlugins = environment.defines[kHasWebPlugins] == 'true';
100
    final Uri importUri = environment.fileSystem.file(targetFile).absolute.uri;
101
    // TODO(zanderso): support configuration of this file.
102
    const String packageFile = '.packages';
103
    final PackageConfig packageConfig = await loadPackageConfigWithLogging(
104
      environment.fileSystem.file(packageFile),
105
      logger: environment.logger,
106
    );
107
    final FlutterProject flutterProject = FlutterProject.current();
108
    final LanguageVersion languageVersion = determineLanguageVersion(
109 110
      environment.fileSystem.file(targetFile),
      packageConfig[flutterProject.manifest.appName],
111
      Cache.flutterRoot!,
112
    );
113

114
    // Use the PackageConfig to find the correct package-scheme import path
115 116 117 118 119 120
    // for the user application. If the application has a mix of package-scheme
    // and relative imports for a library, then importing the entrypoint as a
    // file-scheme will cause said library to be recognized as two distinct
    // libraries. This can cause surprising behavior as types from that library
    // will be considered distinct from each other.
    // By construction, this will only be null if the .packages file does not
121 122
    // have an entry for the user's application or if the main file is
    // outside of the lib/ directory.
123 124
    final String mainImport = packageConfig.toPackageUri(importUri)?.toString()
      ?? importUri.toString();
125 126 127

    String contents;
    if (hasPlugins) {
128
      final Uri generatedUri = environment.projectDir
129 130
        .childDirectory('lib')
        .childFile('generated_plugin_registrant.dart')
131 132 133 134
        .absolute
        .uri;
      final String generatedImport = packageConfig.toPackageUri(generatedUri)?.toString()
        ?? generatedUri.toString();
135
      contents = '''
136
// @dart=${languageVersion.major}.${languageVersion.minor}
137

138 139 140 141
import 'dart:ui' as ui;

import 'package:flutter_web_plugins/flutter_web_plugins.dart';

142 143
import '$generatedImport';
import '$mainImport' as entrypoint;
144 145

Future<void> main() async {
146
  registerPlugins(webPluginRegistrar);
147
  await ui.webOnlyInitializePlatform();
148 149 150 151 152
  entrypoint.main();
}
''';
    } else {
      contents = '''
153
// @dart=${languageVersion.major}.${languageVersion.minor}
154

155 156
import 'dart:ui' as ui;

157
import '$mainImport' as entrypoint;
158 159

Future<void> main() async {
160
  await ui.webOnlyInitializePlatform();
161 162 163 164 165
  entrypoint.main();
}
''';
    }
    environment.buildDir.childFile('main.dart')
166
      .writeAsStringSync(contents);
167 168 169
  }
}

170
/// Compiles a web entry point with dart2js.
171 172 173 174 175 176 177 178
class Dart2JSTarget extends Target {
  const Dart2JSTarget();

  @override
  String get name => 'dart2js';

  @override
  List<Target> get dependencies => const <Target>[
179 180
    WebEntrypointTarget(),
    GenerateLocalizationsTarget(),
181 182 183 184
  ];

  @override
  List<Source> get inputs => const <Source>[
185 186 187
    Source.hostArtifact(HostArtifact.flutterWebSdk),
    Source.hostArtifact(HostArtifact.dart2jsSnapshot),
    Source.hostArtifact(HostArtifact.engineDartBinary),
188
    Source.pattern('{BUILD_DIR}/main.dart'),
189
    Source.pattern('{PROJECT_DIR}/.dart_tool/package_config_subset'),
190 191 192
  ];

  @override
193 194 195 196 197
  List<Source> get outputs => const <Source>[];

  @override
  List<String> get depfiles => const <String>[
    'dart2js.d',
198 199
  ];

200 201 202 203 204 205 206 207 208 209
  String _collectOutput(ProcessResult result) {
    final String stdout = result.stdout is List<int>
        ? utf8.decode(result.stdout as List<int>)
        : result.stdout as String;
    final String stderr = result.stderr is List<int>
        ? utf8.decode(result.stderr as List<int>)
        : result.stderr as String;
    return stdout + stderr;
  }

210 211
  @override
  Future<void> build(Environment environment) async {
212 213 214 215 216
    final String? buildModeEnvironment = environment.defines[kBuildMode];
    if (buildModeEnvironment == null) {
      throw MissingDefineException(kBuildMode, name);
    }
    final BuildMode buildMode = getBuildModeForName(buildModeEnvironment);
217
    final bool sourceMapsEnabled = environment.defines[kSourceMapsEnabled] == 'true';
218
    final bool nativeNullAssertions = environment.defines[kNativeNullAssertions] == 'true';
219 220
    final Artifacts artifacts = globals.artifacts!;
    final String librariesSpec = (artifacts.getHostArtifact(HostArtifact.flutterWebSdk) as Directory).childFile('libraries.json').path;
221
    final List<String> sharedCommandOptions = <String>[
222
      artifacts.getHostArtifact(HostArtifact.engineDartBinary).path,
223
      '--disable-dart-dev',
224
      artifacts.getHostArtifact(HostArtifact.dart2jsSnapshot).path,
225
      '--libraries-spec=$librariesSpec',
226
      ...decodeCommaSeparated(environment.defines, kExtraFrontEndOptions),
227 228
      if (nativeNullAssertions)
        '--native-null-assertions',
229 230 231 232
      if (buildMode == BuildMode.profile)
        '-Ddart.vm.profile=true'
      else
        '-Ddart.vm.product=true',
233
      for (final String dartDefine in decodeDartDefines(environment.defines, kDartDefines))
234
        '-D$dartDefine',
235 236
      if (!sourceMapsEnabled)
        '--no-source-maps',
237 238 239 240 241 242 243 244 245
    ];

    // Run the dart2js compilation in two stages, so that icon tree shaking can
    // parse the kernel file for web builds.
    final ProcessResult kernelResult = await globals.processManager.run(<String>[
      ...sharedCommandOptions,
      '-o',
      environment.buildDir.childFile('app.dill').path,
      '--packages=.packages',
246
      '--cfe-only',
247
      environment.buildDir.childFile('main.dart').path, // dartfile
248 249
    ]);
    if (kernelResult.exitCode != 0) {
250
      throw Exception(_collectOutput(kernelResult));
251
    }
252

253
    final String? dart2jsOptimization = environment.defines[kDart2jsOptimization];
254 255 256
    final File outputJSFile = environment.buildDir.childFile('main.dart.js');
    final bool csp = environment.defines[kCspMode] == 'true';

257
    final ProcessResult javaScriptResult = await environment.processManager.run(<String>[
258 259 260 261
      ...sharedCommandOptions,
      if (dart2jsOptimization != null) '-$dart2jsOptimization' else '-O4',
      if (buildMode == BuildMode.profile) '--no-minify',
      if (csp) '--csp',
262
      '-o',
263 264
      outputJSFile.path,
      environment.buildDir.childFile('app.dill').path, // dartfile
265
    ]);
266
    if (javaScriptResult.exitCode != 0) {
267
      throw Exception(_collectOutput(javaScriptResult));
268
    }
269
    final File dart2jsDeps = environment.buildDir
270
      .childFile('app.dill.deps');
271
    if (!dart2jsDeps.existsSync()) {
272
      globals.printWarning('Warning: dart2js did not produced expected deps list at '
273 274 275
        '${dart2jsDeps.path}');
      return;
    }
276 277 278 279 280
    final DepfileService depfileService = DepfileService(
      fileSystem: globals.fs,
      logger: globals.logger,
    );
    final Depfile depfile = depfileService.parseDart2js(
281
      environment.buildDir.childFile('app.dill.deps'),
282
      outputJSFile,
283
    );
284 285 286 287
    depfileService.writeToFile(
      depfile,
      environment.buildDir.childFile('dart2js.d'),
    );
288 289 290
  }
}

291
/// Unpacks the dart2js compilation and resources to a given output directory.
292 293 294 295 296 297 298 299 300 301 302 303 304 305
class WebReleaseBundle extends Target {
  const WebReleaseBundle();

  @override
  String get name => 'web_release_bundle';

  @override
  List<Target> get dependencies => const <Target>[
    Dart2JSTarget(),
  ];

  @override
  List<Source> get inputs => const <Source>[
    Source.pattern('{BUILD_DIR}/main.dart.js'),
306
    Source.pattern('{PROJECT_DIR}/pubspec.yaml'),
307 308 309 310 311
  ];

  @override
  List<Source> get outputs => const <Source>[
    Source.pattern('{OUTPUT_DIR}/main.dart.js'),
312 313 314 315 316
  ];

  @override
  List<String> get depfiles => const <String>[
    'dart2js.d',
317 318
    'flutter_assets.d',
    'web_resources.d',
319 320 321 322
  ];

  @override
  Future<void> build(Environment environment) async {
323
    for (final File outputFile in environment.buildDir.listSync(recursive: true).whereType<File>()) {
324 325 326 327 328 329
      final String basename = globals.fs.path.basename(outputFile.path);
      if (!basename.contains('main.dart.js')) {
        continue;
      }
      // Do not copy the deps file.
      if (basename.endsWith('.deps')) {
330 331 332
        continue;
      }
      outputFile.copySync(
333
        environment.outputDir.childFile(globals.fs.path.basename(outputFile.path)).path
334 335
      );
    }
336 337 338 339 340

    final String versionInfo = FlutterProject.current().getVersionInfo();
    environment.outputDir
        .childFile('version.json')
        .writeAsStringSync(versionInfo);
341 342
    final Directory outputDirectory = environment.outputDir.childDirectory('assets');
    outputDirectory.createSync(recursive: true);
343 344 345 346 347
    final Depfile depfile = await copyAssets(
      environment,
      environment.outputDir.childDirectory('assets'),
      targetPlatform: TargetPlatform.web_javascript,
    );
348 349 350 351 352 353 354 355
    final DepfileService depfileService = DepfileService(
      fileSystem: globals.fs,
      logger: globals.logger,
    );
    depfileService.writeToFile(
      depfile,
      environment.buildDir.childFile('flutter_assets.d'),
    );
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373

    final Directory webResources = environment.projectDir
      .childDirectory('web');
    final List<File> inputResourceFiles = webResources
      .listSync(recursive: true)
      .whereType<File>()
      .toList();

    // Copy other resource files out of web/ directory.
    final List<File> outputResourcesFiles = <File>[];
    for (final File inputFile in inputResourceFiles) {
      final File outputFile = globals.fs.file(globals.fs.path.join(
        environment.outputDir.path,
        globals.fs.path.relative(inputFile.path, from: webResources.path)));
      if (!outputFile.parent.existsSync()) {
        outputFile.parent.createSync(recursive: true);
      }
      outputResourcesFiles.add(outputFile);
374 375 376
      // insert a random hash into the requests for service_worker.js. This is not a content hash,
      // because it would need to be the hash for the entire bundle and not just the resource
      // in question.
377 378
      if (environment.fileSystem.path.basename(inputFile.path) == 'index.html') {
        final String randomHash = Random().nextInt(4294967296).toString();
379
        String resultString = inputFile.readAsStringSync()
380 381 382 383 384 385
          .replaceFirst(
            'var serviceWorkerVersion = null',
            "var serviceWorkerVersion = '$randomHash'",
          )
          // This is for legacy index.html that still use the old service
          // worker loading mechanism.
386 387 388 389
          .replaceFirst(
            "navigator.serviceWorker.register('flutter_service_worker.js')",
            "navigator.serviceWorker.register('flutter_service_worker.js?v=$randomHash')",
          );
390 391
        final String? baseHref = environment.defines[kBaseHref];
        if (resultString.contains(kBaseHrefPlaceholder) && baseHref == null) {
392
          resultString = resultString.replaceAll(kBaseHrefPlaceholder, '/');
393 394
        } else if (resultString.contains(kBaseHrefPlaceholder) && baseHref != null) {
          resultString = resultString.replaceAll(kBaseHrefPlaceholder, baseHref);
395
        }
396 397 398 399
        outputFile.writeAsStringSync(resultString);
        continue;
      }
      inputFile.copySync(outputFile.path);
400 401
    }
    final Depfile resourceFile = Depfile(inputResourceFiles, outputResourcesFiles);
402 403 404 405
    depfileService.writeToFile(
      resourceFile,
      environment.buildDir.childFile('web_resources.d'),
    );
406 407
  }
}
408

409 410 411 412 413 414
/// Static assets provided by the Flutter SDK that do not change, such as
/// CanvasKit.
///
/// These assets can be cached forever and are only invalidated when the
/// Flutter SDK is upgraded to a new version.
class WebBuiltInAssets extends Target {
415
  const WebBuiltInAssets(this.fileSystem, this.cache);
416 417

  final FileSystem fileSystem;
418
  final Cache cache;
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436

  @override
  String get name => 'web_static_assets';

  @override
  List<Target> get dependencies => const <Target>[];

  @override
  List<String> get depfiles => const <String>[];

  @override
  List<Source> get inputs => const <Source>[];

  @override
  List<Source> get outputs => const <Source>[];

  @override
  Future<void> build(Environment environment) async {
437 438 439 440 441 442 443 444 445
    // TODO(yjbanov): https://github.com/flutter/flutter/issues/52588
    //
    // Update this when we start building CanvasKit from sources. In the
    // meantime, get the Web SDK directory from cache rather than through
    // Artifacts. The latter is sensitive to `--local-engine`, which changes
    // the directory to point to ENGINE/src/out. However, CanvasKit is not yet
    // built as part of the engine, but fetched from CIPD, and so it won't be
    // found in ENGINE/src/out.
    final Directory flutterWebSdk = cache.getWebSdkDirectory();
446 447 448 449 450 451 452 453 454
    final Directory canvasKitDirectory = flutterWebSdk.childDirectory('canvaskit');
    for (final File file in canvasKitDirectory.listSync(recursive: true).whereType<File>()) {
      final String relativePath = fileSystem.path.relative(file.path, from: canvasKitDirectory.path);
      final String targetPath = fileSystem.path.join(environment.outputDir.path, 'canvaskit', relativePath);
      file.copySync(targetPath);
    }
  }
}

455 456
/// Generate a service worker for a web target.
class WebServiceWorker extends Target {
457
  const WebServiceWorker(this.fileSystem, this.cache);
458 459

  final FileSystem fileSystem;
460
  final Cache cache;
461 462 463 464 465

  @override
  String get name => 'web_service_worker';

  @override
466 467 468
  List<Target> get dependencies => <Target>[
    const Dart2JSTarget(),
    const WebReleaseBundle(),
469
    WebBuiltInAssets(fileSystem, cache),
470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490
  ];

  @override
  List<String> get depfiles => const <String>[
    'service_worker.d',
  ];

  @override
  List<Source> get inputs => const <Source>[];

  @override
  List<Source> get outputs => const <Source>[];

  @override
  Future<void> build(Environment environment) async {
    final List<File> contents = environment.outputDir
      .listSync(recursive: true)
      .whereType<File>()
      .where((File file) => !file.path.endsWith('flutter_service_worker.js')
        && !globals.fs.path.basename(file.path).startsWith('.'))
      .toList();
491 492 493 494

    final Map<String, String> urlToHash = <String, String>{};
    for (final File file in contents) {
      // Do not force caching of source maps.
495 496
      if (file.path.endsWith('main.dart.js.map') ||
        file.path.endsWith('.part.js.map')) {
497 498 499 500 501 502 503 504 505
        continue;
      }
      final String url = globals.fs.path.toUri(
        globals.fs.path.relative(
          file.path,
          from: environment.outputDir.path),
        ).toString();
      final String hash = md5.convert(await file.readAsBytes()).toString();
      urlToHash[url] = hash;
506 507 508 509
      // Add an additional entry for the base URL.
      if (globals.fs.path.basename(url) == 'index.html') {
        urlToHash['/'] = hash;
      }
510 511
    }

512 513 514
    final File serviceWorkerFile = environment.outputDir
      .childFile('flutter_service_worker.js');
    final Depfile depfile = Depfile(contents, <File>[serviceWorkerFile]);
515
    final ServiceWorkerStrategy serviceWorkerStrategy = _serviceWorkerStrategyFromString(
516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531
      environment.defines[kServiceWorkerStrategy],
    );
    final String serviceWorker = generateServiceWorker(
      urlToHash,
      <String>[
        '/',
        'main.dart.js',
        'index.html',
        'assets/NOTICES',
        if (urlToHash.containsKey('assets/AssetManifest.json'))
          'assets/AssetManifest.json',
        if (urlToHash.containsKey('assets/FontManifest.json'))
          'assets/FontManifest.json',
      ],
      serviceWorkerStrategy: serviceWorkerStrategy,
    );
532 533
    serviceWorkerFile
      .writeAsStringSync(serviceWorker);
534 535 536 537 538 539 540 541
    final DepfileService depfileService = DepfileService(
      fileSystem: globals.fs,
      logger: globals.logger,
    );
    depfileService.writeToFile(
      depfile,
      environment.buildDir.childFile('service_worker.d'),
    );
542 543 544 545 546 547
  }
}

/// Generate a service worker with an app-specific cache name a map of
/// resource files.
///
548
/// The tool embeds file hashes directly into the worker so that the byte for byte
549 550
/// invalidation will automatically reactivate workers whenever a new
/// version is deployed.
551 552 553
String generateServiceWorker(
  Map<String, String> resources,
  List<String> coreBundle, {
554
  required ServiceWorkerStrategy serviceWorkerStrategy,
555 556 557 558
}) {
  if (serviceWorkerStrategy == ServiceWorkerStrategy.none) {
    return '';
  }
559 560
  return '''
'use strict';
561 562
const MANIFEST = 'flutter-app-manifest';
const TEMP = 'flutter-temp-cache';
563 564 565 566 567
const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {
  ${resources.entries.map((MapEntry<String, String> entry) => '"${entry.key}": "${entry.value}"').join(",\n")}
};

568 569 570 571 572 573
// The application shell files that are downloaded before a service worker can
// start.
const CORE = [
  ${coreBundle.map((String file) => '"$file"').join(',\n')}];
// During install, the TEMP cache is populated with the application shell files.
self.addEventListener("install", (event) => {
574
  self.skipWaiting();
575 576
  return event.waitUntil(
    caches.open(TEMP).then((cache) => {
577
      return cache.addAll(
578
        CORE.map((value) => new Request(value, {'cache': 'reload'})));
579 580 581 582
    })
  );
});

583 584 585 586 587 588 589 590 591 592 593 594 595
// During activate, the cache is populated with the temp files downloaded in
// install. If this service worker is upgrading from one with a saved
// MANIFEST, then use this to retain unchanged resource files.
self.addEventListener("activate", function(event) {
  return event.waitUntil(async function() {
    try {
      var contentCache = await caches.open(CACHE_NAME);
      var tempCache = await caches.open(TEMP);
      var manifestCache = await caches.open(MANIFEST);
      var manifest = await manifestCache.match('manifest');
      // When there is no prior manifest, clear the entire cache.
      if (!manifest) {
        await caches.delete(CACHE_NAME);
596
        contentCache = await caches.open(CACHE_NAME);
597 598 599
        for (var request of await tempCache.keys()) {
          var response = await tempCache.match(request);
          await contentCache.put(request, response);
600
        }
601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642
        await caches.delete(TEMP);
        // Save the manifest to make future upgrades efficient.
        await manifestCache.put('manifest', new Response(JSON.stringify(RESOURCES)));
        return;
      }
      var oldManifest = await manifest.json();
      var origin = self.location.origin;
      for (var request of await contentCache.keys()) {
        var key = request.url.substring(origin.length + 1);
        if (key == "") {
          key = "/";
        }
        // If a resource from the old manifest is not in the new cache, or if
        // the MD5 sum has changed, delete it. Otherwise the resource is left
        // in the cache and can be reused by the new service worker.
        if (!RESOURCES[key] || RESOURCES[key] != oldManifest[key]) {
          await contentCache.delete(request);
        }
      }
      // Populate the cache with the app shell TEMP files, potentially overwriting
      // cache files preserved above.
      for (var request of await tempCache.keys()) {
        var response = await tempCache.match(request);
        await contentCache.put(request, response);
      }
      await caches.delete(TEMP);
      // Save the manifest to make future upgrades efficient.
      await manifestCache.put('manifest', new Response(JSON.stringify(RESOURCES)));
      return;
    } catch (err) {
      // On an unhandled exception the state of the cache cannot be guaranteed.
      console.error('Failed to upgrade service worker: ' + err);
      await caches.delete(CACHE_NAME);
      await caches.delete(TEMP);
      await caches.delete(MANIFEST);
    }
  }());
});

// The fetch handler redirects requests for RESOURCE files to the service
// worker cache.
self.addEventListener("fetch", (event) => {
643 644 645
  if (event.request.method !== 'GET') {
    return;
  }
646 647
  var origin = self.location.origin;
  var key = event.request.url.substring(origin.length + 1);
648
  // Redirect URLs to the index.html
649 650 651 652
  if (key.indexOf('?v=') != -1) {
    key = key.split('?v=')[0];
  }
  if (event.request.url == origin || event.request.url.startsWith(origin + '/#') || key == '') {
653 654
    key = '/';
  }
655 656
  // If the URL is not the RESOURCE list then return to signal that the
  // browser should take over.
657
  if (!RESOURCES[key]) {
658
    return;
659
  }
660 661 662 663
  // If the URL is the index.html, perform an online-first request.
  if (key == '/') {
    return onlineFirst(event);
  }
664 665 666 667
  event.respondWith(caches.open(CACHE_NAME)
    .then((cache) =>  {
      return cache.match(event.request).then((response) => {
        // Either respond with the cached resource, or perform a fetch and
668 669
        // lazily populate the cache.
        return response || fetch(event.request).then((response) => {
670 671 672
          cache.put(event.request, response.clone());
          return response;
        });
673
      })
674
    })
675 676
  );
});
677

678 679 680
self.addEventListener('message', (event) => {
  // SkipWaiting can be used to immediately activate a waiting service worker.
  // This will also require a page refresh triggered by the main worker.
681
  if (event.data === 'skipWaiting') {
682
    self.skipWaiting();
683
    return;
684
  }
685
  if (event.data === 'downloadOffline') {
686
    downloadOffline();
687
    return;
688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703
  }
});

// Download offline will check the RESOURCES for all files not in the cache
// and populate them.
async function downloadOffline() {
  var resources = [];
  var contentCache = await caches.open(CACHE_NAME);
  var currentContent = {};
  for (var request of await contentCache.keys()) {
    var key = request.url.substring(origin.length + 1);
    if (key == "") {
      key = "/";
    }
    currentContent[key] = true;
  }
704
  for (var resourceKey of Object.keys(RESOURCES)) {
705
    if (!currentContent[resourceKey]) {
706
      resources.push(resourceKey);
707 708
    }
  }
709
  return contentCache.addAll(resources);
710
}
711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732

// Attempt to download the resource online before falling back to
// the offline cache.
function onlineFirst(event) {
  return event.respondWith(
    fetch(event.request).then((response) => {
      return caches.open(CACHE_NAME).then((cache) => {
        cache.put(event.request, response.clone());
        return response;
      });
    }).catch((error) => {
      return caches.open(CACHE_NAME).then((cache) => {
        return cache.match(event.request).then((response) => {
          if (response != null) {
            return response;
          }
          throw error;
        });
      });
    })
  );
}
733 734
''';
}