Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial service worker #140

Merged
merged 7 commits into from
Apr 24, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 33 additions & 0 deletions consts-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Copyright 2019 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default function constsPlugin(consts) {
return {
name: "consts-plugin",
async resolveId(id) {
if (id !== "consts:") {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ending both these new plugins with : to signify they're something other than node_modules. Open to better suggestions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m okay with this.

return;
}
return id;
},
load(id) {
if (id !== "consts:") {
return;
}
return Object.entries(consts)
.map(
([key, value]) => `export const ${key} = ${JSON.stringify(value)};`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn’t this just be

export ${JSON.stringify(consts)}

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, that's invalid syntax. You could do export default ${JSON.stringify(consts)}, but then usage can't be minified or tree-shaken.

Copy link
Contributor

@surma surma Apr 24, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL. I thought when you do

const a = 1;
const b = 2;

export {a, b};

that the export is an object literal. It is not :(

)
.join("");
}
};
}
41 changes: 41 additions & 0 deletions resource-list-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const resourceListMarker = "___REPLACE_THIS_WITH_RESOURCE_LIST_LATER";

export default function resourceList() {
return {
name: "dependencygraph",
resolveId(id) {
if (id !== "resource-list:") {
return null;
}
return id;
},
load(id) {
if (id !== "resource-list:") {
return null;
}
return `export default ${resourceListMarker};`;
},
generateBundle(_outputOptions, bundle) {
const resourceListJSON = JSON.stringify(Object.keys(bundle));

for (const item of Object.values(bundle)) {
if (!item.code) {
continue;
}
item.code = item.code.replace(resourceListMarker, resourceListJSON);
}
}
};
}
13 changes: 11 additions & 2 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,27 @@ import { terser } from "rollup-plugin-terser";
import loadz0r from "rollup-plugin-loadz0r";
import dependencyGraph from "./dependency-graph-plugin.js";
import chunkNamePlugin from "./chunk-name-plugin.js";
import resourceListPlugin from "./resource-list-plugin";
import postcss from "rollup-plugin-postcss";
import glsl from "./glsl-plugin.js";
import cssModuleTypes from "./css-module-types.js";
import assetPlugin from "./asset-plugin.js";
import { readFileSync } from "fs";
import constsPlugin from "./consts-plugin.js";

// Delete 'dist'
require("rimraf").sync("dist");

export default {
input: "src/bootstrap.ts",
input: {
bootstrap: "src/bootstrap.ts",
sw: "src/sw/index.ts"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the SW a separate endpoint instead of using the same trick we are using for the Worker?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The worker way gives us a hashed file name 😢

},
output: {
dir: "dist",
format: "amd",
sourcemap: true,
entryFileNames: "[name]-[hash].js",
entryFileNames: "[name].js",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want service worker to have a hash. This also removes the hash from bootstrap, but we always inline it so it's ok. I mentioned this issue to the Rollup folks rollup/rollup#2585 (comment)

chunkFileNames: "[name]-[hash].js"
},
plugins: [
Expand All @@ -47,6 +52,9 @@ export default {
return name.replace(/-\w/g, val => val.slice(1).toUpperCase());
}
}),
constsPlugin({
version: require("./package.json").version
}),
typescript({
// Make sure we are using our version of TypeScript.
typescript: require("typescript"),
Expand Down Expand Up @@ -87,6 +95,7 @@ export default {
dependencyGraph({
propList: ["facadeModuleId", "fileName", "imports", "code", "isAsset"]
}),
resourceListPlugin(),
terser()
]
};
15 changes: 1 addition & 14 deletions sizereport.config.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
const { parse } = require("path");
const rescapeRE = require("escape-string-regexp");

module.exports = {
repo: "GoogleChromeLabs/graviton",
path: "dist/**/*",
branch: "master",
findRenamed(path, newPaths) {
const parsedPath = parse(path);
if (parsedPath.base.startsWith("bootstrap-")) {
const end = /\..*$/.exec(parsedPath.base)[0];
const re = new RegExp(
`^${rescapeRE(parsedPath.dir)}/bootstrap-[^\.]+${end}$`
);
return newPaths.find(path => re.test(path));
}
}
branch: "master"
};
9 changes: 9 additions & 0 deletions src/missing-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ declare module "chunk-name:*" {
export default value;
}

declare module "resource-list:" {
const value: string[];
export default value;
}

declare module "consts:" {
export const version: string;
}

interface Window {
debug?: Promise<typeof import("./services/debug/index.js")>;
}
Expand Down
81 changes: 81 additions & 0 deletions src/offline/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/** Tell the service worker to skip waiting. Resolves once the controller has changed. */
export async function skipWaiting() {
const reg = await navigator.serviceWorker.getRegistration();
if (!reg || !reg.waiting) {
return;
}
reg.waiting.postMessage("skip-waiting");

return new Promise(resolve => {
navigator.serviceWorker.addEventListener(
"controllerchange",
() => resolve(),
{ once: true }
);
});
}

/** Is there currently a waiting worker? */
export let updateReady = false;

/** Wait for an installing worker */
async function installingWorker(
reg: ServiceWorkerRegistration
): Promise<ServiceWorker> {
if (reg.installing) {
return reg.installing;
}
return new Promise<ServiceWorker>(resolve => {
reg.addEventListener("updatefound", () => resolve(reg.installing!), {
once: true
});
});
}

async function watchForUpdate() {
const hasController = !!navigator.serviceWorker.controller;

// If we don't have a controller, we don't need to check for updates – we've just loaded from the
// network.
if (!hasController) {
return;
}

const reg = await navigator.serviceWorker.getRegistration();
// Service worker not registered yet.
if (!reg) {
return;
}

// Look for updates
if (reg.waiting) {
return;
}
const installing = await installingWorker(reg);
await new Promise<void>(resolve => {
installing.addEventListener("statechange", () => {
if (installing.state === "installed") {
resolve();
}
});
});

updateReady = true;
}

/** Set up the service worker and monitor changes */
export async function init() {
const thisURL = new URL(location.href);

if (thisURL.searchParams.has("no-sw")) {
const reg = await navigator.serviceWorker.getRegistration();
if (reg) {
await reg.unregister();
location.reload();
}
return;
}

await navigator.serviceWorker.register("/sw.js");
watchForUpdate();
}
26 changes: 26 additions & 0 deletions src/services/preact-canvas/components/game-loading/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Copyright 2019 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, h } from "preact";
import TopBar from "../top-bar/index.js";
import { loading, loadingInner } from "./style.css";

export default class GameLoading extends Component<{}, {}> {
render() {
return (
<div class={loading}>
<TopBar titleOnly />
<div class={loadingInner}>…Loading…</div>
</div>
);
}
}
25 changes: 25 additions & 0 deletions src/services/preact-canvas/components/game-loading/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.loading {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
position: relative;
}

@keyframes fade-in-out {
50% {
opacity: 1;
}
}

.loadingInner {
display: flex;
flex-flow: column;
padding: var(--bar-avoid) var(--side-margin) 0;
box-sizing: border-box;
width: 100%;
text-align: center;
font-size: 2.3rem;
opacity: 0;
animation: fade-in-out 3s ease-in-out infinite 0.5s;
}
30 changes: 2 additions & 28 deletions src/services/preact-canvas/components/intro/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import {
selectArrow as selectArrowStyle,
settingsRow as settingsRowStyle,
startButton as startButtonStyle,
startButtonTextHide,
startButtonTextShow,
startForm as startFormStyle
} from "./style.css";

Expand Down Expand Up @@ -93,22 +91,19 @@ type PresetName = keyof typeof presets;

export interface Props {
onStartGame: (width: number, height: number, mines: number) => void;
loading: boolean;
}

interface State {
presetName: PresetName | "custom";
width: number;
height: number;
mines: number;
longLoad: boolean;
}

// tslint:disable-next-line:max-classes-per-file
export default class Intro extends Component<Props, State> {
state: State = {
presetName: "beginner",
longLoad: false,
...presets.beginner
};

Expand All @@ -119,16 +114,9 @@ export default class Intro extends Component<Props, State> {

componentDidMount() {
window.scrollTo(0, 0);

setTimeout(() => {
this.setState({ longLoad: true });
}, 1000);
}

render(
{ loading }: Props,
{ width, height, mines, presetName, longLoad }: State
) {
render(_props: Props, { width, height, mines, presetName }: State) {
return (
<div class={introStyle}>
<TopBar titleOnly />
Expand Down Expand Up @@ -185,21 +173,7 @@ export default class Intro extends Component<Props, State> {
</NumberField>
</div>
<div class={settingsRowStyle}>
<button class={startButtonStyle} disabled={loading}>
<span
class={
longLoad || !loading
? startButtonTextShow
: startButtonTextHide
}
>
{loading
? longLoad
? "…Loading…"
: "\u00A0" // non-breaking space, to retain height
: "Start"}
</span>
</button>
<button class={startButtonStyle}>Start</button>
</div>
</form>
</div>
Expand Down
17 changes: 0 additions & 17 deletions src/services/preact-canvas/components/intro/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -108,21 +108,4 @@
margin: 0 var(--item-margin) !important;
text-transform: uppercase;
letter-spacing: 0.15rem;
will-change: opacity;
transition: opacity 0.4s ease-in-out;
}

.start-button:disabled {
opacity: 0.7;
}

.start-button-text-hide {
will-change: opacity;
opacity: 0;
transition: opacity 0.2s ease-in-out;
}

.start-button-text-show {
composes: start-button-text-hide;
opacity: 1;
}