@friggframework/devtools 2.0.0--canary.657.e372ef6.0 → 2.0.0--canary.639.1315452.0
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/frigg-cli/README.md +42 -12
- package/frigg-cli/__tests__/unit/commands/init.test.js +489 -0
- package/frigg-cli/index.js +9 -4
- package/frigg-cli/init-command/backend-first-handler.js +378 -546
- package/frigg-cli/init-command/deployment-modes.js +7 -0
- package/frigg-cli/init-command/index.js +70 -33
- package/frigg-cli/init-command/scaffold-dependencies.js +167 -0
- package/frigg-cli/templates/backend/README.md +106 -0
- package/frigg-cli/templates/backend/env.example +12 -0
- package/frigg-cli/templates/backend/gitignore +10 -0
- package/frigg-cli/templates/backend/index.js +71 -0
- package/frigg-cli/templates/backend/infrastructure.js +20 -0
- package/frigg-cli/test/init-command.test.js +1 -1
- package/infrastructure/domains/integration/integration-builder.test.js +0 -33
- package/infrastructure/domains/scheduler/scheduler-builder.js +1 -7
- package/infrastructure/domains/scheduler/scheduler-builder.test.js +0 -55
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +0 -1
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +0 -12
- package/package.json +9 -7
package/frigg-cli/README.md
CHANGED
|
@@ -10,29 +10,59 @@ The Frigg CLI provides tools for building, deploying, and managing serverless in
|
|
|
10
10
|
|
|
11
11
|
### Core Commands
|
|
12
12
|
|
|
13
|
-
#### `frigg init [options]`
|
|
13
|
+
#### `frigg init [projectName] [options]`
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Initialize a new Frigg application with scaffolding and configuration.
|
|
15
|
+
Scaffold a new Frigg backend application that installs and runs out of the box.
|
|
18
16
|
|
|
19
17
|
**Usage:**
|
|
18
|
+
|
|
20
19
|
```bash
|
|
21
|
-
frigg init
|
|
22
|
-
frigg init my-app
|
|
23
|
-
frigg init --
|
|
20
|
+
frigg init my-app # interactive (when stdin is a TTY)
|
|
21
|
+
frigg init my-app --yes # accept every default, no prompts
|
|
22
|
+
frigg init my-app --yes --mode standalone --no-install --no-git
|
|
23
|
+
frigg init my-app --mode embedded # add to an existing project (./frigg-integration)
|
|
24
|
+
npx @friggframework/devtools init my-app --yes # without a global install
|
|
24
25
|
```
|
|
25
26
|
|
|
26
27
|
**What it does:**
|
|
27
|
-
|
|
28
|
+
|
|
29
|
+
1. Validates the project name and `--mode`, and checks that the target directory is
|
|
30
|
+
empty (only `.git`, `.gitignore`, `README.md` and `.DS_Store` may exist) before asking anything else
|
|
31
|
+
2. Copies the backend template: `index.js` (app definition, named after the project),
|
|
32
|
+
`infrastructure.js`, `README.md`, `.gitignore`, `.env.example` (also seeded into `.env`)
|
|
33
|
+
3. Writes `package.json` with:
|
|
34
|
+
- scripts: `start` (`frigg start`), `build` (`frigg build`), `deploy` (`frigg deploy`),
|
|
35
|
+
`db:setup` (`frigg db:setup`), `test` (`jest`)
|
|
36
|
+
- dependencies: `@friggframework/core`, `@prisma/client`, `prisma`
|
|
37
|
+
- devDependencies: `@friggframework/devtools`, `@friggframework/serverless-plugin`,
|
|
38
|
+
`osls`, and the serverless plugins the composed definition loads
|
|
39
|
+
- Frigg package versions match the devtools that ran `init` (a prerelease is pinned exactly)
|
|
40
|
+
4. Validates the generated app definition against the app-definition schema
|
|
41
|
+
5. Optionally initializes git and runs `npm install`
|
|
42
|
+
6. Prints next steps. API modules picked in the interactive prompt are not wired in
|
|
43
|
+
automatically; the next steps list a `frigg install <module>` command for each
|
|
44
|
+
|
|
45
|
+
In `embedded` mode the backend is written to `./frigg-integration` inside the target
|
|
46
|
+
directory, which may already contain your application. Git is not initialized.
|
|
28
47
|
|
|
29
48
|
**Options:**
|
|
30
|
-
- TBD
|
|
31
49
|
|
|
32
|
-
|
|
33
|
-
-
|
|
50
|
+
- `-n, --name <name>` - Project name (alternative to the positional argument)
|
|
51
|
+
- `-m, --mode <mode>` - `standalone` (default) or `embedded`
|
|
52
|
+
- `-y, --yes` - Accept the default for every prompt. Implied when stdin is not a TTY
|
|
53
|
+
- `--no-install` - Skip `npm install`
|
|
54
|
+
- `--no-git` - Skip `git init` and the initial commit
|
|
55
|
+
- `-f, --force` - Scaffold into a non-empty directory and overwrite an existing `README.md`/`.gitignore`
|
|
56
|
+
- `-v, --verbose` - Verbose output
|
|
34
57
|
|
|
35
|
-
|
|
58
|
+
**Next steps after `init`:**
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
cd my-app
|
|
62
|
+
docker run --name frigg-postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres:16
|
|
63
|
+
npm run db:setup
|
|
64
|
+
npm start
|
|
65
|
+
```
|
|
36
66
|
|
|
37
67
|
---
|
|
38
68
|
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `frigg init`
|
|
3
|
+
*
|
|
4
|
+
* - commander registration (flags exposed on `frigg init --help`)
|
|
5
|
+
* - initCommand argument handling (name, --mode, legacy --template)
|
|
6
|
+
* - BackendFirstHandler scaffolding with --yes into a temp directory
|
|
7
|
+
* - scaffold dependency/version derivation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const fs = require('fs-extra');
|
|
13
|
+
const { spawnSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
const CLI_DIR = path.join(__dirname, '..', '..', '..');
|
|
16
|
+
const CLI_ENTRY = path.join(CLI_DIR, 'index.js');
|
|
17
|
+
const DEVTOOLS_PACKAGE_JSON = require('../../../../package.json');
|
|
18
|
+
|
|
19
|
+
const makeTmpDir = () =>
|
|
20
|
+
fs.mkdtempSync(path.join(os.tmpdir(), 'frigg-init-test-'));
|
|
21
|
+
|
|
22
|
+
/** Prompt implementations that fail the test if any prompt is shown. */
|
|
23
|
+
const failingPrompts = () => ({
|
|
24
|
+
select: jest.fn(() => {
|
|
25
|
+
throw new Error('select prompt should not be shown');
|
|
26
|
+
}),
|
|
27
|
+
confirm: jest.fn(() => {
|
|
28
|
+
throw new Error('confirm prompt should not be shown');
|
|
29
|
+
}),
|
|
30
|
+
checkbox: jest.fn(() => {
|
|
31
|
+
throw new Error('checkbox prompt should not be shown');
|
|
32
|
+
}),
|
|
33
|
+
Separator: class {},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('frigg init', () => {
|
|
37
|
+
let consoleLogSpy;
|
|
38
|
+
let consoleErrorSpy;
|
|
39
|
+
let processExitSpy;
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
jest.resetModules();
|
|
43
|
+
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
44
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
45
|
+
processExitSpy = jest.spyOn(process, 'exit').mockImplementation();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
jest.restoreAllMocks();
|
|
50
|
+
jest.dontMock('../../../init-command/backend-first-handler');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const output = () =>
|
|
54
|
+
[...consoleLogSpy.mock.calls, ...consoleErrorSpy.mock.calls]
|
|
55
|
+
.map((args) => args.join(' '))
|
|
56
|
+
.join('\n');
|
|
57
|
+
|
|
58
|
+
describe('commander registration', () => {
|
|
59
|
+
it('exposes the init options on `frigg init --help`', () => {
|
|
60
|
+
const result = spawnSync(
|
|
61
|
+
process.execPath,
|
|
62
|
+
[CLI_ENTRY, 'init', '--help'],
|
|
63
|
+
{
|
|
64
|
+
cwd: os.tmpdir(),
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
env: {
|
|
67
|
+
...process.env,
|
|
68
|
+
FRIGG_CLI_SKIP_VERSION_CHECK: 'true',
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(result.status).toBe(0);
|
|
74
|
+
const help = result.stdout;
|
|
75
|
+
expect(help).toMatch(/init \[options\] \[projectName\]/);
|
|
76
|
+
expect(help).toMatch(/-y, --yes/);
|
|
77
|
+
expect(help).toMatch(/-m, --mode <mode>/);
|
|
78
|
+
expect(help).toMatch(/--no-install/);
|
|
79
|
+
expect(help).toMatch(/--no-git/);
|
|
80
|
+
expect(help).toMatch(/-f, --force/);
|
|
81
|
+
expect(help).toMatch(/-n, --name <name>/);
|
|
82
|
+
// The demo frontend is gone.
|
|
83
|
+
expect(help).not.toMatch(/frontend/);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('initCommand', () => {
|
|
88
|
+
const mockHandler = () => {
|
|
89
|
+
const initialize = jest.fn().mockResolvedValue();
|
|
90
|
+
const Handler = jest.fn().mockImplementation(() => ({
|
|
91
|
+
initialize,
|
|
92
|
+
}));
|
|
93
|
+
jest.doMock(
|
|
94
|
+
'../../../init-command/backend-first-handler',
|
|
95
|
+
() => Handler
|
|
96
|
+
);
|
|
97
|
+
return { Handler, initialize };
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
it('runs the backend-first handler by default, not the legacy branch', async () => {
|
|
101
|
+
const { Handler, initialize } = mockHandler();
|
|
102
|
+
const { initCommand } = require('../../../init-command');
|
|
103
|
+
const target = path.join(makeTmpDir(), 'my-app');
|
|
104
|
+
|
|
105
|
+
await initCommand(target, { yes: true });
|
|
106
|
+
|
|
107
|
+
expect(Handler).toHaveBeenCalledWith(
|
|
108
|
+
target,
|
|
109
|
+
expect.objectContaining({ yes: true })
|
|
110
|
+
);
|
|
111
|
+
expect(initialize).toHaveBeenCalled();
|
|
112
|
+
expect(processExitSpy).not.toHaveBeenCalled();
|
|
113
|
+
expect(output()).not.toMatch(/Legacy template/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('accepts the project name from --name', async () => {
|
|
117
|
+
const { Handler } = mockHandler();
|
|
118
|
+
const { initCommand } = require('../../../init-command');
|
|
119
|
+
const target = path.join(makeTmpDir(), 'named-app');
|
|
120
|
+
|
|
121
|
+
await initCommand(undefined, { name: target });
|
|
122
|
+
|
|
123
|
+
expect(Handler).toHaveBeenCalledWith(target, expect.any(Object));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('exits with a message when no project name is given', async () => {
|
|
127
|
+
const { Handler } = mockHandler();
|
|
128
|
+
const { initCommand } = require('../../../init-command');
|
|
129
|
+
|
|
130
|
+
await initCommand(undefined, {});
|
|
131
|
+
|
|
132
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
133
|
+
expect(output()).toMatch(/Please specify a project name/);
|
|
134
|
+
expect(Handler).not.toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('rejects an invalid --mode before scaffolding', async () => {
|
|
138
|
+
const { Handler } = mockHandler();
|
|
139
|
+
const { initCommand } = require('../../../init-command');
|
|
140
|
+
const target = path.join(makeTmpDir(), 'my-app');
|
|
141
|
+
|
|
142
|
+
await initCommand(target, { mode: 'serverless' });
|
|
143
|
+
|
|
144
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
145
|
+
expect(output()).toMatch(
|
|
146
|
+
/Invalid --mode "serverless". Expected one of: standalone, embedded/
|
|
147
|
+
);
|
|
148
|
+
expect(Handler).not.toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('rejects the legacy --template option', async () => {
|
|
152
|
+
const { Handler } = mockHandler();
|
|
153
|
+
const { initCommand } = require('../../../init-command');
|
|
154
|
+
|
|
155
|
+
await initCommand('my-app', { template: 'react' });
|
|
156
|
+
|
|
157
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
158
|
+
expect(output()).toMatch(/Legacy template system/);
|
|
159
|
+
expect(Handler).not.toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('BackendFirstHandler', () => {
|
|
164
|
+
let tmpDir;
|
|
165
|
+
|
|
166
|
+
beforeEach(() => {
|
|
167
|
+
tmpDir = makeTmpDir();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
afterEach(() => {
|
|
171
|
+
fs.removeSync(tmpDir);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const createHandler = (target, options = {}) => {
|
|
175
|
+
const BackendFirstHandler = require('../../../init-command/backend-first-handler');
|
|
176
|
+
return new BackendFirstHandler(target, {
|
|
177
|
+
yes: true,
|
|
178
|
+
install: false,
|
|
179
|
+
git: false,
|
|
180
|
+
prompts: failingPrompts(),
|
|
181
|
+
...options,
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
it('--yes scaffolds a standalone app without prompting', async () => {
|
|
186
|
+
const target = path.join(tmpDir, 'demo-app');
|
|
187
|
+
const handler = createHandler(target, { mode: 'standalone' });
|
|
188
|
+
|
|
189
|
+
await handler.initialize();
|
|
190
|
+
|
|
191
|
+
for (const file of [
|
|
192
|
+
'index.js',
|
|
193
|
+
'infrastructure.js',
|
|
194
|
+
'package.json',
|
|
195
|
+
'README.md',
|
|
196
|
+
'.gitignore',
|
|
197
|
+
'.env.example',
|
|
198
|
+
'.env',
|
|
199
|
+
]) {
|
|
200
|
+
expect(fs.existsSync(path.join(target, file))).toBe(true);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const indexJs = fs.readFileSync(
|
|
204
|
+
path.join(target, 'index.js'),
|
|
205
|
+
'utf8'
|
|
206
|
+
);
|
|
207
|
+
expect(indexJs).toContain("name: 'demo-app',");
|
|
208
|
+
expect(indexJs).not.toContain("name: 'frigg-app'");
|
|
209
|
+
|
|
210
|
+
const readme = fs.readFileSync(
|
|
211
|
+
path.join(target, 'README.md'),
|
|
212
|
+
'utf8'
|
|
213
|
+
);
|
|
214
|
+
expect(readme.startsWith('# demo-app\n')).toBe(true);
|
|
215
|
+
|
|
216
|
+
const pkg = fs.readJsonSync(path.join(target, 'package.json'));
|
|
217
|
+
expect(pkg.name).toBe('demo-app');
|
|
218
|
+
expect(pkg.scripts).toEqual(
|
|
219
|
+
expect.objectContaining({
|
|
220
|
+
start: 'frigg start',
|
|
221
|
+
build: 'frigg build',
|
|
222
|
+
deploy: 'frigg deploy',
|
|
223
|
+
test: expect.stringMatching(/^jest/),
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
expect(JSON.stringify(pkg.scripts)).not.toMatch(
|
|
227
|
+
/infrastructure\.js/
|
|
228
|
+
);
|
|
229
|
+
expect(Object.keys(pkg.dependencies)).toEqual(
|
|
230
|
+
expect.arrayContaining([
|
|
231
|
+
'@friggframework/core',
|
|
232
|
+
'@prisma/client',
|
|
233
|
+
'prisma',
|
|
234
|
+
])
|
|
235
|
+
);
|
|
236
|
+
expect(Object.keys(pkg.devDependencies)).toEqual(
|
|
237
|
+
expect.arrayContaining([
|
|
238
|
+
'@friggframework/devtools',
|
|
239
|
+
'@friggframework/serverless-plugin',
|
|
240
|
+
'osls',
|
|
241
|
+
'serverless-esbuild',
|
|
242
|
+
'serverless-offline',
|
|
243
|
+
'serverless-offline-sqs',
|
|
244
|
+
'serverless-dotenv-plugin',
|
|
245
|
+
])
|
|
246
|
+
);
|
|
247
|
+
expect(pkg.workspaces).toBeUndefined();
|
|
248
|
+
expect(fs.existsSync(path.join(target, 'frontend'))).toBe(false);
|
|
249
|
+
expect(handler.prompts.select).not.toHaveBeenCalled();
|
|
250
|
+
expect(handler.prompts.confirm).not.toHaveBeenCalled();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('produces a Definition that loads and passes schema validation', async () => {
|
|
254
|
+
const target = path.join(tmpDir, 'valid-app');
|
|
255
|
+
await createHandler(target).initialize();
|
|
256
|
+
|
|
257
|
+
const { Definition } = require(path.join(target, 'index.js'));
|
|
258
|
+
const {
|
|
259
|
+
validateAppDefinition,
|
|
260
|
+
} = require('@friggframework/schemas');
|
|
261
|
+
|
|
262
|
+
expect(Definition.name).toBe('valid-app');
|
|
263
|
+
expect(validateAppDefinition(Definition).valid).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('uses defaults when stdin is not a TTY even without --yes', () => {
|
|
267
|
+
const BackendFirstHandler = require('../../../init-command/backend-first-handler');
|
|
268
|
+
const original = process.stdin.isTTY;
|
|
269
|
+
try {
|
|
270
|
+
process.stdin.isTTY = false;
|
|
271
|
+
expect(new BackendFirstHandler(tmpDir, {}).interactive).toBe(
|
|
272
|
+
false
|
|
273
|
+
);
|
|
274
|
+
process.stdin.isTTY = true;
|
|
275
|
+
expect(new BackendFirstHandler(tmpDir, {}).interactive).toBe(
|
|
276
|
+
true
|
|
277
|
+
);
|
|
278
|
+
expect(
|
|
279
|
+
new BackendFirstHandler(tmpDir, { yes: true }).interactive
|
|
280
|
+
).toBe(false);
|
|
281
|
+
} finally {
|
|
282
|
+
process.stdin.isTTY = original;
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('rejects an invalid mode', async () => {
|
|
287
|
+
const handler = createHandler(path.join(tmpDir, 'x'), {
|
|
288
|
+
mode: 'bogus',
|
|
289
|
+
});
|
|
290
|
+
await expect(handler.selectDeploymentMode()).rejects.toThrow(
|
|
291
|
+
/Invalid --mode "bogus"/
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('checks for a non-empty directory before prompting', async () => {
|
|
296
|
+
const target = path.join(tmpDir, 'existing');
|
|
297
|
+
fs.outputFileSync(path.join(target, 'server.js'), '// app');
|
|
298
|
+
const handler = createHandler(target, {
|
|
299
|
+
yes: false,
|
|
300
|
+
interactive: true,
|
|
301
|
+
mode: 'standalone',
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await expect(handler.initialize()).rejects.toThrow(
|
|
305
|
+
'Directory not empty'
|
|
306
|
+
);
|
|
307
|
+
expect(handler.prompts.select).not.toHaveBeenCalled();
|
|
308
|
+
expect(handler.prompts.confirm).not.toHaveBeenCalled();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('keeps an existing README.md unless --force', async () => {
|
|
312
|
+
const target = path.join(tmpDir, 'with-readme');
|
|
313
|
+
fs.outputFileSync(path.join(target, 'README.md'), 'mine');
|
|
314
|
+
|
|
315
|
+
await createHandler(target).initialize();
|
|
316
|
+
expect(
|
|
317
|
+
fs.readFileSync(path.join(target, 'README.md'), 'utf8')
|
|
318
|
+
).toBe('mine');
|
|
319
|
+
|
|
320
|
+
const forced = path.join(tmpDir, 'with-readme-forced');
|
|
321
|
+
fs.outputFileSync(path.join(forced, 'README.md'), 'mine');
|
|
322
|
+
await createHandler(forced, { force: true }).initialize();
|
|
323
|
+
expect(
|
|
324
|
+
fs.readFileSync(path.join(forced, 'README.md'), 'utf8')
|
|
325
|
+
).toMatch(/^# with-readme-forced/);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('scaffolds embedded mode into frigg-integration/ of an existing project', async () => {
|
|
329
|
+
const target = path.join(tmpDir, 'existing-app');
|
|
330
|
+
fs.outputFileSync(path.join(target, 'server.js'), '// app');
|
|
331
|
+
|
|
332
|
+
await createHandler(target, { mode: 'embedded' }).initialize();
|
|
333
|
+
|
|
334
|
+
const dir = path.join(target, 'frigg-integration');
|
|
335
|
+
const pkg = fs.readJsonSync(path.join(dir, 'package.json'));
|
|
336
|
+
expect(pkg.name).toBe('existing-app-frigg-integration');
|
|
337
|
+
expect(pkg.scripts.start).toBe('frigg start');
|
|
338
|
+
expect(pkg.dependencies['@friggframework/core']).toBeDefined();
|
|
339
|
+
expect(fs.existsSync(path.join(dir, 'infrastructure.js'))).toBe(
|
|
340
|
+
true
|
|
341
|
+
);
|
|
342
|
+
expect(
|
|
343
|
+
fs.readFileSync(path.join(target, 'server.js'), 'utf8')
|
|
344
|
+
).toBe('// app');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('turns selected API modules into `frigg install` next steps without editing the app', async () => {
|
|
348
|
+
jest.doMock('../../../utils/npm-registry', () => ({
|
|
349
|
+
getModulesByType: jest.fn().mockResolvedValue({
|
|
350
|
+
CRM: [
|
|
351
|
+
{
|
|
352
|
+
name: '@friggframework/api-module-hubspot',
|
|
353
|
+
integrationName: 'HubSpot',
|
|
354
|
+
description: 'CRM',
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
}),
|
|
358
|
+
}));
|
|
359
|
+
const answers = {
|
|
360
|
+
'What are you building with Frigg?': 'exploring',
|
|
361
|
+
'Which cloud provider will you use?': 'aws',
|
|
362
|
+
};
|
|
363
|
+
const prompts = {
|
|
364
|
+
select: jest.fn(async ({ message }) => answers[message]),
|
|
365
|
+
confirm: jest.fn(async ({ message }) =>
|
|
366
|
+
message.startsWith('Would you like to pick API modules')
|
|
367
|
+
),
|
|
368
|
+
checkbox: jest.fn(async () => ['hubspot']),
|
|
369
|
+
Separator: class {},
|
|
370
|
+
};
|
|
371
|
+
const target = path.join(tmpDir, 'with-modules');
|
|
372
|
+
const handler = createHandler(target, {
|
|
373
|
+
yes: false,
|
|
374
|
+
interactive: true,
|
|
375
|
+
mode: 'standalone',
|
|
376
|
+
prompts,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
await handler.initialize();
|
|
380
|
+
|
|
381
|
+
const pkg = fs.readJsonSync(path.join(target, 'package.json'));
|
|
382
|
+
expect(
|
|
383
|
+
Object.keys(pkg.dependencies).filter((d) =>
|
|
384
|
+
d.includes('api-module')
|
|
385
|
+
)
|
|
386
|
+
).toEqual([]);
|
|
387
|
+
const indexJs = fs.readFileSync(
|
|
388
|
+
path.join(target, 'index.js'),
|
|
389
|
+
'utf8'
|
|
390
|
+
);
|
|
391
|
+
expect(indexJs).not.toMatch(
|
|
392
|
+
/HubspotIntegration|src\/integrations\/Hub/
|
|
393
|
+
);
|
|
394
|
+
expect(output()).toMatch(/npx frigg install hubspot/);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
describe('scaffold dependencies', () => {
|
|
399
|
+
const {
|
|
400
|
+
getFriggVersionRange,
|
|
401
|
+
getScaffoldDependencies,
|
|
402
|
+
} = require('../../../init-command/scaffold-dependencies');
|
|
403
|
+
|
|
404
|
+
it.each([
|
|
405
|
+
['2.0.0-next.115', '2.0.0-next.115'],
|
|
406
|
+
['2.0.0--canary.639.abc1234.0', '2.0.0--canary.639.abc1234.0'],
|
|
407
|
+
['2.0.1', '^2.0.1'],
|
|
408
|
+
['2.3.4', '^2.3.4'],
|
|
409
|
+
// The deprecated core@2.0.0 must never be admitted.
|
|
410
|
+
['2.0.0', '^2.0.1'],
|
|
411
|
+
])(
|
|
412
|
+
'derives the Frigg range for devtools %s as %s',
|
|
413
|
+
(version, range) => {
|
|
414
|
+
expect(getFriggVersionRange(version)).toBe(range);
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
it('rejects an invalid version', () => {
|
|
419
|
+
expect(() => getFriggVersionRange('not-a-version')).toThrow(
|
|
420
|
+
/Invalid/
|
|
421
|
+
);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('reads third-party versions from devtools and Frigg ranges from its version', () => {
|
|
425
|
+
const { dependencies, devDependencies } = getScaffoldDependencies();
|
|
426
|
+
const friggRange = getFriggVersionRange(
|
|
427
|
+
DEVTOOLS_PACKAGE_JSON.version
|
|
428
|
+
);
|
|
429
|
+
const devtoolsDeps = {
|
|
430
|
+
...DEVTOOLS_PACKAGE_JSON.dependencies,
|
|
431
|
+
...DEVTOOLS_PACKAGE_JSON.devDependencies,
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
expect(dependencies['@friggframework/core']).toBe(friggRange);
|
|
435
|
+
expect(devDependencies['@friggframework/devtools']).toBe(
|
|
436
|
+
friggRange
|
|
437
|
+
);
|
|
438
|
+
expect(devDependencies['@friggframework/serverless-plugin']).toBe(
|
|
439
|
+
friggRange
|
|
440
|
+
);
|
|
441
|
+
for (const name of [
|
|
442
|
+
'osls',
|
|
443
|
+
'serverless-esbuild',
|
|
444
|
+
'serverless-offline',
|
|
445
|
+
'serverless-offline-sqs',
|
|
446
|
+
'serverless-dotenv-plugin',
|
|
447
|
+
]) {
|
|
448
|
+
expect(devDependencies[name]).toBe(devtoolsDeps[name]);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('covers every serverless plugin the composed definition loads', () => {
|
|
453
|
+
const { devDependencies } = getScaffoldDependencies();
|
|
454
|
+
const {
|
|
455
|
+
createBaseDefinition,
|
|
456
|
+
} = require('../../../../infrastructure/domains/shared/utilities/base-definition-factory');
|
|
457
|
+
const originalArgv = process.argv;
|
|
458
|
+
try {
|
|
459
|
+
// Include the offline-only dotenv plugin.
|
|
460
|
+
process.argv = [...originalArgv, 'offline'];
|
|
461
|
+
const definition = createBaseDefinition(
|
|
462
|
+
{ name: 'x' },
|
|
463
|
+
{},
|
|
464
|
+
{},
|
|
465
|
+
{}
|
|
466
|
+
);
|
|
467
|
+
for (const plugin of definition.plugins) {
|
|
468
|
+
expect(devDependencies[plugin]).toBeDefined();
|
|
469
|
+
}
|
|
470
|
+
} finally {
|
|
471
|
+
process.argv = originalArgv;
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('takes the Prisma versions from core', () => {
|
|
476
|
+
const { dependencies } = getScaffoldDependencies({
|
|
477
|
+
devtoolsPackageJson: DEVTOOLS_PACKAGE_JSON,
|
|
478
|
+
corePackageJson: {
|
|
479
|
+
peerDependencies: {
|
|
480
|
+
'@prisma/client': '^9.9.9',
|
|
481
|
+
prisma: '^9.9.9',
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
expect(dependencies['@prisma/client']).toBe('^9.9.9');
|
|
486
|
+
expect(dependencies.prisma).toBe('^9.9.9');
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
});
|
package/frigg-cli/index.js
CHANGED
|
@@ -91,11 +91,16 @@ const { ssmPushCommand } = require('./ssm-command');
|
|
|
91
91
|
const program = new Command();
|
|
92
92
|
|
|
93
93
|
program
|
|
94
|
-
.command('init [
|
|
94
|
+
.command('init [projectName]')
|
|
95
95
|
.description('Initialize a new Frigg application')
|
|
96
|
-
.option('-
|
|
97
|
-
.option('-
|
|
98
|
-
.option('-
|
|
96
|
+
.option('-n, --name <name>', 'project name (alternative to the positional argument)')
|
|
97
|
+
.option('-m, --mode <mode>', 'deployment mode: standalone or embedded')
|
|
98
|
+
.option('-y, --yes', 'accept the default for every prompt (implied when stdin is not a TTY)')
|
|
99
|
+
.option('--no-install', 'skip installing dependencies')
|
|
100
|
+
.option('--no-git', 'skip initializing a git repository')
|
|
101
|
+
.option('-f, --force', 'scaffold into a non-empty directory and overwrite README.md')
|
|
102
|
+
.option('-v, --verbose', 'enable verbose output')
|
|
103
|
+
.option('-t, --template <template>', 'legacy template (no longer supported)')
|
|
99
104
|
.action(initCommand);
|
|
100
105
|
|
|
101
106
|
program
|