@examplary/cli 1.14.0 ā 1.17.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/README.md +21 -20
- package/dist/commands/init.d.ts +14 -0
- package/dist/commands/init.js +103 -0
- package/dist/commands/package.d.ts +1 -0
- package/dist/commands/package.js +16 -0
- package/dist/commands/publish.d.ts +5 -0
- package/dist/commands/publish.js +57 -0
- package/dist/commands/set-secret.d.ts +6 -0
- package/dist/commands/set-secret.js +29 -0
- package/dist/commands/validate.d.ts +0 -1
- package/dist/commands/validate.js +2 -20
- package/dist/index.js +38 -29
- package/dist/lib/api.d.ts +1 -1
- package/dist/lib/api.js +17 -6
- package/dist/lib/build-app.d.ts +21 -0
- package/dist/lib/build-app.js +179 -0
- package/dist/lib/build-current-app.d.ts +4 -0
- package/dist/lib/build-current-app.js +21 -0
- package/dist/lib/build-question-type.d.ts +8 -0
- package/dist/lib/build-question-type.js +48 -0
- package/dist/lib/bundle.d.ts +4 -3
- package/dist/lib/bundle.js +9 -31
- package/dist/lib/styles.d.ts +2 -1
- package/dist/lib/styles.js +3 -1
- package/dist/lib/validate.d.ts +2 -4
- package/dist/lib/validate.js +7 -26
- package/package.json +4 -24
- package/dist/commands/preview.d.ts +0 -5
- package/dist/commands/preview.js +0 -195
- package/dist/commands/upload.d.ts +0 -5
- package/dist/commands/upload.js +0 -114
- package/dist/lib/read-meta.d.ts +0 -20
- package/dist/lib/read-meta.js +0 -44
- package/dist/lib/safe-path.d.ts +0 -5
- package/dist/lib/safe-path.js +0 -13
- package/dist/preview-ui/assets/icon-nodes-CxaTPv-R.js +0 -2
- package/dist/preview-ui/assets/icon-nodes-CxaTPv-R.js.map +0 -1
- package/dist/preview-ui/assets/index-b38eDKFI.js +0 -3663
- package/dist/preview-ui/assets/index-b38eDKFI.js.map +0 -1
- package/dist/preview-ui/assets/index-boullcXq.css +0 -1
- package/dist/preview-ui/assets/tags-BUvo71XG.js +0 -2
- package/dist/preview-ui/assets/tags-BUvo71XG.js.map +0 -1
- package/dist/preview-ui/index.html +0 -21
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Uploader } from "./build-app.js";
|
|
2
|
+
export declare const dryUpload: Uploader;
|
|
3
|
+
/** Builds the app in the current directory, exiting on any error so a partial app is never published. */
|
|
4
|
+
export declare const buildCurrentApp: (upload: Uploader, quiet?: boolean) => Promise<Record<string, any>>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cwd, exit } from "process";
|
|
2
|
+
import { buildAppManifest, findAppFile, loadAppDefinition, } from "./build-app.js";
|
|
3
|
+
export const dryUpload = async (fileName) => `https://validate.invalid/${fileName}`;
|
|
4
|
+
/** Builds the app in the current directory, exiting on any error so a partial app is never published. */
|
|
5
|
+
export const buildCurrentApp = async (upload, quiet = false) => {
|
|
6
|
+
try {
|
|
7
|
+
const file = await findAppFile(cwd());
|
|
8
|
+
const definition = await loadAppDefinition(file);
|
|
9
|
+
if (!quiet)
|
|
10
|
+
console.log(`Building app: ${definition.id}...`);
|
|
11
|
+
const manifest = await buildAppManifest(file, definition, upload);
|
|
12
|
+
if (!quiet) {
|
|
13
|
+
console.log(`⢠Bundled ${manifest.components.length} component(s), ${manifest.hooks.length} hook(s) and ${manifest.questionTypes.length} question type(s)`);
|
|
14
|
+
}
|
|
15
|
+
return manifest;
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
console.error(`š« ${e.message}`);
|
|
19
|
+
exit(1);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Uploader } from "./build-app.js";
|
|
2
|
+
/**
|
|
3
|
+
* Checks a question type of an app (incl. its response processing test cases),
|
|
4
|
+
* bundles its components, and uploads them with its icon, README
|
|
5
|
+
* and preview image, all by paths relative to the app's directory `dir`.
|
|
6
|
+
* Returns the definition `POST /apps` takes.
|
|
7
|
+
*/
|
|
8
|
+
export declare const buildQuestionType: (questionType: Record<string, any>, dir: string, uploadFile: Uploader) => Promise<Record<string, any>>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { uploadTranslatablePath } from "./build-app.js";
|
|
2
|
+
import { isValid, printValidationResult, validateDefinition, } from "./validate.js";
|
|
3
|
+
/** Uploads a bundle, with its source map next to it. */
|
|
4
|
+
const uploadBundle = async (name, { js, sourcemap }, upload) => {
|
|
5
|
+
const sourcemapUrl = sourcemap && (await upload(`${name}.map`, sourcemap, "application/json"));
|
|
6
|
+
return upload(name, sourcemapUrl ? `${js}\n//# sourceMappingURL=${sourcemapUrl}` : js, "application/javascript");
|
|
7
|
+
};
|
|
8
|
+
/** Uploaded files by language, e.g. a `readme` given as one path or one per language. */
|
|
9
|
+
const uploadByLanguage = async (dir, paths, upload) => {
|
|
10
|
+
const urls = await uploadTranslatablePath(dir, paths, upload);
|
|
11
|
+
return typeof urls === "string" ? { en: urls } : urls;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Checks a question type of an app (incl. its response processing test cases),
|
|
15
|
+
* bundles its components, and uploads them with its icon, README
|
|
16
|
+
* and preview image, all by paths relative to the app's directory `dir`.
|
|
17
|
+
* Returns the definition `POST /apps` takes.
|
|
18
|
+
*/
|
|
19
|
+
export const buildQuestionType = async (questionType, dir, uploadFile) => {
|
|
20
|
+
// A copy: the app's own definition is hashed without the upload URLs
|
|
21
|
+
const { readme, previewImage, ...definition } = structuredClone(questionType);
|
|
22
|
+
// An app has several question types, all with files like `component-print.js`
|
|
23
|
+
const prefix = definition.id?.split(".").pop();
|
|
24
|
+
const upload = (name, contents, contentType) => uploadFile(`${prefix}.${name}`, contents, contentType);
|
|
25
|
+
const result = await validateDefinition(definition, dir);
|
|
26
|
+
if (!isValid(result)) {
|
|
27
|
+
printValidationResult(result);
|
|
28
|
+
throw new Error(`Question type ${definition.id} isn't valid`);
|
|
29
|
+
}
|
|
30
|
+
const components = {};
|
|
31
|
+
for (const [type, output] of Object.entries(result.bundles)) {
|
|
32
|
+
components[type] = await uploadBundle(`component-${type}.js`, output, upload);
|
|
33
|
+
}
|
|
34
|
+
definition.components = components;
|
|
35
|
+
if (result.styles) {
|
|
36
|
+
definition.stylesheet = await upload("styles.css", result.styles, "text/css");
|
|
37
|
+
}
|
|
38
|
+
if (definition.icon && result.iconContents) {
|
|
39
|
+
definition.icon = await upload("icon.svg", result.iconContents, "image/svg+xml");
|
|
40
|
+
}
|
|
41
|
+
definition.readmeUrls = await uploadByLanguage(dir, readme, upload);
|
|
42
|
+
definition.readme = definition.readmeUrls?.en;
|
|
43
|
+
definition.previewImageUrls = await uploadByLanguage(dir, previewImage, upload);
|
|
44
|
+
definition.previewImage = definition.previewImageUrls?.en;
|
|
45
|
+
// Test cases have run during validation, they don't need storing
|
|
46
|
+
delete definition.responseProcessing?.testCases;
|
|
47
|
+
return definition;
|
|
48
|
+
};
|
package/dist/lib/bundle.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/** A bundle built with an inline source map, as the code and the map apart. */
|
|
2
|
+
export declare const splitInlineSourcemap: (output: string) => {
|
|
2
3
|
js: string;
|
|
3
4
|
sourcemap: string;
|
|
4
5
|
sha256: string;
|
|
5
|
-
}
|
|
6
|
-
export declare const
|
|
6
|
+
};
|
|
7
|
+
export declare const buildComponent: (file: string) => Promise<{
|
|
7
8
|
js: string;
|
|
8
9
|
sourcemap: string;
|
|
9
10
|
sha256: string;
|
package/dist/lib/bundle.js
CHANGED
|
@@ -3,6 +3,14 @@ import { build } from "esbuild";
|
|
|
3
3
|
const calculateHash = (compiled) => {
|
|
4
4
|
return createHash("sha256").update(compiled).digest("hex");
|
|
5
5
|
};
|
|
6
|
+
/** A bundle built with an inline source map, as the code and the map apart. */
|
|
7
|
+
export const splitInlineSourcemap = (output) => {
|
|
8
|
+
const [js, encoded] = output.split("\n//# sourceMappingURL=data:application/json;base64,");
|
|
9
|
+
const sourcemap = encoded
|
|
10
|
+
? Buffer.from(encoded.trim(), "base64").toString("utf-8")
|
|
11
|
+
: "";
|
|
12
|
+
return { js: js.trim(), sourcemap, sha256: calculateHash(js.trim()) };
|
|
13
|
+
};
|
|
6
14
|
export const buildComponent = async (file) => {
|
|
7
15
|
const res = await build({
|
|
8
16
|
entryPoints: [file],
|
|
@@ -20,35 +28,5 @@ export const buildComponent = async (file) => {
|
|
|
20
28
|
"react-dom/client",
|
|
21
29
|
],
|
|
22
30
|
});
|
|
23
|
-
|
|
24
|
-
let sourcemap = "";
|
|
25
|
-
const sourceMapStatement = "\n//# sourceMappingURL=data:application/json;base64,";
|
|
26
|
-
if (js.includes(sourceMapStatement)) {
|
|
27
|
-
sourcemap = Buffer.from(js.split(sourceMapStatement)[1].trim(), "base64").toString("utf-8");
|
|
28
|
-
js = js.split(sourceMapStatement)[0].trim();
|
|
29
|
-
}
|
|
30
|
-
const sha256 = calculateHash(js);
|
|
31
|
-
return { js, sourcemap, sha256 };
|
|
32
|
-
};
|
|
33
|
-
export const buildFunction = async (file) => {
|
|
34
|
-
const res = await build({
|
|
35
|
-
entryPoints: [file],
|
|
36
|
-
bundle: true,
|
|
37
|
-
write: false,
|
|
38
|
-
minify: true,
|
|
39
|
-
sourcemap: "inline",
|
|
40
|
-
platform: "neutral",
|
|
41
|
-
mainFields: ["module", "main"],
|
|
42
|
-
format: "iife",
|
|
43
|
-
globalName: "userFunction",
|
|
44
|
-
});
|
|
45
|
-
let js = res.outputFiles[0].text;
|
|
46
|
-
let sourcemap = "";
|
|
47
|
-
const sourceMapStatement = "\n//# sourceMappingURL=data:application/json;base64,";
|
|
48
|
-
if (js.includes(sourceMapStatement)) {
|
|
49
|
-
sourcemap = Buffer.from(js.split(sourceMapStatement)[1].trim(), "base64").toString("utf-8");
|
|
50
|
-
js = js.split(sourceMapStatement)[0].trim();
|
|
51
|
-
}
|
|
52
|
-
const sha256 = calculateHash(js);
|
|
53
|
-
return { js, sourcemap, sha256 };
|
|
31
|
+
return splitInlineSourcemap(res.outputFiles[0].text);
|
|
54
32
|
};
|
package/dist/lib/styles.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
/** The Tailwind classes used by the files in `base`, or null when there are none. */
|
|
2
|
+
export declare const buildStyles: (base?: string) => Promise<string | null>;
|
package/dist/lib/styles.js
CHANGED
|
@@ -4,7 +4,8 @@ import tailwindcss from "@tailwindcss/postcss";
|
|
|
4
4
|
import postcss from "postcss";
|
|
5
5
|
const require = createRequire(import.meta.url);
|
|
6
6
|
const tailwindDir = path.dirname(require.resolve("tailwindcss/package.json"));
|
|
7
|
-
|
|
7
|
+
/** The Tailwind classes used by the files in `base`, or null when there are none. */
|
|
8
|
+
export const buildStyles = async (base = process.cwd()) => {
|
|
8
9
|
const absolutePath = path.resolve(tailwindDir);
|
|
9
10
|
const inputCSS = `
|
|
10
11
|
@import "${absolutePath}/theme.css" layer(theme);
|
|
@@ -12,6 +13,7 @@ export const buildStyles = async () => {
|
|
|
12
13
|
`;
|
|
13
14
|
const result = await postcss([
|
|
14
15
|
tailwindcss({
|
|
16
|
+
base,
|
|
15
17
|
optimize: { minify: true },
|
|
16
18
|
}),
|
|
17
19
|
]).process(inputCSS, { from: "module-css.css" });
|
package/dist/lib/validate.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { QuestionType } from "@examplary/schemas";
|
|
2
2
|
import { TestCaseResult } from "./response-processing.js";
|
|
3
3
|
export type BuiltComponent = {
|
|
4
4
|
js: string;
|
|
@@ -18,8 +18,6 @@ export type ValidationResult = {
|
|
|
18
18
|
styles?: string;
|
|
19
19
|
/** Raw icon file contents, present only when an icon is configured. */
|
|
20
20
|
iconContents?: string;
|
|
21
|
-
/** Bundled function output, keyed by function name, ready to upload. */
|
|
22
|
-
functions: Record<string, BuiltComponent>;
|
|
23
21
|
};
|
|
24
22
|
/**
|
|
25
23
|
* Runs every check the `upload` command performs, short of actually uploading:
|
|
@@ -27,7 +25,7 @@ export type ValidationResult = {
|
|
|
27
25
|
* response processing test cases. Bundled artifacts are returned so callers can
|
|
28
26
|
* reuse them for an upload without bundling twice.
|
|
29
27
|
*/
|
|
30
|
-
export declare const validateDefinition: (definition:
|
|
28
|
+
export declare const validateDefinition: (definition: Partial<QuestionType>, dir: string) => Promise<ValidationResult>;
|
|
31
29
|
/** Whether the definition is safe to upload. */
|
|
32
30
|
export declare const isValid: (result: ValidationResult) => boolean;
|
|
33
31
|
/** Prints warnings, errors, and a test-case summary to the console. */
|
package/dist/lib/validate.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from "fs/promises";
|
|
2
|
-
import { resolve } from "path";
|
|
3
|
-
import { buildComponent
|
|
2
|
+
import { dirname, resolve } from "path";
|
|
3
|
+
import { buildComponent } from "./bundle.js";
|
|
4
4
|
import { deepCompileJsonata } from "./jsonata.js";
|
|
5
5
|
import { runResponseProcessingTestCases, } from "./response-processing.js";
|
|
6
6
|
import { buildStyles } from "./styles.js";
|
|
@@ -20,7 +20,6 @@ export const validateDefinition = async (definition, dir) => {
|
|
|
20
20
|
const errors = [];
|
|
21
21
|
const warnings = [];
|
|
22
22
|
const bundles = {};
|
|
23
|
-
const functions = {};
|
|
24
23
|
let styles;
|
|
25
24
|
let iconContents;
|
|
26
25
|
if (!definition.id)
|
|
@@ -90,24 +89,11 @@ export const validateDefinition = async (definition, dir) => {
|
|
|
90
89
|
errors.push(`Error bundling component ${componentType} from ${componentPath}: ${error.message}`);
|
|
91
90
|
}
|
|
92
91
|
}
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
functions[functionName] = output;
|
|
99
|
-
const sizeInKb = output.js.length / 1000;
|
|
100
|
-
if (sizeInKb > 100) {
|
|
101
|
-
errors.push(`Function ${functionName} is larger than the 100 kB limit (${sizeInKb.toFixed(2)} kB)`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
errors.push(`Error bundling function ${functionName} from ${functionPath}: ${error.message}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
// Build styles if any components are present
|
|
109
|
-
if (Object.keys(bundles).length) {
|
|
110
|
-
styles = (await buildStyles()) ?? undefined;
|
|
92
|
+
// The Tailwind classes used in the folder of its components
|
|
93
|
+
const [componentPath] = Object.values(definition.components || {});
|
|
94
|
+
if (Object.keys(bundles).length && componentPath) {
|
|
95
|
+
styles =
|
|
96
|
+
(await buildStyles(dirname(resolve(dir, componentPath)))) ?? undefined;
|
|
111
97
|
}
|
|
112
98
|
// Validate the icon file
|
|
113
99
|
if (definition.icon) {
|
|
@@ -127,7 +113,6 @@ export const validateDefinition = async (definition, dir) => {
|
|
|
127
113
|
warnings,
|
|
128
114
|
testCaseResults,
|
|
129
115
|
bundles,
|
|
130
|
-
functions,
|
|
131
116
|
styles,
|
|
132
117
|
iconContents,
|
|
133
118
|
};
|
|
@@ -140,10 +125,6 @@ export const printValidationResult = (result) => {
|
|
|
140
125
|
const sizeInKb = (output.js.length / 1000).toFixed(2);
|
|
141
126
|
console.log(`⢠Bundled component ${componentType} (${sizeInKb} kB)`);
|
|
142
127
|
}
|
|
143
|
-
for (const [functionName, output] of Object.entries(result.functions)) {
|
|
144
|
-
const sizeInKb = (output.js.length / 1000).toFixed(2);
|
|
145
|
-
console.log(`⢠Bundled function ${functionName} (${sizeInKb} kB)`);
|
|
146
|
-
}
|
|
147
128
|
for (const warning of result.warnings) {
|
|
148
129
|
console.warn(` ā ļø ${warning.message}`);
|
|
149
130
|
if (warning.file && process.env.GITHUB_ACTIONS) {
|
package/package.json
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@examplary/cli",
|
|
3
|
-
"description": "Command-line tools for building and publishing Examplary
|
|
3
|
+
"description": "Command-line tools for building and publishing Examplary apps.",
|
|
4
4
|
"packageManager": "yarn@4.8.1",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.17.0",
|
|
6
6
|
"bin": {
|
|
7
7
|
"exp": "./dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"release": "semantic-release -e semantic-release-monorepo",
|
|
11
|
-
"build:preview-ui": "vite build",
|
|
12
11
|
"build:js": "tsc --project tsconfig.json",
|
|
13
|
-
"build": "yarn build:js
|
|
14
|
-
"dev": "tsx scripts/dev.ts",
|
|
12
|
+
"build": "yarn build:js",
|
|
15
13
|
"test": "vitest run"
|
|
16
14
|
},
|
|
17
15
|
"publishConfig": {
|
|
@@ -22,36 +20,18 @@
|
|
|
22
20
|
],
|
|
23
21
|
"dependencies": {
|
|
24
22
|
"@examplary/schemas": "*",
|
|
25
|
-
"@examplary/ui": "*",
|
|
26
|
-
"@hono/node-server": "^2.0.10",
|
|
27
23
|
"@tailwindcss/postcss": "^4.3.3",
|
|
28
|
-
"@tailwindcss/vite": "^4.3.0",
|
|
29
24
|
"esbuild": "^0.28.1",
|
|
30
|
-
"hono": "^4.13.11",
|
|
31
25
|
"jsonata": "^2.2.0",
|
|
32
|
-
"
|
|
33
|
-
"open": "^11.0.0",
|
|
26
|
+
"postcss": "^8.5.28",
|
|
34
27
|
"tailwindcss": "^4.2.1",
|
|
35
|
-
"ws": "^8.22.0",
|
|
36
|
-
"yaml": "^2.8.3",
|
|
37
28
|
"yargs": "^18.0.0"
|
|
38
29
|
},
|
|
39
30
|
"devDependencies": {
|
|
40
31
|
"@types/node": "^24.1.0",
|
|
41
32
|
"@types/yargs": "^17.0.35",
|
|
42
|
-
"@vitejs/plugin-react": "^6.0.1",
|
|
43
|
-
"i18next": "^26.3.4",
|
|
44
|
-
"i18next-browser-languagedetector": "^8.2.0",
|
|
45
|
-
"postcss": "^8.5.28",
|
|
46
|
-
"react": "^19.2.8",
|
|
47
|
-
"react-dom": "^19.2.8",
|
|
48
|
-
"react-error-boundary": "^6.1.6",
|
|
49
|
-
"react-i18next": "^17.0.8",
|
|
50
|
-
"swr": "^2.3.6",
|
|
51
33
|
"tsx": "^4.23.15",
|
|
52
34
|
"typescript": "^7.0.2",
|
|
53
|
-
"use-local-storage": "^3.0.0",
|
|
54
|
-
"vite": "^8.3.4",
|
|
55
35
|
"vitest": "^5.0.3"
|
|
56
36
|
},
|
|
57
37
|
"homepage": "https://examplary.ai/developers",
|
package/dist/commands/preview.js
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
#! /usr/bin/env node
|
|
2
|
-
import { watch } from "fs";
|
|
3
|
-
import { readFile } from "fs/promises";
|
|
4
|
-
import { createServer as createHttpServer } from "http";
|
|
5
|
-
import { resolve } from "path";
|
|
6
|
-
import { cwd, exit } from "process";
|
|
7
|
-
import { serve } from "@hono/node-server";
|
|
8
|
-
import { serveStatic } from "@hono/node-server/serve-static";
|
|
9
|
-
import { Hono } from "hono";
|
|
10
|
-
import open from "open";
|
|
11
|
-
import { WebSocketServer } from "ws";
|
|
12
|
-
import { setApiHost, setApiKey } from "../lib/api.js";
|
|
13
|
-
import { buildComponent } from "../lib/bundle.js";
|
|
14
|
-
import { readJsonOrYamlFile, readMetaFile } from "../lib/read-meta.js";
|
|
15
|
-
import { resolveInside } from "../lib/safe-path.js";
|
|
16
|
-
import { buildStyles } from "../lib/styles.js";
|
|
17
|
-
export const previewCommand = async (argv) => {
|
|
18
|
-
setApiHost(argv.host);
|
|
19
|
-
setApiKey(argv.key);
|
|
20
|
-
const dir = cwd();
|
|
21
|
-
// Verify a question-type file exists (json or yml)
|
|
22
|
-
try {
|
|
23
|
-
await readMetaFile();
|
|
24
|
-
}
|
|
25
|
-
catch (e) {
|
|
26
|
-
console.error(`š« ${e.message}`);
|
|
27
|
-
exit(1);
|
|
28
|
-
}
|
|
29
|
-
// Let's start a Vite server to preview the question type
|
|
30
|
-
const root = resolve(import.meta.dirname, "..", "preview-ui");
|
|
31
|
-
const app = new Hono();
|
|
32
|
-
app.get("/api/bundle", async (c) => {
|
|
33
|
-
const path = c.req.query("path");
|
|
34
|
-
const sourcemap = c.req.query("sourcemap") === "1";
|
|
35
|
-
const componentName = c.req.query("componentName");
|
|
36
|
-
try {
|
|
37
|
-
const componentOutput = await buildComponent(resolveInside(dir, path));
|
|
38
|
-
if (sourcemap) {
|
|
39
|
-
return c.body(componentOutput.sourcemap, 200, {
|
|
40
|
-
"Content-Type": "application/json",
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
const sizeInKb = componentOutput.js.length / 1000;
|
|
45
|
-
if (sizeInKb > 100) {
|
|
46
|
-
console.warn(`ā ļø Component ${componentName} is larger than 100 kB! Consider optimizing it.`);
|
|
47
|
-
}
|
|
48
|
-
let output = componentOutput.js;
|
|
49
|
-
if (componentOutput.sourcemap) {
|
|
50
|
-
output += `\n//# sourceMappingURL=/api/bundle?componentName=${componentName}&path=${encodeURIComponent(path || "")}&sourcemap=1`;
|
|
51
|
-
}
|
|
52
|
-
return c.body(output, 200, {
|
|
53
|
-
"Content-Type": "application/javascript",
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
catch (error) {
|
|
58
|
-
console.error(`š« Error bundling component ${componentName} from ${path}: ${error.message}`);
|
|
59
|
-
return c.text(`Error bundling component ${componentName}: ${error.message}`, 500);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
app.get("/api/icon", async (c) => {
|
|
63
|
-
const path = c.req.query("path") || "";
|
|
64
|
-
let icon;
|
|
65
|
-
try {
|
|
66
|
-
icon = await readFile(resolveInside(dir, path), "utf-8");
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
return c.text(`Error reading icon from ${path}: ${error.message}`, 400);
|
|
70
|
-
}
|
|
71
|
-
return c.body(icon, 200, {
|
|
72
|
-
"Content-Type": "image/svg+xml",
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
app.get("/api/stylesheet", async (c) => {
|
|
76
|
-
const componentName = c.req.query("componentName");
|
|
77
|
-
try {
|
|
78
|
-
const componentOutput = await buildStyles();
|
|
79
|
-
return c.body(componentOutput || "", 200, {
|
|
80
|
-
"Content-Type": "text/css",
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
catch (error) {
|
|
84
|
-
console.error(`š« Error building CSS for component ${componentName}: ${error.message}`);
|
|
85
|
-
return c.text(`Error building CSS for component ${componentName}: ${error.message}`, 500);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
app.get("/__reload", async (c) => {
|
|
89
|
-
if (wss) {
|
|
90
|
-
wss.clients.forEach((client) => {
|
|
91
|
-
if (client.readyState === 1) {
|
|
92
|
-
// WebSocket.OPEN
|
|
93
|
-
client.send(JSON.stringify({ type: "reload" }));
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
return c.body(null, 201);
|
|
98
|
-
});
|
|
99
|
-
let wsPort; // WebSocket port will be set when server starts
|
|
100
|
-
app.get("/", async (c) => {
|
|
101
|
-
let questionType;
|
|
102
|
-
try {
|
|
103
|
-
({ definition: questionType } = await readMetaFile());
|
|
104
|
-
questionType.components = questionType.components
|
|
105
|
-
? Object.entries(questionType.components).reduce((acc, [componentName, componentPath]) => ({
|
|
106
|
-
...acc,
|
|
107
|
-
[componentName]: `/api/bundle?componentName=${componentName}&path=${encodeURIComponent(componentPath)}`,
|
|
108
|
-
}), {})
|
|
109
|
-
: questionType.components;
|
|
110
|
-
}
|
|
111
|
-
catch (e) {
|
|
112
|
-
return c.text(`Error reading question type definition: ${e.message}`, 500);
|
|
113
|
-
}
|
|
114
|
-
// Default question/answer payloads for the preview, if a preview file exists
|
|
115
|
-
let previewData = null;
|
|
116
|
-
try {
|
|
117
|
-
previewData = (await readJsonOrYamlFile(dir, "preview"))?.data ?? null;
|
|
118
|
-
}
|
|
119
|
-
catch (e) {
|
|
120
|
-
console.warn(`ā ļø ${e.message}`);
|
|
121
|
-
}
|
|
122
|
-
let template = await readFile(resolve(root, "index.html"), "utf-8");
|
|
123
|
-
// Inject hot reload script
|
|
124
|
-
const hotReloadScript = `
|
|
125
|
-
<script>
|
|
126
|
-
(function() {
|
|
127
|
-
const wsPort = ${wsPort || "null"};
|
|
128
|
-
if (wsPort) {
|
|
129
|
-
const ws = new WebSocket('ws://localhost:' + wsPort);
|
|
130
|
-
ws.onmessage = function(event) {
|
|
131
|
-
const data = JSON.parse(event.data);
|
|
132
|
-
if (data.type === 'reload') {
|
|
133
|
-
console.log('š Reloading page due to file change...');
|
|
134
|
-
window.location.reload();
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
ws.onopen = function() {
|
|
138
|
-
console.log('š Hot reload connected');
|
|
139
|
-
};
|
|
140
|
-
ws.onclose = function() {
|
|
141
|
-
console.log('š Hot reload disconnected');
|
|
142
|
-
};
|
|
143
|
-
ws.onerror = function(error) {
|
|
144
|
-
console.log('ā Hot reload error:', error);
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
})();
|
|
148
|
-
</script>
|
|
149
|
-
`;
|
|
150
|
-
template =
|
|
151
|
-
"<script>window._questionType = " +
|
|
152
|
-
JSON.stringify(questionType) +
|
|
153
|
-
";window._previewData = " +
|
|
154
|
-
JSON.stringify(previewData) +
|
|
155
|
-
`;</script>${template}${hotReloadScript}`;
|
|
156
|
-
return c.html(template);
|
|
157
|
-
});
|
|
158
|
-
app.use("/assets/*", serveStatic({ root }));
|
|
159
|
-
// Set up file watcher
|
|
160
|
-
const watchDir = cwd();
|
|
161
|
-
console.log(`š Watching ${watchDir} for changes...`);
|
|
162
|
-
let wss; // Will be initialized after server starts
|
|
163
|
-
const watcher = watch(watchDir, { recursive: true }, (filename) => {
|
|
164
|
-
if (filename && !filename.startsWith(".")) {
|
|
165
|
-
// Broadcast reload message to all connected clients
|
|
166
|
-
if (wss) {
|
|
167
|
-
wss.clients.forEach((client) => {
|
|
168
|
-
if (client.readyState === 1) {
|
|
169
|
-
// WebSocket.OPEN
|
|
170
|
-
client.send(JSON.stringify({ type: "reload" }));
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
// Clean up watcher on process exit
|
|
177
|
-
process.on("SIGINT", () => {
|
|
178
|
-
watcher.close();
|
|
179
|
-
process.exit(0);
|
|
180
|
-
});
|
|
181
|
-
serve({
|
|
182
|
-
fetch: app.fetch,
|
|
183
|
-
hostname: "localhost",
|
|
184
|
-
port: 5777,
|
|
185
|
-
}, (address) => {
|
|
186
|
-
const port = address.port;
|
|
187
|
-
wsPort = port + 1234;
|
|
188
|
-
const wsServer = createHttpServer();
|
|
189
|
-
wss = new WebSocketServer({ server: wsServer });
|
|
190
|
-
wsServer.listen(wsPort);
|
|
191
|
-
const url = `http://localhost:${port}`;
|
|
192
|
-
console.log(`š Preview server running at ${url}`);
|
|
193
|
-
open(url);
|
|
194
|
-
});
|
|
195
|
-
};
|
package/dist/commands/upload.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
#! /usr/bin/env node
|
|
2
|
-
import { readFile } from "fs/promises";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { cwd, exit } from "process";
|
|
5
|
-
import { setApiHost, setApiKey, uploadFile } from "../lib/api.js";
|
|
6
|
-
import { readJsonOrYamlFile, readMetaFile } from "../lib/read-meta.js";
|
|
7
|
-
import { isValid, printValidationResult, validateDefinition, } from "../lib/validate.js";
|
|
8
|
-
export const uploadCommand = async (argv) => {
|
|
9
|
-
setApiHost(argv.host);
|
|
10
|
-
setApiKey(argv.key);
|
|
11
|
-
const dir = cwd();
|
|
12
|
-
let definition = {};
|
|
13
|
-
try {
|
|
14
|
-
({ definition } = await readMetaFile());
|
|
15
|
-
}
|
|
16
|
-
catch (e) {
|
|
17
|
-
console.error(`š« ${e.message}`);
|
|
18
|
-
exit(1);
|
|
19
|
-
}
|
|
20
|
-
console.log(`Uploading question type: ${definition.id}...`);
|
|
21
|
-
// Run all checks (incl. response processing test cases) before uploading
|
|
22
|
-
const result = await validateDefinition(definition, dir);
|
|
23
|
-
printValidationResult(result);
|
|
24
|
-
if (!isValid(result)) {
|
|
25
|
-
console.error(`\nš« Validation failed, aborting upload`);
|
|
26
|
-
exit(1);
|
|
27
|
-
}
|
|
28
|
-
// Upload the bundled components produced during validation
|
|
29
|
-
const components = {};
|
|
30
|
-
for (const [componentType, output] of Object.entries(result.bundles)) {
|
|
31
|
-
// Upload source map first if it exists
|
|
32
|
-
let sourceMapStatement = "";
|
|
33
|
-
if (output.sourcemap) {
|
|
34
|
-
const url = await uploadFile(`component-${componentType}.js.map`, output.sourcemap, "application/json");
|
|
35
|
-
sourceMapStatement = `\n//# sourceMappingURL=${url}`;
|
|
36
|
-
}
|
|
37
|
-
components[componentType] = await uploadFile(`component-${componentType}.js`, output.js + sourceMapStatement, "application/javascript");
|
|
38
|
-
console.log(`⢠Uploaded component ${componentType}`);
|
|
39
|
-
}
|
|
40
|
-
definition.components = components;
|
|
41
|
-
// Upload the bundled functions
|
|
42
|
-
const functions = {};
|
|
43
|
-
for (const [functionName, output] of Object.entries(result.functions)) {
|
|
44
|
-
// Upload source map first if it exists
|
|
45
|
-
let sourceMapStatement = "";
|
|
46
|
-
if (output.sourcemap) {
|
|
47
|
-
const url = await uploadFile(`function-${functionName}.js.map`, output.sourcemap, "application/json");
|
|
48
|
-
sourceMapStatement = `\n//# sourceMappingURL=${url}`;
|
|
49
|
-
}
|
|
50
|
-
functions[functionName] = await uploadFile(`function-${functionName}.js`, output.js + sourceMapStatement, "application/javascript");
|
|
51
|
-
console.log(`⢠Uploaded function ${functionName}`);
|
|
52
|
-
}
|
|
53
|
-
definition.functions = functions;
|
|
54
|
-
// Upload styles if present
|
|
55
|
-
if (result.styles) {
|
|
56
|
-
definition.stylesheet = await uploadFile("styles.css", result.styles, "text/css");
|
|
57
|
-
const sizeInKb = (result.styles.length / 1000).toFixed(2);
|
|
58
|
-
console.log(`⢠Uploaded styles (${sizeInKb} kB)`);
|
|
59
|
-
}
|
|
60
|
-
// Upload icon if specified
|
|
61
|
-
if (definition.icon && result.iconContents) {
|
|
62
|
-
definition.icon = await uploadFile("icon.svg", result.iconContents, "image/svg+xml");
|
|
63
|
-
console.log(`⢠Uploaded icon`);
|
|
64
|
-
}
|
|
65
|
-
// Upload the README if present
|
|
66
|
-
const readme = await readFile(join(dir, "README.md"), "utf-8").catch(() => null);
|
|
67
|
-
if (readme) {
|
|
68
|
-
definition.readme = await uploadFile("README.md", readme, "text/markdown");
|
|
69
|
-
console.log(`⢠Uploaded README`);
|
|
70
|
-
}
|
|
71
|
-
// Upload the preview image if present
|
|
72
|
-
const previewImage = await readFile(join(dir, "preview.png")).catch(() => null);
|
|
73
|
-
if (previewImage) {
|
|
74
|
-
definition.previewImage = await uploadFile("preview.png", previewImage, "image/png");
|
|
75
|
-
console.log(`⢠Uploaded preview image`);
|
|
76
|
-
}
|
|
77
|
-
// Upload the preview data if present, always converted to JSON
|
|
78
|
-
try {
|
|
79
|
-
const preview = await readJsonOrYamlFile(dir, "preview");
|
|
80
|
-
if (preview) {
|
|
81
|
-
definition.previewData = await uploadFile("preview.json", JSON.stringify(preview.data), "application/json");
|
|
82
|
-
console.log(`⢠Uploaded preview data`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
catch (e) {
|
|
86
|
-
console.error(`š« ${e.message}`);
|
|
87
|
-
exit(1);
|
|
88
|
-
}
|
|
89
|
-
// Test cases have already run during validation, no need to store them
|
|
90
|
-
if (definition.responseProcessing?.testCases) {
|
|
91
|
-
delete definition.responseProcessing?.testCases;
|
|
92
|
-
}
|
|
93
|
-
// Upload the main question type definition
|
|
94
|
-
const res = await fetch(`${argv.host}/question-types`, {
|
|
95
|
-
headers: {
|
|
96
|
-
Authorization: `Bearer ${argv.key}`,
|
|
97
|
-
"Content-Type": "application/json",
|
|
98
|
-
},
|
|
99
|
-
method: "POST",
|
|
100
|
-
body: JSON.stringify(definition),
|
|
101
|
-
});
|
|
102
|
-
if (!res.ok) {
|
|
103
|
-
console.error(`š« Uploading question type failed`);
|
|
104
|
-
try {
|
|
105
|
-
const errorResponse = await res.json();
|
|
106
|
-
console.log(errorResponse);
|
|
107
|
-
}
|
|
108
|
-
catch (e) {
|
|
109
|
-
console.log(e);
|
|
110
|
-
}
|
|
111
|
-
exit(1);
|
|
112
|
-
}
|
|
113
|
-
console.log(`⢠Finished uploading definition\n`);
|
|
114
|
-
};
|