@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 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.20",
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"
File without changes