Compare commits

...
Author SHA1 Message Date
Quinlan Jung a4eb9ff37b functions start 2022-05-10 17:27:48 -07:00
Quinlan Jung ddfe541af2 put deps into eascli 2022-05-10 17:27:48 -07:00
Quinlan Jung cde1ab36d2 last log is not always done 2022-05-10 17:27:48 -07:00
Quinlan Jung 087074d831 extra debug 2022-05-10 17:27:48 -07:00
Quinlan Jung 365e2d7e07 tmp 2022-05-10 17:27:48 -07:00
Quinlan Jung 1691f3271f tmp 2022-05-10 17:27:47 -07:00
17 changed files with 1961 additions and 24 deletions

No files matched your search

@@ -0,0 +1,7 @@
{
"extends": "@tsconfig/node16/tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"strict": true
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
overwrite: true overwrite: true
schema: 'https://staging-api.expo.dev/graphql' schema: 'http://localhost:3000/graphql'
documents: documents:
- 'src/graphql/**/!(*.d).{ts,tsx}' - 'src/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}' - 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
+347
View File
@@ -2162,6 +2162,39 @@
"name": "AccountUsageMetrics", "name": "AccountUsageMetrics",
"description": null, "description": null,
"fields": [ "fields": [
{
"name": "byBillingPeriod",
"description": null,
"args": [
{
"name": "date",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "UsageMetricTotal",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "metricsForServiceMetric", "name": "metricsForServiceMetric",
"description": null, "description": null,
@@ -13755,6 +13788,33 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "DeployFunctionResult",
"description": null,
"fields": [
{
"name": "success",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Deployment", "name": "Deployment",
@@ -14564,6 +14624,210 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "FunctionMutation",
"description": null,
"fields": [
{
"name": "createFunction",
"description": null,
"args": [
{
"name": "appId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "bucketKey",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DeployFunctionResult",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "getFunctionStatus",
"description": null,
"args": [
{
"name": "appId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "FunctionStatusResult",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "getSignedFunctionUploadSpecification",
"description": null,
"args": [
{
"name": "appId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GetFunctionUploadUrlResult",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "FunctionStatusResult",
"description": null,
"fields": [
{
"name": "logUrls",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "status",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GetFunctionUploadUrlResult",
"description": null,
"fields": [
{
"name": "specification",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "GetSignedAssetUploadSpecificationsResult", "name": "GetSignedAssetUploadSpecificationsResult",
@@ -19657,6 +19921,22 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "function",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "FunctionMutation",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "googleServiceAccountKey", "name": "googleServiceAccountKey",
"description": "Mutations that modify a Google Service Account Key", "description": "Mutations that modify a Google Service Account Key",
@@ -22979,6 +23259,73 @@
], ],
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "UsageMetricTotal",
"description": null,
"fields": [
{
"name": "billingPeriod",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "BillingPeriod",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "metrics",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "AccountUsageMetric",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "total",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Float",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "ENUM", "kind": "ENUM",
"name": "UsageMetricType", "name": "UsageMetricType",
+11
View File
@@ -17,6 +17,7 @@
"@expo/eas-json": "0.52.0", "@expo/eas-json": "0.52.0",
"@expo/json-file": "8.2.34", "@expo/json-file": "8.2.34",
"@expo/multipart-body-parser": "1.1.0", "@expo/multipart-body-parser": "1.1.0",
"@expo/package-manager": "0.0.50",
"@expo/pkcs12": "0.0.7", "@expo/pkcs12": "0.0.7",
"@expo/plist": "0.0.17", "@expo/plist": "0.0.17",
"@expo/plugin-warn-if-update-available": "2.1.0", "@expo/plugin-warn-if-update-available": "2.1.0",
@@ -30,6 +31,7 @@
"@oclif/plugin-help": "5.1.12", "@oclif/plugin-help": "5.1.12",
"@urql/core": "2.4.3", "@urql/core": "2.4.3",
"@urql/exchange-retry": "0.3.2", "@urql/exchange-retry": "0.3.2",
"adm-zip": "0.5.9",
"chalk": "4.1.2", "chalk": "4.1.2",
"cli-progress": "3.10.0", "cli-progress": "3.10.0",
"cli-table3": "0.6.1", "cli-table3": "0.6.1",
@@ -42,6 +44,7 @@
"form-data": "4.0.0", "form-data": "4.0.0",
"fs-extra": "10.0.1", "fs-extra": "10.0.1",
"getenv": "1.0.0", "getenv": "1.0.0",
"got": "^11.1.4",
"gradle-to-js": "2.0.0", "gradle-to-js": "2.0.0",
"graphql": "16.3.0", "graphql": "16.3.0",
"graphql-tag": "2.12.6", "graphql-tag": "2.12.6",
@@ -49,6 +52,7 @@
"ignore": "5.2.0", "ignore": "5.2.0",
"indent-string": "4.0.0", "indent-string": "4.0.0",
"jks-js": "1.0.2", "jks-js": "1.0.2",
"js-yaml": "^3.13.1",
"joi": "17.6.0", "joi": "17.6.0",
"keychain": "1.3.0", "keychain": "1.3.0",
"log-symbols": "4.1.0", "log-symbols": "4.1.0",
@@ -62,16 +66,20 @@
"pkg-dir": "4.2.0", "pkg-dir": "4.2.0",
"prompts": "2.4.2", "prompts": "2.4.2",
"qrcode-terminal": "0.12.0", "qrcode-terminal": "0.12.0",
"pnp-webpack-plugin": "1.7.0",
"react-dev-utils": "12.0.0",
"resolve-from": "5.0.0", "resolve-from": "5.0.0",
"semver": "7.3.5", "semver": "7.3.5",
"slash": "3.0.0", "slash": "3.0.0",
"strip-ansi": "6.0.0", "strip-ansi": "6.0.0",
"tar": "6.1.11", "tar": "6.1.11",
"terminal-link": "2.1.1", "terminal-link": "2.1.1",
"ts-loader": "^9.2.8",
"tslib": "2.3.1", "tslib": "2.3.1",
"turndown": "7.1.1", "turndown": "7.1.1",
"untildify": "4.0.0", "untildify": "4.0.0",
"uuid": "8.3.2", "uuid": "8.3.2",
"webpack": "5.72.0",
"wrap-ansi": "7.0.0" "wrap-ansi": "7.0.0"
}, },
"devDependencies": { "devDependencies": {
@@ -80,10 +88,12 @@
"@graphql-codegen/typescript": "2.4.8", "@graphql-codegen/typescript": "2.4.8",
"@graphql-codegen/typescript-operations": "2.3.5", "@graphql-codegen/typescript-operations": "2.3.5",
"@types/cli-progress": "3.9.2", "@types/cli-progress": "3.9.2",
"@types/adm-zip": "0.4.34",
"@types/dateformat": "3.0.1", "@types/dateformat": "3.0.1",
"@types/envinfo": "7.8.1", "@types/envinfo": "7.8.1",
"@types/fs-extra": "9.0.13", "@types/fs-extra": "9.0.13",
"@types/getenv": "^1.0.0", "@types/getenv": "^1.0.0",
"@types/js-yaml": "^3.12.2",
"@types/mime": "2.0.3", "@types/mime": "2.0.3",
"@types/node-fetch": "^2.5.7", "@types/node-fetch": "^2.5.7",
"@types/node-forge": "1.0.1", "@types/node-forge": "1.0.1",
@@ -92,6 +102,7 @@
"@types/tar": "6.1.1", "@types/tar": "6.1.1",
"@types/tough-cookie": "4.0.1", "@types/tough-cookie": "4.0.1",
"@types/uuid": "8.3.4", "@types/uuid": "8.3.4",
"@types/webpack": "5.28.0",
"@types/wrap-ansi": "3.0.0", "@types/wrap-ansi": "3.0.0",
"axios": "0.26.1", "axios": "0.26.1",
"eslint-plugin-graphql": "4.0.0", "eslint-plugin-graphql": "4.0.0",
@@ -1,3 +1,4 @@
import admzip from 'adm-zip';
import chalk from 'chalk'; import chalk from 'chalk';
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
@@ -122,6 +123,63 @@ export async function makeProjectTarballAsync(): Promise<{ path: string; size: n
return { size, path: tarPath }; return { size, path: tarPath };
} }
export async function makeZipAsync(pathToZip: string): Promise<{ path: string; size: number }> {
const spinner = ora('Compressing project files');
await fs.mkdirp(getTmpDirectory());
const shallowClonePath = path.join(getTmpDirectory(), `${uuidv4()}-shallow-clone`);
try {
process.chdir(pathToZip);
await getVcsClient().makeShallowCopyAsync(shallowClonePath);
process.chdir('..');
const zipPath = path.join(getTmpDirectory(), `${uuidv4()}.zip`);
// If the compression takes longer then a second, show the spinner.
// This can happen when the user has a lot of resources or doesn't ignore their CocoaPods.
// A basic project on a Mac can compress in roughly ~40ms.
// A fairly complex project without CocoaPods ignored can take up to 30s.
const timer = setTimeout(
() => {
spinner.start();
},
Log.isDebug ? 1 : 1000
);
// TODO: Possibly warn after more time about unoptimized assets.
const compressTimerLabel = 'makeProjectZipAsync';
startTimer(compressTimerLabel);
const zip = new admzip();
zip.addLocalFolder(shallowClonePath);
await zipAsync(zip, zipPath);
clearTimeout(timer);
const { size } = await fs.stat(zipPath);
const duration = endTimer(compressTimerLabel);
if (spinner.isSpinning) {
const prettyTime = formatMilliseconds(duration);
spinner.succeed(
`Compressed project files ${chalk.dim(`${prettyTime} (${formatBytes(size)})`)}`
);
}
return { size, path: zipPath };
} catch (err) {
if (spinner.isSpinning) {
spinner.fail();
}
throw err;
} finally {
await fs.remove(shallowClonePath);
}
}
async function zipAsync(zip: admzip, targetFileName?: string): Promise<void> {
return new Promise((resolve, reject) => {
zip.writeZip(targetFileName, (error: Error | null) => (error ? reject(error) : resolve()));
});
}
enum ShouldCommitChanges { enum ShouldCommitChanges {
Yes, Yes,
ShowDiffFirst, ShowDiffFirst,
@@ -0,0 +1,151 @@
import { getConfig } from '@expo/config';
import spawnAsync, { SpawnOptions, SpawnResult } from '@expo/spawn-async';
import admzip from 'adm-zip';
import fs from 'fs';
import fsPromise from 'fs/promises';
import got from 'got';
import fetch from 'node-fetch';
import path from 'path';
import { pipeline } from 'stream/promises';
import { makeZipAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { bundleFunctionsAsync, prepareFunctionEntriesAsync } from '../../functions/webpack';
import { FunctionMutation } from '../../graphql/mutations/FunctionMutation';
import { PresignedPost } from '../../graphql/mutations/UploadSessionMutation';
import Log from '../../log';
import { ora } from '../../ora';
import {
findProjectRootAsync,
getProjectAccountName,
getProjectIdAsync,
} from '../../project/projectUtils';
import { uploadWithPresignedPostAsync } from '../../uploads';
import { ensureLoggedInAsync } from '../../user/actions';
import { expoCommandAsync } from '../../utils/expoCli';
import { sleepAsync } from '../../utils/promise';
export default class FunctionsCreate extends EasCommand {
static description = 'create a function';
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const downloadSpinner = ora('Downloading functions template...').start();
const functionsTemplateUrl =
'https://github.com/quinlanj/functions-entry-point/archive/refs/heads/main.zip';
const functionsBuild = 'functions-build';
const functionsBuildZip = `${functionsBuild}.zip`;
const originalUnzippedDirectory = 'functions-entry-point-main';
// ensure empty build directory
await fsPromise.rm(path.join(projectDir, functionsBuild), { force: true, recursive: true });
// download and unzip the functions template
const downloadStream = got.stream(functionsTemplateUrl);
await pipeline(downloadStream, fs.createWriteStream(path.join(projectDir, functionsBuildZip)));
const zip = new admzip(functionsBuildZip);
zip.extractAllTo(/*target path*/ path.join(projectDir), /*overwrite*/ true);
// rename to desired dir, remove zip
await Promise.all([
fsPromise.rename(
path.join(projectDir, originalUnzippedDirectory),
path.join(projectDir, functionsBuild)
),
fsPromise.rm(path.join(projectDir, functionsBuildZip)),
]);
downloadSpinner.succeed('Downloaded functions template');
const permissionSpinner = ora('Modifying permissions...').start();
const promises = [];
for (const file of await fsPromise.readdir(path.join(projectDir, functionsBuild, 'scripts'))) {
promises.push(fsPromise.chmod(path.join(projectDir, functionsBuild, 'scripts', file), 0o777));
}
await Promise.all(promises);
permissionSpinner.succeed('Modified permissions');
const gitSpinner = ora('Initializing inner git...').start();
process.chdir(path.join(projectDir, functionsBuild));
const { status, stderr } = await spawnAsync('git', ['init']);
if (status !== 0) {
throw new Error('Failed to initialize git: ' + stderr);
}
process.chdir('..');
gitSpinner.succeed('Initialized inner git');
Log.log('Bundling functions...');
const functionDirectory = path.join(projectDir, 'functions');
const functionEntries = await prepareFunctionEntriesAsync(functionDirectory);
const outputDirectory = path.join(projectDir, functionsBuild, 'functions');
await bundleFunctionsAsync(projectDir, functionEntries, outputDirectory);
Log.log('Successfully bundled functions...');
Log.log('Doing web build...');
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const functionName = `${accountName}-${exp.slug}`; // TODO: set up a Cloudflare enterprise domain so we don't have to do this
const region = 'us-east1';
const cloudFunctionProject = 'eas-functions';
const publicUrl = `https://${region}-${cloudFunctionProject}.cloudfunctions.net/${functionName}`;
const webOutputDirectory = path.join(projectDir, functionsBuild, 'web-build');
await expoCommandAsync(projectDir, ['build:web'], {
env: { WEBPACK_BUILD_OUTPUT_PATH: webOutputDirectory, WEB_PUBLIC_URL: publicUrl },
});
Log.log('Finished web build...');
const { path: compressedProjectPath } = await makeZipAsync(
path.join(projectDir, functionsBuild)
);
const uploadSpinner = ora('Uploading project...').start();
let bucketKey;
try {
const specification = await FunctionMutation.uploadAsync(projectId);
const presignedPost: PresignedPost = JSON.parse(specification);
await uploadWithPresignedPostAsync(compressedProjectPath, presignedPost);
bucketKey = presignedPost.fields.key;
uploadSpinner.succeed(`Successfully uploaded project to bucket: ${bucketKey}`);
} catch (err) {
uploadSpinner.fail('Failed to upload project');
throw err;
}
const spinner = ora('Creating a function...').start();
try {
const success = await FunctionMutation.createAsync(bucketKey, projectId);
spinner.succeed(`Successfully created function: ${success}`);
} catch (err) {
spinner.fail('Failed to create a function');
throw err;
}
Log.log('Streaming build logs....');
const logsPrinted = new Set<string>();
let sawLastLog = false;
while (true) {
const { status, logUrls } = await FunctionMutation.getStatusAsync(projectId);
Log.log(`Function status: ${status}`);
const logsUrlsToPrint = logUrls.filter(url => !logsPrinted.has(url)).sort();
const logResponses = await Promise.all(logsUrlsToPrint.map(url => fetch(url)));
const logBodies = await Promise.all(logResponses.map(body => body.text()));
const logPayloads = logBodies.map(body => JSON.parse(body)).map(body => body.textPayload);
logPayloads.forEach(response => Log.log(response));
logsUrlsToPrint.forEach(logUrl => logsPrinted.add(logUrl));
if (!sawLastLog) {
sawLastLog = logPayloads.some(log => log === 'DONE');
}
if (sawLastLog && status === 'ACTIVE') {
Log.log(`🎉🎉🎉 Hooray! Your function is complete!`);
Log.log(`🎉🎉🎉 Your web-app is available at: ${publicUrl}`);
for (const submodule of Object.keys(functionEntries)) {
Log.log(`🎉🎉🎉 ${submodule} is available at: ${publicUrl}/functions/${submodule}`);
}
break;
} else if (status !== 'ACTIVE' && status !== 'DEPLOY_IN_PROGRESS') {
Log.log(`🤔 Uh oh...your function is ${status}`);
break;
}
await sleepAsync(6000);
}
}
}
@@ -0,0 +1,25 @@
import path from 'path';
import EasCommand from '../../commandUtils/EasCommand';
import { installNodeDependenciesAsync } from '../../functions/CreateApp';
import { YarnPackageManager } from '../../functions/PackageManager';
import Log from '../../log';
import { ora } from '../../ora';
import { findProjectRootAsync } from '../../project/projectUtils';
export default class FunctionsCreate extends EasCommand {
static description = 'start a function';
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const functionsBuild = 'functions-build';
const installSpinner = ora('Installing dependencies...').start();
await installNodeDependenciesAsync(path.join(projectDir, functionsBuild), 'yarn');
installSpinner.succeed('Modified permissions');
Log.log('serving up the function...');
const yarn = new YarnPackageManager({ cwd: path.join(projectDir, functionsBuild) });
await yarn.runAsync('dev');
}
}
@@ -0,0 +1,41 @@
import * as PackageManager from '@expo/package-manager';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import * as path from 'path';
import semver from 'semver';
import Log from '../log';
export async function installNodeDependenciesAsync(
projectRoot: string,
packageManager: 'yarn' | 'npm'
): Promise<void> {
const options = { cwd: projectRoot };
if (packageManager === 'yarn') {
const yarn = new PackageManager.YarnPackageManager(options);
const version = await yarn.versionAsync();
const nodeLinker = await yarn.getConfigAsync('nodeLinker');
if (semver.satisfies(version, '>=2.0.0-rc.24') && nodeLinker !== 'node-modules') {
const yarnRc = path.join(projectRoot, '.yarnrc.yml');
let yamlString = '';
try {
yamlString = await fs.readFile(yarnRc, 'utf8');
} catch (error: any) {
if (error.code !== 'ENOENT') {
throw error;
}
}
const config: any = yamlString ? yaml.safeLoad(yamlString) : {};
config.nodeLinker = 'node-modules';
Log.warn(
`Yarn v${version} detected, enabling experimental Yarn v2 support using the node-modules plugin.`
);
Log.log(`Writing ${yarnRc}...`);
await fs.writeFile(yarnRc, yaml.safeDump(config));
}
await yarn.installAsync();
} else {
await new PackageManager.NpmPackageManager(options).installAsync();
}
}
@@ -0,0 +1,66 @@
import spawnAsync, { SpawnOptions, SpawnResult } from '@expo/spawn-async';
import { Transform } from 'stream';
import Log from '../log';
function ansiRegex({ onlyFirst = false } = {}): RegExp {
const pattern = [
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))',
].join('|');
return new RegExp(pattern, onlyFirst ? undefined : 'g');
}
const ansi = `(?:${ansiRegex().source})*`;
const yarnPeerDependencyWarningPattern = new RegExp(
`${ansi}warning${ansi} "[^"]+" has (?:unmet|incorrect) peer dependency "[^"]+"\\.\n`,
'g'
);
class YarnStderrTransform extends Transform {
_transform(
chunk: Buffer,
_encoding: string,
callback: (error?: Error | null, data?: any) => void
): void {
this.push(chunk.toString().replace(yarnPeerDependencyWarningPattern, ''));
callback();
}
}
export class YarnPackageManager {
options: SpawnOptions;
constructor({ cwd, silent }: { cwd: string; silent?: boolean }) {
this.options = {
env: {
...process.env,
},
cwd,
...(silent
? { ignoreStdio: true }
: {
stdio: ['inherit', 'inherit', 'pipe'],
}),
};
}
async runAsync(command: string): Promise<void> {
await this.runPrivateAsync([command]);
}
// Private
private async runPrivateAsync(args: string[]): Promise<SpawnResult> {
if (!this.options.ignoreStdio) {
Log.log(`> yarn ${args.join(' ')}`);
}
// Have spawnAsync consume stdio but we don't actually do anything with it if it's ignored
const promise = spawnAsync('yarnpkg', args, { ...this.options, ignoreStdio: false });
if (promise.child.stderr && !this.options.ignoreStdio) {
promise.child.stderr.pipe(new YarnStderrTransform()).pipe(process.stderr);
}
return promise;
}
}
@@ -0,0 +1,123 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const friendlySyntaxErrorLabel = 'Syntax error:';
function isLikelyASyntaxError(message: string | string[]): boolean {
return message.indexOf(friendlySyntaxErrorLabel) !== -1;
}
// Cleans up webpack error messages.
function formatMessage(message: any): any {
let lines = [];
if (typeof message === 'string') {
lines = message.split('\n');
} else if ('message' in message) {
lines = message['message'].split('\n');
} else if (Array.isArray(message)) {
message.forEach(message => {
if ('message' in message) {
lines = message['message'].split('\n');
}
});
}
// Strip webpack-added headers off errors/warnings
// https://github.com/webpack/webpack/blob/master/lib/ModuleError.js
lines = lines.filter((line: string) => !/Module [A-z ]+\(from/.test(line));
// Transform parsing error into syntax error
// TODO: move this to our ESLint formatter?
lines = lines.map((line: string) => {
const parsingError = /Line (\d+):(?:(\d+):)?\s*Parsing error: (.+)$/.exec(line);
if (!parsingError) {
return line;
}
const [, errorLine, errorColumn, errorMessage] = parsingError;
return `${friendlySyntaxErrorLabel} ${errorMessage} (${errorLine}:${errorColumn})`;
});
message = lines.join('\n');
// Smoosh syntax errors (commonly found in CSS)
message = message.replace(
/SyntaxError\s+\((\d+):(\d+)\)\s*(.+?)\n/g,
`${friendlySyntaxErrorLabel} $3 ($1:$2)\n`
);
// Clean up export errors
message = message.replace(
/^.*export '(.+?)' was not found in '(.+?)'.*$/gm,
`Attempted import error: '$1' is not exported from '$2'.`
);
message = message.replace(
/^.*export 'default' \(imported as '(.+?)'\) was not found in '(.+?)'.*$/gm,
`Attempted import error: '$2' does not contain a default export (imported as '$1').`
);
message = message.replace(
/^.*export '(.+?)' \(imported as '(.+?)'\) was not found in '(.+?)'.*$/gm,
`Attempted import error: '$1' is not exported from '$3' (imported as '$2').`
);
lines = message.split('\n');
// Remove leading newline
if (lines.length > 2 && lines[1].trim() === '') {
lines.splice(1, 1);
}
// Clean up file name
lines[0] = lines[0].replace(/^(.*) \d+:\d+-\d+$/, '$1');
// Cleans up verbose "module not found" messages for files and packages.
if (lines[1] && lines[1].indexOf('Module not found: ') === 0) {
lines = [
lines[0],
lines[1]
.replace('Error: ', '')
.replace('Module not found: Cannot find file:', 'Cannot find file:'),
];
}
// Add helpful message for users trying to use Sass for the first time
if (lines[1] && lines[1].match(/Cannot find module.+sass/)) {
lines[1] = 'To import Sass files, you first need to install sass.\n';
lines[1] += 'Run `npm install sass` or `yarn add sass` inside your workspace.';
}
message = lines.join('\n');
// Internal stacks are generally useless so we strip them... with the
// exception of stacks containing `webpack:` because they're normally
// from user code generated by webpack. For more information see
// https://github.com/facebook/create-react-app/pull/1050
message = message.replace(/^\s*at\s((?!webpack:).)*:\d+:\d+[\s)]*(\n|$)/gm, ''); // at ... ...:x:y
message = message.replace(/^\s*at\s<anonymous>(\n|$)/gm, ''); // at <anonymous>
lines = message.split('\n');
// Remove duplicated newlines
lines = lines.filter(
(line: string, index: number, arr: string[]) =>
index === 0 || line.trim() !== '' || line.trim() !== arr[index - 1].trim()
);
// Reassemble the message
message = lines.join('\n');
return message.trim();
}
export function formatWebpackMessages(json: any): {
errors: any[];
warnings: any[];
} {
const formattedErrors = json.errors.map(formatMessage);
const formattedWarnings = json.warnings.map(formatMessage);
const result = { errors: formattedErrors, warnings: formattedWarnings };
if (result.errors.some(isLikelyASyntaxError)) {
// If there are any syntax errors, show just them.
result.errors = result.errors.filter(isLikelyASyntaxError);
}
return result;
}
+132
View File
@@ -0,0 +1,132 @@
import chalk from 'chalk';
import fs from 'fs/promises';
import path from 'path';
import PnpWebpackPlugin from 'pnp-webpack-plugin';
import { Compiler, Configuration, EntryObject, webpack } from 'webpack';
import Log from '../log';
import { formatWebpackMessages } from './formatWebpackMessages';
async function getWebpackFunctionConfigurationAsync(
projectDir: string,
entry: EntryObject,
outputPath: string
): Promise<Configuration> {
const webpackConfig = {
entry,
target: 'node',
mode: 'development' as 'development',
// Disable file info logs.
//stats: 'none',
// https://webpack.js.org/configuration/other-options/#bail
// Fail out on the first error instead of tolerating it.
bail: true,
module: {
rules: [
{
test: /\.tsx?$/,
use: {
loader: require.resolve('ts-loader'),
options: {
configFile: require.resolve('../../functions.tsconfig.webpack.json'),
context: projectDir,
},
},
exclude: /node_modules/,
},
],
},
resolve: {
plugins: [PnpWebpackPlugin],
extensions: ['.tsx', '.ts', '.js'],
},
resolveLoader: {
plugins: [PnpWebpackPlugin.moduleLoader(module)],
},
output: {
filename: '[name].js',
path: outputPath,
library: {
type: 'commonjs',
},
},
};
return webpackConfig;
}
async function compileWebAppAsync(compiler: Compiler): Promise<any> {
// We generate the stats.json file in the webpack-config
const { warnings } = await new Promise((resolve, reject) =>
compiler.run((error, stats) => {
let messages;
if (error) {
if (!error.message) {
return reject(error);
}
messages = formatWebpackMessages({
errors: [error.message],
warnings: [],
_showErrors: true,
_showWarnings: true,
});
} else {
messages = formatWebpackMessages(
stats?.toJson({ all: false, warnings: true, errors: true })
);
}
if (messages.errors.length) {
// Only keep the first error. Others are often indicative
// of the same problem, but confuse the reader with noise.
if (messages.errors.length > 1) {
messages.errors.length = 1;
}
return reject(messages.errors.join('\n\n'));
}
resolve({
warnings: messages.warnings,
});
})
);
return { warnings };
}
export async function bundleFunctionsAsync(
projectDir: string,
entry: EntryObject,
outputPath: string
): Promise<void> {
const config = await getWebpackFunctionConfigurationAsync(projectDir, entry, outputPath);
const compiler = webpack(config);
try {
const { warnings } = await compileWebAppAsync(compiler);
if (warnings.length) {
Log.warn(chalk.yellow('Compiled with warnings.\n'));
Log.warn(warnings.join('\n\n'));
} else {
Log.log(chalk.green('Compiled successfully.\n'));
}
} catch (error) {
Log.error(chalk.red('Failed to compile.\n'));
Log.error(error);
throw error;
}
}
export async function prepareFunctionEntriesAsync(functionsPath: string): Promise<EntryObject> {
const files = await fs.readdir(functionsPath);
if (files.length === 0) {
throw new Error(`No functions found in ${functionsPath}`);
}
const entries = {} as EntryObject;
for (const file of files) {
const filenameNoExtension = path.parse(file).name;
entries[filenameNoExtension] = path.join(functionsPath, file);
}
return entries;
}
+75
View File
@@ -401,10 +401,16 @@ export type AccountUsageMetric = {
export type AccountUsageMetrics = { export type AccountUsageMetrics = {
__typename?: 'AccountUsageMetrics'; __typename?: 'AccountUsageMetrics';
byBillingPeriod: UsageMetricTotal;
metricsForServiceMetric: Array<AccountUsageMetric>; metricsForServiceMetric: Array<AccountUsageMetric>;
}; };
export type AccountUsageMetricsByBillingPeriodArgs = {
date: Scalars['DateTime'];
};
export type AccountUsageMetricsMetricsForServiceMetricArgs = { export type AccountUsageMetricsMetricsForServiceMetricArgs = {
granularity: UsageMetricsGranularity; granularity: UsageMetricsGranularity;
metric: EasServiceMetric; metric: EasServiceMetric;
@@ -2019,6 +2025,11 @@ export type DeleteWebhookResult = {
id: Scalars['ID']; id: Scalars['ID'];
}; };
export type DeployFunctionResult = {
__typename?: 'DeployFunctionResult';
success: Scalars['Boolean'];
};
/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */ /** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */
export type Deployment = { export type Deployment = {
__typename?: 'Deployment'; __typename?: 'Deployment';
@@ -2149,6 +2160,40 @@ export enum Feature {
Teams = 'TEAMS' Teams = 'TEAMS'
} }
export type FunctionMutation = {
__typename?: 'FunctionMutation';
createFunction: DeployFunctionResult;
getFunctionStatus: FunctionStatusResult;
getSignedFunctionUploadSpecification: GetFunctionUploadUrlResult;
};
export type FunctionMutationCreateFunctionArgs = {
appId: Scalars['String'];
bucketKey: Scalars['String'];
};
export type FunctionMutationGetFunctionStatusArgs = {
appId: Scalars['String'];
};
export type FunctionMutationGetSignedFunctionUploadSpecificationArgs = {
appId: Scalars['String'];
};
export type FunctionStatusResult = {
__typename?: 'FunctionStatusResult';
logUrls: Array<Scalars['String']>;
status: Scalars['String'];
};
export type GetFunctionUploadUrlResult = {
__typename?: 'GetFunctionUploadUrlResult';
specification: Scalars['String'];
};
export type GetSignedAssetUploadSpecificationsResult = { export type GetSignedAssetUploadSpecificationsResult = {
__typename?: 'GetSignedAssetUploadSpecificationsResult'; __typename?: 'GetSignedAssetUploadSpecificationsResult';
specifications: Array<Scalars['String']>; specifications: Array<Scalars['String']>;
@@ -2866,6 +2911,7 @@ export type RootMutation = {
emailSubscription: EmailSubscriptionMutation; emailSubscription: EmailSubscriptionMutation;
/** Mutations that create and delete EnvironmentSecrets */ /** Mutations that create and delete EnvironmentSecrets */
environmentSecret: EnvironmentSecretMutation; environmentSecret: EnvironmentSecretMutation;
function: FunctionMutation;
/** Mutations that modify a Google Service Account Key */ /** Mutations that modify a Google Service Account Key */
googleServiceAccountKey: GoogleServiceAccountKeyMutation; googleServiceAccountKey: GoogleServiceAccountKeyMutation;
/** Mutations that modify the build credentials for an iOS app */ /** Mutations that modify the build credentials for an iOS app */
@@ -3395,6 +3441,13 @@ export enum UploadSessionType {
EasSubmitAppArchive = 'EAS_SUBMIT_APP_ARCHIVE' EasSubmitAppArchive = 'EAS_SUBMIT_APP_ARCHIVE'
} }
export type UsageMetricTotal = {
__typename?: 'UsageMetricTotal';
billingPeriod: BillingPeriod;
metrics: Array<AccountUsageMetric>;
total: Scalars['Float'];
};
export enum UsageMetricType { export enum UsageMetricType {
Request = 'REQUEST' Request = 'REQUEST'
} }
@@ -4291,6 +4344,28 @@ export type DeleteEnvironmentSecretMutationVariables = Exact<{
export type DeleteEnvironmentSecretMutation = { __typename?: 'RootMutation', environmentSecret: { __typename?: 'EnvironmentSecretMutation', deleteEnvironmentSecret: { __typename?: 'DeleteEnvironmentSecretResult', id: string } } }; export type DeleteEnvironmentSecretMutation = { __typename?: 'RootMutation', environmentSecret: { __typename?: 'EnvironmentSecretMutation', deleteEnvironmentSecret: { __typename?: 'DeleteEnvironmentSecretResult', id: string } } };
export type GetSignedUploadFunctionMutationVariables = Exact<{
appId: Scalars['String'];
}>;
export type GetSignedUploadFunctionMutation = { __typename?: 'RootMutation', function: { __typename?: 'FunctionMutation', getSignedFunctionUploadSpecification: { __typename?: 'GetFunctionUploadUrlResult', specification: string } } };
export type CreateFunctionMutationVariables = Exact<{
bucketKey: Scalars['String'];
appId: Scalars['String'];
}>;
export type CreateFunctionMutation = { __typename?: 'RootMutation', function: { __typename?: 'FunctionMutation', createFunction: { __typename?: 'DeployFunctionResult', success: boolean } } };
export type GetFunctionStatusMutationVariables = Exact<{
appId: Scalars['String'];
}>;
export type GetFunctionStatusMutation = { __typename?: 'RootMutation', function: { __typename?: 'FunctionMutation', getFunctionStatus: { __typename?: 'FunctionStatusResult', status: string, logUrls: Array<string> } } };
export type CreateKeystoreGenerationUrlMutationVariables = Exact<{ [key: string]: never; }>; export type CreateKeystoreGenerationUrlMutationVariables = Exact<{ [key: string]: never; }>;
@@ -0,0 +1,79 @@
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import {
CreateFunctionMutation,
CreateFunctionMutationVariables,
GetFunctionStatusMutation,
GetFunctionStatusMutationVariables,
GetSignedUploadFunctionMutation,
GetSignedUploadFunctionMutationVariables,
} from '../generated';
export const FunctionMutation = {
async uploadAsync(appId: string): Promise<string> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<GetSignedUploadFunctionMutation, GetSignedUploadFunctionMutationVariables>(
gql`
mutation GetSignedUploadFunctionMutation($appId: String!) {
function {
getSignedFunctionUploadSpecification(appId: $appId) {
specification
}
}
}
`,
{
appId,
}
)
.toPromise()
);
return data.function.getSignedFunctionUploadSpecification.specification;
},
async createAsync(bucketKey: string, appId: string): Promise<boolean> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateFunctionMutation, CreateFunctionMutationVariables>(
gql`
mutation CreateFunctionMutation($bucketKey: String!, $appId: String!) {
function {
createFunction(bucketKey: $bucketKey, appId: $appId) {
success
}
}
}
`,
{
bucketKey,
appId,
}
)
.toPromise()
);
return data.function.createFunction.success;
},
async getStatusAsync(appId: string): Promise<{ status: string; logUrls: string[] }> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<GetFunctionStatusMutation, GetFunctionStatusMutationVariables>(
gql`
mutation GetFunctionStatusMutation($appId: String!) {
function {
getFunctionStatus(appId: $appId) {
status
logUrls
}
}
}
`,
{
appId,
}
)
.toPromise()
);
return data.function.getFunctionStatus;
},
};
+5 -1
View File
@@ -7,11 +7,15 @@ import Log from '../log';
export async function expoCommandAsync( export async function expoCommandAsync(
projectDir: string, projectDir: string,
args: string[], args: string[],
{ silent = false }: { silent?: boolean } = {} { silent = false, env }: { silent?: boolean; env?: { [key: string]: string } } = {}
): Promise<void> { ): Promise<void> {
const expoCliPath = resolveFrom(projectDir, 'expo/bin/cli.js'); const expoCliPath = resolveFrom(projectDir, 'expo/bin/cli.js');
const spawnPromise = spawnAsync(expoCliPath, args, { const spawnPromise = spawnAsync(expoCliPath, args, {
stdio: ['inherit', 'pipe', 'pipe'], // inherit stdin so user can install a missing expo-cli from inside this command stdio: ['inherit', 'pipe', 'pipe'], // inherit stdin so user can install a missing expo-cli from inside this command
env: {
...process.env,
...env,
},
}); });
const { const {
child: { stdout, stderr }, child: { stdout, stderr },
+4 -3
View File
@@ -78,11 +78,12 @@ export async function makeShallowCopyAsync(src: string, dst: string): Promise<vo
const ignore = new Ignore(src); const ignore = new Ignore(src);
await ignore.initIgnoreAsync(); await ignore.initIgnoreAsync();
await fs.copy(src, dst, { await fs.copy(src, dst, {
filter: (srcFilePath: string) => { filter: (_srcFilePath: string) => {
if (srcFilePath === src) { return true; // TODO: handle ignored build folder
/* if (srcFilePath === src) {
return true; return true;
} }
return !ignore.ignores(path.relative(src, srcFilePath)); return !ignore.ignores(path.relative(src, srcFilePath)); */
}, },
}); });
} }
+5
View File
@@ -0,0 +1,5 @@
declare module 'pnp-webpack-plugin' {
function apply(resolver: any /* EnhancedResolve.Resolver */): void;
function moduleLoader(module: NodeModule): any;
function forkTsCheckerOptions(config: any): any;
}
Generated
+831 -19
View File
File diff suppressed because it is too large. Load diff