@pikku/cli 0.6.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/CHANGELOG.md +445 -0
- package/README.md +3 -0
- package/bin/pikku-all.ts +110 -0
- package/bin/pikku-channels-map.ts +53 -0
- package/bin/pikku-channels.ts +57 -0
- package/bin/pikku-fetch.ts +54 -0
- package/bin/pikku-function-types.ts +76 -0
- package/bin/pikku-nextjs.test.ts +279 -0
- package/bin/pikku-nextjs.ts +113 -0
- package/bin/pikku-openapi.ts +71 -0
- package/bin/pikku-routes-map.ts +54 -0
- package/bin/pikku-routes.ts +55 -0
- package/bin/pikku-scheduler.ts +61 -0
- package/bin/pikku-schemas.ts +51 -0
- package/bin/pikku-websocket.ts +54 -0
- package/bin/pikku.ts +26 -0
- package/cli.schema.json +212 -0
- package/dist/bin/pikku-all.d.ts +4 -0
- package/dist/bin/pikku-all.js +71 -0
- package/dist/bin/pikku-channels-map.d.ts +5 -0
- package/dist/bin/pikku-channels-map.js +27 -0
- package/dist/bin/pikku-channels.d.ts +5 -0
- package/dist/bin/pikku-channels.js +32 -0
- package/dist/bin/pikku-fetch.d.ts +6 -0
- package/dist/bin/pikku-fetch.js +25 -0
- package/dist/bin/pikku-function-types.d.ts +6 -0
- package/dist/bin/pikku-function-types.js +32 -0
- package/dist/bin/pikku-nextjs.d.ts +7 -0
- package/dist/bin/pikku-nextjs.js +40 -0
- package/dist/bin/pikku-openapi.d.ts +5 -0
- package/dist/bin/pikku-openapi.js +41 -0
- package/dist/bin/pikku-routes-map.d.ts +5 -0
- package/dist/bin/pikku-routes-map.js +27 -0
- package/dist/bin/pikku-routes.d.ts +5 -0
- package/dist/bin/pikku-routes.js +33 -0
- package/dist/bin/pikku-scheduler.d.ts +5 -0
- package/dist/bin/pikku-scheduler.js +32 -0
- package/dist/bin/pikku-schemas.d.ts +5 -0
- package/dist/bin/pikku-schemas.js +27 -0
- package/dist/bin/pikku-websocket.d.ts +6 -0
- package/dist/bin/pikku-websocket.js +25 -0
- package/dist/bin/pikku.d.ts +2 -0
- package/dist/bin/pikku.js +23 -0
- package/dist/src/channels/serialize-channels.d.ts +3 -0
- package/dist/src/channels/serialize-channels.js +19 -0
- package/dist/src/channels/serialize-typed-channel-map.d.ts +3 -0
- package/dist/src/channels/serialize-typed-channel-map.js +87 -0
- package/dist/src/channels/serialize-websocket-wrapper.d.ts +1 -0
- package/dist/src/channels/serialize-websocket-wrapper.js +61 -0
- package/dist/src/core/serialize-import-map.d.ts +2 -0
- package/dist/src/core/serialize-import-map.js +23 -0
- package/dist/src/core/serialize-pikku-types.d.ts +4 -0
- package/dist/src/core/serialize-pikku-types.js +48 -0
- package/dist/src/http/serialize-fetch-wrapper.d.ts +1 -0
- package/dist/src/http/serialize-fetch-wrapper.js +57 -0
- package/dist/src/http/serialize-route-imports.d.ts +1 -0
- package/dist/src/http/serialize-route-imports.js +13 -0
- package/dist/src/http/serialize-route-meta.d.ts +2 -0
- package/dist/src/http/serialize-route-meta.js +6 -0
- package/dist/src/http/serialize-typed-route-map.d.ts +3 -0
- package/dist/src/http/serialize-typed-route-map.js +107 -0
- package/dist/src/inspector-glob.d.ts +2 -0
- package/dist/src/inspector-glob.js +12 -0
- package/dist/src/nextjs/serialize-nextjs-wrapper.d.ts +1 -0
- package/dist/src/nextjs/serialize-nextjs-wrapper.js +149 -0
- package/dist/src/openapi/openapi-spec-generator.d.ts +79 -0
- package/dist/src/openapi/openapi-spec-generator.js +135 -0
- package/dist/src/pikku-cli-config.d.ts +31 -0
- package/dist/src/pikku-cli-config.js +113 -0
- package/dist/src/scheduler/serialize-schedulers.d.ts +3 -0
- package/dist/src/scheduler/serialize-schedulers.js +22 -0
- package/dist/src/schema/schema-generator.d.ts +5 -0
- package/dist/src/schema/schema-generator.js +79 -0
- package/dist/src/utils.d.ts +34 -0
- package/dist/src/utils.js +109 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +42 -0
- package/run-tests.sh +53 -0
- package/src/channels/serialize-channels.ts +34 -0
- package/src/channels/serialize-typed-channel-map.ts +133 -0
- package/src/channels/serialize-websocket-wrapper.ts +61 -0
- package/src/core/serialize-import-map.ts +33 -0
- package/src/core/serialize-pikku-types.ts +53 -0
- package/src/http/serialize-fetch-wrapper.ts +57 -0
- package/src/http/serialize-route-imports.ts +24 -0
- package/src/http/serialize-route-meta.ts +10 -0
- package/src/http/serialize-typed-route-map.ts +147 -0
- package/src/inspector-glob.ts +27 -0
- package/src/nextjs/serialize-nextjs-wrapper.ts +156 -0
- package/src/openapi/openapi-spec-generator.ts +229 -0
- package/src/pikku-cli-config.ts +189 -0
- package/src/scheduler/serialize-schedulers.ts +43 -0
- package/src/schema/schema-generator.ts +117 -0
- package/src/utils.ts +219 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getFileImportRelativePath } from '../utils.js';
|
|
2
|
+
export const serializeSchedulers = (outputPath, filesWithScheduledTasks, packageMappings = {}) => {
|
|
3
|
+
const serializedOutput = [
|
|
4
|
+
'/* The files with an addSerializedTasks function call */',
|
|
5
|
+
];
|
|
6
|
+
Array.from(filesWithScheduledTasks)
|
|
7
|
+
.sort()
|
|
8
|
+
.forEach((path) => {
|
|
9
|
+
const filePath = getFileImportRelativePath(outputPath, path, packageMappings);
|
|
10
|
+
serializedOutput.push(`import '${filePath}'`);
|
|
11
|
+
});
|
|
12
|
+
return serializedOutput.join('\n');
|
|
13
|
+
};
|
|
14
|
+
export const serializeSchedulerMeta = (scheduledTasksMeta) => {
|
|
15
|
+
const serializedOutput = [];
|
|
16
|
+
serializedOutput.push("import { setScheduledTasksMeta } from '@pikku/core/scheduler'");
|
|
17
|
+
serializedOutput.push(`setScheduledTasksMeta(${JSON.stringify(scheduledTasksMeta, null, 2)})`);
|
|
18
|
+
if (scheduledTasksMeta.length > 0) {
|
|
19
|
+
serializedOutput.push(`export type ScheduledTaskNames = '${scheduledTasksMeta.map((s) => s.name).join("' | '")}'`);
|
|
20
|
+
}
|
|
21
|
+
return serializedOutput.join('\n');
|
|
22
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { JSONValue } from '@pikku/core';
|
|
2
|
+
import { HTTPRoutesMeta } from '@pikku/core/http';
|
|
3
|
+
import { TypesMap } from '@pikku/inspector';
|
|
4
|
+
export declare function generateSchemas(tsconfig: string, typesMap: TypesMap, routesMeta: HTTPRoutesMeta): Promise<Record<string, JSONValue>>;
|
|
5
|
+
export declare function saveSchemas(schemaParentDir: string, schemas: Record<string, JSONValue>, typesMap: TypesMap, routesMeta: HTTPRoutesMeta, supportsImportAttributes: boolean): Promise<void>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createGenerator, RootlessError } from 'ts-json-schema-generator';
|
|
2
|
+
import { writeFileInDir } from '../utils.js';
|
|
3
|
+
import { mkdir, writeFile } from 'fs/promises';
|
|
4
|
+
export async function generateSchemas(tsconfig, typesMap, routesMeta) {
|
|
5
|
+
const schemasSet = new Set(typesMap.customTypes.keys());
|
|
6
|
+
for (const { input, output, inputTypes } of routesMeta) {
|
|
7
|
+
if (input) {
|
|
8
|
+
schemasSet.add(typesMap.getTypeMeta(input).uniqueName);
|
|
9
|
+
}
|
|
10
|
+
if (output) {
|
|
11
|
+
schemasSet.add(typesMap.getTypeMeta(output).uniqueName);
|
|
12
|
+
}
|
|
13
|
+
if (inputTypes?.body) {
|
|
14
|
+
schemasSet.add(inputTypes.body);
|
|
15
|
+
}
|
|
16
|
+
if (inputTypes?.query) {
|
|
17
|
+
schemasSet.add(inputTypes.query);
|
|
18
|
+
}
|
|
19
|
+
if (inputTypes?.params) {
|
|
20
|
+
schemasSet.add(inputTypes.params);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const generator = createGenerator({
|
|
24
|
+
tsconfig,
|
|
25
|
+
skipTypeCheck: true,
|
|
26
|
+
topRef: false,
|
|
27
|
+
discriminatorType: 'open-api',
|
|
28
|
+
});
|
|
29
|
+
const schemas = {};
|
|
30
|
+
schemasSet.forEach((schema) => {
|
|
31
|
+
try {
|
|
32
|
+
schemas[schema] = generator.createSchema(schema);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
// Ignore rootless errors
|
|
36
|
+
if (e instanceof RootlessError) {
|
|
37
|
+
console.log('Error generating schema since it has no root:', schema);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
throw e;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return schemas;
|
|
44
|
+
}
|
|
45
|
+
export async function saveSchemas(schemaParentDir, schemas, typesMap, routesMeta, supportsImportAttributes) {
|
|
46
|
+
await writeFileInDir(`${schemaParentDir}/register.gen.ts`, 'export const empty = null;');
|
|
47
|
+
const desiredSchemas = new Set([
|
|
48
|
+
...routesMeta
|
|
49
|
+
.map(({ input, output }) => [
|
|
50
|
+
input ? typesMap.getUniqueName(input) : undefined,
|
|
51
|
+
output ? typesMap.getUniqueName(output) : undefined,
|
|
52
|
+
])
|
|
53
|
+
.flat()
|
|
54
|
+
.filter((s) => !!s &&
|
|
55
|
+
!['boolean', 'string', 'number', 'null', 'undefined'].includes(s)),
|
|
56
|
+
...typesMap.customTypes.keys(),
|
|
57
|
+
]);
|
|
58
|
+
if (desiredSchemas.size === 0) {
|
|
59
|
+
console.log(`\x1b[34m• Skipping schemas since none found.\x1b[0m`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await mkdir(`${schemaParentDir}/schemas`, { recursive: true });
|
|
63
|
+
await Promise.all(Object.entries(schemas).map(async ([schemaName, schema]) => {
|
|
64
|
+
if (desiredSchemas.has(schemaName)) {
|
|
65
|
+
await writeFile(`${schemaParentDir}/schemas/${schemaName}.schema.json`, JSON.stringify(schema), 'utf-8');
|
|
66
|
+
}
|
|
67
|
+
}));
|
|
68
|
+
const schemaImports = Array.from(desiredSchemas)
|
|
69
|
+
.map((schema) => `
|
|
70
|
+
import * as ${schema} from './schemas/${schema}.schema.json' ${supportsImportAttributes ? `with { type: 'json' }` : ''}
|
|
71
|
+
addSchema('${schema}', ${schema})
|
|
72
|
+
// addSchema('${schema}', require('./schemas/${schema}.schema.json'))
|
|
73
|
+
`)
|
|
74
|
+
.join('\n');
|
|
75
|
+
await writeFileInDir(`${schemaParentDir}/register.gen.ts`, `import { addSchema } from '@pikku/core/schema'
|
|
76
|
+
// import { createRequire } from "module"
|
|
77
|
+
// const require = createRequire(import.meta.url)
|
|
78
|
+
${schemaImports}`);
|
|
79
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { InspectorState } from '@pikku/inspector';
|
|
2
|
+
export declare const getFileImportRelativePath: (from: string, to: string, packageMappings: Record<string, string>) => string;
|
|
3
|
+
interface Meta {
|
|
4
|
+
file: string;
|
|
5
|
+
variable: string;
|
|
6
|
+
type: string;
|
|
7
|
+
typePath: string;
|
|
8
|
+
}
|
|
9
|
+
export type FilesAndMethods = {
|
|
10
|
+
userSessionType: Meta;
|
|
11
|
+
sessionServicesType: Meta;
|
|
12
|
+
pikkuConfigFactory: Meta;
|
|
13
|
+
singletonServicesFactory: Meta;
|
|
14
|
+
sessionServicesFactory: Meta;
|
|
15
|
+
};
|
|
16
|
+
export interface PikkuCLIOptions {
|
|
17
|
+
config?: string;
|
|
18
|
+
configFileType?: string;
|
|
19
|
+
userSessionType?: string;
|
|
20
|
+
singletonServicesFactoryType?: string;
|
|
21
|
+
sessionServicesFactoryType?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare const getPikkuFilesAndMethods: ({ sessionServicesTypeImportMap: httpSessionServicesTypeImportMap, userSessionTypeImportMap, sessionServicesFactories, singletonServicesFactories, configFactories, }: InspectorState, packageMappings: Record<string, string>, outputFile: string, { configFileType, singletonServicesFactoryType, sessionServicesFactoryType, }: PikkuCLIOptions, requires?: Partial<{
|
|
24
|
+
config: boolean;
|
|
25
|
+
sessionServiceType: boolean;
|
|
26
|
+
userSessionType: boolean;
|
|
27
|
+
singletonServicesFactory: boolean;
|
|
28
|
+
sessionServicesFactory: boolean;
|
|
29
|
+
}>) => Promise<FilesAndMethods>;
|
|
30
|
+
export declare const writeFileInDir: (path: string, content: string, ignoreModifyComment?: boolean) => Promise<void>;
|
|
31
|
+
export declare const logCommandInfoAndTime: (commandStart: string, commandEnd: string, [skipCondition, skipMessage]: [boolean] | [boolean, string], callback: (...args: any[]) => Promise<unknown>) => Promise<boolean>;
|
|
32
|
+
export declare const logPikkuLogo: () => void;
|
|
33
|
+
export declare const DO_NOT_MODIFY_COMMENT = "/**\n * This file was generated by the @pikku/cli\n */\n";
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// import packageInfo from '../package.json'
|
|
2
|
+
import { relative, dirname } from 'path';
|
|
3
|
+
import { mkdir, writeFile } from 'fs/promises';
|
|
4
|
+
export const getFileImportRelativePath = (from, to, packageMappings) => {
|
|
5
|
+
let filePath = relative(dirname(from), to);
|
|
6
|
+
if (!/^\.+\//.test(filePath)) {
|
|
7
|
+
filePath = `./${filePath}`;
|
|
8
|
+
}
|
|
9
|
+
let usesPackageName = false;
|
|
10
|
+
for (const [path, packageName] of Object.entries(packageMappings)) {
|
|
11
|
+
if (filePath.includes(path)) {
|
|
12
|
+
usesPackageName = true;
|
|
13
|
+
filePath = filePath.replace(new RegExp(`.*${path}`), packageName);
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (usesPackageName) {
|
|
18
|
+
return filePath.replace('.ts', '');
|
|
19
|
+
}
|
|
20
|
+
return filePath.replace('.ts', '.js');
|
|
21
|
+
};
|
|
22
|
+
const getMetaTypes = (type, errors, map, desiredType) => {
|
|
23
|
+
if (desiredType) {
|
|
24
|
+
const entries = Object.entries(map);
|
|
25
|
+
for (const [file, meta] of entries) {
|
|
26
|
+
for (const { type, variable, typePath } of meta) {
|
|
27
|
+
if (type === desiredType) {
|
|
28
|
+
return { file, variable, type, typePath };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
errors.set(`No ${desiredType} found that extends ${type}`, map);
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const totalValues = Object.values(map).flat();
|
|
36
|
+
if (totalValues.length === 0) {
|
|
37
|
+
errors.set(`No ${type} found`, map);
|
|
38
|
+
}
|
|
39
|
+
else if (totalValues.length > 1) {
|
|
40
|
+
errors.set(`More than one ${type} found`, map);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const entry = Object.entries(map)[0];
|
|
44
|
+
if (entry) {
|
|
45
|
+
const [file, [{ type, variable, typePath }]] = entry;
|
|
46
|
+
return { file, type, variable, typePath };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
};
|
|
51
|
+
export const getPikkuFilesAndMethods = async ({ sessionServicesTypeImportMap: httpSessionServicesTypeImportMap, userSessionTypeImportMap, sessionServicesFactories, singletonServicesFactories, configFactories, }, packageMappings, outputFile, { configFileType, singletonServicesFactoryType, sessionServicesFactoryType, }, requires = {
|
|
52
|
+
config: false,
|
|
53
|
+
sessionServiceType: false,
|
|
54
|
+
userSessionType: false,
|
|
55
|
+
singletonServicesFactory: false,
|
|
56
|
+
sessionServicesFactory: false,
|
|
57
|
+
}) => {
|
|
58
|
+
let errors = new Map();
|
|
59
|
+
const result = {
|
|
60
|
+
userSessionType: getMetaTypes('CoreUserSession', requires.userSessionType ? errors : new Map(), userSessionTypeImportMap, configFileType),
|
|
61
|
+
sessionServicesType: getMetaTypes('CoreServices', requires.sessionServiceType ? errors : new Map(), httpSessionServicesTypeImportMap),
|
|
62
|
+
pikkuConfigFactory: getMetaTypes('CoreConfig', requires.config ? errors : new Map(), configFactories, configFileType),
|
|
63
|
+
singletonServicesFactory: getMetaTypes('CreateSingletonServices', requires.singletonServicesFactory ? errors : new Map(), singletonServicesFactories, singletonServicesFactoryType),
|
|
64
|
+
sessionServicesFactory: getMetaTypes('CreateSessionServices', requires.sessionServicesFactory ? errors : new Map(), sessionServicesFactories, sessionServicesFactoryType),
|
|
65
|
+
};
|
|
66
|
+
if (errors.size > 0) {
|
|
67
|
+
const result = ['Found errors:'];
|
|
68
|
+
errors.forEach((filesAndMethods, message) => {
|
|
69
|
+
result.push(`- ${message}`);
|
|
70
|
+
for (const [file, methods] of Object.entries(filesAndMethods)) {
|
|
71
|
+
result.push(`\t* file: ${getFileImportRelativePath(outputFile, file, packageMappings)}`);
|
|
72
|
+
result.push(`\t* methods: ${methods.map(({ variable, type }) => `${variable}: ${type}`).join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
console.error(result.join('\n'));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
};
|
|
80
|
+
export const writeFileInDir = async (path, content, ignoreModifyComment = false) => {
|
|
81
|
+
if (content.includes('server-only')) {
|
|
82
|
+
content = content.replace("'server-only'", `'server-only'\n\n${ignoreModifyComment ? '' : DO_NOT_MODIFY_COMMENT}`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
content = `${ignoreModifyComment ? '' : DO_NOT_MODIFY_COMMENT}${content}`;
|
|
86
|
+
}
|
|
87
|
+
await mkdir(dirname(path), { recursive: true });
|
|
88
|
+
await writeFile(path, content, 'utf-8');
|
|
89
|
+
console.log(`\x1b[32m✓ File written to ${path}\x1b[0m`);
|
|
90
|
+
};
|
|
91
|
+
export const logCommandInfoAndTime = async (commandStart, commandEnd, [skipCondition, skipMessage = 'none found'], callback) => {
|
|
92
|
+
if (skipCondition === true) {
|
|
93
|
+
console.log(`\x1b[34m• Skipping ${commandStart} since ${skipMessage}.\x1b[0m`);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const start = Date.now();
|
|
97
|
+
console.log(`\x1b[34m• ${commandStart}...\x1b[0m`);
|
|
98
|
+
await callback();
|
|
99
|
+
console.log(`\x1b[32m✓ ${commandEnd} in ${Date.now() - start}ms.\x1b[0m`);
|
|
100
|
+
return true;
|
|
101
|
+
};
|
|
102
|
+
export const logPikkuLogo = () => {
|
|
103
|
+
console.log(`\x1b[33m⚙️ PIKKU CLI ⚙️\n-------------------\x1b[0m`);
|
|
104
|
+
};
|
|
105
|
+
// TODO: add version back in once the ESM dust settles
|
|
106
|
+
export const DO_NOT_MODIFY_COMMENT = `/**
|
|
107
|
+
* This file was generated by the @pikku/cli
|
|
108
|
+
*/
|
|
109
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["../bin/pikku-all.ts","../bin/pikku-channels-map.ts","../bin/pikku-channels.ts","../bin/pikku-fetch.ts","../bin/pikku-function-types.ts","../bin/pikku-nextjs.ts","../bin/pikku-openapi.ts","../bin/pikku-routes-map.ts","../bin/pikku-routes.ts","../bin/pikku-scheduler.ts","../bin/pikku-schemas.ts","../bin/pikku-websocket.ts","../bin/pikku.ts","../src/inspector-glob.ts","../src/pikku-cli-config.ts","../src/utils.ts","../src/channels/serialize-channels.ts","../src/channels/serialize-typed-channel-map.ts","../src/channels/serialize-websocket-wrapper.ts","../src/core/serialize-import-map.ts","../src/core/serialize-pikku-types.ts","../src/http/serialize-fetch-wrapper.ts","../src/http/serialize-route-imports.ts","../src/http/serialize-route-meta.ts","../src/http/serialize-typed-route-map.ts","../src/nextjs/serialize-nextjs-wrapper.ts","../src/openapi/openapi-spec-generator.ts","../src/scheduler/serialize-schedulers.ts","../src/schema/schema-generator.ts"],"version":"5.7.3"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pikku/cli",
|
|
3
|
+
"version": "0.6.6",
|
|
4
|
+
"author": "yasser.fadl@gmail.com",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pikku": "dist/bin/pikku.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "dist/bin/pikku.js",
|
|
11
|
+
"module": "dist/bin/pikku.js",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"tsc": "tsc",
|
|
14
|
+
"build:esm": "tsc -b",
|
|
15
|
+
"schema": "ts-json-schema-generator -o cli.schema.json --path 'src/pikku-cli-config.ts' --type 'PikkuCLIConfig'",
|
|
16
|
+
"build": "yarn build:esm && yarn schema",
|
|
17
|
+
"ncu": "ncu -x '/.*glob.*/'",
|
|
18
|
+
"release": "yarn build && npm test",
|
|
19
|
+
"test": "bash run-tests.sh",
|
|
20
|
+
"test:watch": "bash run-tests.sh --watch",
|
|
21
|
+
"test:coverage": "bash run-tests.sh --coverage"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@openapi-contrib/json-schema-to-openapi-schema": "^3.0.2",
|
|
25
|
+
"@pikku/core": "^0.6.7",
|
|
26
|
+
"@pikku/inspector": "^0.6.2",
|
|
27
|
+
"@types/cookie": "^0.6.0",
|
|
28
|
+
"@types/uuid": "^10.0.0",
|
|
29
|
+
"commander": "^12",
|
|
30
|
+
"glob": "^10",
|
|
31
|
+
"path-to-regexp": "^8.2.0",
|
|
32
|
+
"ts-json-schema-generator": "^2.3.0",
|
|
33
|
+
"typescript": "^5.6",
|
|
34
|
+
"yaml": "^2.6.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.7.8"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/run-tests.sh
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Enable nullglob to handle cases where no files match the pattern
|
|
4
|
+
shopt -s nullglob
|
|
5
|
+
|
|
6
|
+
# Initialize variables for options
|
|
7
|
+
watch_mode=false
|
|
8
|
+
coverage_mode=false
|
|
9
|
+
|
|
10
|
+
# Parse command-line options
|
|
11
|
+
while [[ $# -gt 0 ]]; do
|
|
12
|
+
case $1 in
|
|
13
|
+
--watch)
|
|
14
|
+
watch_mode=true
|
|
15
|
+
shift
|
|
16
|
+
;;
|
|
17
|
+
--coverage)
|
|
18
|
+
coverage_mode=true
|
|
19
|
+
shift
|
|
20
|
+
;;
|
|
21
|
+
*)
|
|
22
|
+
echo "Unknown option: $1"
|
|
23
|
+
exit 1
|
|
24
|
+
;;
|
|
25
|
+
esac
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
# Define the pattern to match your test files
|
|
29
|
+
pattern="src/*.test.ts"
|
|
30
|
+
|
|
31
|
+
# Expand the pattern into an array of files
|
|
32
|
+
files=($(find src -type f -name "*.test.ts"))
|
|
33
|
+
|
|
34
|
+
# Check if any files matched the pattern
|
|
35
|
+
if [ ${#files[@]} -eq 0 ]; then
|
|
36
|
+
echo "No test files found matching pattern: $pattern"
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Construct the node command
|
|
41
|
+
node_cmd="node --import tsx --test"
|
|
42
|
+
|
|
43
|
+
# Append options based on flags
|
|
44
|
+
if [ "$watch_mode" = true ]; then
|
|
45
|
+
node_cmd="$node_cmd --watch"
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
if [ "$coverage_mode" = true ]; then
|
|
49
|
+
node_cmd="$node_cmd --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Execute the node command with the expanded list of files
|
|
53
|
+
$node_cmd "${files[@]}"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ChannelsMeta } from '@pikku/core/channel'
|
|
2
|
+
import { getFileImportRelativePath } from '../utils.js'
|
|
3
|
+
|
|
4
|
+
export const serializeChannels = (
|
|
5
|
+
outputPath: string,
|
|
6
|
+
filesWithChannels: Set<string>,
|
|
7
|
+
packageMappings: Record<string, string> = {}
|
|
8
|
+
) => {
|
|
9
|
+
const serializedOutput: string[] = [
|
|
10
|
+
'/* The files with an addChannel function call */',
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
Array.from(filesWithChannels)
|
|
14
|
+
.sort()
|
|
15
|
+
.forEach((path) => {
|
|
16
|
+
const filePath = getFileImportRelativePath(
|
|
17
|
+
outputPath,
|
|
18
|
+
path,
|
|
19
|
+
packageMappings
|
|
20
|
+
)
|
|
21
|
+
serializedOutput.push(`import '${filePath}'`)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return serializedOutput.join('\n')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const serializeChannelMeta = (channelsMeta: ChannelsMeta) => {
|
|
28
|
+
const serializedOutput: string[] = []
|
|
29
|
+
serializedOutput.push("import { setChannelsMeta } from '@pikku/core/channel'")
|
|
30
|
+
serializedOutput.push(
|
|
31
|
+
`setChannelsMeta(${JSON.stringify(channelsMeta, null, 2)})`
|
|
32
|
+
)
|
|
33
|
+
return serializedOutput.join('\n')
|
|
34
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { ChannelsMeta } from '@pikku/core/channel'
|
|
2
|
+
import { serializeImportMap } from '../core/serialize-import-map.js'
|
|
3
|
+
import { TypesMap } from '@pikku/inspector'
|
|
4
|
+
|
|
5
|
+
export const serializeTypedChannelsMap = (
|
|
6
|
+
relativeToPath: string,
|
|
7
|
+
packageMappings: Record<string, string>,
|
|
8
|
+
typesMap: TypesMap,
|
|
9
|
+
channelsMeta: ChannelsMeta
|
|
10
|
+
): string => {
|
|
11
|
+
const { channels, requiredTypes } = generateChannels(channelsMeta)
|
|
12
|
+
typesMap.customTypes.forEach(({ references }) => {
|
|
13
|
+
for (const reference of references) {
|
|
14
|
+
requiredTypes.add(reference)
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
const imports = serializeImportMap(
|
|
18
|
+
relativeToPath,
|
|
19
|
+
packageMappings,
|
|
20
|
+
typesMap,
|
|
21
|
+
requiredTypes
|
|
22
|
+
)
|
|
23
|
+
return `/**
|
|
24
|
+
* This provides the structure needed for TypeScript to be aware of channels
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
${imports}
|
|
28
|
+
|
|
29
|
+
interface ChannelHandler<I, O> {
|
|
30
|
+
input: I;
|
|
31
|
+
output: O;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
${channels}
|
|
35
|
+
|
|
36
|
+
export type ChannelDefaultHandlerOf<Channel extends keyof ChannelsMap> =
|
|
37
|
+
ChannelsMap[Channel]['defaultMessage'] extends { input: infer I; output: infer O }
|
|
38
|
+
? ChannelHandler<I, O>
|
|
39
|
+
: never;
|
|
40
|
+
|
|
41
|
+
export type ChannelRouteHandlerOf<
|
|
42
|
+
Channel extends keyof ChannelsMap,
|
|
43
|
+
Route extends keyof ChannelsMap[Channel]['routes'],
|
|
44
|
+
Method extends keyof ChannelsMap[Channel]['routes'][Route],
|
|
45
|
+
> =
|
|
46
|
+
ChannelsMap[Channel]['routes'][Route][Method] extends { input: infer I; output: infer O }
|
|
47
|
+
? ChannelHandler<I, O>
|
|
48
|
+
: never;
|
|
49
|
+
`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function generateChannels(channelsMeta: ChannelsMeta) {
|
|
53
|
+
const requiredTypes = new Set<string>()
|
|
54
|
+
const channelsObject: Record<
|
|
55
|
+
string,
|
|
56
|
+
{
|
|
57
|
+
message: { inputs: string[] | null; outputs: string[] | null } | null
|
|
58
|
+
routes: Record<
|
|
59
|
+
string,
|
|
60
|
+
Record<
|
|
61
|
+
string,
|
|
62
|
+
{
|
|
63
|
+
inputTypes: string[] | null
|
|
64
|
+
outputTypes: string[] | null
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
>
|
|
68
|
+
}
|
|
69
|
+
> = {}
|
|
70
|
+
|
|
71
|
+
for (const meta of channelsMeta) {
|
|
72
|
+
const { name, messageRoutes, message } = meta
|
|
73
|
+
|
|
74
|
+
if (!channelsObject[name]) {
|
|
75
|
+
channelsObject[name] = { message, routes: {} }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const [key, route] of Object.entries(messageRoutes)) {
|
|
79
|
+
if (!channelsObject[name].routes[key]) {
|
|
80
|
+
channelsObject[name].routes[key] = {}
|
|
81
|
+
}
|
|
82
|
+
for (const [method, { inputs, outputs }] of Object.entries(route)) {
|
|
83
|
+
const inputTypes = inputs || null
|
|
84
|
+
const outputTypes = outputs || null
|
|
85
|
+
channelsObject[name].routes[key][method] = {
|
|
86
|
+
inputTypes,
|
|
87
|
+
outputTypes,
|
|
88
|
+
}
|
|
89
|
+
inputTypes?.forEach((type) => requiredTypes.add(type))
|
|
90
|
+
outputTypes?.forEach((type) => requiredTypes.add(type))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let routesStr = 'export type ChannelsMap = {\n'
|
|
96
|
+
|
|
97
|
+
for (const [channelPath, { routes, message }] of Object.entries(
|
|
98
|
+
channelsObject
|
|
99
|
+
)) {
|
|
100
|
+
routesStr += ` readonly '${channelPath}': {\n`
|
|
101
|
+
|
|
102
|
+
// Add `routes` object
|
|
103
|
+
routesStr += ` readonly routes: {\n`
|
|
104
|
+
for (const [key, methods] of Object.entries(routes)) {
|
|
105
|
+
routesStr += ` readonly ${key}: {\n`
|
|
106
|
+
for (const [method, handler] of Object.entries(methods)) {
|
|
107
|
+
routesStr += ` readonly ${method}: ChannelHandler<${formatTypeArray(
|
|
108
|
+
handler.inputTypes
|
|
109
|
+
)}, ${formatTypeArray(handler.outputTypes)}>,\n`
|
|
110
|
+
}
|
|
111
|
+
routesStr += ' },\n'
|
|
112
|
+
}
|
|
113
|
+
routesStr += ' },\n'
|
|
114
|
+
|
|
115
|
+
// Add `defaultMessage` outside `routes`
|
|
116
|
+
if (message) {
|
|
117
|
+
routesStr += ` readonly defaultMessage: ChannelHandler<${formatTypeArray(
|
|
118
|
+
message.inputs
|
|
119
|
+
)}, ${formatTypeArray(message.outputs)}>,\n`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
routesStr += ' },\n'
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
routesStr += '};'
|
|
126
|
+
|
|
127
|
+
return { channels: routesStr, requiredTypes }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Utility to format type arrays
|
|
131
|
+
function formatTypeArray(types: string[] | null): string {
|
|
132
|
+
return types ? types.join(' | ') : 'null'
|
|
133
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const serializeWebsocketWrapper = (channelsMapPath: string) => {
|
|
2
|
+
return `import { AbstractPikkuWebsocket, AbstractPikkuRouteHandler } from '@pikku/websocket'
|
|
3
|
+
import { ChannelDefaultHandlerOf, ChannelRouteHandlerOf, ChannelsMap } from '${channelsMapPath}';
|
|
4
|
+
|
|
5
|
+
class PikkuWebSocketRoute<Channel extends keyof ChannelsMap, Route extends keyof ChannelsMap[Channel]['routes']> extends AbstractPikkuRouteHandler {
|
|
6
|
+
public subscribe<
|
|
7
|
+
Method extends keyof ChannelsMap[Channel]['routes'][Route],
|
|
8
|
+
Data extends ChannelRouteHandlerOf<Channel, Route, Method>['output']
|
|
9
|
+
>(method: Method, callback: (data: Data) => void
|
|
10
|
+
) {
|
|
11
|
+
super.subscribe(method.toString(), callback)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public unsubscribe<
|
|
15
|
+
Method extends keyof ChannelsMap[Channel]['routes'][Route],
|
|
16
|
+
Data extends ChannelRouteHandlerOf<Channel, Route, Method>['output']
|
|
17
|
+
>(method: Method, callback?: (data: Data) => void) {
|
|
18
|
+
super.unsubscribe(method.toString(), callback)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public send<
|
|
22
|
+
Method extends keyof ChannelsMap[Channel]['routes'][Route],
|
|
23
|
+
Data extends ChannelRouteHandlerOf<Channel, Route, Method>['input']
|
|
24
|
+
>(method: Method, data: Data) {
|
|
25
|
+
super.send(method.toString(), data)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class PikkuWebSocket<Channel extends keyof ChannelsMap> extends AbstractPikkuWebsocket {
|
|
30
|
+
/**
|
|
31
|
+
* Send a message to a specific route and method.
|
|
32
|
+
* Validates the input data type.
|
|
33
|
+
*/
|
|
34
|
+
public getRoute<Route extends keyof ChannelsMap[Channel]['routes']>(route: Route): PikkuWebSocketRoute<Channel, Route> {
|
|
35
|
+
return super.getRoute(route)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Subscribe to a specific route and method.
|
|
40
|
+
*/
|
|
41
|
+
public subscribe<Data extends ChannelDefaultHandlerOf<Channel>['output']>(
|
|
42
|
+
callback: (data: Data) => void
|
|
43
|
+
) {
|
|
44
|
+
super.subscribe(callback)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Subscribe to a specific route and method.
|
|
49
|
+
*/
|
|
50
|
+
public unsubscribe<Data extends ChannelDefaultHandlerOf<Channel>['output']>(
|
|
51
|
+
callback?: (data: Data) => void
|
|
52
|
+
) {
|
|
53
|
+
super.unsubscribe(callback)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public send(data: ChannelDefaultHandlerOf<Channel>['input']) {
|
|
57
|
+
super.send(data)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
`
|
|
61
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { TypesMap } from '@pikku/inspector'
|
|
2
|
+
import { getFileImportRelativePath } from '../utils.js'
|
|
3
|
+
|
|
4
|
+
export const serializeImportMap = (
|
|
5
|
+
relativeToPath: string,
|
|
6
|
+
packageMappings: Record<string, string>,
|
|
7
|
+
typesMap: TypesMap,
|
|
8
|
+
requiredTypes: Set<string>
|
|
9
|
+
) => {
|
|
10
|
+
const paths = new Map<string, string[]>()
|
|
11
|
+
Array.from(requiredTypes).forEach((requiredType) => {
|
|
12
|
+
const { originalName, uniqueName, path } =
|
|
13
|
+
typesMap.getTypeMeta(requiredType)
|
|
14
|
+
if (!path) {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
const variables = paths.get(path) || []
|
|
18
|
+
if (originalName === uniqueName) {
|
|
19
|
+
variables.push(originalName)
|
|
20
|
+
} else {
|
|
21
|
+
variables.push(`${originalName} as ${uniqueName}`)
|
|
22
|
+
}
|
|
23
|
+
paths.set(path, variables)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const imports: string[] = []
|
|
27
|
+
for (const [path, variables] of paths.entries()) {
|
|
28
|
+
imports.push(
|
|
29
|
+
`import type { ${variables.join(', ')} } from '${getFileImportRelativePath(relativeToPath, path, packageMappings)}'`
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
return imports.join('\n')
|
|
33
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
*/
|
|
4
|
+
export const serializePikkuTypes = (
|
|
5
|
+
userSessionTypeImport: string,
|
|
6
|
+
userSessionTypeName: string,
|
|
7
|
+
sessionServicesTypeImport: string,
|
|
8
|
+
servicesTypeName: string
|
|
9
|
+
) => {
|
|
10
|
+
return `/**
|
|
11
|
+
* This is used to provide the application types in the typescript project
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { CoreAPIFunction, CoreAPIFunctionSessionless, CoreAPIPermission, MakeRequired } from '@pikku/core'
|
|
15
|
+
import { CoreHTTPFunctionRoute, AssertRouteParams } from '@pikku/core/http'
|
|
16
|
+
import { CoreScheduledTask } from '@pikku/core/scheduler'
|
|
17
|
+
import { CoreAPIChannel, CoreChannelConnection, CoreChannelDisconnection, CoreChannelMessage, PikkuChannel } from '@pikku/core/channel'
|
|
18
|
+
|
|
19
|
+
${userSessionTypeImport}
|
|
20
|
+
${sessionServicesTypeImport}
|
|
21
|
+
|
|
22
|
+
export type APIPermission<In = unknown, RequiredServices = ${servicesTypeName}> = CoreAPIPermission<In, RequiredServices, ${userSessionTypeName}>
|
|
23
|
+
|
|
24
|
+
export type APIFunctionSessionless<In = unknown, Out = never, RequiredServices = ${servicesTypeName}> = CoreAPIFunctionSessionless<In, Out, RequiredServices, ${userSessionTypeName}>
|
|
25
|
+
export type APIFunction<In = unknown, Out = never, RequiredServices = ${servicesTypeName}> = CoreAPIFunction<In, Out, RequiredServices, ${userSessionTypeName}>
|
|
26
|
+
type APIRoute<In, Out, Route extends string> = CoreHTTPFunctionRoute<In, Out, Route, APIFunction<In, Out>, APIFunctionSessionless<In, Out>, APIPermission<In>>
|
|
27
|
+
|
|
28
|
+
export type ChannelConnection<Out = never, ChannelData = unknown, RequiredServices extends ${servicesTypeName} = ${servicesTypeName}> = (services: MakeRequired<Services, 'eventHub'>, channel: PikkuChannel<${userSessionTypeName}, ChannelData, Out>) => Promise<void>
|
|
29
|
+
export type ChannelDisconnection<ChannelData = unknown, RequiredServices extends ${servicesTypeName} = ${servicesTypeName}> = (services: MakeRequired<Services, 'eventHub'>, channel: PikkuChannel<${userSessionTypeName}, ChannelData, never>) => Promise<void>
|
|
30
|
+
export type ChannelMessage<In, Out = never, ChannelData = unknown, RequiredServices extends ${servicesTypeName} = ${servicesTypeName}> = (services: MakeRequired<Services, 'eventHub'>, channel: PikkuChannel<${userSessionTypeName}, ChannelData, Out>, data: In) => Promise<Out | void>
|
|
31
|
+
type APIChannel<ChannelData, Channel extends string, In extends unknown, Out extends unknown> = CoreAPIChannel<ChannelData, Channel, ChannelConnection, ChannelDisconnection, ChannelMessage<In, Out, ChannelData>>
|
|
32
|
+
|
|
33
|
+
type ScheduledTask = CoreScheduledTask<APIFunctionSessionless<void, void>, ${userSessionTypeName}>
|
|
34
|
+
|
|
35
|
+
declare module "@pikku/core" {
|
|
36
|
+
// type APIPermission<In = unknown, RequiredServices = ${servicesTypeName}> = CoreAPIPermission<In, RequiredServices, ${userSessionTypeName}>
|
|
37
|
+
// type APIFunction = <In = unknown, Out = never, RequiredServices = ${servicesTypeName}> = CoreAPIFunction<In, Out, RequiredServices, ${userSessionTypeName}>
|
|
38
|
+
// type APIFunctionSessionless = <In = unknown, Out = never, RequiredServices = ${servicesTypeName}> = CoreAPIFunctionSessionless<In, Out, RequiredServices, ${userSessionTypeName}>
|
|
39
|
+
|
|
40
|
+
function addChannel<ChannelData, Channel extends string>(
|
|
41
|
+
channel: APIChannel<ChannelData, Channel> & AssertRouteParams<ChannelData, Channel>
|
|
42
|
+
): void;
|
|
43
|
+
|
|
44
|
+
function addRoute<In, Out, Route extends string>(
|
|
45
|
+
route: APIRoute<In, Out, Route> & AssertRouteParams<In, Route>
|
|
46
|
+
): void;
|
|
47
|
+
|
|
48
|
+
function addScheduledTask(
|
|
49
|
+
task: ScheduledTask
|
|
50
|
+
): void;
|
|
51
|
+
}
|
|
52
|
+
`
|
|
53
|
+
}
|