๐Ÿ—๏ธ Moved to Vite, only dev mode works for now

main
Basil 3 months ago
parent 5362713eec
commit b120d490e9

@ -1,79 +0,0 @@
#!/usr/bin/env node
/**
* Copyright 2020 Google LLC
*
* 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.
*/
/**
* @fileoverview This file compares the current commit hash against the staging site. If they're
* different, and no other build tasks are running, we kick off "staging-deploy".
*/
const { ErrorReporting } = require('@google-cloud/error-reporting');
const { CloudBuildClient } = require('@google-cloud/cloudbuild');
const { getDeployedVersion, getCurrentVersion } = require('../build/git-version.js');
const client = new CloudBuildClient();
const errors = new ErrorReporting();
// This is the trigger ID of "staging-deploy" for "santa-staging".
const deployTriggerId = 'd6401587-de8b-4507-ae71-bc516fdfc64a';
(async () => {
const deployedVersion = await getDeployedVersion();
const currentVersion = await getCurrentVersion();
console.log(`version deployed="${deployedVersion}" local="${currentVersion}""`);
if (deployedVersion && deployedVersion === currentVersion) {
console.log(
'The current and deployed versions are the same, not continuing build.'
);
return;
}
console.log(
'The current and deployed versions are different, kicking off deploy build.'
);
// Check if there are any existing builds.
const ret = client.listBuildsAsync({
projectId: process.env.PROJECT_ID,
pageSize: 1,
filter: `trigger_id="${deployTriggerId}" AND (status="WORKING" OR status="QUEUED")`,
});
// This is an async iterable, check if we have at least one, if so, there's an active build.
let activeBuild = false;
for await (const _build of ret) {
activeBuild = true;
break;
}
if (activeBuild) {
console.log(
'There is a current active or queued build. Not starting another.'
);
return;
}
try {
// This just waits for the build to be kicked off, not for its completion (it
// returns a LROperation).
await client.runBuildTrigger({
projectId: process.env.PROJECT_ID,
triggerId: deployTriggerId,
});
} catch (e) {
errors.report(e);
}
})();

@ -1,16 +0,0 @@
# This Cloud Build task can kick off the staging deploy if the hash has changed.
steps:
- name: node
id: 'Install dependencies'
entrypoint: npm
args: ['ci']
- name: node
id: 'Verify and maybe kick off build for new version'
entrypoint: npm
args: ['run', 'staging-check']
options:
env:
- 'PROJECT_ID=$PROJECT_ID'

@ -1,23 +0,0 @@
#!/bin/bash
set -eu
BASEURL="https://santa-staging.firebaseapp.com/"
# move to the root directory of santa-tracker-web
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STAGING_ROOT="$ROOT/staging"
cd $ROOT/..
# build!
node ./release.js --baseurl=$BASEURL --minify=false
# move prod to GaE skeleton
rm -rf $STAGING_ROOT/appengine/prod
mv dist/prod $STAGING_ROOT/appengine/prod
# move static to firebase
# Note that when we deploy, we clobber the "old" deploy. People mucking with the staging site will
# be suddenly surprised when they can't load content!
mkdir -p $STAGING_ROOT/firebase/public
mv dist/_static/* $STAGING_ROOT/firebase/public

@ -1,36 +0,0 @@
# This Cloud Build task runs the deploy to staging.
steps:
- name: node
id: 'Install dependencies'
entrypoint: npm
args: ['ci']
# This is an optional dependency of "google-closure-compiler", but it doesn't always install on
# Cloud Build for some reason. We need this as we can't use the Java compiler in the Node image.
- name: node
id: 'Force install Closure native Linux binary'
entrypoint: 'npm'
args: ['install', 'google-closure-compiler-linux']
- name: node
id: 'Build'
entrypoint: bash
args: ['.cloudbuild/staging-deploy.sh']
- name: 'gcr.io/$PROJECT_ID/firebase'
dir: '.cloudbuild/staging/firebase'
args: ['deploy', '--only', 'hosting', '--project', 'santa-staging']
- name: 'gcr.io/cloud-builders/gcloud'
dir: '.cloudbuild/staging/appengine'
entrypoint: 'bash'
args: ['-c', 'gcloud app deploy --version hohoho --project santa-staging']
options:
machineType: 'E2_HIGHCPU_32' # yolo
env:
- 'PROJECT_ID=$PROJECT_ID'
- 'NODE_OPTIONS="--max-old-space-size=32768"'
timeout: 1800s

@ -1,2 +0,0 @@
appengine/prod
firebase/public

@ -1,6 +0,0 @@
runtime: go115
handlers:
- url: /.*
script: auto
secure: always

@ -1,3 +0,0 @@
module github.com/google/santa-tracker-web
go 1.15

@ -1,53 +0,0 @@
package main
import (
"fmt"
"log"
"net/http"
"os"
"regexp"
)
var (
intlMatcher = regexp.MustCompile(`^/intl/([-_\w]+)(/.*|)$`)
)
func main() {
fileServer := http.FileServer(http.Dir("prod"))
handler := rewriteLang(fileServer)
http.Handle("/", handler)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
addr := ":" + port
log.Printf("Serving on %s...", addr)
http.ListenAndServe(addr, nil)
}
// pathForLangFile serves the specified "rest" file for the given lang, falling
// back to the top-level if it doesn't exist there.
func pathForLangFile(lang, rest string) string {
if len(rest) != 0 && rest[0] == '/' {
rest = rest[1:] // trim leading slash
}
check := fmt.Sprintf("prod/intl/%s_ALL/%s", lang, rest)
if _, err := os.Stat(check); err != nil && os.IsNotExist(err) {
return fmt.Sprintf("/%s", rest)
}
return fmt.Sprintf("/intl/%s_ALL/%s", lang, rest)
}
func rewriteLang(wrapped http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
found := intlMatcher.FindStringSubmatch(r.URL.Path)
if found != nil {
// this was an /intl/de/... request, rewrite it
lang, rest := found[1], found[2]
r.URL.Path = pathForLangFile(lang, rest)
}
wrapped.ServeHTTP(w, r)
})
}

@ -1,20 +0,0 @@
{
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*"
],
"headers": [
{
"source": "**",
"headers": [
{
"key": "Access-Control-Allow-Origin",
"value": "*"
}
]
}
]
}
}

@ -1,24 +0,0 @@
---
name: Bug report
about: Found a bug? Let us know!
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.

@ -1,13 +0,0 @@
---
name: Feature request
about: Suggest an idea for Santa Tracker.
title: ''
labels: feature request
assignees: ''
---
**What would you like to see?**
A clear and concise description of what you want to appear on the site.
**Additional context**
Add any other context or screenshots about the feature request here.

@ -1,8 +0,0 @@
---
name: Other
about: Questions or something else?
title: ''
labels: other
assignees: ''
---

@ -1,77 +0,0 @@
/**
* Copyright 2020 Google LLC
*
* 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 fs = require('fs');
const path = require('path');
// TODO(samthor): generate on-demand
const nodeModulesPath = path.join(__dirname, '..', '..', 'node_modules');
/**
* @param {string} filename that is including other files
* @return {!Object} Babel plugin
*/
module.exports = function buildResolveBareSpecifiers(filename) {
const dir = path.dirname(filename);
const handler = (nodePath) => {
const node = nodePath.node;
if (node.source === null) {
return;
}
const specifier = node.source.value;
if (specifier.startsWith('./') || specifier.startsWith('../')) {
return; // do nothing, is a relative URL
}
try {
new URL(specifier);
return; // do nothing, is a real URL
} catch (e) {
// ignore
}
const ext = path.extname(specifier);
const cand = path.join(nodeModulesPath, specifier);
if (ext === '.js') {
node.source.value = path.relative(dir, cand);
return;
}
// look for package.json in same folder, OR add a .js ext
let def;
try {
const raw = fs.readFileSync(path.join(cand, 'package.json'), 'utf8');
def = JSON.parse(raw);
} catch (e) {
node.source.value = path.relative(dir, cand) + `.js`;
return; // best chance is just to append .js
}
const f = def['module'] || def['jsnext:main'] || def['main'] || 'index.js';
node.source.value = path.relative(dir, path.join(cand, f));
};
return {
visitor: {
ImportDeclaration: handler,
ExportNamedDeclaration: handler,
ExportAllDeclaration: handler,
},
};
};

@ -1,77 +0,0 @@
/**
* Copyright 2020 Google LLC
*
* 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 babel = require('@babel/core');
const path = require('path');
const t = babel.types;
/**
* @param {function(string, string): (string|undefined)}
* @return {!Object} Babel plugin
*/
module.exports = function buildTemplateTagReplacer(mapper) {
// nb. not an arrow function, so we get the context this
const handler = function(nodePath) {
const filename = this.file.opts.filename;
const dirname = path.dirname(filename);
const {node, parent} = nodePath;
const {tag, quasi} = node;
const qnode = quasi.quasis[0];
if (quasi.quasis.length !== 1 || qnode.type !== 'TemplateElement') {
return; // not sure what to do here
}
const key = qnode.value.raw;
const raw = mapper(tag.name, key, dirname);
if (raw === undefined) {
return;
} else if (raw instanceof Buffer) {
// fine
} else if (typeof raw !== 'string') {
throw new TypeError(`handler returned non-string for tag '${tag.name}': ${typeof raw}`);
}
const update = raw.toString(); // catches Buffer
// see if we're the direct child of a literal, e.g. ${_msg`foo`}
const index = (parent.expressions || []).indexOf(node);
if (parent.type !== 'TemplateLiteral' || index === -1) {
// ... we're not, just insert the string whereever
nodePath.replaceWith(t.stringLiteral(update));
return;
}
// merge the prev/next quasis with the updated value
const qnew = parent.quasis.slice();
const qprev = qnew.slice(index, index + 2);
qnew.splice(index, 2, t.templateElement({
raw: qprev[0].value.raw + update + qprev[1].value.raw,
cooked: qprev[0].value.cooked + update + qprev[1].value.cooked,
}));
// remove the expression
const enew = parent.expressions.slice();
enew.splice(index, 1);
// replace the whole parent templateLiteral
const replacement = t.templateLiteral(qnew, enew);
nodePath.parentPath.replaceWith(replacement);
};
return {
visitor: {TaggedTemplateExpression: handler},
};
};

@ -1,85 +0,0 @@
/**
* Copyright 2020 Google LLC
*
* 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 dom = require('./dom.js');
const {minify} = require('html-minifier');
const terser = require('terser');
/**
* @param {string} filename
* @param {{
* compile: boolean,
* messages: function(string): string,
* body: (!Object<string, string>|undefined),
* }}
*/
module.exports = async (filename, options) => {
const document = await dom.read(filename);
// apply data-key attributes to body
if (options.body) {
Object.keys(options.body).forEach((key) => {
const value = options.body[key];
if (value != null || value !== false) {
document.body.setAttribute(`data-${key}`, value === true ? '' : value);
}
});
}
// replace all [msgid] strings
const msgs = Array.from(document.querySelectorAll('[msgid]'));
msgs.forEach((node) => {
const string = options.messages(node.getAttribute('msgid'));
if (node.localName === 'meta') {
node.setAttribute('content', string);
} else if (node.closest('head') && node.localName !== 'title') {
throw new Error(`unhandled <head> node: ${node.localName}`);
} else {
node.innerHTML = string;
}
node.removeAttribute('msgid');
});
// return early if not compiling
if (!options.compile) {
return dom.serialize(document);
}
const out = dom.serialize(document);
const mo = {
collapseBooleanAttributes: true,
collapseWhitespace: true,
includeAutoGeneratedTags: false,
keepClosingSlash: true,
minifyCSS: true,
minifyJS: (code) => {
// nb. html-minifier does NOT see scripts of `type="module"`, which is fine for now as they
// should be compiled away only in production anyway.
const result = terser.minify(code);
if (result.error) {
throw new Error(`terser error: ${result.error}`);
}
return result.code;
},
removeRedundantAttributes: true,
sortAttributes: true,
sortClassName: true,
};
return minify(out, mo);
};

@ -1,132 +0,0 @@
/**
* Copyright 2020 Google LLC
*
* 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 fs = require('fs');
const path = require('path');
const sass = require('sass');
const url = require('url');
const compressed = true;
// Define helpers that call out to native functions. Native functions can't read the current scope,
// so these exist to pass the current value of $__filename.
const fixedPreamble = `// fixed preamble for Santa SASS compilation, should never be seen
$__filename: "";
@function _rel($arg) {
@return -native-rel($__filename, $arg);
}
`;
/**
* Explicitly sync function to compile SASS to CSS for Santa Tracker.
*
* This adds support for relative URL helpers in a reasonably gross way.
*
* @param {string} filename to compile
* @param {?string=} scope URL scope to position files absolutely under
* @param {string=} root to position assets relative to, null for file
*/
module.exports = (filename, scope=null, root='.') => {
const dirname = path.dirname(filename);
if (scope === null) {
// This is the 'include CSS via <link>' case, where assets are relative to the loaded URL.
root = path.dirname(path.resolve(filename));
}
const functions = {
'-native-rel($filename, $target)': (filenameArg, targetArg) => {
const dirname = path.dirname(filenameArg.getValue());
const found = path.join(dirname, targetArg.getValue());
const rel = path.relative(root, found);
const u = scope ? url.resolve(scope, rel) : rel;
return new sass.types.String(`url(${encodeURI(u)})`);
},
};
const options = {
data: `${fixedPreamble}@import "${encodeURI(filename)}"`,
sourceMap: true,
sourceMapContents: false,
sourceMapEmbed: false,
omitSourceMapUrl: true,
// sourceMapRoot: '.',
outFile: filename, // just used for relative paths
functions,
};
if (compressed) {
options.outputStyle = 'compressed';
}
let result;
const sourceMapContents = {};
const originalReadFileSync = fs.readFileSync;
try {
// yes -- really. Apologies to those reading this in future.
// This magic ensures that $__filename is set to the currently executing file during its run.
// We save the real source contents to insert into the sourceMap later.
fs.readFileSync = (p, o) => {
if (o !== 'utf8') {
throw new Error('expected dart-sass to read with options=utf8');
}
if (!path.isAbsolute(p)) {
throw new Error(`expected dart-sass to read absolute URL: ${p}`)
}
const fileContents = originalReadFileSync(p, o);
sourceMapContents[p] = fileContents;
// This hack works because source maps are only per-line, and the prefix here doesn't effect
// a browser's ability to map back to the original source.
return `$__held_filename:$__filename;$__filename:"${p}";${fileContents}
$__filename:$__held_filename;`; // must be on newline to prevent comments leaking
};
result = sass.renderSync(options);
} finally {
fs.readFileSync = originalReadFileSync;
}
const map = JSON.parse(result.map.toString());
map.sourcesContent = [];
map.sources = map.sources.map((source) => {
// SASS sometimes returns us absolute files that probably start with file://.
if (source.startsWith('file://')) {
source = source.substr(7);
if (!path.isAbsolute(source)) {
throw new Error(`had unexpected relative URL from sass: ${source}`);
}
}
// If this isn't absolute then it's actually relative to the original filename, not to the
// current working directory (which is what path.resolve would use).
if (!path.isAbsolute(source)) {
source = path.join(dirname, source);
}
map.sourcesContent.push(sourceMapContents[source] || null);
return path.relative(dirname, source);
});
return {
code: result.css.toString(),
map,
};
};

@ -1,178 +0,0 @@
/**
* Copyright 2020 Google LLC
*
* 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 closureCompiler = require('google-closure-compiler');
const closureCompilerUtils = require('google-closure-compiler/lib/utils.js');
const fs = require('fs').promises;
const tmp = require('tmp');
const EXTERNS = [
'build/transpile/magic-externs.js',
'node_modules/google-closure-compiler/contrib/externs/maps/google_maps_api_v3_exp.js',
'node_modules/google-closure-compiler/contrib/externs/jquery-3.3.js',
];
const CLOSURE_DISABLE_WARNINGS = [
// Causes complaints about misordered goog.require().
'underscore',
// Lots of library code generates unused vars.
'unusedLocalVariables',
// This includes checks for: missing semicolons, missing ? or ! on object types, etc.
// This would be a lot of work to resolve.
'lintChecks',
// These have to do with goog.require()/goog.provide() and how we "leak" some objects (such as
// Constants, LevelUp, etc). These could be fixed up.
'missingSourcesWarnings',
'extraRequire',
'missingProvide',
'strictMissingRequire',
'missingRequire',
];
const syntheticSourceRe = /\[synthetic:(.*?)\]/;
/**
* Process a raw source map, including adding source file contents. This is a tiny performance hit
* and creates a HUGE source map, but it should only be served in dev.
*
* @param {!Buffer} buf raw sourceMap to process
* @param {string} root path to apply to sourceMap
* @return {!Object} updated sourceMap containing all source file contents
*/
async function processSourceMap(buf, root='../../') {
const o = JSON.parse(buf.toString());
o.sourceRoot = root;
o.sourcesContent = [];
for (let i = 0; i < o.sources.length; ++i) {
const source = o.sources[i];
// This is an ES6 synthetic source file for transpilation. Ignore it, but it still has to be
// returned so the source map isn't confused.
const m = syntheticSourceRe.exec(source);
if (m) {
o.sources[i] = '';
o.sourcesContent.push('');
continue;
}
const buf = await fs.readFile(source, 'utf8');
o.sourcesContent.push(buf);
}
return o;
}
/**
* @param {!closureCompiler.compiler} compiler
* @return {!Promise<string>}
*/
function invokeCompiler(compiler) {
return new Promise((resolve) => {
const compilerCallback = (status, stdout, stderr) => {
resolve({status, code: stdout, log: stderr});
};
compiler.run(compilerCallback);
});
}
/**
* @param {string} sceneName
* @param {boolean=} compile
* @return {{code: string, map: !Object}}
*/
module.exports = async function compile(sceneName, compile=true) {
const compilerSrc = [
'build/transpile/export.js',
'static/scenes/_shared/js',
`static/scenes/${sceneName}/js`,
'!**_test.js',
];
// Scenes that require the Closure Library need to be compiled (and will fail if compile is
// false). For others, we can provide a quick fake base.js that includes basic polyfills for
// goog.require()/goog.provide() and friends.
if (!compile) {
compilerSrc.unshift('build/transpile/base.js');
}
// This function works by compiling scenes with Closure and then re-exporting them as ES modules.
// We expect `app.Game` to be provided (see build/transpile.export.js), and place it on the var
// `_globalExport` (see below).
// Additionally, import `_msg` and `_static` from our magic script. This lets scenes interact
// with their environment (although Rollup will complain if they're not used).
const outputWrapper =
'import {_msg, _static} from \'../../src/magic.js\';' +
'var _globalExport;(function(){%output%}).call(self);export default _globalExport;';
// Create a temporary place to store the source map. Closure can only write this to a real file.
const sourceMapTemp = tmp.fileSync();
const compilerFlags = {
js: compilerSrc,
externs: EXTERNS,
create_source_map: sourceMapTemp.name,
assume_function_wrapper: true,
dependency_mode: 'STRICT', // ignore all but exported via _globalExportEntry, below
entry_point: '_globalExportEntry',
compilation_level: compile ? 'SIMPLE_OPTIMIZATIONS' : 'WHITESPACE_ONLY',
warning_level: 'VERBOSE',
language_in: 'ECMASCRIPT_NEXT',
language_out: 'ECMASCRIPT_2019',
process_closure_primitives: true,
jscomp_off: CLOSURE_DISABLE_WARNINGS,
output_wrapper: outputWrapper,
rewrite_polyfills: false,
inject_libraries: true, // injects $jscomp when using the Closure Library, harmless otherwise
use_types_for_optimization: true,
};
try {
const compiler = new closureCompiler.compiler(compilerFlags);
// Use any native image available, as this can be up to 10x (!) speed improvement on Java.
const nativeImage = closureCompilerUtils.getNativeImagePath();
if (nativeImage) {
console.warn(`Compiling Closure scene ${sceneName} with native image...`);
compiler.JAR_PATH = undefined;
compiler.javaPath = nativeImage;
} else {
console.warn(`Compiling Closure scene ${sceneName} with Java (unsupported platform=${process.platform})...`);
}
const {status, code, log} = await invokeCompiler(compiler);
if (log.length) {
console.warn(`# ${sceneName}\n${log}`);
}
if (status) {
throw new Error(`failed to compile ${sceneName}: ${code}`);
}
const map = await processSourceMap(await fs.readFile(sourceMapTemp.name));
// nb. used so that listening callers can watch the whole dir for changes.
map.sources.push(`static/scenes/${sceneName}/js`, `static/scenes/_shared/js`);
map.sourcesContent.push(null, null);
return {code, map};
} finally {
sourceMapTemp.removeCallback();
}
};

@ -1,79 +0,0 @@
/**
* Copyright 2020 Google LLC
*
* 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 fsp = require('./fsp.js');
const parse5 = require('parse5');
const parser = new (require('jsdom/lib/jsdom/living')).DOMParser();
/**
* Minimal adapter for JSDom.
*/
const treeAdapter = {
getFirstChild: (node) => node.childNodes[0],
getChildNodes: (node) => node.childNodes,
getParentNode: (node) => node.parentNode,
getAttrList: (node) => node.attributes,
getTagName: (node) => node.tagName.toLowerCase(),
getNamespaceURI: (node) => node.namespaceURI || 'http://www.w3.org/1999/xhtml',
getTemplateContent: (node) => node.content,
getTextNodeContent: (node) => node.nodeValue,
getCommentNodeContent: (node) => node.nodeValue,
getDocumentTypeNodeName: (node) => node.name,
getDocumentTypeNodePublicId: (node) => doctypeNode.publicId || null,
getDocumentTypeNodeSystemId: (node) => doctypeNode.systemId || null,
isTextNode: (node) => node.nodeName === '#text',
isCommentNode: (node) => node.nodeName === '#comment',
isDocumentTypeNode: (node) => node.nodeType === 10,
isElementNode: (node) => Boolean(node.tagName),
};
/**
* Parse the input into a JSDom document.
*
* @param {string|!Buffer} src
* @return {!Document}
*/
function parse(src) {
return parser.parseFromString(src.toString(), 'text/html');
}
module.exports = {
parse,
/**
* Parse the file into a JSDom document.
*
* @param {string} filename
* @return {!Promise<!Document>}
*/
async read(filename) {
const raw = await fsp.readFile(filename, 'utf8');
return parse(raw);
},
/**
* Seralize the JSDom document or node.
*
* @param {!Node|!Document} node
*/
serialize(node) {
if ('innerHTML' in node) {
return node.innerHTML;
}
return parse5.serialize(node, {treeAdapter});
},
};

@ -1,65 +0,0 @@
/**
* Copyright 2020 Google LLC
*
* 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 fsRaw = require('fs');
const fs = fsRaw.promises;
const rimraf = require('rimraf');
module.exports = Object.assign({}, fs, {
/**
* @param {string} f to load stats for
* @return {Promise<fsRaw.Stats|null>}
*/
async statOrNull(f) {
return fs.stat(f).catch((err) => null);
},
/**
* @param {string} f to check
* @return {Promise<boolean>} does this file exits?
*/
async exists(f) {
return this.statOrNull(f).then((out) => out !== null);
},
/**
* @param {string} dir to create
* @return {Promise<void>}
*/
mkdirp(dir) {
return fs.mkdir(dir, {recursive: true});
},
/**
* @param {string} f to unlink
* @return {Promise<void>}
*/
unlinkAll(f) {
if (fs.rm) {
return fs.rm(f, { recursive: true, force: true });
}
// We keep rimraf around for Node < 14.14, as `fs.rm` was only added then.
// Don't use util.promisify as we pass options to disable glob.
return new Promise((resolve, reject) => {
rimraf(f, {glob: false}, (err) => {
err ? reject(err) : resolve();
});
});
},
});

@ -1,48 +0,0 @@
/**
* Copyright 2021 Google LLC
*
* 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 git = require('git-last-commit');
const { default: fetch } = require('node-fetch');
/**
* @returns {Promise<string>}
*/
const getDeployedVersion = async () => {
const text = await fetch('https://santa-staging.appspot.com/hash')
.then((res) => (res.ok ? res.text() : ''));
// This looks like "git_hash:build_version", e.g., "ab123141f1e:v20211123...", and we only want
// the git hash on the left.
return text.split(':')[0];
};
/**
* @return {Promise<string>}
*/
const getCurrentVersion = () => {
return new Promise((resolve) => {
git.getLastCommit((err, commit) => {
if (err) {
console.warn(`Could not retrieve current git revision`, err)
}
resolve(commit && commit.hash || '');
});
});
};
module.exports = {
getDeployedVersion,
getCurrentVersion,
};

@ -1,51 +0,0 @@
/**
* Copyright 2020 Google LLC
*
* 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 glob = require('glob');
/**
* Performs a synchronous glob over all requests, supporting Closure's negation syntax. e.g.:
* 'foo*', '!foo-bar' => returns all `foo*` but not `foo-bar`
*
* If a non-magic glob (i.e., no * or glob charaters) doesn't match a file, then this method
* throws Error.
*
* @param {...string} req
* @return {!Array<string>}
*/
module.exports = (...req) => {
const out = new Set();
const options = {mark: true};
for (let cand of req) {
const negate = cand[0] === '!';
if (negate) {
cand = cand.substr(1);
}
const result = glob.sync(cand, options);
if (!result.length && !glob.hasMagic(cand)) {
throw new Error(`couldn't match file: ${cand}`);
}
for (const each of result) {
negate ? out.delete(each) : out.add(each);
}
}
// filter out directories
return [...out].filter((cand) => !cand.endsWith('/'));
};

@ -1,75 +0,0 @@
/**
* Copyright 2020 Google LLC
*
* 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 os = require('os');
const cpuCount = os.cpus().length;
class WorkGroup {
constructor(tasks) {
tasks = ~~tasks;
if (tasks <= 0) {
throw new TypeError('must have at least one task');
}
this._tasks = tasks;
this._used = 0;
this._releasePromise = null;
this._resolveRelease = () => {};
}
async work(fn) {
await this._take();
try {
return await fn();
} finally {
this._done();
}
}
async _take() {
for (;;) {
if (this._used < this._tasks) {
++this._used;
if (this._used === this._tasks) {
this._releasePromise = new Promise((resolve) => {
this._resolveRelease = resolve;
});
}
return true;
}
await this._releasePromise;
}
}
_done() {
if (this._used === 0) {
throw new TypeError(`returned too many tasks`);
}
const shouldResolve = (this._used === this._tasks);
--this._used;
shouldResolve && this._resolveRelease();
}
}
module.exports = (tasks = cpuCount*0.75) => {
const w = new WorkGroup(tasks)
return (fn) => w.work(fn);
};

@ -1,94 +0,0 @@
/**
* Copyright 2020 Google LLC
*
* 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 fs = require('fs');
const path = require('path');
const Entities = require('html-entities').AllHtmlEntities;
const entities = new Entities();
const emptyFunc = () => {};
const fallback = require('../en_src_messages.json');
/**
* @param {string} lang
* @param {function(string): ?string} callback
* @return {function(?string): string}
*/
function lookup(lang, callback=emptyFunc) {
const data = require(`../_messages/${lang}.json`);
// Support e.g. "fr" for "fr-CA", or "es" for "es-419".
let similarLangData = null;
const similarLang = lang.split('-')[0];
if (similarLang !== lang) {
try {
similarLangData = require(`../_messages/${similarLang}.json`)
} catch (e) {
similarLangData = null;
}
}
return (msgid) => {
if (msgid === null) {
return lang;
}
let o = data[msgid];
if (!o) {
const out = callback(msgid);
if (typeof out === 'string') {
return out;
} else if (out !== undefined) {
return '?';
}
o = (similarLangData ? similarLangData[msgid] : null) || fallback[msgid] || null;
}
if (o && o['raw']) {
// This is a fallback message, so tease out the actual string. Each <ph...> contains real
// text and an optional <ex></ex>.
const r = o['raw'];
return r.replace(/<ph.*?>(.*?)<\/ph>/g, (match, part) => {
// remove <ex></ex> if we find it
part = part.replace(/<ex>.*?<\/ex>/g, '');
if (!part) {
throw new Error(`got invalid part for raw string: ${r}`);
}
return entities.decode(part);
});
}
return o && (o['message'] || o['raw']) || '?';
};
}
let langCache;
lookup.all = function(callback=emptyFunc) {
if (langCache === undefined) {
const cands = fs.readdirSync(path.join(__dirname, '..', '_messages'));
langCache = cands.map((file) => file.split('.')[0]);
}
const out = {};
langCache.forEach((lang) => {
out[lang] = lookup(lang, (msgid) => callback(lang, msgid));
});
return out;
};
module.exports = lookup;

@ -1,138 +0,0 @@
/**
* Copyright 2020 Google LLC
*
* 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 path = require('path');
const alreadyResolvedMatch = /^(\.{0,2}\/|[a-z]\w*\:)/; // matches start of './' or 'https:' etc
if (path.sep !== '/') {
throw new Error(`importUtils is unsupported on Windows (path.sep=${path.sep})`);
}
module.exports = {
/**
* Returns whether the given string is a URL or URL-like.
*
* @param {string|?URL} cand
* @return {boolean}
*/
isUrl(cand) {
if (cand instanceof URL || cand.startsWith('//')) {
return true;
} else if (!cand) {
return false;
}
try {
new URL(cand); // doesn't allow "//-prefix"
return true;
} catch (e) {
// ignore
}
return false;
},
/**
* Returns the pathname of this URL, or the string itself (if not a URL).
*
* @param {string|!URL} cand to check
* @param {string} root optional root if not a URL
* @return {string}
*/
pathname(cand, root='/') {
if (this.isUrl(cand)) {
return new URL(cand).pathname;
} else if (path.isAbsolute(cand)) {
return cand; // looks like "/foo"
} else {
return path.join(root, cand);
}
},
/**
* Join a URL path or other components.
*
* @param {string|!URL} cand
* @param {string} rest
* @return {string}
*/
join(cand, rest) {
if (this.isUrl(cand)) {
const u = new URL(rest, cand); // order is addition, then base
return u.toString();
}
return path.join(cand, rest);
},
/**
* Joins a URL path or other components, but ensures a trailing slash.
*
* @param {string|!URL} cand
* @param {string} rest
* @return {string}
*/
joinDir(cand, rest) {
const out = rest ? this.join(cand, rest) : cand;
if (!out.endsWith('/')) {
return `${out}/`;
}
return out;
},
/**
* Is the passed candidate string already fully resolved?
*
* @param {string|?URL} cand
* @return {boolean}
*/
alreadyResolved(cand) {
if (cand instanceof URL) {
return true;
} else if (!cand) {
return false;
}
// TODO(samthor): can ES modules import "//domain.com/blah.js"?
return Boolean(alreadyResolvedMatch.exec(cand));
},
/**
* Ensure that the specified ID is suitable for import as an ES6 module path.
*
* @param {string|!URL} cand
* @return {string}
*/
relativize(cand) {
if (this.alreadyResolved(cand)) {
return cand.toString();
}
return `./${cand}`;
},
/**
* Builds an ES6 module which simply imports the given targets for their side-effects.
*
* @param {...string} resources
* @return {string}
*/
staticImport(...resources) {
return resources.map((resource) => {
// TODO(samthor): escape resource.
return `import '${this.relativize(resource)}';\n`;
}).join('');
},
};

141