@rettangoli/vt 0.0.14 → 1.0.0-rc2

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.
@@ -0,0 +1,139 @@
1
+ import path from "path";
2
+ import { stripViewportSuffix } from "./viewport.js";
3
+
4
+ function toList(value) {
5
+ if (value === undefined || value === null) return [];
6
+ if (Array.isArray(value)) return value;
7
+ return [value];
8
+ }
9
+
10
+ function normalizePathValue(value) {
11
+ return String(value)
12
+ .trim()
13
+ .replace(/\\/g, "/")
14
+ .replace(/^\.?\//, "")
15
+ .replace(/\/+$/, "");
16
+ }
17
+
18
+ function normalizeItemKey(value) {
19
+ const normalized = normalizePathValue(value);
20
+ return normalized.replace(/\.(html?|ya?ml|md)$/i, "");
21
+ }
22
+
23
+ export function normalizeSelectors(raw = {}) {
24
+ const folders = toList(raw.folder)
25
+ .map(normalizePathValue)
26
+ .filter((item) => item.length > 0);
27
+ const groups = toList(raw.group)
28
+ .map((item) => String(item).trim().toLowerCase())
29
+ .filter((item) => item.length > 0);
30
+ const items = toList(raw.item)
31
+ .map(normalizeItemKey)
32
+ .filter((item) => item.length > 0);
33
+
34
+ return {
35
+ folders: [...new Set(folders)],
36
+ groups: [...new Set(groups)],
37
+ items: [...new Set(items)],
38
+ };
39
+ }
40
+
41
+ export function hasSelectors(selectors) {
42
+ return (
43
+ selectors.folders.length > 0
44
+ || selectors.groups.length > 0
45
+ || selectors.items.length > 0
46
+ );
47
+ }
48
+
49
+ function matchesFolderPrefix(filePath, folderPrefix) {
50
+ return filePath === folderPrefix || filePath.startsWith(`${folderPrefix}/`);
51
+ }
52
+
53
+ function resolveGroupFolders(configSections = [], groupSelectors = []) {
54
+ if (groupSelectors.length === 0) {
55
+ return [];
56
+ }
57
+
58
+ const groupFolderMap = new Map();
59
+ for (const section of configSections) {
60
+ if (section.type === "groupLabel" && Array.isArray(section.items)) {
61
+ for (const item of section.items) {
62
+ groupFolderMap.set(String(item.title).toLowerCase(), normalizePathValue(item.files));
63
+ }
64
+ continue;
65
+ }
66
+ if (section.files) {
67
+ groupFolderMap.set(String(section.title).toLowerCase(), normalizePathValue(section.files));
68
+ }
69
+ }
70
+
71
+ const missing = [];
72
+ const folders = [];
73
+ for (const selector of groupSelectors) {
74
+ const folder = groupFolderMap.get(selector);
75
+ if (!folder) {
76
+ missing.push(selector);
77
+ continue;
78
+ }
79
+ folders.push(folder);
80
+ }
81
+ if (missing.length > 0) {
82
+ throw new Error(
83
+ `Unknown group selector(s): ${missing.join(", ")}.`,
84
+ );
85
+ }
86
+ return [...new Set(folders)];
87
+ }
88
+
89
+ function toGeneratedFileItemKey(filePath) {
90
+ const normalized = normalizePathValue(filePath);
91
+ const ext = path.extname(normalized);
92
+ return normalized.slice(0, normalized.length - ext.length);
93
+ }
94
+
95
+ export function filterGeneratedFilesBySelectors(generatedFiles, selectors, configSections = []) {
96
+ if (!hasSelectors(selectors)) {
97
+ return generatedFiles;
98
+ }
99
+
100
+ const folderSelectors = [
101
+ ...selectors.folders,
102
+ ...resolveGroupFolders(configSections, selectors.groups),
103
+ ];
104
+
105
+ return generatedFiles.filter((file) => {
106
+ const normalizedPath = normalizePathValue(file.path);
107
+ const itemKey = toGeneratedFileItemKey(normalizedPath);
108
+
109
+ if (selectors.items.includes(itemKey)) {
110
+ return true;
111
+ }
112
+ return folderSelectors.some((folder) => matchesFolderPrefix(normalizedPath, folder));
113
+ });
114
+ }
115
+
116
+ function toScreenshotItemKey(relativeScreenshotPath) {
117
+ const normalized = normalizePathValue(relativeScreenshotPath).replace(/\.webp$/i, "");
118
+ const withoutOrdinal = normalized.replace(/-\d{1,3}$/i, "");
119
+ return stripViewportSuffix(withoutOrdinal);
120
+ }
121
+
122
+ export function filterRelativeScreenshotPathsBySelectors(relativePaths, selectors, configSections = []) {
123
+ if (!hasSelectors(selectors)) {
124
+ return relativePaths;
125
+ }
126
+
127
+ const folderSelectors = [
128
+ ...selectors.folders,
129
+ ...resolveGroupFolders(configSections, selectors.groups),
130
+ ];
131
+
132
+ return relativePaths.filter((relativePath) => {
133
+ const itemKey = toScreenshotItemKey(relativePath);
134
+ if (selectors.items.includes(itemKey)) {
135
+ return true;
136
+ }
137
+ return folderSelectors.some((folder) => matchesFolderPrefix(itemKey, folder));
138
+ });
139
+ }
@@ -0,0 +1,33 @@
1
+ export const STEP_COMMANDS = Object.freeze([
2
+ "assert",
3
+ "blur",
4
+ "check",
5
+ "clear",
6
+ "click",
7
+ "customEvent",
8
+ "dblclick",
9
+ "focus",
10
+ "goto",
11
+ "hover",
12
+ "keypress",
13
+ "mouseDown",
14
+ "mouseUp",
15
+ "move",
16
+ "rclick",
17
+ "rightMouseDown",
18
+ "rightMouseUp",
19
+ "setViewport",
20
+ "screenshot",
21
+ "scroll",
22
+ "select",
23
+ "selectOption",
24
+ "uncheck",
25
+ "upload",
26
+ "wait",
27
+ "waitFor",
28
+ "write",
29
+ ]);
30
+
31
+ export const BLOCK_COMMANDS = Object.freeze([
32
+ "select",
33
+ ]);
@@ -0,0 +1,304 @@
1
+ import { normalizeViewportField } from "./viewport.js";
2
+
3
+ function isPlainObject(value) {
4
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5
+ }
6
+
7
+ function valueType(value) {
8
+ if (Array.isArray(value)) return "array";
9
+ if (value === null) return "null";
10
+ return typeof value;
11
+ }
12
+
13
+ function assert(condition, message) {
14
+ if (!condition) {
15
+ throw new Error(message);
16
+ }
17
+ }
18
+
19
+ function validateOptionalString(value, path, options = {}) {
20
+ const { allowEmpty = false } = options;
21
+ if (value === undefined || value === null) {
22
+ return;
23
+ }
24
+ assert(typeof value === "string", `"${path}" must be a string, got ${valueType(value)}.`);
25
+ if (!allowEmpty) {
26
+ assert(value.trim().length > 0, `"${path}" cannot be empty.`);
27
+ }
28
+ }
29
+
30
+ function validateOptionalBoolean(value, path) {
31
+ if (value === undefined || value === null) {
32
+ return;
33
+ }
34
+ assert(typeof value === "boolean", `"${path}" must be a boolean, got ${valueType(value)}.`);
35
+ }
36
+
37
+ function validateOptionalNumber(value, path, options = {}) {
38
+ const { integer = false, min, max } = options;
39
+ if (value === undefined || value === null) {
40
+ return;
41
+ }
42
+
43
+ assert(
44
+ typeof value === "number" && Number.isFinite(value),
45
+ `"${path}" must be a finite number, got ${valueType(value)} (${String(value)}).`,
46
+ );
47
+
48
+ if (integer) {
49
+ assert(Number.isInteger(value), `"${path}" must be an integer, got ${value}.`);
50
+ }
51
+ if (min !== undefined) {
52
+ assert(value >= min, `"${path}" must be >= ${min}, got ${value}.`);
53
+ }
54
+ if (max !== undefined) {
55
+ assert(value <= max, `"${path}" must be <= ${max}, got ${value}.`);
56
+ }
57
+ }
58
+
59
+ function validateOptionalEnum(value, path, allowedValues) {
60
+ if (value === undefined || value === null) {
61
+ return;
62
+ }
63
+ validateOptionalString(value, path);
64
+ assert(
65
+ allowedValues.includes(value),
66
+ `"${path}" must be one of: ${allowedValues.join(", ")}. Got "${value}".`,
67
+ );
68
+ }
69
+
70
+ function validateCaptureConfig(captureConfig, sourcePath) {
71
+ if (captureConfig === undefined || captureConfig === null) {
72
+ return;
73
+ }
74
+
75
+ assert(
76
+ isPlainObject(captureConfig),
77
+ `Invalid VT config in ${sourcePath}: "vt.capture" must be an object when provided, got ${valueType(captureConfig)}.`,
78
+ );
79
+ assert(
80
+ Object.keys(captureConfig).length === 0,
81
+ `Invalid VT config in ${sourcePath}: "vt.capture" is internal and no longer user-configurable. Remove this section.`,
82
+ );
83
+ }
84
+
85
+ const LEGACY_CAPTURE_FIELDS = {
86
+ screenshotWaitTime: true,
87
+ waitSelector: true,
88
+ waitStrategy: true,
89
+ workerCount: true,
90
+ isolationMode: true,
91
+ navigationTimeout: true,
92
+ readyTimeout: true,
93
+ screenshotTimeout: true,
94
+ maxRetries: true,
95
+ recycleEvery: true,
96
+ metricsPath: true,
97
+ headless: true,
98
+ };
99
+
100
+ const SECTION_PAGE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
101
+
102
+ function assertNoLegacyCaptureFields(vtConfig, sourcePath) {
103
+ for (const legacyField of Object.keys(LEGACY_CAPTURE_FIELDS)) {
104
+ if (Object.prototype.hasOwnProperty.call(vtConfig, legacyField)) {
105
+ throw new Error(
106
+ `Invalid VT config in ${sourcePath}: "vt.${legacyField}" was removed and is no longer user-configurable.`,
107
+ );
108
+ }
109
+ }
110
+ }
111
+
112
+ function assertValidSectionPageKey(value, path) {
113
+ assert(
114
+ typeof value === "string" && value.trim().length > 0,
115
+ `"${path}" is required.`,
116
+ );
117
+ assert(
118
+ SECTION_PAGE_KEY_PATTERN.test(value),
119
+ `"${path}" must contain only letters, numbers, "-" or "_", and cannot include spaces.`,
120
+ );
121
+ }
122
+
123
+ function validateSection(section, index) {
124
+ const sectionPath = `vt.sections[${index}]`;
125
+ assert(isPlainObject(section), `"${sectionPath}" must be an object, got ${valueType(section)}.`);
126
+
127
+ validateOptionalString(section.title, `${sectionPath}.title`);
128
+ assert(typeof section.title === "string" && section.title.trim().length > 0, `"${sectionPath}.title" is required.`);
129
+ validateOptionalString(section.description, `${sectionPath}.description`, { allowEmpty: true });
130
+
131
+ if (section.type !== undefined) {
132
+ validateOptionalString(section.type, `${sectionPath}.type`);
133
+ assert(
134
+ section.type === "groupLabel",
135
+ `"${sectionPath}.type" must be "groupLabel" when provided, got "${section.type}".`,
136
+ );
137
+ }
138
+
139
+ if (section.type === "groupLabel") {
140
+ assert(Array.isArray(section.items), `"${sectionPath}.items" must be an array for groupLabel sections.`);
141
+ assert(section.items.length > 0, `"${sectionPath}.items" cannot be empty.`);
142
+
143
+ section.items.forEach((item, itemIndex) => {
144
+ const itemPath = `${sectionPath}.items[${itemIndex}]`;
145
+ assert(isPlainObject(item), `"${itemPath}" must be an object, got ${valueType(item)}.`);
146
+ validateOptionalString(item.title, `${itemPath}.title`);
147
+ validateOptionalString(item.files, `${itemPath}.files`);
148
+ validateOptionalString(item.description, `${itemPath}.description`, { allowEmpty: true });
149
+
150
+ assert(typeof item.title === "string" && item.title.trim().length > 0, `"${itemPath}.title" is required.`);
151
+ assert(typeof item.files === "string" && item.files.trim().length > 0, `"${itemPath}.files" is required.`);
152
+ assertValidSectionPageKey(item.title, `${itemPath}.title`);
153
+ });
154
+ return;
155
+ }
156
+
157
+ validateOptionalString(section.files, `${sectionPath}.files`);
158
+ assert(typeof section.files === "string" && section.files.trim().length > 0, `"${sectionPath}.files" is required.`);
159
+ assertValidSectionPageKey(section.title, `${sectionPath}.title`);
160
+ }
161
+
162
+ function collectSectionPageKeys(vtConfig) {
163
+ const keys = [];
164
+ vtConfig.sections.forEach((section) => {
165
+ if (section.type === "groupLabel" && Array.isArray(section.items)) {
166
+ section.items.forEach((item) => keys.push(item.title));
167
+ return;
168
+ }
169
+ if (section.files) {
170
+ keys.push(section.title);
171
+ }
172
+ });
173
+ return keys;
174
+ }
175
+
176
+ function assertUniqueSectionPageKeys(vtConfig, sourcePath) {
177
+ const seen = new Map();
178
+ const pageKeys = collectSectionPageKeys(vtConfig);
179
+
180
+ pageKeys.forEach((pageKey) => {
181
+ const canonicalKey = pageKey.toLowerCase();
182
+ const existing = seen.get(canonicalKey);
183
+ if (existing) {
184
+ throw new Error(
185
+ `Invalid VT config in ${sourcePath}: section page key "${pageKey}" conflicts with "${existing}". Page keys must be unique (case-insensitive).`,
186
+ );
187
+ }
188
+ seen.set(canonicalKey, pageKey);
189
+ });
190
+ }
191
+
192
+ function validateStepObject(step, stepPath) {
193
+ assert(isPlainObject(step), `"${stepPath}" must be an object with one key or a string.`);
194
+ const keys = Object.keys(step);
195
+ assert(
196
+ keys.length === 1,
197
+ `"${stepPath}" must have exactly one key (e.g. "select my-id"), got ${keys.length}.`,
198
+ );
199
+
200
+ const [blockCommand] = keys;
201
+ validateOptionalString(blockCommand, `${stepPath} key`);
202
+
203
+ const nestedSteps = step[blockCommand];
204
+ assert(Array.isArray(nestedSteps), `"${stepPath}.${blockCommand}" must be an array of step strings.`);
205
+ nestedSteps.forEach((nestedStep, nestedIndex) => {
206
+ const nestedPath = `${stepPath}.${blockCommand}[${nestedIndex}]`;
207
+ assert(typeof nestedStep === "string", `"${nestedPath}" must be a string step command.`);
208
+ assert(nestedStep.trim().length > 0, `"${nestedPath}" cannot be empty.`);
209
+ });
210
+ }
211
+
212
+ export function validateVtConfig(vtConfig, sourcePath = "rettangoli.config.yaml") {
213
+ assert(
214
+ isPlainObject(vtConfig),
215
+ `Invalid VT config in ${sourcePath}: "vt" must be an object, got ${valueType(vtConfig)}.`,
216
+ );
217
+
218
+ validateOptionalString(vtConfig.path, "vt.path");
219
+ validateOptionalString(vtConfig.url, "vt.url");
220
+ validateOptionalString(vtConfig.name, "vt.name", { allowEmpty: true });
221
+ validateOptionalString(vtConfig.description, "vt.description", { allowEmpty: true });
222
+ validateOptionalString(vtConfig.compareMethod, "vt.compareMethod");
223
+ validateOptionalBoolean(vtConfig.skipScreenshots, "vt.skipScreenshots");
224
+ validateOptionalNumber(vtConfig.port, "vt.port", { integer: true, min: 1, max: 65535 });
225
+ validateOptionalNumber(vtConfig.concurrency, "vt.concurrency", { integer: true, min: 1 });
226
+ validateOptionalNumber(vtConfig.timeout, "vt.timeout", { integer: true, min: 1 });
227
+ validateOptionalString(vtConfig.waitEvent, "vt.waitEvent");
228
+ normalizeViewportField(vtConfig.viewport, "vt.viewport");
229
+ validateOptionalNumber(vtConfig.colorThreshold, "vt.colorThreshold", { min: 0, max: 1 });
230
+ validateOptionalNumber(vtConfig.diffThreshold, "vt.diffThreshold", { min: 0, max: 100 });
231
+ assertNoLegacyCaptureFields(vtConfig, sourcePath);
232
+ validateCaptureConfig(vtConfig.capture, sourcePath);
233
+
234
+ assert(Array.isArray(vtConfig.sections), `Invalid VT config in ${sourcePath}: "vt.sections" is required and must be an array.`);
235
+ assert(vtConfig.sections.length > 0, `Invalid VT config in ${sourcePath}: "vt.sections" cannot be empty.`);
236
+ vtConfig.sections.forEach(validateSection);
237
+ assertUniqueSectionPageKeys(vtConfig, sourcePath);
238
+
239
+ return vtConfig;
240
+ }
241
+
242
+ export function validateFrontMatter(frontMatter, specPath) {
243
+ if (frontMatter === null || frontMatter === undefined) {
244
+ return;
245
+ }
246
+
247
+ assert(
248
+ isPlainObject(frontMatter),
249
+ `Invalid front matter in "${specPath}": expected an object, got ${valueType(frontMatter)}.`,
250
+ );
251
+
252
+ validateOptionalString(frontMatter.title, `${specPath}: frontMatter.title`, { allowEmpty: true });
253
+ validateOptionalString(frontMatter.description, `${specPath}: frontMatter.description`, { allowEmpty: true });
254
+ validateOptionalString(frontMatter.template, `${specPath}: frontMatter.template`);
255
+ validateOptionalString(frontMatter.url, `${specPath}: frontMatter.url`);
256
+ validateOptionalString(frontMatter.waitEvent, `${specPath}: frontMatter.waitEvent`);
257
+ validateOptionalString(frontMatter.waitSelector, `${specPath}: frontMatter.waitSelector`);
258
+ normalizeViewportField(frontMatter.viewport, `${specPath}: frontMatter.viewport`);
259
+ validateOptionalEnum(
260
+ frontMatter.waitStrategy,
261
+ `${specPath}: frontMatter.waitStrategy`,
262
+ ["networkidle", "load", "event", "selector"],
263
+ );
264
+ validateOptionalBoolean(frontMatter.skipScreenshot, `${specPath}: frontMatter.skipScreenshot`);
265
+
266
+ if (frontMatter.waitStrategy === "event") {
267
+ assert(
268
+ typeof frontMatter.waitEvent === "string" && frontMatter.waitEvent.trim().length > 0,
269
+ `"${specPath}: frontMatter.waitEvent" is required when waitStrategy is "event".`,
270
+ );
271
+ }
272
+ if (frontMatter.waitStrategy === "selector") {
273
+ assert(
274
+ typeof frontMatter.waitSelector === "string" && frontMatter.waitSelector.trim().length > 0,
275
+ `"${specPath}: frontMatter.waitSelector" is required when waitStrategy is "selector".`,
276
+ );
277
+ }
278
+
279
+ if (frontMatter.specs !== undefined) {
280
+ assert(Array.isArray(frontMatter.specs), `"${specPath}: frontMatter.specs" must be an array of strings.`);
281
+ frontMatter.specs.forEach((spec, index) => {
282
+ const specPathField = `${specPath}: frontMatter.specs[${index}]`;
283
+ assert(typeof spec === "string", `"${specPathField}" must be a string.`);
284
+ assert(spec.trim().length > 0, `"${specPathField}" cannot be empty.`);
285
+ });
286
+ }
287
+
288
+ if (frontMatter.steps !== undefined) {
289
+ assert(Array.isArray(frontMatter.steps), `"${specPath}: frontMatter.steps" must be an array.`);
290
+ frontMatter.steps.forEach((step, index) => {
291
+ const stepPath = `${specPath}: frontMatter.steps[${index}]`;
292
+ if (typeof step === "string") {
293
+ assert(step.trim().length > 0, `"${stepPath}" cannot be empty.`);
294
+ return;
295
+ }
296
+ validateStepObject(step, stepPath);
297
+ });
298
+ }
299
+ }
300
+
301
+ export function validateFiniteNumber(value, fieldName, options = {}) {
302
+ validateOptionalNumber(value, fieldName, options);
303
+ return value;
304
+ }
@@ -0,0 +1,99 @@
1
+ const VIEWPORT_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
2
+
3
+ export const DEFAULT_VIEWPORT = Object.freeze({
4
+ width: 1280,
5
+ height: 720,
6
+ });
7
+
8
+ function valueType(value) {
9
+ if (Array.isArray(value)) return "array";
10
+ if (value === null) return "null";
11
+ return typeof value;
12
+ }
13
+
14
+ function assert(condition, message) {
15
+ if (!condition) {
16
+ throw new Error(message);
17
+ }
18
+ }
19
+
20
+ function validateViewportEntry(entry, path) {
21
+ assert(
22
+ entry !== null && typeof entry === "object" && !Array.isArray(entry),
23
+ `"${path}" must be an object, got ${valueType(entry)}.`,
24
+ );
25
+
26
+ assert(
27
+ typeof entry.id === "string" && entry.id.trim().length > 0,
28
+ `"${path}.id" is required and must be a non-empty string.`,
29
+ );
30
+ assert(
31
+ VIEWPORT_ID_PATTERN.test(entry.id),
32
+ `"${path}.id" must contain only letters, numbers, "-" or "_".`,
33
+ );
34
+ assert(
35
+ typeof entry.width === "number" && Number.isInteger(entry.width) && entry.width >= 1,
36
+ `"${path}.width" must be an integer >= 1.`,
37
+ );
38
+ assert(
39
+ typeof entry.height === "number" && Number.isInteger(entry.height) && entry.height >= 1,
40
+ `"${path}.height" must be an integer >= 1.`,
41
+ );
42
+
43
+ return {
44
+ id: entry.id,
45
+ width: entry.width,
46
+ height: entry.height,
47
+ };
48
+ }
49
+
50
+ export function normalizeViewportField(rawViewport, path = "viewport") {
51
+ if (rawViewport === undefined || rawViewport === null) {
52
+ return undefined;
53
+ }
54
+
55
+ const rawEntries = Array.isArray(rawViewport) ? rawViewport : [rawViewport];
56
+ assert(rawEntries.length > 0, `"${path}" cannot be an empty array.`);
57
+
58
+ const entries = rawEntries.map((entry, index) =>
59
+ validateViewportEntry(entry, `${path}[${index}]`),
60
+ );
61
+
62
+ const seen = new Map();
63
+ entries.forEach((entry, index) => {
64
+ const canonicalId = entry.id.toLowerCase();
65
+ const existingIndex = seen.get(canonicalId);
66
+ assert(
67
+ existingIndex === undefined,
68
+ `"${path}[${index}].id" duplicates "${path}[${existingIndex}].id" (case-insensitive).`,
69
+ );
70
+ seen.set(canonicalId, index);
71
+ });
72
+
73
+ return entries;
74
+ }
75
+
76
+ export function resolveViewports(frontMatterViewport, configViewport) {
77
+ const selected = frontMatterViewport ?? configViewport;
78
+ if (selected === undefined || selected === null) {
79
+ return [
80
+ {
81
+ id: null,
82
+ width: DEFAULT_VIEWPORT.width,
83
+ height: DEFAULT_VIEWPORT.height,
84
+ },
85
+ ];
86
+ }
87
+ return normalizeViewportField(selected, "viewport");
88
+ }
89
+
90
+ export function appendViewportToBaseName(baseName, viewportId) {
91
+ if (!viewportId) {
92
+ return baseName;
93
+ }
94
+ return `${baseName}--${viewportId}`;
95
+ }
96
+
97
+ export function stripViewportSuffix(itemKey) {
98
+ return itemKey.replace(/--[A-Za-z0-9_-]+$/, "");
99
+ }
package/docker/Dockerfile DELETED
@@ -1,21 +0,0 @@
1
- FROM mcr.microsoft.com/playwright:v1.57.0-noble
2
-
3
- # Install dependencies for Bun
4
- RUN apt-get update && apt-get install -y unzip curl && rm -rf /var/lib/apt/lists/*
5
-
6
- # Install Bun to system location directly (latest version)
7
- RUN curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr/local bash
8
-
9
- # Install rtgl globally
10
- RUN bun install -g rtgl@0.0.36
11
-
12
- # Copy bun global packages to /usr/local/lib/rtgl (accessible to all users)
13
- RUN mkdir -p /usr/local/lib/rtgl && \
14
- cp -r /root/.bun/install/global /usr/local/lib/rtgl/ && \
15
- chmod -R a+rx /usr/local/lib/rtgl/
16
-
17
- # Create wrapper script that uses copied packages
18
- RUN echo "#!/bin/sh" > /usr/local/bin/rtgl && \
19
- echo "BUN_INSTALL_LOCKFILE=/usr/local/lib/rtgl/bun/install/cache/bun-install.lock" >> /usr/local/bin/rtgl && \
20
- echo "exec bun /usr/local/lib/rtgl/global/node_modules/rtgl/cli.js \"\$@\"" >> /usr/local/bin/rtgl && \
21
- chmod +x /usr/local/bin/rtgl
@@ -1,16 +0,0 @@
1
- #!/bin/bash
2
- set -e
3
-
4
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
- IMAGE_NAME="playwright-v1.57.0-rtgl-v0.0.36"
6
- REGISTRY="${REGISTRY:-docker.io}"
7
- REPO="${REPO:-han4wluc/rtgl}"
8
-
9
- FULL_TAG="$REGISTRY/$REPO:$IMAGE_NAME"
10
-
11
- docker build -t "$IMAGE_NAME" "$SCRIPT_DIR"
12
- echo "Built image: $IMAGE_NAME"
13
-
14
- docker tag "$IMAGE_NAME" "$FULL_TAG"
15
- docker push "$FULL_TAG"
16
- echo "Pushed: $FULL_TAG"