@pygmalionjs/pygmalion 0.6.19 → 0.6.20
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/inspect.d.ts +35 -0
- package/node/inspect-plugin.mjs +1 -1
- package/node/inspect-writeback.mjs +163 -0
- package/node/inspect.mjs +5 -0
- package/node/vite.mjs +6 -0
- package/node/webpack-loader.cjs +59 -0
- package/package.json +10 -1
- package/vite.d.ts +28 -0
package/inspect.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { InspectApplyPayload } from './dist-lib/types/lib';
|
|
2
|
+
|
|
3
|
+
export interface PygmalionInspectWritebackOptions {
|
|
4
|
+
sourceDirectory?: string;
|
|
5
|
+
normalizeValue?: (property: string, value: string) => string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PygmalionInspectPreviewResult {
|
|
9
|
+
files: { file: string; diff: string }[];
|
|
10
|
+
affectedFiles: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PygmalionInspectWriteResult {
|
|
14
|
+
applied: number;
|
|
15
|
+
changed: number;
|
|
16
|
+
files: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export declare function previewInspectChanges(
|
|
20
|
+
root: string,
|
|
21
|
+
input: InspectApplyPayload,
|
|
22
|
+
options?: PygmalionInspectWritebackOptions,
|
|
23
|
+
): Promise<PygmalionInspectPreviewResult>;
|
|
24
|
+
|
|
25
|
+
export declare function writeInspectChanges(
|
|
26
|
+
root: string,
|
|
27
|
+
input: InspectApplyPayload,
|
|
28
|
+
options?: PygmalionInspectWritebackOptions,
|
|
29
|
+
): Promise<PygmalionInspectWriteResult>;
|
|
30
|
+
|
|
31
|
+
export declare function findAffectedSourceFiles(
|
|
32
|
+
root: string,
|
|
33
|
+
componentFiles: string[],
|
|
34
|
+
options?: { sourceDirectory?: string },
|
|
35
|
+
): Promise<string[]>;
|
package/node/inspect-plugin.mjs
CHANGED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { writeStyleEdit, writeTextEdit } from './inspect-plugin.mjs';
|
|
5
|
+
import { createUnifiedDiff } from './source-diff.mjs';
|
|
6
|
+
import { writeSourceOperations } from './source-operations.mjs';
|
|
7
|
+
|
|
8
|
+
const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.mts', '.cjs', '.cts']);
|
|
9
|
+
|
|
10
|
+
function normalizeSourceDirectory(value = 'src') {
|
|
11
|
+
return value.replace(/^\.?\//, '').replace(/\/$/, '') || '.';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function validateRelativeSourcePath(root, value, sourceDirectory, extensions) {
|
|
15
|
+
if (
|
|
16
|
+
typeof value !== 'string' ||
|
|
17
|
+
path.isAbsolute(value) ||
|
|
18
|
+
value.includes('\0') ||
|
|
19
|
+
value.split(/[\\/]/).includes('..') ||
|
|
20
|
+
!extensions.has(path.extname(value).toLowerCase())
|
|
21
|
+
) {
|
|
22
|
+
throw new Error(`Source path cannot be modified: ${String(value)}`);
|
|
23
|
+
}
|
|
24
|
+
const normalized = value.split(path.sep).join('/');
|
|
25
|
+
if (
|
|
26
|
+
sourceDirectory !== '.' &&
|
|
27
|
+
normalized !== sourceDirectory &&
|
|
28
|
+
!normalized.startsWith(`${sourceDirectory}/`)
|
|
29
|
+
) {
|
|
30
|
+
throw new Error(`Only ${sourceDirectory} source files can be modified`);
|
|
31
|
+
}
|
|
32
|
+
const absolutePath = path.resolve(root, normalized);
|
|
33
|
+
const relativePath = path.relative(root, absolutePath);
|
|
34
|
+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
35
|
+
throw new Error('Source path escapes app root');
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
absolutePath,
|
|
39
|
+
relativePath: relativePath.split(path.sep).join('/'),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeInspectChanges(input) {
|
|
44
|
+
const styles = Array.isArray(input?.styles) ? input.styles : [];
|
|
45
|
+
const texts = Array.isArray(input?.texts) ? input.texts : [];
|
|
46
|
+
const operations = Array.isArray(input?.operations) ? input.operations : [];
|
|
47
|
+
const operationCount = styles.length + texts.length + operations.length;
|
|
48
|
+
if (operationCount === 0 || operationCount > 100) {
|
|
49
|
+
throw new Error('Inspect changes must contain between 1 and 100 operations');
|
|
50
|
+
}
|
|
51
|
+
return { styles, texts, operations, operationCount };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function collectInspectFiles(root, changes, sourceDirectory) {
|
|
55
|
+
const files = new Map();
|
|
56
|
+
for (const style of changes.styles) {
|
|
57
|
+
const file = validateRelativeSourcePath(
|
|
58
|
+
root,
|
|
59
|
+
style?.file,
|
|
60
|
+
sourceDirectory,
|
|
61
|
+
new Set(['.scss']),
|
|
62
|
+
);
|
|
63
|
+
files.set(file.relativePath, file.absolutePath);
|
|
64
|
+
}
|
|
65
|
+
for (const text of changes.texts) {
|
|
66
|
+
const file = validateRelativeSourcePath(
|
|
67
|
+
root,
|
|
68
|
+
text?.componentFile,
|
|
69
|
+
sourceDirectory,
|
|
70
|
+
SOURCE_EXTENSIONS,
|
|
71
|
+
);
|
|
72
|
+
files.set(file.relativePath, file.absolutePath);
|
|
73
|
+
}
|
|
74
|
+
for (const operation of changes.operations) {
|
|
75
|
+
const file = validateRelativeSourcePath(
|
|
76
|
+
root,
|
|
77
|
+
operation?.identity?.componentFile,
|
|
78
|
+
sourceDirectory,
|
|
79
|
+
SOURCE_EXTENSIONS,
|
|
80
|
+
);
|
|
81
|
+
files.set(file.relativePath, file.absolutePath);
|
|
82
|
+
}
|
|
83
|
+
return files;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function applyInspectChanges(root, changes, options) {
|
|
87
|
+
const results = [];
|
|
88
|
+
for (const style of changes.styles) {
|
|
89
|
+
results.push(await writeStyleEdit(root, style, options));
|
|
90
|
+
}
|
|
91
|
+
const sourceResult = changes.operations.length
|
|
92
|
+
? await writeSourceOperations(root, changes.operations, options)
|
|
93
|
+
: null;
|
|
94
|
+
for (const text of changes.texts) {
|
|
95
|
+
results.push(await writeTextEdit(root, text, options));
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
applied: results.length + (sourceResult?.operations ?? 0),
|
|
99
|
+
changed: results.filter((result) => result.changed).length + (sourceResult?.changed ?? 0),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function previewInspectChanges(root, input, options = {}) {
|
|
104
|
+
const appRoot = path.resolve(root);
|
|
105
|
+
const sourceDirectory = normalizeSourceDirectory(options.sourceDirectory);
|
|
106
|
+
const changes = normalizeInspectChanges(input);
|
|
107
|
+
const files = collectInspectFiles(appRoot, changes, sourceDirectory);
|
|
108
|
+
const before = new Map();
|
|
109
|
+
for (const [relativePath, absolutePath] of files) {
|
|
110
|
+
before.set(relativePath, await fsp.readFile(absolutePath, 'utf8'));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const temporaryRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'pygmalion-inspect-'));
|
|
114
|
+
try {
|
|
115
|
+
for (const [relativePath, source] of before) {
|
|
116
|
+
const target = path.join(temporaryRoot, relativePath);
|
|
117
|
+
await fsp.mkdir(path.dirname(target), { recursive: true });
|
|
118
|
+
await fsp.writeFile(target, source);
|
|
119
|
+
}
|
|
120
|
+
await applyInspectChanges(temporaryRoot, changes, {
|
|
121
|
+
...options,
|
|
122
|
+
sourceDirectory,
|
|
123
|
+
});
|
|
124
|
+
const diffs = [];
|
|
125
|
+
for (const [relativePath, source] of before) {
|
|
126
|
+
const next = await fsp.readFile(path.join(temporaryRoot, relativePath), 'utf8');
|
|
127
|
+
const diff = createUnifiedDiff(source, next, relativePath);
|
|
128
|
+
if (diff) diffs.push({ file: relativePath, diff });
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
files: diffs,
|
|
132
|
+
affectedFiles: [...files.keys()].sort(),
|
|
133
|
+
};
|
|
134
|
+
} finally {
|
|
135
|
+
await fsp.rm(temporaryRoot, { recursive: true, force: true });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function writeInspectChanges(root, input, options = {}) {
|
|
140
|
+
const appRoot = path.resolve(root);
|
|
141
|
+
const sourceDirectory = normalizeSourceDirectory(options.sourceDirectory);
|
|
142
|
+
const changes = normalizeInspectChanges(input);
|
|
143
|
+
const files = collectInspectFiles(appRoot, changes, sourceDirectory);
|
|
144
|
+
const backups = new Map();
|
|
145
|
+
for (const [relativePath, absolutePath] of files) {
|
|
146
|
+
backups.set(relativePath, await fsp.readFile(absolutePath, 'utf8'));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const result = await applyInspectChanges(appRoot, changes, {
|
|
151
|
+
...options,
|
|
152
|
+
sourceDirectory,
|
|
153
|
+
});
|
|
154
|
+
return { ...result, files: [...files.keys()].sort() };
|
|
155
|
+
} catch (error) {
|
|
156
|
+
await Promise.all(
|
|
157
|
+
[...backups].map(([relativePath, source]) =>
|
|
158
|
+
fsp.writeFile(files.get(relativePath), source),
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
package/node/inspect.mjs
ADDED
package/node/vite.mjs
CHANGED
|
@@ -84,6 +84,10 @@ import {
|
|
|
84
84
|
validateQaCaptureRequest,
|
|
85
85
|
} from './qa-capture-plugin.mjs';
|
|
86
86
|
import { resolveGitSourceState } from './source-revision.mjs';
|
|
87
|
+
import {
|
|
88
|
+
previewInspectChanges,
|
|
89
|
+
writeInspectChanges,
|
|
90
|
+
} from './inspect-writeback.mjs';
|
|
87
91
|
|
|
88
92
|
function requiredPath(value, name, base) {
|
|
89
93
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
@@ -414,6 +418,8 @@ export {
|
|
|
414
418
|
writeSourceOperations,
|
|
415
419
|
writeStyleEdit,
|
|
416
420
|
writeTextEdit,
|
|
421
|
+
previewInspectChanges,
|
|
422
|
+
writeInspectChanges,
|
|
417
423
|
convertRoutePreviewArtifactV1ToV2,
|
|
418
424
|
convertRoutePreviewArtifactV2ToV1,
|
|
419
425
|
createRoutePreviewArtifactV2,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
|
|
3
|
+
const inspectPlugins = new Map();
|
|
4
|
+
|
|
5
|
+
function normalizeSourceDirectory(value) {
|
|
6
|
+
if (typeof value !== 'string') return 'src';
|
|
7
|
+
return value.replace(/^\.?\//, '').replace(/\/$/, '') || '.';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isWithinSourceDirectory(relativePath, sourceDirectory) {
|
|
11
|
+
return (
|
|
12
|
+
sourceDirectory === '.' ||
|
|
13
|
+
relativePath === sourceDirectory ||
|
|
14
|
+
relativePath.startsWith(`${sourceDirectory}/`)
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function inspectPlugin(root, sourceDirectory) {
|
|
19
|
+
const key = `${root}\0${sourceDirectory}`;
|
|
20
|
+
let promise = inspectPlugins.get(key);
|
|
21
|
+
if (!promise) {
|
|
22
|
+
promise = import('./inspect-plugin.mjs').then(({ pygmalionInspectPlugin }) =>
|
|
23
|
+
pygmalionInspectPlugin({ root, sourceDirectory }),
|
|
24
|
+
);
|
|
25
|
+
inspectPlugins.set(key, promise);
|
|
26
|
+
}
|
|
27
|
+
return promise;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = function pygmalionWebpackLoader(source, inputMap) {
|
|
31
|
+
const callback = this.async();
|
|
32
|
+
const options = typeof this.getOptions === 'function' ? this.getOptions() : {};
|
|
33
|
+
const root = path.resolve(options.root || this.rootContext || process.cwd());
|
|
34
|
+
const sourceDirectory = normalizeSourceDirectory(options.sourceDirectory);
|
|
35
|
+
const componentFile = path
|
|
36
|
+
.relative(root, this.resourcePath)
|
|
37
|
+
.split(path.sep)
|
|
38
|
+
.join('/');
|
|
39
|
+
|
|
40
|
+
if (
|
|
41
|
+
componentFile.startsWith('../') ||
|
|
42
|
+
path.isAbsolute(componentFile) ||
|
|
43
|
+
!isWithinSourceDirectory(componentFile, sourceDirectory)
|
|
44
|
+
) {
|
|
45
|
+
callback(null, source, inputMap);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
inspectPlugin(root, sourceDirectory)
|
|
50
|
+
.then((plugin) => {
|
|
51
|
+
const transformed = plugin.transform(String(source), this.resourcePath);
|
|
52
|
+
if (!transformed) {
|
|
53
|
+
callback(null, source, inputMap);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
callback(null, transformed.code, transformed.map);
|
|
57
|
+
})
|
|
58
|
+
.catch((error) => callback(error));
|
|
59
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pygmalionjs/pygmalion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.20",
|
|
4
4
|
"description": "Code-backed DOM design sandbox and visual QA editor",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"publishConfig": {
|
|
@@ -23,7 +23,12 @@
|
|
|
23
23
|
"types": "./storyboard.d.ts",
|
|
24
24
|
"default": "./node/storyboard.mjs"
|
|
25
25
|
},
|
|
26
|
+
"./inspect": {
|
|
27
|
+
"types": "./inspect.d.ts",
|
|
28
|
+
"default": "./node/inspect.mjs"
|
|
29
|
+
},
|
|
26
30
|
"./dev-view": "./node/dev-view.vite.mjs",
|
|
31
|
+
"./webpack-loader": "./node/webpack-loader.cjs",
|
|
27
32
|
"./testing": {
|
|
28
33
|
"types": "./dist-lib/types/testing.d.ts",
|
|
29
34
|
"default": "./dist-lib/testing.js"
|
|
@@ -40,6 +45,8 @@
|
|
|
40
45
|
"node/dev-mirror.mjs",
|
|
41
46
|
"node/dev-view.vite.mjs",
|
|
42
47
|
"node/inspect-plugin.mjs",
|
|
48
|
+
"node/inspect.mjs",
|
|
49
|
+
"node/inspect-writeback.mjs",
|
|
43
50
|
"node/preview-artifact-plugin.mjs",
|
|
44
51
|
"node/preview-artifact-store.mjs",
|
|
45
52
|
"node/preview-capture-worker.mjs",
|
|
@@ -59,6 +66,8 @@
|
|
|
59
66
|
"node/storyboard-environment.mjs",
|
|
60
67
|
"node/storyboard.mjs",
|
|
61
68
|
"node/vite.mjs",
|
|
69
|
+
"node/webpack-loader.cjs",
|
|
70
|
+
"inspect.d.ts",
|
|
62
71
|
"qa.d.ts",
|
|
63
72
|
"storyboard.d.ts",
|
|
64
73
|
"vite.d.ts"
|
package/vite.d.ts
CHANGED
|
@@ -264,6 +264,34 @@ export declare function writeTextEdit(
|
|
|
264
264
|
options?: { sourceDirectory?: string },
|
|
265
265
|
): Promise<unknown>;
|
|
266
266
|
|
|
267
|
+
export interface PygmalionInspectWritebackOptions {
|
|
268
|
+
sourceDirectory?: string;
|
|
269
|
+
normalizeValue?: (property: string, value: string) => string;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export interface PygmalionInspectPreviewResult {
|
|
273
|
+
files: { file: string; diff: string }[];
|
|
274
|
+
affectedFiles: string[];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface PygmalionInspectWriteResult {
|
|
278
|
+
applied: number;
|
|
279
|
+
changed: number;
|
|
280
|
+
files: string[];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export declare function previewInspectChanges(
|
|
284
|
+
root: string,
|
|
285
|
+
input: import('./types').InspectApplyPayload,
|
|
286
|
+
options?: PygmalionInspectWritebackOptions,
|
|
287
|
+
): Promise<PygmalionInspectPreviewResult>;
|
|
288
|
+
|
|
289
|
+
export declare function writeInspectChanges(
|
|
290
|
+
root: string,
|
|
291
|
+
input: import('./types').InspectApplyPayload,
|
|
292
|
+
options?: PygmalionInspectWritebackOptions,
|
|
293
|
+
): Promise<PygmalionInspectWriteResult>;
|
|
294
|
+
|
|
267
295
|
export interface PygmalionSourceOperationApplyResult {
|
|
268
296
|
code: string;
|
|
269
297
|
changed: boolean;
|