@appsemble/cli 0.23.5 → 0.23.7
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/README.md +3 -3
- package/commands/block/build.js +4 -3
- package/commands/block/extract-messages.js +5 -4
- package/commands/block/publish.js +6 -4
- package/commands/serve.js +11 -8
- package/commands/start.js +3 -4
- package/lib/app.js +23 -1
- package/lib/block.d.ts +9 -34
- package/lib/block.js +25 -195
- package/lib/block.test.js +10 -16
- package/lib/config.d.ts +40 -0
- package/lib/{getBlockConfigFromTypeScript.js → config.js} +164 -68
- package/lib/{getBlockConfigFromTypeScript.test.js → config.test.js} +32 -32
- package/lib/project.d.ts +12 -0
- package/lib/project.js +145 -0
- package/package.json +6 -5
- package/server/options/createSettings.js +19 -4
- package/server/options/getBlockAsset.d.ts +2 -2
- package/server/options/getBlockAsset.js +2 -0
- package/server/options/getCsp.js +1 -1
- package/templates/apps/empty/app-definition.yaml +2 -2
- package/templates/blocks/mini-jsx/package.json +1 -1
- package/templates/blocks/preact/package.json +2 -2
- package/templates/blocks/vanilla/package.json +1 -1
- package/lib/getBlockConfigFromTypeScript.d.ts +0 -10
- package/lib/loadWebpackConfig.d.ts +0 -15
- package/lib/loadWebpackConfig.js +0 -45
- /package/__fixtures__/{makePayload → makeProjectPayload}/no-icon/output/block.js +0 -0
- /package/__fixtures__/{makePayload → makeProjectPayload}/with-icon/output/block.js +0 -0
- /package/lib/{getBlockConfigFromTypeScript.test.d.ts → config.test.d.ts} +0 -0
|
@@ -1,8 +1,108 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, relative } from 'node:path';
|
|
4
|
+
import { inspect } from 'node:util';
|
|
5
|
+
import { AppsembleError, getWorkspaces, logger, readData } from '@appsemble/node-utils';
|
|
6
|
+
import { prefixBlockURL } from '@appsemble/utils';
|
|
7
|
+
import chalk from 'chalk';
|
|
3
8
|
import { parse } from 'comment-parser';
|
|
9
|
+
import { cosmiconfig } from 'cosmiconfig';
|
|
4
10
|
import normalizePath from 'normalize-path';
|
|
5
11
|
import { createFormatter, createParser, SchemaGenerator, ts } from 'ts-json-schema-generator';
|
|
12
|
+
/**
|
|
13
|
+
* Get the build configuration from a project directory.
|
|
14
|
+
*
|
|
15
|
+
* @param dir The directory in which to search for the configuration file.
|
|
16
|
+
* @returns The configuration.
|
|
17
|
+
*/
|
|
18
|
+
export async function getProjectBuildConfig(dir) {
|
|
19
|
+
const explorer = cosmiconfig('appsemble', { stopDir: dir });
|
|
20
|
+
const found = await explorer.search(dir);
|
|
21
|
+
let foundInParent;
|
|
22
|
+
if (!found) {
|
|
23
|
+
foundInParent = await explorer.search(dirname(dir));
|
|
24
|
+
if (!foundInParent) {
|
|
25
|
+
throw new AppsembleError(`No Appsemble configuration file found searching ${dir}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const { config, filepath } = found || foundInParent;
|
|
29
|
+
logger.info(`Found configuration file: ${filepath}`);
|
|
30
|
+
const [pkg] = await readData(join(dir, 'package.json'));
|
|
31
|
+
if (!pkg.private) {
|
|
32
|
+
logger.warn(`It is ${chalk.underline.yellow('highly recommended')} to set “${chalk.green('"private"')}: ${chalk.cyan('true')}” in package.json`);
|
|
33
|
+
}
|
|
34
|
+
let longDescription;
|
|
35
|
+
if (existsSync(join(dir, 'README.md'))) {
|
|
36
|
+
longDescription = await readFile(join(dir, 'README.md'), 'utf8');
|
|
37
|
+
}
|
|
38
|
+
const result = {
|
|
39
|
+
description: pkg.description,
|
|
40
|
+
longDescription,
|
|
41
|
+
name: pkg.name,
|
|
42
|
+
version: pkg.version,
|
|
43
|
+
dir,
|
|
44
|
+
...config,
|
|
45
|
+
};
|
|
46
|
+
logger.verbose(`Resolved project configuration: ${inspect(result, { colors: true })}`);
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Discover Appsemble projects based on workspaces in a monorepo.
|
|
51
|
+
*
|
|
52
|
+
* Both Lerna and Yarn workspaces are supported.
|
|
53
|
+
*
|
|
54
|
+
* @param root The project root in which to find workspaces.
|
|
55
|
+
* @returns Discovered Appsemble projects.
|
|
56
|
+
*/
|
|
57
|
+
export async function getProjectsBuildConfigs(root) {
|
|
58
|
+
const dirs = await getWorkspaces(root);
|
|
59
|
+
const projectBuildConfigs = await Promise.all(dirs
|
|
60
|
+
.concat(root)
|
|
61
|
+
.map((path) => getProjectBuildConfig(path))
|
|
62
|
+
// Ignore non-project workspaces.
|
|
63
|
+
.map((p) => p.catch(() => null)));
|
|
64
|
+
return projectBuildConfigs.filter(Boolean);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get the TypeScript program for a given path.
|
|
68
|
+
*
|
|
69
|
+
* @param path The path for which to get the TypeScript program.
|
|
70
|
+
* @returns The TypeScript program.
|
|
71
|
+
*/
|
|
72
|
+
function getProgram(path) {
|
|
73
|
+
const diagnosticHost = {
|
|
74
|
+
getNewLine: () => ts.sys.newLine,
|
|
75
|
+
getCurrentDirectory: ts.sys.getCurrentDirectory,
|
|
76
|
+
getCanonicalFileName: (x) => x,
|
|
77
|
+
};
|
|
78
|
+
const tsConfigPath = ts.findConfigFile(path, ts.sys.fileExists);
|
|
79
|
+
const { config, error } = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
|
|
80
|
+
if (error) {
|
|
81
|
+
throw new AppsembleError(ts.formatDiagnostic(error, diagnosticHost));
|
|
82
|
+
}
|
|
83
|
+
if (!config.files || !config.include) {
|
|
84
|
+
config.files = ts.sys.readDirectory(path, ['.ts', '.tsx']).map((f) => relative(path, f));
|
|
85
|
+
}
|
|
86
|
+
const { errors, fileNames, options } = ts.parseJsonConfigFileContent(config, ts.sys, path, undefined, tsConfigPath);
|
|
87
|
+
// Filter: 'rootDir' is expected to contain all source files.
|
|
88
|
+
const diagnostics = errors.filter(({ code }) => code !== 6059);
|
|
89
|
+
if (diagnostics.length) {
|
|
90
|
+
throw new AppsembleError(ts.formatDiagnosticsWithColorAndContext(diagnostics, diagnosticHost));
|
|
91
|
+
}
|
|
92
|
+
options.noEmit = true;
|
|
93
|
+
delete options.out;
|
|
94
|
+
delete options.outDir;
|
|
95
|
+
delete options.outFile;
|
|
96
|
+
delete options.declaration;
|
|
97
|
+
delete options.declarationDir;
|
|
98
|
+
delete options.declarationMap;
|
|
99
|
+
const program = ts.createProgram(fileNames, options);
|
|
100
|
+
const preEmitDiagnostics = ts.getPreEmitDiagnostics(program);
|
|
101
|
+
if (preEmitDiagnostics.length) {
|
|
102
|
+
throw new AppsembleError(ts.formatDiagnosticsWithColorAndContext(preEmitDiagnostics, diagnosticHost));
|
|
103
|
+
}
|
|
104
|
+
return program;
|
|
105
|
+
}
|
|
6
106
|
/**
|
|
7
107
|
* Get the tsdoc comment for a TypeScript node.
|
|
8
108
|
*
|
|
@@ -104,7 +204,7 @@ function processEvents(eventListenerInterface, eventEmitterInterface, checker) {
|
|
|
104
204
|
*
|
|
105
205
|
* @param program The TypeScript program from which to extract parameters.
|
|
106
206
|
* @param iface The interface node from which to extract parameters.
|
|
107
|
-
* @returns The JSON schema for the
|
|
207
|
+
* @returns The JSON schema for the project parameters.
|
|
108
208
|
*/
|
|
109
209
|
function processParameters(program, iface) {
|
|
110
210
|
if (!iface) {
|
|
@@ -128,7 +228,7 @@ function processParameters(program, iface) {
|
|
|
128
228
|
if (schema.definitions && !Object.keys(schema.definitions).length) {
|
|
129
229
|
delete schema.definitions;
|
|
130
230
|
}
|
|
131
|
-
// This is the tsdoc that has been added to the SDK to aid the
|
|
231
|
+
// This is the tsdoc that has been added to the SDK to aid the developer.
|
|
132
232
|
delete schema.description;
|
|
133
233
|
return schema;
|
|
134
234
|
}
|
|
@@ -143,67 +243,22 @@ function processMessages(iface, checker) {
|
|
|
143
243
|
return processInterface(iface, checker, (name, description) => [name, { description }]);
|
|
144
244
|
}
|
|
145
245
|
/**
|
|
146
|
-
*
|
|
246
|
+
* Generate project implementations from the project metadata and TypeScript project.
|
|
147
247
|
*
|
|
148
|
-
*
|
|
149
|
-
* @returns The TypeScript program.
|
|
150
|
-
*/
|
|
151
|
-
function getProgram(blockPath) {
|
|
152
|
-
const diagnosticHost = {
|
|
153
|
-
getNewLine: () => ts.sys.newLine,
|
|
154
|
-
getCurrentDirectory: ts.sys.getCurrentDirectory,
|
|
155
|
-
getCanonicalFileName: (x) => x,
|
|
156
|
-
};
|
|
157
|
-
const tsConfigPath = ts.findConfigFile(blockPath, ts.sys.fileExists);
|
|
158
|
-
const { config, error } = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
|
|
159
|
-
if (error) {
|
|
160
|
-
throw new AppsembleError(ts.formatDiagnostic(error, diagnosticHost));
|
|
161
|
-
}
|
|
162
|
-
if (!config.files || !config.include) {
|
|
163
|
-
config.files = ts.sys
|
|
164
|
-
.readDirectory(blockPath, ['.ts', '.tsx'])
|
|
165
|
-
.map((f) => relative(blockPath, f));
|
|
166
|
-
}
|
|
167
|
-
const { errors, fileNames, options } = ts.parseJsonConfigFileContent(config, ts.sys, blockPath, undefined, tsConfigPath);
|
|
168
|
-
// Filter: 'rootDir' is expected to contain all source files.
|
|
169
|
-
const diagnostics = errors.filter(({ code }) => code !== 6059);
|
|
170
|
-
if (diagnostics.length) {
|
|
171
|
-
throw new AppsembleError(ts.formatDiagnosticsWithColorAndContext(diagnostics, diagnosticHost));
|
|
172
|
-
}
|
|
173
|
-
options.noEmit = true;
|
|
174
|
-
delete options.out;
|
|
175
|
-
delete options.outDir;
|
|
176
|
-
delete options.outFile;
|
|
177
|
-
delete options.declaration;
|
|
178
|
-
delete options.declarationDir;
|
|
179
|
-
delete options.declarationMap;
|
|
180
|
-
const program = ts.createProgram(fileNames, options);
|
|
181
|
-
const preEmitDiagnostics = ts.getPreEmitDiagnostics(program);
|
|
182
|
-
if (preEmitDiagnostics.length) {
|
|
183
|
-
throw new AppsembleError(ts.formatDiagnosticsWithColorAndContext(preEmitDiagnostics, diagnosticHost));
|
|
184
|
-
}
|
|
185
|
-
return program;
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Generate a block manifest from the block metadata and TypeScript project.
|
|
248
|
+
* Uses the .appsemblerc file and the type definitions of the project.
|
|
189
249
|
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* @param blockConfig The block configuration
|
|
193
|
-
* @returns The block configuration appended from the TypeScript project.
|
|
250
|
+
* @param buildConfig The project build configuration
|
|
251
|
+
* @returns The project implementations from the TypeScript project.
|
|
194
252
|
*/
|
|
195
|
-
export function
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
logger.info(`Extracting data from TypeScript project ${blockConfig.dir}`);
|
|
200
|
-
const program = getProgram(blockConfig.dir);
|
|
253
|
+
export function getProjectImplementations(buildConfig) {
|
|
254
|
+
logger.info(`Extracting data from TypeScript project ${buildConfig.dir}`);
|
|
255
|
+
const program = getProgram(buildConfig.dir);
|
|
201
256
|
const checker = program.getTypeChecker();
|
|
202
257
|
let actionInterface;
|
|
203
258
|
let eventEmitterInterface;
|
|
204
259
|
let eventListenerInterface;
|
|
205
260
|
let messagesInterface;
|
|
206
|
-
let
|
|
261
|
+
let parametersInterface;
|
|
207
262
|
for (const sourceFile of program.getSourceFiles()) {
|
|
208
263
|
const fileName = relative(process.cwd(), sourceFile.fileName);
|
|
209
264
|
// Filter TypeScript default libs
|
|
@@ -254,10 +309,10 @@ export function getBlockConfigFromTypeScript(blockConfig) {
|
|
|
254
309
|
break;
|
|
255
310
|
case 'Parameters':
|
|
256
311
|
logger.info(`Found augmented interface 'Parameters' in '${loc}'`);
|
|
257
|
-
if (
|
|
312
|
+
if (parametersInterface) {
|
|
258
313
|
throw new AppsembleError(`Found duplicate interface 'Parameters' in '${loc}'`);
|
|
259
314
|
}
|
|
260
|
-
|
|
315
|
+
parametersInterface = iface;
|
|
261
316
|
break;
|
|
262
317
|
case 'Messages':
|
|
263
318
|
logger.info(`Found augmented interface 'Messages' in '${loc}'`);
|
|
@@ -270,16 +325,57 @@ export function getBlockConfigFromTypeScript(blockConfig) {
|
|
|
270
325
|
});
|
|
271
326
|
}
|
|
272
327
|
return {
|
|
273
|
-
actions: 'actions' in
|
|
274
|
-
events: 'events' in
|
|
275
|
-
?
|
|
328
|
+
actions: 'actions' in buildConfig ? buildConfig.actions : processActions(actionInterface, checker),
|
|
329
|
+
events: 'events' in buildConfig
|
|
330
|
+
? buildConfig.events
|
|
276
331
|
: processEvents(eventListenerInterface, eventEmitterInterface, checker),
|
|
277
|
-
parameters: 'parameters' in
|
|
278
|
-
?
|
|
279
|
-
: processParameters(program,
|
|
280
|
-
messages: 'messages' in
|
|
281
|
-
?
|
|
332
|
+
parameters: 'parameters' in buildConfig
|
|
333
|
+
? buildConfig.parameters
|
|
334
|
+
: processParameters(program, parametersInterface),
|
|
335
|
+
messages: 'messages' in buildConfig
|
|
336
|
+
? buildConfig.messages
|
|
282
337
|
: processMessages(messagesInterface, checker),
|
|
283
338
|
};
|
|
284
339
|
}
|
|
285
|
-
|
|
340
|
+
/**
|
|
341
|
+
* Load a webpack configuration file.
|
|
342
|
+
*
|
|
343
|
+
* A webpack configuration file may export either an webpack configuration object, or a synchronous
|
|
344
|
+
* or asynchronous function which returns a webpack configuration object. This function supports
|
|
345
|
+
* all 3 use cases.
|
|
346
|
+
*
|
|
347
|
+
* @param buildConfig The project build config.
|
|
348
|
+
* @param mode The env that would be passed to webpack by invoking `webpack --env $env`.
|
|
349
|
+
* @param outputPath The path where the build will be output on disk.
|
|
350
|
+
* @returns The webpack configuration as exposed by the webpack configuration file.
|
|
351
|
+
*/
|
|
352
|
+
export async function getProjectWebpackConfig(buildConfig, mode, outputPath) {
|
|
353
|
+
let configPath;
|
|
354
|
+
if (buildConfig.webpack) {
|
|
355
|
+
configPath = join(buildConfig.dir, buildConfig.webpack);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
configPath = join(buildConfig.dir, 'webpack.config.js');
|
|
359
|
+
try {
|
|
360
|
+
await stat(configPath);
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
configPath = '@appsemble/webpack-config';
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
logger.info(`Using webpack config from ${configPath}`);
|
|
367
|
+
const publicPath = prefixBlockURL({ type: buildConfig.name, version: buildConfig.version }, '');
|
|
368
|
+
let config = await import(String(configPath));
|
|
369
|
+
config = await (config.default || config);
|
|
370
|
+
config = config instanceof Function ? await config(buildConfig, { mode, publicPath }) : config;
|
|
371
|
+
// Koa-webpack serves assets on the `output.path` path. Normally this field describes where to
|
|
372
|
+
// output the files on the file system. This is monkey patched to support usage with our dev
|
|
373
|
+
// server.
|
|
374
|
+
config.output = config.output || {};
|
|
375
|
+
config.output.path = outputPath || publicPath;
|
|
376
|
+
logger.verbose(`Patched webpack config output.path to ${config.output.path}`);
|
|
377
|
+
config.output.publicPath = publicPath;
|
|
378
|
+
logger.verbose(`Patched webpack config output.publicPath to ${config.output.publicPath}`);
|
|
379
|
+
return config;
|
|
380
|
+
}
|
|
381
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -4,19 +4,19 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { AppsembleError, resolveFixture } from '@appsemble/node-utils';
|
|
5
5
|
import { ts } from 'ts-json-schema-generator';
|
|
6
6
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
-
import {
|
|
7
|
+
import { getProjectImplementations } from './config.js';
|
|
8
8
|
beforeEach(() => {
|
|
9
9
|
vi.spyOn(process, 'cwd').mockReturnValue(resolveFixture('.'));
|
|
10
10
|
});
|
|
11
|
-
describe('
|
|
11
|
+
describe('getProjectImplementations', () => {
|
|
12
12
|
it('should extract configuration from a TypeScript project', () => {
|
|
13
|
-
const result =
|
|
13
|
+
const result = getProjectImplementations({
|
|
14
14
|
name: '',
|
|
15
15
|
layout: 'float',
|
|
16
16
|
version: '1.33.7',
|
|
17
17
|
webpack: '',
|
|
18
18
|
output: '',
|
|
19
|
-
dir: resolveFixture('
|
|
19
|
+
dir: resolveFixture('getProjectImplementations/valid'),
|
|
20
20
|
});
|
|
21
21
|
expect(result).toStrictEqual({
|
|
22
22
|
actions: { testAction: { description: undefined } },
|
|
@@ -62,87 +62,87 @@ describe('getBlockConfigFromTypeScript', () => {
|
|
|
62
62
|
version: '1.33.7',
|
|
63
63
|
webpack: '',
|
|
64
64
|
output: '',
|
|
65
|
-
dir: resolveFixture('
|
|
65
|
+
dir: resolveFixture('getProjectImplementations/valid'),
|
|
66
66
|
name: '',
|
|
67
67
|
};
|
|
68
|
-
const result =
|
|
68
|
+
const result = getProjectImplementations(input);
|
|
69
69
|
expect(result).toBe(input);
|
|
70
70
|
expect(ts.createProgram).not.toHaveBeenCalled();
|
|
71
71
|
});
|
|
72
72
|
it('should prefer actions overrides over TypeScript actions', () => {
|
|
73
|
-
const result =
|
|
73
|
+
const result = getProjectImplementations({
|
|
74
74
|
actions: {},
|
|
75
75
|
version: '1.33.7',
|
|
76
76
|
webpack: '',
|
|
77
77
|
output: '',
|
|
78
|
-
dir: resolveFixture('
|
|
78
|
+
dir: resolveFixture('getProjectImplementations/valid'),
|
|
79
79
|
name: '',
|
|
80
80
|
});
|
|
81
81
|
expect(result.actions).toStrictEqual({});
|
|
82
82
|
});
|
|
83
83
|
it('should prefer events overrides over TypeScript events', () => {
|
|
84
|
-
const result =
|
|
84
|
+
const result = getProjectImplementations({
|
|
85
85
|
events: { emit: { onSuccess: {} } },
|
|
86
86
|
version: '1.33.7',
|
|
87
87
|
webpack: '',
|
|
88
88
|
output: '',
|
|
89
|
-
dir: resolveFixture('
|
|
89
|
+
dir: resolveFixture('getProjectImplementations/valid'),
|
|
90
90
|
name: '',
|
|
91
91
|
});
|
|
92
92
|
expect(result.events).toStrictEqual({ emit: { onSuccess: {} } });
|
|
93
93
|
});
|
|
94
94
|
it('should prefer parameters overrides over TypeScript parameters', () => {
|
|
95
|
-
const result =
|
|
95
|
+
const result = getProjectImplementations({
|
|
96
96
|
parameters: { type: 'object' },
|
|
97
97
|
version: '1.33.7',
|
|
98
98
|
webpack: '',
|
|
99
99
|
output: '',
|
|
100
|
-
dir: resolveFixture('
|
|
100
|
+
dir: resolveFixture('getProjectImplementations/valid'),
|
|
101
101
|
name: '',
|
|
102
102
|
});
|
|
103
103
|
expect(result.parameters).toStrictEqual({ type: 'object' });
|
|
104
104
|
});
|
|
105
105
|
it('should throw if duplicate Actions are found', () => {
|
|
106
|
-
expect(() =>
|
|
106
|
+
expect(() => getProjectImplementations({
|
|
107
107
|
webpack: '',
|
|
108
108
|
output: '',
|
|
109
|
-
dir: resolveFixture('
|
|
109
|
+
dir: resolveFixture('getProjectImplementations/duplicateActions'),
|
|
110
110
|
name: '',
|
|
111
111
|
version: '1.33.7',
|
|
112
|
-
})).toThrow(new AppsembleError("Found duplicate interface 'Actions' in '
|
|
112
|
+
})).toThrow(new AppsembleError("Found duplicate interface 'Actions' in 'getProjectImplementations/duplicateActions/index.ts:31'"));
|
|
113
113
|
});
|
|
114
114
|
it('should throw if duplicate EventEmitters are found', () => {
|
|
115
|
-
expect(() =>
|
|
115
|
+
expect(() => getProjectImplementations({
|
|
116
116
|
webpack: '',
|
|
117
117
|
output: '',
|
|
118
|
-
dir: resolveFixture('
|
|
118
|
+
dir: resolveFixture('getProjectImplementations/duplicateEventEmitters'),
|
|
119
119
|
name: '',
|
|
120
120
|
version: '1.33.7',
|
|
121
|
-
})).toThrow(new AppsembleError("Found duplicate interface 'EventEmitters' in '
|
|
121
|
+
})).toThrow(new AppsembleError("Found duplicate interface 'EventEmitters' in 'getProjectImplementations/duplicateEventEmitters/index.ts:31'"));
|
|
122
122
|
});
|
|
123
123
|
it('should throw if duplicate EventListeners are found', () => {
|
|
124
|
-
expect(() =>
|
|
124
|
+
expect(() => getProjectImplementations({
|
|
125
125
|
webpack: '',
|
|
126
126
|
output: '',
|
|
127
|
-
dir: resolveFixture('
|
|
127
|
+
dir: resolveFixture('getProjectImplementations/duplicateEventListeners'),
|
|
128
128
|
name: '',
|
|
129
129
|
version: '1.33.7',
|
|
130
|
-
})).toThrow(new AppsembleError("Found duplicate interface 'EventListeners' in '
|
|
130
|
+
})).toThrow(new AppsembleError("Found duplicate interface 'EventListeners' in 'getProjectImplementations/duplicateEventListeners/index.ts:31'"));
|
|
131
131
|
});
|
|
132
132
|
it('should throw if duplicate Parameters are found', () => {
|
|
133
|
-
expect(() =>
|
|
133
|
+
expect(() => getProjectImplementations({
|
|
134
134
|
webpack: '',
|
|
135
135
|
output: '',
|
|
136
|
-
dir: resolveFixture('
|
|
136
|
+
dir: resolveFixture('getProjectImplementations/duplicateParameters'),
|
|
137
137
|
name: '',
|
|
138
138
|
version: '1.33.7',
|
|
139
|
-
})).toThrow(new AppsembleError("Found duplicate interface 'Parameters' in '
|
|
139
|
+
})).toThrow(new AppsembleError("Found duplicate interface 'Parameters' in 'getProjectImplementations/duplicateParameters/index.ts:31'"));
|
|
140
140
|
});
|
|
141
141
|
it('should handle fontawesome icons', () => {
|
|
142
|
-
const result =
|
|
142
|
+
const result = getProjectImplementations({
|
|
143
143
|
webpack: '',
|
|
144
144
|
output: '',
|
|
145
|
-
dir: resolveFixture('
|
|
145
|
+
dir: resolveFixture('getProjectImplementations/fontawesomeParameters'),
|
|
146
146
|
name: '',
|
|
147
147
|
version: '1.33.7',
|
|
148
148
|
});
|
|
@@ -162,26 +162,26 @@ describe('getBlockConfigFromTypeScript', () => {
|
|
|
162
162
|
});
|
|
163
163
|
it('should handle TypeScript pre emit diagnostics', () => {
|
|
164
164
|
function fn() {
|
|
165
|
-
|
|
165
|
+
getProjectImplementations({
|
|
166
166
|
name: '',
|
|
167
167
|
layout: 'float',
|
|
168
168
|
version: '1.33.7',
|
|
169
169
|
webpack: '',
|
|
170
170
|
output: '',
|
|
171
|
-
dir: resolveFixture('
|
|
171
|
+
dir: resolveFixture('getProjectImplementations/tsError'),
|
|
172
172
|
});
|
|
173
173
|
}
|
|
174
174
|
expect(fn).toThrow(AppsembleError);
|
|
175
175
|
expect(fn).toThrow(/'unused' is declared but its value is never read/);
|
|
176
176
|
});
|
|
177
177
|
it('should extract comments', () => {
|
|
178
|
-
const result =
|
|
178
|
+
const result = getProjectImplementations({
|
|
179
179
|
name: '',
|
|
180
180
|
layout: 'float',
|
|
181
181
|
version: '1.33.7',
|
|
182
182
|
webpack: '',
|
|
183
183
|
output: '',
|
|
184
|
-
dir: resolveFixture('
|
|
184
|
+
dir: resolveFixture('getProjectImplementations/comments'),
|
|
185
185
|
});
|
|
186
186
|
expect(result).toStrictEqual({
|
|
187
187
|
actions: {
|
|
@@ -223,7 +223,7 @@ describe('getBlockConfigFromTypeScript', () => {
|
|
|
223
223
|
const blocksDir = new URL('../../../blocks/', import.meta.url);
|
|
224
224
|
it.each(readdirSync(blocksDir))('%s', (name) => {
|
|
225
225
|
const dir = fileURLToPath(new URL(name, blocksDir));
|
|
226
|
-
const result =
|
|
226
|
+
const result = getProjectImplementations({
|
|
227
227
|
name,
|
|
228
228
|
version: '',
|
|
229
229
|
output: '',
|
|
@@ -234,4 +234,4 @@ describe('getBlockConfigFromTypeScript', () => {
|
|
|
234
234
|
});
|
|
235
235
|
});
|
|
236
236
|
});
|
|
237
|
-
//# sourceMappingURL=
|
|
237
|
+
//# sourceMappingURL=config.test.js.map
|
package/lib/project.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ProjectBuildConfig, type ProjectImplementations } from '@appsemble/types';
|
|
2
|
+
import { type BuildResult } from 'esbuild';
|
|
3
|
+
import FormData from 'form-data';
|
|
4
|
+
export declare function buildProject(config: ProjectBuildConfig): Promise<BuildResult>;
|
|
5
|
+
/**
|
|
6
|
+
* Configure the payload for a new project upload.
|
|
7
|
+
*
|
|
8
|
+
* @param buildConfig The project configuration
|
|
9
|
+
* @returns The payload that should be sent to the endpoint.
|
|
10
|
+
*/
|
|
11
|
+
export declare function makeProjectPayload(buildConfig: ProjectBuildConfig): Promise<[FormData, ProjectImplementations]>;
|
|
12
|
+
export declare function publishProject(buildConfig: ProjectBuildConfig, url: string, ignoreConflict?: boolean): Promise<void>;
|
package/lib/project.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, extname, join, relative, resolve as resolvePath } from 'node:path';
|
|
4
|
+
import { inspect } from 'node:util';
|
|
5
|
+
import { AppsembleError, logger, opendirSafe, readData } from '@appsemble/node-utils';
|
|
6
|
+
import { compareStrings } from '@appsemble/utils';
|
|
7
|
+
import axios from 'axios';
|
|
8
|
+
import { build } from 'esbuild';
|
|
9
|
+
import FormData from 'form-data';
|
|
10
|
+
import { getProjectImplementations } from './config.js';
|
|
11
|
+
export async function buildProject(config) {
|
|
12
|
+
const buildOptions = {
|
|
13
|
+
entryPoints: ['index.ts'],
|
|
14
|
+
bundle: true,
|
|
15
|
+
write: false,
|
|
16
|
+
absWorkingDir: join(config.dir),
|
|
17
|
+
loader: {
|
|
18
|
+
'.png': 'dataurl',
|
|
19
|
+
'.svg': 'dataurl',
|
|
20
|
+
},
|
|
21
|
+
logLevel: 'info',
|
|
22
|
+
minify: true,
|
|
23
|
+
};
|
|
24
|
+
logger.info(`Building ${config.name}@${config.version} 🔨`);
|
|
25
|
+
const buildResult = await build(buildOptions);
|
|
26
|
+
if (buildResult.errors.length > 0) {
|
|
27
|
+
throw new AppsembleError(String(buildResult.errors));
|
|
28
|
+
}
|
|
29
|
+
return buildResult;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Configure the payload for a new project upload.
|
|
33
|
+
*
|
|
34
|
+
* @param buildConfig The project configuration
|
|
35
|
+
* @returns The payload that should be sent to the endpoint.
|
|
36
|
+
*/
|
|
37
|
+
export async function makeProjectPayload(buildConfig) {
|
|
38
|
+
const { dir, output } = buildConfig;
|
|
39
|
+
const distPath = output ? resolvePath(dir, output) : undefined;
|
|
40
|
+
const form = new FormData();
|
|
41
|
+
const gatheredData = {};
|
|
42
|
+
const { description, layout, longDescription, name, version, visibility } = buildConfig;
|
|
43
|
+
const { actions, events, messages, parameters } = getProjectImplementations(buildConfig);
|
|
44
|
+
const files = await readdir(dir);
|
|
45
|
+
const icon = files.find((entry) => entry.match(/^icon\.(png|svg)$/));
|
|
46
|
+
function append(field, value) {
|
|
47
|
+
if (value) {
|
|
48
|
+
const serialized = typeof value === 'string' ? value : JSON.stringify(value);
|
|
49
|
+
logger.verbose(`Using ${field}: ${inspect(value, { colors: true, depth: 20 })}`);
|
|
50
|
+
form.append(field, serialized);
|
|
51
|
+
gatheredData[field] = value;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
logger.silly(`Skipping parameter ${field}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (visibility) {
|
|
58
|
+
append('visibility', visibility);
|
|
59
|
+
}
|
|
60
|
+
append('actions', actions);
|
|
61
|
+
append('description', description);
|
|
62
|
+
append('longDescription', longDescription);
|
|
63
|
+
append('events', events);
|
|
64
|
+
append('layout', layout);
|
|
65
|
+
append('name', name);
|
|
66
|
+
append('parameters', parameters);
|
|
67
|
+
append('version', version);
|
|
68
|
+
if (icon) {
|
|
69
|
+
const iconPath = join(dir, icon);
|
|
70
|
+
logger.info(`Using icon: ${iconPath}`);
|
|
71
|
+
form.append('icon', createReadStream(iconPath));
|
|
72
|
+
gatheredData.iconUrl = basename(iconPath, extname(iconPath));
|
|
73
|
+
}
|
|
74
|
+
if (messages) {
|
|
75
|
+
if (!files.includes('i18n')) {
|
|
76
|
+
throw new AppsembleError('This block has messages defined, but the message files could not be found. Try running extract-messages');
|
|
77
|
+
}
|
|
78
|
+
const messageKeys = Object.keys(messages).sort(compareStrings);
|
|
79
|
+
const messagesResult = {};
|
|
80
|
+
const messagesPath = join(dir, 'i18n');
|
|
81
|
+
const translations = (await readdir(messagesPath)).map((language) => language.toLowerCase());
|
|
82
|
+
if (!translations.includes('en.json')) {
|
|
83
|
+
throw new AppsembleError('Could not find ‘en.json’. Try running extract-messages');
|
|
84
|
+
}
|
|
85
|
+
const duplicates = translations.filter((language, index) => translations.indexOf(language) !== index);
|
|
86
|
+
if (duplicates.length) {
|
|
87
|
+
throw new AppsembleError(`Found duplicate language codes: ‘${duplicates.join('’, ')}`);
|
|
88
|
+
}
|
|
89
|
+
for (const languageFile of translations.filter((t) => t.endsWith('.json'))) {
|
|
90
|
+
const language = basename(languageFile, '.json');
|
|
91
|
+
const languagePath = join(messagesPath, languageFile);
|
|
92
|
+
const [m] = await readData(languagePath);
|
|
93
|
+
const languageKeys = Object.keys(m).sort(compareStrings);
|
|
94
|
+
if (languageKeys.length !== messageKeys.length ||
|
|
95
|
+
languageKeys.some((key) => !messageKeys.includes(key))) {
|
|
96
|
+
throw new AppsembleError(`‘${languagePath}’ contains mismatching message keys. Try running extract-messages`);
|
|
97
|
+
}
|
|
98
|
+
logger.info(`Including ${language} translations from ‘${languagePath}’`);
|
|
99
|
+
messagesResult[language] = m;
|
|
100
|
+
}
|
|
101
|
+
form.append('messages', JSON.stringify(messagesResult));
|
|
102
|
+
gatheredData.messages = messagesResult;
|
|
103
|
+
}
|
|
104
|
+
if (files.includes('examples')) {
|
|
105
|
+
await opendirSafe(join(dir, 'examples'), async (file, stat) => {
|
|
106
|
+
if (!stat.isFile() || !file.endsWith('.yaml')) {
|
|
107
|
+
throw new AppsembleError(`Expected ${file} to be a YAML file`);
|
|
108
|
+
}
|
|
109
|
+
logger.info(`Adding example file ${file}`);
|
|
110
|
+
form.append('examples', await readFile(file, 'utf8'));
|
|
111
|
+
gatheredData.examples = [...(gatheredData.examples || []), basename(file, extname(file))];
|
|
112
|
+
}, { allowMissing: true });
|
|
113
|
+
}
|
|
114
|
+
if (distPath) {
|
|
115
|
+
await opendirSafe(distPath, (fullpath, stat) => {
|
|
116
|
+
if (!stat.isFile()) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const relativePath = relative(distPath, fullpath);
|
|
120
|
+
const realPath = relative(process.cwd(), fullpath);
|
|
121
|
+
logger.info(`Adding file: “${realPath}” as “${relativePath}”`);
|
|
122
|
+
form.append('files', createReadStream(fullpath), {
|
|
123
|
+
filename: encodeURIComponent(relativePath),
|
|
124
|
+
});
|
|
125
|
+
gatheredData.files = [...(gatheredData.files || []), relativePath];
|
|
126
|
+
}, { recursive: true });
|
|
127
|
+
}
|
|
128
|
+
return [form, gatheredData];
|
|
129
|
+
}
|
|
130
|
+
export async function publishProject(buildConfig, url, ignoreConflict) {
|
|
131
|
+
logger.info(`Publishing ${buildConfig.name}@${buildConfig.version}…`);
|
|
132
|
+
const [form] = await makeProjectPayload(buildConfig);
|
|
133
|
+
try {
|
|
134
|
+
await axios.post(url, form);
|
|
135
|
+
logger.info(`Successfully published ${buildConfig.name}@${buildConfig.version} 🎉`);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
if (ignoreConflict && axios.isAxiosError(err) && err.response.status === 409) {
|
|
139
|
+
logger.warn(`${buildConfig.name}@${buildConfig.version} was already published.`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=project.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appsemble/cli",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.7",
|
|
4
4
|
"description": "The CLI for developing with Appsemble apps and blocks",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"test": "vitest"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@appsemble/node-utils": "0.23.
|
|
46
|
-
"@appsemble/types": "0.23.
|
|
47
|
-
"@appsemble/utils": "0.23.
|
|
45
|
+
"@appsemble/node-utils": "0.23.7",
|
|
46
|
+
"@appsemble/types": "0.23.7",
|
|
47
|
+
"@appsemble/utils": "0.23.7",
|
|
48
48
|
"@fortawesome/fontawesome-common-types": "^6.0.0",
|
|
49
49
|
"@koa/cors": "^4.0.0",
|
|
50
50
|
"@odata/parser": "^0.2.0",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"comment-parser": "^1.0.0",
|
|
54
54
|
"cosmiconfig": "^7.0.0",
|
|
55
55
|
"csvtojson": "^2.0.0",
|
|
56
|
+
"esbuild": "^0.19.0",
|
|
56
57
|
"fast-glob": "^3.0.0",
|
|
57
58
|
"form-data": "^4.0.0",
|
|
58
59
|
"global-cache-dir": "^6.0.0",
|
|
@@ -85,7 +86,7 @@
|
|
|
85
86
|
"yargs": "^17.0.0"
|
|
86
87
|
},
|
|
87
88
|
"devDependencies": {
|
|
88
|
-
"@appsemble/types": "0.23.
|
|
89
|
+
"@appsemble/types": "0.23.7",
|
|
89
90
|
"@types/concat-stream": "^2.0.0",
|
|
90
91
|
"@types/inquirer": "^9.0.0",
|
|
91
92
|
"@types/normalize-path": "^3.0.0",
|