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('');
|
||||
},
|
||||
|
||||
};
|