@examplary/cli 1.15.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 -24
- 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 -125
- 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
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,125 +0,0 @@
|
|
|
1
|
-
#! /usr/bin/env node
|
|
2
|
-
import { readdir, 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
|
-
/**
|
|
9
|
-
* Uploads e.g. `README.md` (English) and its translations like `README.nl.md`,
|
|
10
|
-
* returning their URLs keyed by language code.
|
|
11
|
-
*/
|
|
12
|
-
const uploadLocalizedFiles = async (dir, baseName, extension, contentType) => {
|
|
13
|
-
const pattern = new RegExp(`^${baseName}(?:\\.([a-z]{2}))?\\.${extension}$`);
|
|
14
|
-
const urls = {};
|
|
15
|
-
for (const file of await readdir(dir)) {
|
|
16
|
-
const match = file.match(pattern);
|
|
17
|
-
if (!match)
|
|
18
|
-
continue;
|
|
19
|
-
const contents = await readFile(join(dir, file));
|
|
20
|
-
if (!contents.length)
|
|
21
|
-
continue;
|
|
22
|
-
urls[match[1] || "en"] = await uploadFile(file, contents, contentType);
|
|
23
|
-
console.log(`• Uploaded ${file}`);
|
|
24
|
-
}
|
|
25
|
-
return Object.keys(urls).length ? urls : undefined;
|
|
26
|
-
};
|
|
27
|
-
export const uploadCommand = async (argv) => {
|
|
28
|
-
setApiHost(argv.host);
|
|
29
|
-
setApiKey(argv.key);
|
|
30
|
-
const dir = cwd();
|
|
31
|
-
let definition = {};
|
|
32
|
-
try {
|
|
33
|
-
({ definition } = await readMetaFile());
|
|
34
|
-
}
|
|
35
|
-
catch (e) {
|
|
36
|
-
console.error(`🚫 ${e.message}`);
|
|
37
|
-
exit(1);
|
|
38
|
-
}
|
|
39
|
-
console.log(`Uploading question type: ${definition.id}...`);
|
|
40
|
-
// Run all checks (incl. response processing test cases) before uploading
|
|
41
|
-
const result = await validateDefinition(definition, dir);
|
|
42
|
-
printValidationResult(result);
|
|
43
|
-
if (!isValid(result)) {
|
|
44
|
-
console.error(`\n🚫 Validation failed, aborting upload`);
|
|
45
|
-
exit(1);
|
|
46
|
-
}
|
|
47
|
-
// Upload the bundled components produced during validation
|
|
48
|
-
const components = {};
|
|
49
|
-
for (const [componentType, output] of Object.entries(result.bundles)) {
|
|
50
|
-
// Upload source map first if it exists
|
|
51
|
-
let sourceMapStatement = "";
|
|
52
|
-
if (output.sourcemap) {
|
|
53
|
-
const url = await uploadFile(`component-${componentType}.js.map`, output.sourcemap, "application/json");
|
|
54
|
-
sourceMapStatement = `\n//# sourceMappingURL=${url}`;
|
|
55
|
-
}
|
|
56
|
-
components[componentType] = await uploadFile(`component-${componentType}.js`, output.js + sourceMapStatement, "application/javascript");
|
|
57
|
-
console.log(`• Uploaded component ${componentType}`);
|
|
58
|
-
}
|
|
59
|
-
definition.components = components;
|
|
60
|
-
// Upload the bundled functions
|
|
61
|
-
const functions = {};
|
|
62
|
-
for (const [functionName, output] of Object.entries(result.functions)) {
|
|
63
|
-
// Upload source map first if it exists
|
|
64
|
-
let sourceMapStatement = "";
|
|
65
|
-
if (output.sourcemap) {
|
|
66
|
-
const url = await uploadFile(`function-${functionName}.js.map`, output.sourcemap, "application/json");
|
|
67
|
-
sourceMapStatement = `\n//# sourceMappingURL=${url}`;
|
|
68
|
-
}
|
|
69
|
-
functions[functionName] = await uploadFile(`function-${functionName}.js`, output.js + sourceMapStatement, "application/javascript");
|
|
70
|
-
console.log(`• Uploaded function ${functionName}`);
|
|
71
|
-
}
|
|
72
|
-
definition.functions = functions;
|
|
73
|
-
// Upload styles if present
|
|
74
|
-
if (result.styles) {
|
|
75
|
-
definition.stylesheet = await uploadFile("styles.css", result.styles, "text/css");
|
|
76
|
-
const sizeInKb = (result.styles.length / 1000).toFixed(2);
|
|
77
|
-
console.log(`• Uploaded styles (${sizeInKb} kB)`);
|
|
78
|
-
}
|
|
79
|
-
// Upload icon if specified
|
|
80
|
-
if (definition.icon && result.iconContents) {
|
|
81
|
-
definition.icon = await uploadFile("icon.svg", result.iconContents, "image/svg+xml");
|
|
82
|
-
console.log(`• Uploaded icon`);
|
|
83
|
-
}
|
|
84
|
-
definition.readmeUrls = await uploadLocalizedFiles(dir, "README", "md", "text/markdown");
|
|
85
|
-
definition.readme = definition.readmeUrls?.en;
|
|
86
|
-
definition.previewImageUrls = await uploadLocalizedFiles(dir, "preview", "png", "image/png");
|
|
87
|
-
definition.previewImage = definition.previewImageUrls?.en;
|
|
88
|
-
// Upload the preview data if present, always converted to JSON
|
|
89
|
-
try {
|
|
90
|
-
const preview = await readJsonOrYamlFile(dir, "preview");
|
|
91
|
-
if (preview) {
|
|
92
|
-
definition.previewData = await uploadFile("preview.json", JSON.stringify(preview.data), "application/json");
|
|
93
|
-
console.log(`• Uploaded preview data`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
catch (e) {
|
|
97
|
-
console.error(`🚫 ${e.message}`);
|
|
98
|
-
exit(1);
|
|
99
|
-
}
|
|
100
|
-
// Test cases have already run during validation, no need to store them
|
|
101
|
-
if (definition.responseProcessing?.testCases) {
|
|
102
|
-
delete definition.responseProcessing?.testCases;
|
|
103
|
-
}
|
|
104
|
-
// Upload the main question type definition
|
|
105
|
-
const res = await fetch(`${argv.host}/question-types`, {
|
|
106
|
-
headers: {
|
|
107
|
-
Authorization: `Bearer ${argv.key}`,
|
|
108
|
-
"Content-Type": "application/json",
|
|
109
|
-
},
|
|
110
|
-
method: "POST",
|
|
111
|
-
body: JSON.stringify(definition),
|
|
112
|
-
});
|
|
113
|
-
if (!res.ok) {
|
|
114
|
-
console.error(`🚫 Uploading question type failed`);
|
|
115
|
-
try {
|
|
116
|
-
const errorResponse = await res.json();
|
|
117
|
-
console.log(errorResponse);
|
|
118
|
-
}
|
|
119
|
-
catch (e) {
|
|
120
|
-
console.log(e);
|
|
121
|
-
}
|
|
122
|
-
exit(1);
|
|
123
|
-
}
|
|
124
|
-
console.log(`• Finished uploading definition\n`);
|
|
125
|
-
};
|
package/dist/lib/read-meta.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { QuestionType } from "@examplary/schemas";
|
|
2
|
-
export type QuestionTypeDefinition = Partial<QuestionType> & {
|
|
3
|
-
$schema?: string;
|
|
4
|
-
};
|
|
5
|
-
/**
|
|
6
|
-
* Reads and parses a `<baseName>.json` or `<baseName>.yml` file from the
|
|
7
|
-
* given directory, preferring JSON over YAML. Returns null if neither exists.
|
|
8
|
-
*/
|
|
9
|
-
export declare const readJsonOrYamlFile: (dir: string, baseName: string) => Promise<{
|
|
10
|
-
data: any;
|
|
11
|
-
path: string;
|
|
12
|
-
} | null>;
|
|
13
|
-
/**
|
|
14
|
-
* Reads and parses the question-type definition file.
|
|
15
|
-
* Prefers question-type.json over question-type.yml.
|
|
16
|
-
*/
|
|
17
|
-
export declare const readMetaFile: () => Promise<{
|
|
18
|
-
definition: QuestionTypeDefinition;
|
|
19
|
-
path: string;
|
|
20
|
-
}>;
|
package/dist/lib/read-meta.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { readFile } from "fs/promises";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import { cwd } from "process";
|
|
4
|
-
import yaml from "yaml";
|
|
5
|
-
/**
|
|
6
|
-
* Reads and parses a `<baseName>.json` or `<baseName>.yml` file from the
|
|
7
|
-
* given directory, preferring JSON over YAML. Returns null if neither exists.
|
|
8
|
-
*/
|
|
9
|
-
export const readJsonOrYamlFile = async (dir, baseName) => {
|
|
10
|
-
for (const { ext, parse, format } of [
|
|
11
|
-
{ ext: "json", parse: (v) => JSON.parse(v), format: "JSON" },
|
|
12
|
-
{ ext: "yml", parse: (v) => yaml.parse(v), format: "YAML" },
|
|
13
|
-
]) {
|
|
14
|
-
const path = join(dir, `${baseName}.${ext}`);
|
|
15
|
-
let contents;
|
|
16
|
-
try {
|
|
17
|
-
contents = await readFile(path, "utf-8");
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
try {
|
|
23
|
-
return { data: parse(contents), path };
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
throw new Error(`Invalid ${format} in ${path}: ${error.message}`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return null;
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* Reads and parses the question-type definition file.
|
|
33
|
-
* Prefers question-type.json over question-type.yml.
|
|
34
|
-
*/
|
|
35
|
-
export const readMetaFile = async () => {
|
|
36
|
-
const result = await readJsonOrYamlFile(cwd(), "question-type");
|
|
37
|
-
if (!result) {
|
|
38
|
-
throw new Error("No question-type.json or question-type.yml found in the current directory");
|
|
39
|
-
}
|
|
40
|
-
const definition = result.data;
|
|
41
|
-
if (definition.$schema)
|
|
42
|
-
delete definition.$schema;
|
|
43
|
-
return { definition, path: result.path };
|
|
44
|
-
};
|
package/dist/lib/safe-path.d.ts
DELETED
package/dist/lib/safe-path.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { resolve, sep } from "path";
|
|
2
|
-
/**
|
|
3
|
-
* Resolves `path` against `dir` and throws if the result escapes `dir`,
|
|
4
|
-
* preventing path traversal via `../` segments or absolute paths.
|
|
5
|
-
*/
|
|
6
|
-
export const resolveInside = (dir, path) => {
|
|
7
|
-
const base = resolve(dir);
|
|
8
|
-
const resolved = resolve(base, path);
|
|
9
|
-
if (resolved !== base && !resolved.startsWith(base + sep)) {
|
|
10
|
-
throw new Error(`Path "${path}" is outside of the question type directory`);
|
|
11
|
-
}
|
|
12
|
-
return resolved;
|
|
13
|
-
};
|