@foxtware/mineral 0.1.20 → 0.1.22
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/.creds.yml.sample +7 -0
- package/_build_scripts/spinOff.js +323 -0
- package/bin/mineral.js +9 -0
- package/package.json +2 -1
- package/tagalys/.gitkeep +0 -0
package/.creds.yml.sample
CHANGED
|
@@ -112,3 +112,10 @@ gorgias:
|
|
|
112
112
|
google:
|
|
113
113
|
SERVICE_ACCOUNT_JSON:
|
|
114
114
|
...
|
|
115
|
+
|
|
116
|
+
tagalys:
|
|
117
|
+
store:
|
|
118
|
+
BASE_URL: ____________________________ # Server URL
|
|
119
|
+
CLIENT_CODE: _________________________
|
|
120
|
+
API_KEY: _____________________________
|
|
121
|
+
STORE_ID: ____________________________
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// spinOff — scaffolds a new Mineral workspace by copying mineral's own
|
|
3
|
+
// structure into a target directory, then running `npm run dev` there.
|
|
4
|
+
// It is deliberately unopinionated: it only copies the files mineral ships
|
|
5
|
+
// (samples, gitignore, api/_example.js, hosting files) and writes a minimal
|
|
6
|
+
// package.json named after the directory. No bespoke templates.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// npm run spin_off -- <targetDir> [options]
|
|
10
|
+
// mineral spin_off <targetDir> [options]
|
|
11
|
+
//
|
|
12
|
+
// <targetDir> directory to scaffold (default: current directory)
|
|
13
|
+
//
|
|
14
|
+
// Options:
|
|
15
|
+
// --name, -n <name> name for package.json / Hello handler (default: from dir name)
|
|
16
|
+
// --force, -f overwrite files that already exist
|
|
17
|
+
// --no-install skip npm install
|
|
18
|
+
// --no-dev skip running npm run dev (implies --no-install)
|
|
19
|
+
// --help, -h show help
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const { spawn } = require('child_process');
|
|
24
|
+
|
|
25
|
+
const MINERAL_ROOT = path.join(__dirname, '..');
|
|
26
|
+
const MINERAL_VERSION = require('../package.json').version;
|
|
27
|
+
|
|
28
|
+
// --- What a new workspace needs ----------------------------------------------
|
|
29
|
+
|
|
30
|
+
// Folders to create.
|
|
31
|
+
const DIRECTORIES = [
|
|
32
|
+
'api',
|
|
33
|
+
'hosting',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Structure files mineral ships, copied verbatim (same relative path in both).
|
|
37
|
+
const FILES_TO_COPY = [
|
|
38
|
+
'.env.sample',
|
|
39
|
+
'.creds.yml.sample',
|
|
40
|
+
'.gitignore',
|
|
41
|
+
'.gcloudignore',
|
|
42
|
+
'api/_example.js',
|
|
43
|
+
'hosting/.hosting.yml.sample',
|
|
44
|
+
'hosting/wrappers.js',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// The .sample files are also copied to their real, git-ignored names so the
|
|
48
|
+
// workspace runs out of the box.
|
|
49
|
+
const SAMPLE_DUPLICATES = [
|
|
50
|
+
['.env.sample', '.env'],
|
|
51
|
+
['.creds.yml.sample', '.creds.yml'],
|
|
52
|
+
['hosting/.hosting.yml.sample', 'hosting/.hosting.yml'],
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
// --- Workspace-specific files (no mineral equivalent) ------------------------
|
|
56
|
+
|
|
57
|
+
// package.json is the one file that can't be copied — it must be named after
|
|
58
|
+
// the directory. Scripts point at the installed @foxtware/mineral.
|
|
59
|
+
const packageJson = (name) => JSON.stringify({
|
|
60
|
+
name,
|
|
61
|
+
version: '0.0.1',
|
|
62
|
+
private: true,
|
|
63
|
+
description: 'Private Mineral workspace.',
|
|
64
|
+
type: 'commonjs',
|
|
65
|
+
main: 'hosting/hosted.js',
|
|
66
|
+
scripts: {
|
|
67
|
+
new: 'node node_modules/@foxtware/mineral/_build_scripts/createNewFunction.js',
|
|
68
|
+
dev: 'node --watch --watch-path=./api --watch-path=./.creds.yml --watch-path=./.env --watch-path=./hosting node_modules/@foxtware/mineral/server.js --workspace . --api_dirs api',
|
|
69
|
+
hosting_preview: 'node --watch --watch-path=./api --watch-path=./hosting/.hosting.yml --watch-path=./hosting/wrappers.js --watch-path=./.creds.yml --watch-path=./.env node_modules/@foxtware/mineral/bin/mineral.js hosting_preview --workspace . --api_dirs api',
|
|
70
|
+
tunnel: 'ngrok http http://localhost:8000',
|
|
71
|
+
serve: 'PORT=8100 mineral --workspace . --api_dirs api',
|
|
72
|
+
host: 'mineral host --workspace . --api_dirs api',
|
|
73
|
+
},
|
|
74
|
+
dependencies: {
|
|
75
|
+
'@foxtware/mineral': `^${ MINERAL_VERSION }`,
|
|
76
|
+
},
|
|
77
|
+
devDependencies: {
|
|
78
|
+
ngrok: '^5.0.0-beta.2',
|
|
79
|
+
},
|
|
80
|
+
}, null, 2) + '\n';
|
|
81
|
+
|
|
82
|
+
// A tiny Hello handler so the route list isn't empty on first boot.
|
|
83
|
+
const helloHandler = (name) => (
|
|
84
|
+
`const ${ name }Hi = async () => {
|
|
85
|
+
console.log('hi');
|
|
86
|
+
return {
|
|
87
|
+
ok: true,
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
${ name }Hi,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
curl -X POST "http://localhost:8000/${ name }Hi"
|
|
97
|
+
*/
|
|
98
|
+
`
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// --- Small helpers -----------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
const printHelp = () => {
|
|
104
|
+
console.log(`
|
|
105
|
+
Usage:
|
|
106
|
+
npm run spin_off -- <targetDir> [options]
|
|
107
|
+
mineral spin_off <targetDir> [options]
|
|
108
|
+
|
|
109
|
+
Spins off a new Mineral workspace by copying mineral's structure (samples,
|
|
110
|
+
.gitignore, api/_example.js, hosting files) into <targetDir> and running
|
|
111
|
+
"npm run dev" there.
|
|
112
|
+
|
|
113
|
+
Arguments:
|
|
114
|
+
<targetDir> directory to scaffold. Defaults to the current directory.
|
|
115
|
+
|
|
116
|
+
Options:
|
|
117
|
+
--name, -n <name> name for package.json / Hello handler (default: from dir name)
|
|
118
|
+
--force, -f overwrite files that already exist
|
|
119
|
+
--no-install skip npm install
|
|
120
|
+
--no-dev skip running npm run dev (implies --no-install)
|
|
121
|
+
--help, -h show this help
|
|
122
|
+
|
|
123
|
+
Examples:
|
|
124
|
+
npm run spin_off -- myworkspace
|
|
125
|
+
mineral spin_off . # when already cd'd into an empty target dir
|
|
126
|
+
`);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const parseArgs = (argv) => {
|
|
130
|
+
const args = {
|
|
131
|
+
target: null,
|
|
132
|
+
name: null,
|
|
133
|
+
force: false,
|
|
134
|
+
install: true,
|
|
135
|
+
dev: true,
|
|
136
|
+
help: false,
|
|
137
|
+
};
|
|
138
|
+
const positional = [];
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < argv.length; i++) {
|
|
141
|
+
const arg = argv[i];
|
|
142
|
+
if (arg === 'spinoff') {
|
|
143
|
+
continue; // subcommand token when invoked via bin/mineral.js
|
|
144
|
+
}
|
|
145
|
+
if (arg === '--help' || arg === '-h') {
|
|
146
|
+
args.help = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (arg === '--force' || arg === '-f') {
|
|
150
|
+
args.force = true;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (arg === '--no-install') {
|
|
154
|
+
args.install = false;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (arg === '--no-dev') {
|
|
158
|
+
args.dev = false;
|
|
159
|
+
args.install = false;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (arg === '--name' || arg === '-n') {
|
|
163
|
+
args.name = argv[++i];
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (arg.startsWith('--name=')) {
|
|
167
|
+
args.name = arg.slice('--name='.length);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (arg.startsWith('-')) {
|
|
171
|
+
args.help = true;
|
|
172
|
+
console.error(`Unknown option: ${ arg }`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
positional.push(arg);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (args.target === null) {
|
|
179
|
+
args.target = positional[0] || process.cwd();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return args;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const toCamelCase = (input) => input
|
|
186
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
187
|
+
.trim()
|
|
188
|
+
.split(' ')
|
|
189
|
+
.filter(Boolean)
|
|
190
|
+
.map((word, index) => (
|
|
191
|
+
index === 0
|
|
192
|
+
? word.toLowerCase()
|
|
193
|
+
: `${ word[0].toUpperCase() }${ word.slice(1).toLowerCase() }`
|
|
194
|
+
))
|
|
195
|
+
.join('');
|
|
196
|
+
|
|
197
|
+
const isValidIdentifier = (value) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value);
|
|
198
|
+
|
|
199
|
+
const deriveName = (dirName) => {
|
|
200
|
+
const name = toCamelCase(dirName);
|
|
201
|
+
if (!name || !isValidIdentifier(name)) {
|
|
202
|
+
throw new Error(`Could not make a valid name from "${ dirName }". Use --name <name>.`);
|
|
203
|
+
}
|
|
204
|
+
return name;
|
|
205
|
+
};
|
|
206
|
+
const writeFile = (target, relativePath, contents, { force }) => {
|
|
207
|
+
const absolutePath = path.join(target, relativePath);
|
|
208
|
+
if (!force && fs.existsSync(absolutePath)) {
|
|
209
|
+
console.log(` - ${ relativePath } (exists, kept)`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
213
|
+
fs.writeFileSync(absolutePath, contents);
|
|
214
|
+
console.log(` + ${ relativePath }`);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const copyFile = (relativePath, target, { force }) => {
|
|
218
|
+
const from = path.join(MINERAL_ROOT, relativePath);
|
|
219
|
+
const to = path.join(target, relativePath);
|
|
220
|
+
if (!force && fs.existsSync(to)) {
|
|
221
|
+
console.log(` - ${ relativePath } (exists, kept)`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
225
|
+
fs.copyFileSync(from, to);
|
|
226
|
+
console.log(` + ${ relativePath } (copied from mineral)`);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const runInDir = (command, args, cwd) => new Promise((resolve, reject) => {
|
|
230
|
+
const child = spawn(command, args, { cwd, stdio: 'inherit' });
|
|
231
|
+
child.on('close', (code) => {
|
|
232
|
+
if (code === 0) {
|
|
233
|
+
resolve();
|
|
234
|
+
} else {
|
|
235
|
+
reject(new Error(`"${ command } ${ args.join(' ') }" exited with code ${ code }`));
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
child.on('error', reject);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// `npm run dev` is a long-running watch server — stay attached to it.
|
|
242
|
+
const runDev = (cwd) => new Promise((resolve) => {
|
|
243
|
+
const child = spawn('npm', ['run', 'dev'], { cwd, stdio: 'inherit' });
|
|
244
|
+
child.on('close', resolve);
|
|
245
|
+
child.on('error', (error) => {
|
|
246
|
+
console.error(`Failed to start npm run dev: ${ error.message }`);
|
|
247
|
+
resolve();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// --- Main --------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
const spinOff = async (argv = process.argv.slice(2)) => {
|
|
254
|
+
const args = parseArgs(argv);
|
|
255
|
+
if (args.help) {
|
|
256
|
+
printHelp();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const target = path.resolve(args.target);
|
|
261
|
+
const name = args.name || deriveName(path.basename(target));
|
|
262
|
+
|
|
263
|
+
console.log(`\nSpinning off "${ name }" into ${ target }\n`);
|
|
264
|
+
|
|
265
|
+
// 1. Create the target directory itself if it doesn't exist yet.
|
|
266
|
+
fs.mkdirSync(target, { recursive: true });
|
|
267
|
+
console.log(` (directory ready) ${ target }`);
|
|
268
|
+
|
|
269
|
+
// 2. Create the folders.
|
|
270
|
+
console.log('Folders:');
|
|
271
|
+
for (const dir of DIRECTORIES) {
|
|
272
|
+
fs.mkdirSync(path.join(target, dir), { recursive: true });
|
|
273
|
+
console.log(` + ${ dir }/`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 3. Copy mineral's structure.
|
|
277
|
+
console.log('\nStructure (copied from mineral):');
|
|
278
|
+
for (const relativePath of FILES_TO_COPY) {
|
|
279
|
+
copyFile(relativePath, target, { force: args.force });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 4. Workspace-specific files.
|
|
283
|
+
console.log('\nGenerated:');
|
|
284
|
+
writeFile(target, 'package.json', packageJson(name), { force: args.force });
|
|
285
|
+
writeFile(target, path.join('api', `${ name }Hi.js`), helloHandler(name), { force: args.force });
|
|
286
|
+
|
|
287
|
+
// 5. Duplicate samples to their real, git-ignored names.
|
|
288
|
+
console.log('\nSamples -> real files:');
|
|
289
|
+
for (const [from, to] of SAMPLE_DUPLICATES) {
|
|
290
|
+
const fromPath = path.join(target, from);
|
|
291
|
+
const toPath = path.join(target, to);
|
|
292
|
+
if (!fs.existsSync(fromPath) || fs.existsSync(toPath)) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
fs.copyFileSync(fromPath, toPath);
|
|
296
|
+
console.log(` + ${ to } (copied from ${ from })`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!args.install) {
|
|
300
|
+
console.log('\nSkipped npm install / npm run dev.\n');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 5. Get up and running.
|
|
305
|
+
console.log('\nInstalling dependencies...\n');
|
|
306
|
+
await runInDir('npm', ['install'], target);
|
|
307
|
+
|
|
308
|
+
console.log('\nStarting "npm run dev"...\n');
|
|
309
|
+
await runDev(target);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
module.exports = {
|
|
313
|
+
spinOff,
|
|
314
|
+
deriveName,
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
if (require.main === module) {
|
|
318
|
+
spinOff().catch((error) => {
|
|
319
|
+
console.error(error.message || error);
|
|
320
|
+
process.exitCode = 1;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
package/bin/mineral.js
CHANGED
|
@@ -17,6 +17,15 @@ if (command === 'hosting_preview') {
|
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
if (command === 'spin_off') {
|
|
21
|
+
const { spinOff } = require('../_build_scripts/spinOff');
|
|
22
|
+
spinOff().catch((error) => {
|
|
23
|
+
console.error(error.message || error);
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
const { startServer } = require('../server');
|
|
21
30
|
|
|
22
31
|
startServer();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foxtware/mineral",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"bin": {
|
|
5
5
|
"mineral": "bin/mineral.js"
|
|
6
6
|
},
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"hosting_preview": "node --watch --watch-path=./api --watch-path=./hosting/.hosting.yml --watch-path=./hosting/wrappers.js --watch-path=./.creds.yml hosting/hostingPreview.js",
|
|
15
15
|
"tunnel": "ngrok http http://localhost:8000",
|
|
16
16
|
"new": "node _build_scripts/createNewFunction.js",
|
|
17
|
+
"spin_off": "node _build_scripts/spinOff.js",
|
|
17
18
|
"serve": "PORT=8100 node server.js",
|
|
18
19
|
"start": "node server.js",
|
|
19
20
|
"npm:publish": "node _build_scripts/publish.js"
|
package/tagalys/.gitkeep
ADDED
|
File without changes
|