@foxtware/mineral 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.gcloudignore CHANGED
@@ -7,11 +7,8 @@
7
7
  # $ gcloud topic gcloudignore
8
8
  #
9
9
  .gcloudignore
10
- # If you would like to upload your .git directory, .gitignore file or files
11
- # from your .gitignore file, remove the corresponding line
12
- # below:
13
10
  .git
14
11
  .gitignore
15
12
 
16
13
  node_modules
17
- #!include:.gitignore
14
+ .creds.yml
package/README.md CHANGED
@@ -51,4 +51,4 @@ The [bedrock](https://github.com/GorgonFreeman/bedrock) middleware, refactored f
51
51
  - **Monorepo structure**
52
52
  Mineral gets pushed to from a larger repo that can also contain private functions. Mineral should be strictly useful stuff for the public, and can be used standalone, but needs to be instantiated for serving, setting stuff like which creds file to use. This allows it to be used as part of another repo in the same HTTP/curl way as by itself. Pass `--workspace` to locate `.creds.yml` and `--api_dirs` to serve additional function directories.
53
53
 
54
- For cloud deploy, workspaces use `.hosting.yml` and `npm run host` (same `--workspace` / `--api_dirs` flags as dev/serve). See `.hosting.yml.sample`.
54
+ For cloud deploy, workspaces use `hosting/.hosting.yml` and `npm run host` (same `--workspace` / `--api_dirs` flags as dev/serve). See `hosting/.hosting.yml.sample`.
@@ -3,31 +3,51 @@ const path = require('path');
3
3
  const { spawn } = require('child_process');
4
4
  const { askQuestion, capitaliseString } = require('../api/utils');
5
5
 
6
- const apiDirectory = path.join(__dirname, '../api');
7
- const rootExampleJsPath = path.join(apiDirectory, '_example.js');
6
+ const mineralRoot = path.join(__dirname, '..');
7
+ const mineralApiDirectory = path.join(mineralRoot, 'api');
8
+ const rootExampleJsPath = path.join(mineralApiDirectory, '_example.js');
8
9
 
9
10
  const excludedDirs = new Set([
10
11
  'node_modules',
11
12
  ]);
12
13
 
13
- const printHelp = () => {
14
+ const normalizePath = (dirPath) => dirPath.replace(/\/$/, '');
15
+
16
+ const isMineralCwd = () => normalizePath(process.cwd()) === normalizePath(mineralRoot);
17
+
18
+ const getContext = () => {
19
+ const inMineral = isMineralCwd();
20
+
21
+ return {
22
+ inMineral,
23
+ apiDirectory: inMineral ? mineralApiDirectory : path.join(process.cwd(), 'api'),
24
+ mineralApiDirectory,
25
+ };
26
+ };
27
+
28
+ const printHelp = (context) => {
29
+ const templateHelp = context.inMineral
30
+ ? `example template to copy (default: _example.js in dir, else api/_example.js)
31
+ examples: _example.js, getsingle, _example.getsingle.js`
32
+ : `mineral example template to copy (default: _example.js)
33
+ examples: _example.js, shopify/_example.js, shopify/getsingle`;
34
+
14
35
  console.log(`
15
36
  Usage:
16
37
  npm run new
17
38
  npm run new -- --dir <apiSubdir> --name <name> [--template <exampleTemplate>] [--commit]
18
39
 
19
40
  Options:
20
- --dir, -d api subdirectory (e.g. peoplevox, shopify)
41
+ --dir, -d api subdirectory (e.g. peoplevox, shopify). Omit for flat api/ dirs.
21
42
  --name, -n function name suffix (e.g. orderGet → peoplevoxOrderGet)
22
- --template, -t example template to copy (default: _example.js in dir, else api/_example.js)
23
- examples: _example.js, getsingle, _example.getsingle.js
43
+ --template, -t ${ templateHelp }
24
44
  --commit auto-commit the stub after creation
25
45
  --help, -h show this help
26
46
 
27
47
  Examples:
28
48
  npm run new -- --dir peoplevox --name orderGet
29
- npm run new -- --dir peoplevox --name orderGet --template _example.js
30
49
  npm run new -- --dir shopify --name pageGet --template getsingle
50
+ npm run new -- --name geodeBye --template shopify/_example.js
31
51
  `);
32
52
  };
33
53
 
@@ -124,11 +144,51 @@ const findExampleFiles = async (dirPath) => {
124
144
  }
125
145
  };
126
146
 
147
+ const collectMineralExampleFiles = async (dirPath, relativePrefix = '') => {
148
+ const results = [];
149
+ const localExamples = await findExampleFiles(dirPath);
150
+
151
+ for (const file of localExamples) {
152
+ const relativePath = relativePrefix ? `${ relativePrefix }/${ file.filename }` : file.filename;
153
+
154
+ results.push({
155
+ ...file,
156
+ relativePath,
157
+ displayName: relativePath,
158
+ });
159
+ }
160
+
161
+ let childDirs = [];
162
+
163
+ try {
164
+ const dirents = await fs.readdir(dirPath, { withFileTypes: true });
165
+ childDirs = dirents
166
+ .filter(dirent => dirent.isDirectory())
167
+ .map(dirent => dirent.name)
168
+ .filter(name => name[0] !== '_' && name[0] !== '.')
169
+ .filter(name => !excludedDirs.has(name));
170
+ } catch (err) {
171
+ return results;
172
+ }
173
+
174
+ for (const childDir of childDirs) {
175
+ const childPrefix = relativePrefix ? `${ relativePrefix }/${ childDir }` : childDir;
176
+ const childResults = await collectMineralExampleFiles(path.join(dirPath, childDir), childPrefix);
177
+ results.push(...childResults);
178
+ }
179
+
180
+ return results;
181
+ };
182
+
127
183
  const sortExampleFiles = (exampleFiles) => {
128
184
  return [...exampleFiles].sort((a, b) => {
129
- if (a.displayName === '_example.js') return -1;
130
- if (b.displayName === '_example.js') return 1;
131
- return a.displayName.localeCompare(b.displayName);
185
+ const aKey = a.relativePath || a.displayName;
186
+ const bKey = b.relativePath || b.displayName;
187
+
188
+ if (aKey === '_example.js') return -1;
189
+ if (bKey === '_example.js') return 1;
190
+
191
+ return aKey.localeCompare(bKey);
132
192
  });
133
193
  };
134
194
 
@@ -148,7 +208,68 @@ const normalizeTemplateArg = (templateArg) => {
148
208
  return `_example.${ templateArg }.js`;
149
209
  };
150
210
 
151
- const resolveTemplateFromArg = async (exampleFiles, templateArg, dir) => {
211
+ const findWorkspaceTemplate = (exampleFiles, templateArg) => {
212
+ if (!templateArg) {
213
+ const defaultTemplate = exampleFiles.find(file => file.relativePath === '_example.js');
214
+ if (defaultTemplate) {
215
+ return defaultTemplate;
216
+ }
217
+
218
+ if (exampleFiles.length === 0) {
219
+ throw new Error('No mineral example templates found');
220
+ }
221
+
222
+ return exampleFiles[0];
223
+ }
224
+
225
+ const directMatch = exampleFiles.find(file => file.relativePath === templateArg);
226
+ if (directMatch) {
227
+ return directMatch;
228
+ }
229
+
230
+ const normalizedFilename = normalizeTemplateArg(templateArg);
231
+ const filenameMatches = exampleFiles.filter(file => file.filename === normalizedFilename);
232
+
233
+ if (templateArg.includes('/')) {
234
+ const [ prefix, suffix ] = templateArg.split('/');
235
+ const normalizedSuffix = normalizeTemplateArg(suffix);
236
+ const prefixedMatch = exampleFiles.find(file => file.relativePath === `${ prefix }/${ normalizedSuffix }`);
237
+
238
+ if (prefixedMatch) {
239
+ return prefixedMatch;
240
+ }
241
+ }
242
+
243
+ if (filenameMatches.length === 1) {
244
+ return filenameMatches[0];
245
+ }
246
+
247
+ const suffixMatches = exampleFiles.filter(file => file.relativePath.endsWith(`/${ normalizedFilename }`));
248
+
249
+ if (suffixMatches.length === 1) {
250
+ return suffixMatches[0];
251
+ }
252
+
253
+ if (suffixMatches.length > 1) {
254
+ throw new Error(
255
+ `Ambiguous template "${ templateArg }". Use one of: ${ suffixMatches.map(file => file.relativePath).join(', ') }`,
256
+ );
257
+ }
258
+
259
+ throw new Error(`Template not found: ${ templateArg }`);
260
+ };
261
+
262
+ const resolveTemplateFromArg = async ({
263
+ exampleFiles,
264
+ templateArg,
265
+ dir,
266
+ inMineral,
267
+ }) => {
268
+ if (!inMineral) {
269
+ const selectedTemplate = findWorkspaceTemplate(exampleFiles, templateArg);
270
+ return selectedTemplate.fullPath;
271
+ }
272
+
152
273
  if (!templateArg) {
153
274
  const defaultInDir = exampleFiles.find(file => file.displayName === '_example.js');
154
275
  if (defaultInDir) {
@@ -184,13 +305,51 @@ const scriptFileContents = async (name, selectedTemplate) => {
184
305
  return exampleFileContents.replace(/FUNC/g, name);
185
306
  };
186
307
 
187
- const getDirs = async () => {
188
- const dirents = await fs.readdir(apiDirectory, { withFileTypes: true });
189
- return dirents
190
- .filter(dirent => dirent.isDirectory())
191
- .map(dirent => dirent.name)
192
- .filter(name => name[0] !== '_' && name[0] !== '.')
193
- .filter(name => !excludedDirs.has(name));
308
+ const getDirs = async (targetApiDirectory) => {
309
+ try {
310
+ const dirents = await fs.readdir(targetApiDirectory, { withFileTypes: true });
311
+ return dirents
312
+ .filter(dirent => dirent.isDirectory())
313
+ .map(dirent => dirent.name)
314
+ .filter(name => name[0] !== '_' && name[0] !== '.')
315
+ .filter(name => !excludedDirs.has(name));
316
+ } catch (err) {
317
+ if (err.code === 'ENOENT') {
318
+ return [];
319
+ }
320
+
321
+ throw err;
322
+ }
323
+ };
324
+
325
+ const normalizeDirArg = (dirArg) => {
326
+ if (!dirArg || dirArg === '.' || dirArg === 'api') {
327
+ return '';
328
+ }
329
+
330
+ return dirArg;
331
+ };
332
+
333
+ const resolveDirArg = ({ dirArg, dirs, inMineral, name }) => {
334
+ const normalizedDir = normalizeDirArg(dirArg);
335
+
336
+ if (dirs.includes(normalizedDir)) {
337
+ return normalizedDir;
338
+ }
339
+
340
+ if (!inMineral && dirs.length === 0 && normalizedDir === '') {
341
+ return '';
342
+ }
343
+
344
+ if (!inMineral && !dirArg && name) {
345
+ return '';
346
+ }
347
+
348
+ if (inMineral && !dirArg) {
349
+ return null;
350
+ }
351
+
352
+ return normalizedDir;
194
353
  };
195
354
 
196
355
  const buildFuncName = (dir, name) => {
@@ -201,6 +360,11 @@ const buildFuncName = (dir, name) => {
201
360
  };
202
361
 
203
362
  const selectDirInteractive = async (dirs) => {
363
+ if (dirs.length === 0) {
364
+ console.log('\nCreating in api/');
365
+ return '';
366
+ }
367
+
204
368
  const dirIndex = await askQuestion(`Where does your new function live? \n${
205
369
  dirs.map((dir, index) => {
206
370
  return `[${ index + 1 }] ${ dir }`;
@@ -216,33 +380,54 @@ const selectDirInteractive = async (dirs) => {
216
380
  return dir;
217
381
  };
218
382
 
219
- const selectTemplateInteractive = async (exampleFiles, dir) => {
383
+ const getExampleFilesForContext = async ({ context, dir }) => {
384
+ if (context.inMineral) {
385
+ return sortExampleFiles(await findExampleFiles(path.join(context.apiDirectory, dir)));
386
+ }
387
+
388
+ return sortExampleFiles(await collectMineralExampleFiles(context.mineralApiDirectory));
389
+ };
390
+
391
+ const formatTemplateLabel = (file, { inMineral }) => {
392
+ if (inMineral) {
393
+ return file.displayName;
394
+ }
395
+
396
+ return file.relativePath;
397
+ };
398
+
399
+ const selectTemplateInteractive = async ({ exampleFiles, dir, inMineral }) => {
220
400
  if (exampleFiles.length === 0) {
221
401
  await fs.access(rootExampleJsPath);
222
- console.log('\nUsing template: api/_example.js');
402
+ console.log('\nUsing template: _example.js');
223
403
  return rootExampleJsPath;
224
404
  }
225
405
 
226
406
  if (exampleFiles.length === 1) {
227
- console.log(`\nUsing template: ${ exampleFiles[0].displayName }`);
407
+ console.log(`\nUsing template: ${ formatTemplateLabel(exampleFiles[0], { inMineral }) }`);
228
408
  return exampleFiles[0].fullPath;
229
409
  }
230
410
 
231
- console.log(`\nFound ${ exampleFiles.length } template(s) in api/${ dir }:`);
411
+ const templateScope = inMineral ? `in api/${ dir }` : 'from mineral';
412
+ console.log(`\nFound ${ exampleFiles.length } template(s) ${ templateScope }:`);
413
+
232
414
  const templateIndex = await askQuestion(`Which template would you like to use? (press enter for _example.js) \n${
233
415
  exampleFiles.map((file, index) => {
234
- return `[${ index + 1 }] ${ file.displayName }`;
416
+ return `[${ index + 1 }] ${ formatTemplateLabel(file, { inMineral }) }`;
235
417
  }).join('\n')
236
418
  }\n`);
237
419
 
238
420
  if (!templateIndex || templateIndex.trim() === '') {
239
- const defaultFile = exampleFiles.find(file => file.displayName === '_example.js');
421
+ const defaultFile = exampleFiles.find(file => (
422
+ file.relativePath === '_example.js' || file.displayName === '_example.js'
423
+ ));
424
+
240
425
  if (defaultFile) {
241
426
  return defaultFile.fullPath;
242
427
  }
243
428
 
244
429
  await fs.access(rootExampleJsPath);
245
- console.log('\nUsing template: api/_example.js');
430
+ console.log('\nUsing template: _example.js');
246
431
  return rootExampleJsPath;
247
432
  }
248
433
 
@@ -266,6 +451,7 @@ const selectNameInteractive = async (dir) => {
266
451
  };
267
452
 
268
453
  const writeNewFunction = async ({
454
+ apiDirectory,
269
455
  dir,
270
456
  name,
271
457
  selectedTemplate,
@@ -283,6 +469,8 @@ const writeNewFunction = async ({
283
469
  }
284
470
  }
285
471
 
472
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
473
+
286
474
  const script = await scriptFileContents(funcName, selectedTemplate);
287
475
  await fs.writeFile(outputPath, script);
288
476
 
@@ -296,38 +484,55 @@ const writeNewFunction = async ({
296
484
  };
297
485
 
298
486
  const createNewFunction = async () => {
487
+ const context = getContext();
299
488
  const cliArgs = parseCliArgs(process.argv.slice(2));
300
489
 
301
490
  if (cliArgs.help) {
302
- printHelp();
491
+ printHelp(context);
303
492
  return;
304
493
  }
305
494
 
306
- const dirs = await getDirs();
495
+ const dirs = await getDirs(context.apiDirectory);
307
496
  const nonInteractive = Boolean(cliArgs.dir || cliArgs.name);
308
497
 
309
498
  if (nonInteractive) {
310
- if (!cliArgs.dir || !cliArgs.name) {
311
- console.error('Non-interactive mode requires both --dir and --name.');
312
- printHelp();
499
+ if (!cliArgs.name) {
500
+ console.error('Non-interactive mode requires --name.');
501
+ printHelp(context);
313
502
  process.exitCode = 1;
314
503
  return;
315
504
  }
316
505
 
317
- if (!dirs.includes(cliArgs.dir)) {
318
- console.error(`Invalid --dir "${ cliArgs.dir }". Available: ${ dirs.join(', ') }`);
506
+ const dir = resolveDirArg({
507
+ dirArg: cliArgs.dir,
508
+ dirs,
509
+ inMineral: context.inMineral,
510
+ name: cliArgs.name,
511
+ });
512
+
513
+ if (dir === null || (!dirs.includes(dir) && !(dir === '' && dirs.length === 0 && !context.inMineral))) {
514
+ const availableDirs = dirs.length ? dirs.join(', ') : 'api/ (flat)';
515
+ console.error(`Invalid --dir "${ cliArgs.dir }". Available: ${ availableDirs }`);
319
516
  process.exitCode = 1;
320
517
  return;
321
518
  }
322
519
 
323
- const exampleFiles = sortExampleFiles(await findExampleFiles(path.join(apiDirectory, cliArgs.dir)));
520
+ const exampleFiles = await getExampleFilesForContext({ context, dir });
324
521
 
325
522
  try {
326
- const selectedTemplate = await resolveTemplateFromArg(exampleFiles, cliArgs.template, cliArgs.dir);
327
- console.log(`Using template: ${ path.relative(path.join(__dirname, '..'), selectedTemplate) }`);
523
+ const selectedTemplate = await resolveTemplateFromArg({
524
+ exampleFiles,
525
+ templateArg: cliArgs.template,
526
+ dir,
527
+ inMineral: context.inMineral,
528
+ });
529
+
530
+ const templateLabel = path.relative(mineralRoot, selectedTemplate);
531
+ console.log(`Using template: ${ templateLabel }`);
328
532
 
329
533
  await writeNewFunction({
330
- dir: cliArgs.dir,
534
+ apiDirectory: context.apiDirectory,
535
+ dir,
331
536
  name: cliArgs.name,
332
537
  selectedTemplate,
333
538
  shouldAutoCommit: process.env.AUTO_COMMIT_STUBS === 'true' || cliArgs.commit,
@@ -342,11 +547,16 @@ const createNewFunction = async () => {
342
547
 
343
548
  try {
344
549
  const dir = await selectDirInteractive(dirs);
345
- const exampleFiles = sortExampleFiles(await findExampleFiles(path.join(apiDirectory, dir)));
346
- const selectedTemplate = await selectTemplateInteractive(exampleFiles, dir);
550
+ const exampleFiles = await getExampleFilesForContext({ context, dir });
551
+ const selectedTemplate = await selectTemplateInteractive({
552
+ exampleFiles,
553
+ dir,
554
+ inMineral: context.inMineral,
555
+ });
347
556
  const name = await selectNameInteractive(dir);
348
557
 
349
558
  await writeNewFunction({
559
+ apiDirectory: context.apiDirectory,
350
560
  dir,
351
561
  name,
352
562
  selectedTemplate,
package/bin/mineral.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const command = process.argv[2];
4
4
 
5
5
  if (command === 'host') {
6
- const { deployFromHostingYml } = require('../_deploy_scripts/deployFromHostingYml');
6
+ const { deployFromHostingYml } = require('../hosting/deployFromHostingYml');
7
7
  deployFromHostingYml().catch((error) => {
8
8
  console.error(error);
9
9
  process.exit(1);
@@ -0,0 +1,23 @@
1
+ google_cloud_info:
2
+ project:
3
+ region:
4
+
5
+ # TODO: consider credsPayload in google_cloud_info instead of workspace .creds.yml
6
+
7
+ functions:
8
+ exampleFunction:
9
+ max_instances: 1
10
+ timeout: 300s
11
+ wrappers:
12
+ - requireHostedApiKey
13
+ - allowCrossOriginCallsAndHandleOptions
14
+ # entry_point: otherHandlerName
15
+
16
+ pokemonPokeballThrow:
17
+ max_instances: 1
18
+ wrappers:
19
+ - checkTrainer
20
+
21
+ groups:
22
+ example_group:
23
+ - exampleFunction
@@ -0,0 +1,48 @@
1
+ const fs = require('fs');
2
+ const yaml = require('yaml');
3
+
4
+ const copyCredsToEnv = (workspace) => {
5
+ const normalizedWorkspace = workspace.replace(/\/$/, '');
6
+ const credsPath = `${ normalizedWorkspace }/.creds.yml`;
7
+ const envPath = `${ normalizedWorkspace }/.env`;
8
+
9
+ if (!fs.existsSync(credsPath)) {
10
+ throw new Error(`Missing .creds.yml in workspace: ${ workspace }`);
11
+ }
12
+
13
+ const credsText = fs.readFileSync(credsPath, 'utf8');
14
+ const credsFromYml = yaml.parse(credsText);
15
+ const newCredsLine = `CREDS=${ JSON.stringify(credsFromYml) }`;
16
+
17
+ let envFileContents = '';
18
+ if (fs.existsSync(envPath)) {
19
+ envFileContents = fs.readFileSync(envPath, 'utf8');
20
+ }
21
+
22
+ if (!envFileContents) {
23
+ fs.writeFileSync(envPath, `${ newCredsLine }\n`);
24
+ return;
25
+ }
26
+
27
+ if (/^CREDS=/m.test(envFileContents)) {
28
+ const updatedFileContents = envFileContents.replace(/^CREDS=.*$/m, newCredsLine);
29
+
30
+ if (updatedFileContents !== envFileContents) {
31
+ fs.writeFileSync(envPath, updatedFileContents);
32
+ }
33
+
34
+ return;
35
+ }
36
+
37
+ fs.appendFileSync(envPath, `\n\n${ newCredsLine }\n`);
38
+ };
39
+
40
+ if (require.main === module) {
41
+ const workspace = process.argv[2] || process.cwd();
42
+ copyCredsToEnv(workspace);
43
+ console.log('Copied CREDS from .creds.yml to .env');
44
+ }
45
+
46
+ module.exports = {
47
+ copyCredsToEnv,
48
+ };
@@ -1,14 +1,13 @@
1
1
  const readline = require('readline');
2
- const { toAbsolutePath, setWorkspace } = require('../api/workspace');
2
+ const { toAbsolutePath, setWorkspace, loadWorkspaceEnv } = require('../api/workspace');
3
3
  const { getApiDirs, readCliFlag } = require('../cli');
4
4
  const { loadHandlers } = require('../server');
5
5
  const {
6
6
  readHostingYml,
7
- getCredsJsonForDeploy,
8
- validateEnvVarsForDeploy,
7
+ ensureWorkspaceEnvForDeploy,
9
8
  resolveHostedHandlersForDeploy,
10
9
  functionUsesWrapper,
11
- } = require('../hosting.utils');
10
+ } = require('./hosting.utils');
12
11
  const { writeHostedJs } = require('./generateHosted');
13
12
  const { execCommand } = require('./execCommand');
14
13
  const { formatSetEnvVarsForGcloud, shellQuoteSingle } = require('./setEnvVarsGcloud');
@@ -91,8 +90,6 @@ const deployFunction = async ({
91
90
  functionConfig,
92
91
  googleCloudInfo,
93
92
  workspace,
94
- credsJson,
95
- deployEnvVars = {},
96
93
  }) => {
97
94
  const config = {
98
95
  ...googleCloudInfo,
@@ -107,20 +104,16 @@ const deployFunction = async ({
107
104
  allow_unauthenticated: allowUnauthenticated = true,
108
105
  gen2 = true,
109
106
  set_env_vars: extraSetEnvVars,
110
- entry_point: entryPoint,
107
+ entry_point,
111
108
  schedules,
112
109
  groups,
113
110
  wrappers,
114
111
  source,
112
+ env,
115
113
  ...gcloudArgs
116
114
  } = config;
117
115
 
118
- const resolvedEntryPoint = entryPoint || functionName;
119
- const envParts = [
120
- 'HOSTED=true',
121
- `CREDS=${ credsJson }`,
122
- ...Object.entries(deployEnvVars).map(([envName, envValue]) => `${ envName }=${ envValue }`),
123
- ];
116
+ const envParts = ['HOSTED=true'];
124
117
 
125
118
  if (extraSetEnvVars) {
126
119
  envParts.push(extraSetEnvVars);
@@ -134,7 +127,7 @@ const deployFunction = async ({
134
127
  `--project ${ project }`,
135
128
  `--region ${ region }`,
136
129
  `--source ${ shellQuoteSingle(workspace) }`,
137
- `--entry-point ${ resolvedEntryPoint }`,
130
+ `--entry-point ${ functionName }`,
138
131
  `--trigger-${ trigger }`,
139
132
  `--runtime ${ runtime }`,
140
133
  `--set-env-vars ${ shellQuoteSingle(setEnvVarsForGcloud) }`,
@@ -157,7 +150,7 @@ const deployFunction = async ({
157
150
  }
158
151
 
159
152
  const requiresHostedApiKey = functionUsesWrapper(functionConfig, 'requireHostedApiKey');
160
- const hostedApiKey = deployEnvVars.HOSTED_API_KEY;
153
+ const hostedApiKey = process.env.HOSTED_API_KEY;
161
154
 
162
155
  for (const schedule of schedules) {
163
156
  const {
@@ -203,17 +196,18 @@ const deployFunction = async ({
203
196
  const deployFromHostingYml = async (options = {}) => {
204
197
  const config = getHostConfig(options);
205
198
  setWorkspace(config.workspace);
199
+ ensureWorkspaceEnvForDeploy(config.workspace);
200
+ loadWorkspaceEnv();
206
201
 
207
202
  const hostingConfig = readHostingYml(config.workspace);
208
203
  const {
209
204
  google_cloud_info: googleCloudInfo,
210
- env: envNames = [],
211
205
  functions = {},
212
206
  groups = {},
213
207
  } = hostingConfig;
214
208
 
215
209
  if (!googleCloudInfo?.project || !googleCloudInfo?.region) {
216
- throw new Error('.hosting.yml requires google_cloud_info.project and google_cloud_info.region');
210
+ throw new Error('hosting/.hosting.yml requires google_cloud_info.project and google_cloud_info.region');
217
211
  }
218
212
 
219
213
  const handlers = loadHandlers({
@@ -227,19 +221,16 @@ const deployFromHostingYml = async (options = {}) => {
227
221
  handlersByName,
228
222
  });
229
223
 
230
- const deployEnvVars = validateEnvVarsForDeploy(config.workspace, envNames);
231
-
232
224
  writeHostedJs({
233
225
  workspace: config.workspace,
234
226
  hostedHandlers,
235
227
  });
236
228
 
237
- const credsJson = getCredsJsonForDeploy(config.workspace);
238
229
  const deployArgs = getDeployArgs();
239
230
 
240
231
  const deployOne = async (functionName) => {
241
232
  if (!functions[functionName]) {
242
- console.log(`Function ${ functionName } not found in .hosting.yml, skipping`);
233
+ console.log(`Function ${ functionName } not found in hosting/.hosting.yml, skipping`);
243
234
  return;
244
235
  }
245
236
 
@@ -248,15 +239,13 @@ const deployFromHostingYml = async (options = {}) => {
248
239
  functionConfig: functions[functionName],
249
240
  googleCloudInfo,
250
241
  workspace: config.workspace,
251
- credsJson,
252
- deployEnvVars,
253
242
  });
254
243
  };
255
244
 
256
245
  if (deployArgs.includes('group')) {
257
246
  const groupNames = Object.keys(groups);
258
247
  if (!groupNames.length) {
259
- console.log('No groups defined in .hosting.yml');
248
+ console.log('No groups defined in hosting/.hosting.yml');
260
249
  return;
261
250
  }
262
251
 
@@ -270,7 +259,7 @@ const deployFromHostingYml = async (options = {}) => {
270
259
  if (deployArgs.includes('function')) {
271
260
  const functionNames = Object.keys(functions);
272
261
  if (!functionNames.length) {
273
- console.log('No functions defined in .hosting.yml');
262
+ console.log('No functions defined in hosting/.hosting.yml');
274
263
  return;
275
264
  }
276
265
 
@@ -305,14 +294,3 @@ module.exports = {
305
294
  deployFromHostingYml,
306
295
  getHostConfig,
307
296
  };
308
-
309
- /*
310
- Deploy everything:
311
- mineral host all
312
-
313
- Deploy a single function:
314
- mineral host function
315
-
316
- Deploy a group of functions:
317
- mineral host group
318
- */
@@ -0,0 +1,60 @@
1
+ const fs = require('fs');
2
+ const { wrapHostedFunction } = require('./hosting.utils');
3
+
4
+ const formatResolvedWrapperForHostedJs = ({ modulePath, wrapperName }) => (
5
+ `require('${ modulePath }').${ wrapperName }`
6
+ );
7
+
8
+ const formatWrappersArg = (resolvedWrappers = []) => {
9
+ if (!resolvedWrappers.length) {
10
+ return '';
11
+ }
12
+
13
+ const wrapperLines = resolvedWrappers.map((resolvedWrapper) => (
14
+ ` ${ formatResolvedWrapperForHostedJs(resolvedWrapper) },`
15
+ ));
16
+ return `, [\n${ wrapperLines.join('\n') }\n ]`;
17
+ };
18
+
19
+ const generateHostedJs = ({
20
+ hostedHandlers,
21
+ }) => {
22
+ const exportLines = [];
23
+
24
+ for (const hostedHandler of hostedHandlers) {
25
+ const { hostedName, handlerName, resolvedWrappers = [], requirePath } = hostedHandler;
26
+ const wrappersArg = formatWrappersArg(resolvedWrappers);
27
+
28
+ exportLines.push(
29
+ ` ${ hostedName }: wrapHostedFunction(() => require('${ requirePath }'), '${ handlerName }'${ wrappersArg }),`,
30
+ );
31
+ }
32
+
33
+ return `// Generated by mineral — do not edit
34
+ const { wrapHostedFunction } = require('@foxtware/mineral/hosting/hosting.utils');
35
+
36
+ module.exports = {
37
+ ${ exportLines.join('\n') }
38
+ };
39
+ `;
40
+ };
41
+
42
+ const writeHostedJs = ({
43
+ workspace,
44
+ hostedHandlers,
45
+ }) => {
46
+ const { getHostingDir } = require('./hosting.utils');
47
+ const hostedPath = `${ getHostingDir(workspace) }/hosted.js`;
48
+ const content = generateHostedJs({
49
+ hostedHandlers,
50
+ });
51
+
52
+ fs.mkdirSync(getHostingDir(workspace), { recursive: true });
53
+ fs.writeFileSync(hostedPath, content);
54
+ return hostedPath;
55
+ };
56
+
57
+ module.exports = {
58
+ generateHostedJs,
59
+ writeHostedJs,
60
+ };
@@ -0,0 +1,23 @@
1
+ const MINERAL_ROOT = `${ __dirname }/..`;
2
+ const MINERAL_API_DIR = `${ MINERAL_ROOT }/api`;
3
+
4
+ const getRequirePathForHandler = (handler, workspace) => {
5
+ const normalizedWorkspace = workspace.replace(/\/$/, '');
6
+ const { filePath } = handler;
7
+
8
+ if (filePath.startsWith(`${ normalizedWorkspace }/`)) {
9
+ return `./${ filePath.slice(normalizedWorkspace.length + 1) }`;
10
+ }
11
+
12
+ if (filePath.startsWith(`${ MINERAL_API_DIR }/`)) {
13
+ const relativePath = filePath.slice(MINERAL_API_DIR.length + 1);
14
+ return `@foxtware/mineral/api/${ relativePath }`;
15
+ }
16
+
17
+ throw new Error(`Handler "${ handler.routeName }" is not in workspace or mineral api dirs`);
18
+ };
19
+
20
+ module.exports = {
21
+ MINERAL_API_DIR,
22
+ getRequirePathForHandler,
23
+ };
@@ -1,6 +1,9 @@
1
1
  const fs = require('fs');
2
+ const path = require('path');
3
+ const dotenv = require('dotenv');
2
4
  const { createRequire } = require('module');
3
5
  const yaml = require('yaml');
6
+ const { getRequirePathForHandler } = require('./handlerPaths');
4
7
  const {
5
8
  respondJson,
6
9
  errorToReadable,
@@ -9,63 +12,71 @@ const {
9
12
  funcApi,
10
13
  wrapFunction,
11
14
  statusCodeFromResult,
12
- } = require('./server.utils');
15
+ } = require('../server.utils');
13
16
 
14
- const parseWrapperRef = (wrapperRef) => {
15
- const hashIndex = wrapperRef.lastIndexOf('#');
17
+ dotenv.config({
18
+ path: path.join(process.cwd(), '.env'),
19
+ });
16
20
 
17
- if (hashIndex === -1) {
18
- throw new Error(`Invalid wrapper ref (expected path#export): ${ wrapperRef }`);
19
- }
21
+ const MINERAL_WRAPPERS_MODULE = '@foxtware/mineral/hosting/wrappers.js';
22
+ const WORKSPACE_WRAPPERS_MODULE = './hosting/wrappers.js';
20
23
 
21
- return {
22
- modulePath: wrapperRef.slice(0, hashIndex),
23
- exportName: wrapperRef.slice(hashIndex + 1),
24
- };
25
- };
24
+ const getHostingDir = (workspace) => `${ workspace.replace(/\/$/, '') }/hosting`;
26
25
 
27
- const validateWrapperRef = (wrapperRef, workspaceRequire) => {
28
- const { modulePath, exportName } = parseWrapperRef(wrapperRef);
29
- workspaceRequire.resolve(modulePath);
30
- const moduleExports = workspaceRequire(modulePath);
26
+ const getMineralWrappers = (workspaceRequire) => {
27
+ try {
28
+ return workspaceRequire(MINERAL_WRAPPERS_MODULE);
29
+ } catch (error) {
30
+ if (error.code !== 'MODULE_NOT_FOUND') {
31
+ throw error;
32
+ }
31
33
 
32
- if (typeof moduleExports[exportName] !== 'function') {
33
- throw new Error(`Wrapper export not found: ${ exportName } in ${ modulePath }`);
34
+ return require('./wrappers.js');
34
35
  }
35
36
  };
36
37
 
37
- const wrapperExportName = (wrapperRef) => (
38
- parseWrapperRef(wrapperRef).exportName
39
- );
40
-
41
- const getHostedEntries = (functions = {}) => {
42
- const byEntryPoint = new Map();
38
+ const resolveWrapperName = (wrapperName, workspaceRequire) => {
39
+ if (typeof wrapperName !== 'string' || !wrapperName.trim()) {
40
+ throw new Error(`Invalid wrapper name: ${ wrapperName }`);
41
+ }
43
42
 
44
- for (const [functionName, functionConfig] of Object.entries(functions)) {
45
- const entryPoint = functionConfig.entry_point || functionConfig.entryPoint || functionName;
46
- const existing = byEntryPoint.get(entryPoint) || {
47
- entryPoint,
48
- wrappers: [],
43
+ const mineralWrappers = getMineralWrappers(workspaceRequire);
44
+ if (typeof mineralWrappers[wrapperName] === 'function') {
45
+ return {
46
+ wrapperName,
47
+ modulePath: MINERAL_WRAPPERS_MODULE,
49
48
  };
49
+ }
50
50
 
51
- if (Array.isArray(functionConfig.wrappers)) {
52
- existing.wrappers.push(...functionConfig.wrappers);
53
- existing.wrappers = [...new Set(existing.wrappers)];
51
+ try {
52
+ const workspaceWrappers = workspaceRequire(WORKSPACE_WRAPPERS_MODULE);
53
+ if (typeof workspaceWrappers[wrapperName] === 'function') {
54
+ return {
55
+ wrapperName,
56
+ modulePath: WORKSPACE_WRAPPERS_MODULE,
57
+ };
54
58
  }
55
-
56
- if (functionConfig.source) {
57
- existing.source = functionConfig.source;
59
+ } catch (error) {
60
+ if (error.code !== 'MODULE_NOT_FOUND') {
61
+ throw error;
58
62
  }
59
-
60
- byEntryPoint.set(entryPoint, existing);
61
63
  }
62
64
 
63
- return [...byEntryPoint.values()];
65
+ throw new Error(
66
+ `Wrapper "${ wrapperName }" not found in ${ MINERAL_WRAPPERS_MODULE } or ${ WORKSPACE_WRAPPERS_MODULE }`,
67
+ );
64
68
  };
65
69
 
66
- const functionUsesWrapper = (functionConfig = {}, exportName) => (
67
- Array.isArray(functionConfig.wrappers)
68
- && functionConfig.wrappers.some((wrapperRef) => wrapperExportName(wrapperRef) === exportName)
70
+ const getHostedEntries = (functions = {}) => (
71
+ Object.entries(functions).map(([hostedName, functionConfig = {}]) => ({
72
+ hostedName,
73
+ handlerName: functionConfig.entry_point || functionConfig.entryPoint || hostedName,
74
+ wrappers: Array.isArray(functionConfig.wrappers) ? functionConfig.wrappers : [],
75
+ }))
76
+ );
77
+
78
+ const functionUsesWrapper = (functionConfig = {}, wrapperName) => (
79
+ Array.isArray(functionConfig.wrappers) && functionConfig.wrappers.includes(wrapperName)
69
80
  );
70
81
 
71
82
  const getFuncApiConfig = ({
@@ -153,17 +164,17 @@ const wrapHostedFunction = (loader, exportName, wrappers = []) => {
153
164
  };
154
165
 
155
166
  const readHostingYml = (workspace) => {
156
- const hostingPath = `${ workspace }/.hosting.yml`;
167
+ const hostingPath = `${ getHostingDir(workspace) }/.hosting.yml`;
157
168
 
158
169
  if (!fs.existsSync(hostingPath)) {
159
- throw new Error(`Missing .hosting.yml in workspace: ${ workspace }`);
170
+ throw new Error(`Missing hosting/.hosting.yml in workspace: ${ workspace }`);
160
171
  }
161
172
 
162
173
  const hostingText = fs.readFileSync(hostingPath, 'utf8');
163
174
  const hostingConfig = yaml.parse(hostingText);
164
175
 
165
176
  if (!hostingConfig || typeof hostingConfig !== 'object' || Array.isArray(hostingConfig)) {
166
- throw new Error('Invalid .hosting.yml');
177
+ throw new Error('Invalid hosting/.hosting.yml');
167
178
  }
168
179
 
169
180
  return hostingConfig;
@@ -180,33 +191,15 @@ const getCredsJsonForDeploy = (workspace) => {
180
191
  return JSON.stringify(yaml.parse(credsText));
181
192
  };
182
193
 
183
- const getEnvValueForDeploy = (workspace, envName) => {
184
- const envPath = `${ workspace }/.env`;
194
+ const ensureWorkspaceEnvForDeploy = (workspace) => {
195
+ const { copyCredsToEnv } = require('./copyCredsToEnv');
196
+ const envPath = `${ workspace.replace(/\/$/, '') }/.env`;
185
197
 
186
- if (!fs.existsSync(envPath)) {
187
- return '';
188
- }
189
-
190
- const envText = fs.readFileSync(envPath, 'utf8');
191
- const match = envText.match(new RegExp(`^${ envName }=(.*)$`, 'm'));
192
- return match ? match[1].trim() : '';
193
- };
194
-
195
- const getEnvVarsForDeploy = (workspace, envNames = []) => (
196
- Object.fromEntries(
197
- envNames.map((envName) => [envName, getEnvValueForDeploy(workspace, envName)]),
198
- )
199
- );
200
-
201
- const validateEnvVarsForDeploy = (workspace, envNames = []) => {
202
- const envVars = getEnvVarsForDeploy(workspace, envNames);
203
- const missing = envNames.filter((envName) => !envVars[envName]);
198
+ copyCredsToEnv(workspace);
204
199
 
205
- if (missing.length) {
206
- throw new Error(`Missing required .env values: ${ missing.join(', ') }`);
200
+ if (!fs.existsSync(envPath)) {
201
+ throw new Error(`Missing .env in workspace: ${ workspace }`);
207
202
  }
208
-
209
- return envVars;
210
203
  };
211
204
 
212
205
  const resolveHostedHandlersForDeploy = ({
@@ -218,35 +211,23 @@ const resolveHostedHandlersForDeploy = ({
218
211
  const hostedEntries = getHostedEntries(functions);
219
212
 
220
213
  return hostedEntries.map((hostedEntry) => {
221
- const { entryPoint, source, wrappers = [] } = hostedEntry;
214
+ const { handlerName, wrappers = [] } = hostedEntry;
222
215
 
223
- for (const wrapperRef of wrappers) {
224
- validateWrapperRef(wrapperRef, workspaceRequire);
225
- }
226
-
227
- if (source) {
228
- workspaceRequire.resolve(source);
229
- const moduleExports = workspaceRequire(source);
230
- if (typeof moduleExports[entryPoint] !== 'function') {
231
- throw new Error(`Hosted export not found: ${ entryPoint } in ${ source }`);
232
- }
233
-
234
- return {
235
- ...hostedEntry,
236
- requirePath: source,
237
- };
238
- }
216
+ const resolvedWrappers = wrappers.map((wrapperName) => (
217
+ resolveWrapperName(wrapperName, workspaceRequire)
218
+ ));
239
219
 
240
- const handler = handlersByName.get(entryPoint);
220
+ const handler = handlersByName.get(handlerName);
241
221
  if (!handler) {
242
222
  throw new Error(
243
- `entry_point "${ entryPoint }" not found in workspace handlers add api/ handler or source in .hosting.yml`,
223
+ `Function "${ handlerName }" not found add a handler in workspace api dirs or mineral api`,
244
224
  );
245
225
  }
246
226
 
247
227
  return {
248
228
  ...hostedEntry,
249
- requirePath: `./${ handler.filePath.slice(workspace.length + 1) }`,
229
+ resolvedWrappers,
230
+ requirePath: getRequirePathForHandler(handler, workspace),
250
231
  };
251
232
  });
252
233
  };
@@ -254,15 +235,16 @@ const resolveHostedHandlersForDeploy = ({
254
235
  // TODO: support credsPayload in google_cloud_info instead of full workspace .creds.yml
255
236
 
256
237
  module.exports = {
257
- parseWrapperRef,
238
+ getHostingDir,
239
+ resolveWrapperName,
258
240
  getFuncApiConfig,
259
241
  wrapHostedFunction,
260
242
  readHostingYml,
261
243
  getCredsJsonForDeploy,
262
- getEnvVarsForDeploy,
263
- validateEnvVarsForDeploy,
244
+ ensureWorkspaceEnvForDeploy,
264
245
  getHostedEntries,
265
246
  resolveHostedHandlersForDeploy,
266
247
  functionUsesWrapper,
267
- wrapperExportName,
248
+ MINERAL_WRAPPERS_MODULE,
249
+ WORKSPACE_WRAPPERS_MODULE,
268
250
  };
@@ -0,0 +1,37 @@
1
+ const { HOSTED } = require('../api/constants');
2
+
3
+ const requireHostedApiKey = async (req) => {
4
+ if (!HOSTED) {
5
+ return;
6
+ }
7
+
8
+ if (req.headers['x-api-key'] !== process.env.HOSTED_API_KEY) {
9
+ return {
10
+ ok: false,
11
+ error: {
12
+ code: 'UNAUTHORIZED',
13
+ message: 'Unauthorized',
14
+ statusCode: 401,
15
+ },
16
+ };
17
+ }
18
+ };
19
+
20
+ const allowCrossOriginCallsAndHandleOptions = async (req, res) => {
21
+ const { origin } = req.headers;
22
+
23
+ res.setHeader('Access-Control-Allow-Origin', origin || '*');
24
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
25
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, x-api-key, x-wf-token, x-wf-value');
26
+
27
+ if (req.method === 'OPTIONS') {
28
+ res.writeHead(204);
29
+ res.end();
30
+ return { handled: true };
31
+ }
32
+ };
33
+
34
+ module.exports = {
35
+ requireHostedApiKey,
36
+ allowCrossOriginCallsAndHandleOptions,
37
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxtware/mineral",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "bin": {
5
5
  "mineral": "bin/mineral.js"
6
6
  },
@@ -9,7 +9,7 @@
9
9
  "access": "public"
10
10
  },
11
11
  "scripts": {
12
- "creds_to_env": "node _build_scripts/copyCredsToEnv.js",
12
+ "creds_to_env": "node hosting/copyCredsToEnv.js",
13
13
  "dev": "node --watch --watch-path=./api --watch-path=./server.js --watch-path=./server.utils.js --watch-path=./.creds.yml server.js",
14
14
  "new": "node _build_scripts/createNewFunction.js",
15
15
  "serve": "PORT=8100 node server.js",
package/server.js CHANGED
@@ -3,9 +3,8 @@ const http = require('http');
3
3
  const { respondJson, errorToReadable, getRequestBody, argsFromBody, funcApi, statusCodeFromResult } = require('./server.utils');
4
4
  const { getWorkspace, setWorkspace, loadWorkspaceEnv, toAbsolutePath } = require('./api/workspace');
5
5
  const { getApiDirs, readCliFlag } = require('./cli');
6
- const { getFuncApiConfig } = require('./hosting.utils');
7
-
8
- const MINERAL_API_DIR = `${ __dirname }/api`;
6
+ const { getFuncApiConfig } = require('./hosting/hosting.utils');
7
+ const { MINERAL_API_DIR } = require('./hosting/handlerPaths');
9
8
 
10
9
  const getConfig = (options = {}) => ({
11
10
  port: Number(options.port ?? process.env.PORT ?? 8000),
@@ -69,7 +68,6 @@ const listJsFiles = (directory) => {
69
68
  const directoriesToScan = ({
70
69
  workspace,
71
70
  api_dirs,
72
- host_mode = false,
73
71
  }) => {
74
72
  const extraDirs = api_dirs.map((dir) => (
75
73
  dir.startsWith('/')
@@ -77,10 +75,6 @@ const directoriesToScan = ({
77
75
  : `${ workspace }/${ dir }`
78
76
  ));
79
77
 
80
- if (host_mode && api_dirs.length) {
81
- return extraDirs;
82
- }
83
-
84
78
  return [MINERAL_API_DIR, ...extraDirs];
85
79
  };
86
80
 
@@ -225,6 +219,7 @@ const startServer = (options = {}) => {
225
219
  module.exports = {
226
220
  startServer,
227
221
  loadHandlers,
222
+ MINERAL_API_DIR,
228
223
  get server() {
229
224
  if (!server) {
230
225
  startServer();
package/server.utils.js CHANGED
@@ -1,6 +1,5 @@
1
1
  const { logDeep } = require('./api/utils');
2
2
  const { StringDecoder } = require('string_decoder');
3
- const { HOSTED } = require('./api/constants');
4
3
 
5
4
  const respondJson = (res, statusCode, payload) => {
6
5
  logDeep(payload);
@@ -132,37 +131,6 @@ const wrapFunction = (func, wrappers = []) => async (req, res, ...rest) => {
132
131
  return func(req, res, ...rest);
133
132
  };
134
133
 
135
- const requireHostedApiKey = async (req) => {
136
- if (!HOSTED) {
137
- return;
138
- }
139
-
140
- if (req.headers['x-api-key'] !== process.env.HOSTED_API_KEY) {
141
- return {
142
- ok: false,
143
- error: {
144
- code: 'UNAUTHORIZED',
145
- message: 'Unauthorized',
146
- statusCode: 401,
147
- },
148
- };
149
- }
150
- };
151
-
152
- const allowCrossOriginCallsAndHandleOptions = async (req, res) => {
153
- const { origin } = req.headers;
154
-
155
- res.setHeader('Access-Control-Allow-Origin', origin || '*');
156
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
157
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, x-api-key, x-wf-token, x-wf-value');
158
-
159
- if (req.method === 'OPTIONS') {
160
- res.writeHead(204);
161
- res.end();
162
- return { handled: true };
163
- }
164
- };
165
-
166
134
  const statusCodeFromResult = (result) => {
167
135
  if (result?.ok === false) {
168
136
  return result?.error?.statusCode ?? 400;
@@ -280,8 +248,6 @@ module.exports = {
280
248
  getRequestBody,
281
249
  argsFromBody,
282
250
  wrapFunction,
283
- requireHostedApiKey,
284
- allowCrossOriginCallsAndHandleOptions,
285
251
  statusCodeFromResult,
286
252
  funcApi,
287
253
  };
@@ -1,27 +0,0 @@
1
- google_cloud_info:
2
- project:
3
- region:
4
-
5
- # TODO: consider credsPayload in google_cloud_info instead of workspace .creds.yml
6
-
7
- env:
8
- - HOSTED_API_KEY
9
-
10
- functions:
11
- exampleFunction:
12
- max_instances: 1
13
- timeout: 300s
14
- wrappers:
15
- - '@foxtware/mineral/server.utils#requireHostedApiKey'
16
- - '@foxtware/mineral/server.utils#allowCrossOriginCallsAndHandleOptions'
17
- # entry_point: otherExportName
18
-
19
- packagedFunction:
20
- source: '@foxtware/mineral/api/pokemon/pokemonPokeballThrow.js'
21
- max_instances: 1
22
- wrappers:
23
- - '@foxtware/mineral/server.utils#checkTrainer'
24
-
25
- groups:
26
- example_group:
27
- - exampleFunction
@@ -1,45 +0,0 @@
1
- const fs = require('fs').promises;
2
- const path = require('path');
3
- const yaml = require('yaml');
4
-
5
- const mineralRoot = path.join(__dirname, '..');
6
- const credsYmlPath = path.join(mineralRoot, '.creds.yml');
7
- const envPath = path.join(mineralRoot, '.env');
8
-
9
- (async () => {
10
- const credsText = await fs.readFile(credsYmlPath, 'utf8');
11
- const credsFromYml = yaml.parse(credsText);
12
- const credsJsonString = JSON.stringify(credsFromYml);
13
- const newCredsLine = `CREDS=${ credsJsonString }`;
14
-
15
- let envFileContents = '';
16
- try {
17
- envFileContents = await fs.readFile(envPath, 'utf8');
18
- } catch (err) {
19
- if (err.code !== 'ENOENT') {
20
- throw err;
21
- }
22
- }
23
-
24
- if (!envFileContents) {
25
- await fs.writeFile(envPath, `${ newCredsLine }\n`);
26
- console.log('Created .env with CREDS');
27
- return;
28
- }
29
-
30
- if (/^CREDS=/m.test(envFileContents)) {
31
- const updatedFileContents = envFileContents.replace(/^CREDS=.*$/m, newCredsLine);
32
-
33
- if (updatedFileContents === envFileContents) {
34
- console.log('CREDS already up to date');
35
- return;
36
- }
37
-
38
- await fs.writeFile(envPath, updatedFileContents);
39
- console.log('Updated CREDS in .env');
40
- return;
41
- }
42
-
43
- await fs.appendFile(envPath, `\n\n${ newCredsLine }\n`);
44
- console.log('Appended CREDS to .env');
45
- })();
@@ -1,57 +0,0 @@
1
- const fs = require('fs');
2
- const { wrapHostedFunction, parseWrapperRef } = require('../hosting.utils');
3
-
4
- const formatWrapperRefForHostedJs = (wrapperRef) => {
5
- const { modulePath, exportName } = parseWrapperRef(wrapperRef);
6
- return `require('${ modulePath }').${ exportName }`;
7
- };
8
-
9
- const formatWrappersArg = (wrappers = []) => {
10
- if (!wrappers.length) {
11
- return '';
12
- }
13
-
14
- const wrapperLines = wrappers.map((wrapperRef) => ` ${ formatWrapperRefForHostedJs(wrapperRef) },`);
15
- return `, [\n${ wrapperLines.join('\n') }\n ]`;
16
- };
17
-
18
- const generateHostedJs = ({
19
- hostedHandlers,
20
- }) => {
21
- const exportLines = [];
22
-
23
- for (const hostedHandler of hostedHandlers) {
24
- const { entryPoint, wrappers = [], requirePath } = hostedHandler;
25
- const wrappersArg = formatWrappersArg(wrappers);
26
-
27
- exportLines.push(
28
- ` ${ entryPoint }: wrapHostedFunction(() => require('${ requirePath }'), '${ entryPoint }'${ wrappersArg }),`,
29
- );
30
- }
31
-
32
- return `// Generated by mineral — do not edit
33
- const { wrapHostedFunction } = require('@foxtware/mineral/hosting.utils');
34
-
35
- module.exports = {
36
- ${ exportLines.join('\n') }
37
- };
38
- `;
39
- };
40
-
41
- const writeHostedJs = ({
42
- workspace,
43
- hostedHandlers,
44
- }) => {
45
- const hostedPath = `${ workspace.replace(/\/$/, '') }/hosted.js`;
46
- const content = generateHostedJs({
47
- hostedHandlers,
48
- });
49
-
50
- fs.writeFileSync(hostedPath, content);
51
- return hostedPath;
52
- };
53
-
54
- module.exports = {
55
- generateHostedJs,
56
- writeHostedJs,
57
- };
File without changes