From dfdadbda6168450fefa8dfef74508341a3425f4f Mon Sep 17 00:00:00 2001 From: Fredrik Ekholdt Date: Mon, 9 Sep 2024 16:46:10 +0200 Subject: [PATCH 1/6] Init tooling package: intended for use in VS Code, CLI... Both the VS Code extension, CLI and possibly the server (through the UI), needs to to generate and apply fixes to validation errors. It feels like there might be other things as well which all tooling requires. This package unifies that. --- packages/tooling/CHANGELOG.md | 0 packages/tooling/package.json | 27 +++++++++++++++++++++++++++ packages/tooling/src/index.ts | 2 ++ packages/tooling/tsconfig.json | 11 +++++++++++ 4 files changed, 40 insertions(+) create mode 100644 packages/tooling/CHANGELOG.md create mode 100644 packages/tooling/package.json create mode 100644 packages/tooling/src/index.ts create mode 100644 packages/tooling/tsconfig.json diff --git a/packages/tooling/CHANGELOG.md b/packages/tooling/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/packages/tooling/package.json b/packages/tooling/package.json new file mode 100644 index 000000000..b9277ad94 --- /dev/null +++ b/packages/tooling/package.json @@ -0,0 +1,27 @@ +{ + "name": "@valbuild/tooling", + "version": "0.63.5", + "description": "Utilities to build tooling for Val (VS Code extension, CLI, ...)", + "main": "dist/valbuild-tooling.cjs.js", + "module": "dist/valbuild-tooling.esm.js", + "exports": { + ".": { + "module": "./dist/valbuild-tooling.esm.js", + "default": "./dist/valbuild-tooling.cjs.js" + }, + "./package.json": "./package.json" + }, + "scripts": { + "typecheck": "tsc --noEmit", + "test": "jest" + }, + "preconstruct": { + "entrypoints": [ + "index.ts" + ] + }, + "dependencies": { + "@valbuild/core": "~0.63.5" + }, + "devDependencies": {} +} diff --git a/packages/tooling/src/index.ts b/packages/tooling/src/index.ts new file mode 100644 index 000000000..a1542689b --- /dev/null +++ b/packages/tooling/src/index.ts @@ -0,0 +1,2 @@ +// TODO: Add tooling code here +export default {}; diff --git a/packages/tooling/tsconfig.json b/packages/tooling/tsconfig.json new file mode 100644 index 000000000..b8e692ce5 --- /dev/null +++ b/packages/tooling/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "lib": [ + "es2020" + ], + "strict": true, + "isolatedModules": true, + "noEmit": true, + "skipLibCheck": true + } +} \ No newline at end of file From 23fa837a97d1caf50ad23154e7bd85357d07d622 Mon Sep 17 00:00:00 2001 From: Fredrik Ekholdt Date: Mon, 9 Sep 2024 16:58:50 +0200 Subject: [PATCH 2/6] Add (old) module path map from vs code extension --- packages/tooling/src/modulePathMap.test.ts | 211 +++++++++++++++++++ packages/tooling/src/modulePathMap.ts | 227 +++++++++++++++++++++ 2 files changed, 438 insertions(+) create mode 100644 packages/tooling/src/modulePathMap.test.ts create mode 100644 packages/tooling/src/modulePathMap.ts diff --git a/packages/tooling/src/modulePathMap.test.ts b/packages/tooling/src/modulePathMap.test.ts new file mode 100644 index 000000000..d2ea54820 --- /dev/null +++ b/packages/tooling/src/modulePathMap.test.ts @@ -0,0 +1,211 @@ +import * as ts from "typescript"; +import { createModulePathMap, getModulePathRange } from "./modulePathMap"; + +describe("Should map source path to line / cols", () => { + test("test 1", () => { + const text = `import type { InferSchemaType } from '@valbuild/next'; +import { s, c } from '../val.config'; + +const commons = { + keepAspectRatio: s.boolean().optional(), + size: s.union(s.literal('xs'), s.literal('md'), s.literal('lg')).optional(), +}; + +export const schema = s.object({ + text: s.string({ minLength: 10 }), + nested: s.object({ + text: s.string({ minLength: 10 }), + }), + testText: s + .richtext({ + a: true, + bold: true, + headings: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], + lineThrough: true, + italic: true, + link: true, + img: true, + ul: true, + ol: true, + }) + .optional(), + testUnion: s.union( + 'type', + s.object({ + ...commons, + type: s.literal('singleImage'), + image: s.image().optional(), + }), + s.object({ + ...commons, + type: s.literal('doubleImage'), + image1: s.image().optional(), + image2: s.image().optional(), + }) + ), +}); +export type TestContent = InferSchemaType; + +export default c.define( + '/oj/test', // <- NOTE: this must be the same path as the file + schema, + { + testText: c.richtext\` +Hei dere! +Dette er gøy! +\`, + text: 'hei', + nested: { + text: 'hei', + }, + testUnion: { + type: 'singleImage', + keepAspectRatio: true, + size: 'xs', + image: c.file('/public/Screenshot 2023-11-30 at 20.20.11_dbcdb.png'), + }, + } +); +`; + const sourceFile = ts.createSourceFile( + "./oj/test.val.ts", + text, + ts.ScriptTarget.ES2015, + ); + + const modulePathMap = createModulePathMap(sourceFile); + + if (!modulePathMap) { + return expect(!!modulePathMap).toEqual("modulePathMap is undefined"); + } + + expect(getModulePathRange('"text"', modulePathMap)).toEqual({ + end: { character: 6, line: 51 }, + start: { character: 2, line: 51 }, + }); + expect(getModulePathRange('"nested"."text"', modulePathMap)).toEqual({ + end: { character: 8, line: 53 }, + start: { character: 4, line: 53 }, + }); + }); + + test("test 2", () => { + const text = `import { s, c } from '../val.config'; + +const commons = { + keepAspectRatio: s.boolean().optional(), + size: s.union(s.literal('xs'), s.literal('md'), s.literal('lg')).optional(), +}; + +export const schema = s.object({ + ingress: s.string({ maxLength: 1 }), + theme: s.string().raw(), + header: s.string(), + image: s.image(), +}); + +export default c.define('/content/aboutUs', schema, { + ingress: + 'Vi elsker å bytestgge digitale tjenester som betyr noe for folk, helt fra bunn av, og helt ferdig. Vi tror på iterative utviklingsprosesser, tverrfaglige team, designdrevet produktutvikling og brukersentrerte designmetoder.', + header: 'SPESIALISTER PÅ DIGITAL PRODUKTUTVIKLING', + image: c.file( + '/public/368032148_1348297689148655_444423253678040057_n_64374.png', + { + sha256: + '6437456f9b596355e54df8bbbe9bf32228a7b79ddbdd17cca5679931bd80ea84', + width: 1283, + height: 1121, + } + ), +}); +`; + const sourceFile = ts.createSourceFile( + "./oj/test.val.ts", + text, + ts.ScriptTarget.ES2015, + ); + + const modulePathMap = createModulePathMap(sourceFile); + if (!modulePathMap) { + return expect(!!modulePathMap).toEqual("modulePathMap is undefined"); + } + + // console.log(getModulePathRange('"ingress"', modulePathMap)); + expect(getModulePathRange('"ingress"', modulePathMap)).toEqual({ + start: { line: 15, character: 2 }, + end: { line: 15, character: 9 }, + }); + }); + + test("test 3", () => { + const text = `import { s, c } from '../val.config'; + +export const schema = s.object({ + first: s.array(s.object({ second: s.record(s.array(s.string()))})) +}); + +export default c.define('/content', schema, { + first: [{ second: { a: ['a', 'b'] } }] +}); +`; + const sourceFile = ts.createSourceFile( + "./content.val.ts", + text, + ts.ScriptTarget.ES2015, + ); + + const modulePathMap = createModulePathMap(sourceFile); + if (!modulePathMap) { + return expect(!!modulePathMap).toEqual("modulePathMap is undefined"); + } + expect( + getModulePathRange('"first".0."second"."a".1', modulePathMap), + ).toEqual({ + start: { line: 7, character: 31 }, + end: { line: 7, character: 34 }, + }); + }); + + test("test 4: string literal object properties", () => { + const text = `import { c } from "../../val.config"; +import { docsSchema } from "./docsSchema.val"; + +export default c.define("/app/docs/docs", docsSchema, { + "getting-started": { + title: "Getting started", + content: c.richtext\` +Text +\`, + subPages: { + installation: { + title: "Installation", + subPagesL2: null, + content: null, + }, + }, + }, +}); +`; + + const sourceFile = ts.createSourceFile( + "./content.val.ts", + text, + ts.ScriptTarget.ES2015, + ); + + const modulePathMap = createModulePathMap(sourceFile); + if (!modulePathMap) { + return expect(!!modulePathMap).toEqual("modulePathMap is undefined"); + } + // console.log(JSON.stringify(modulePathMap, null, 2)); + expect( + getModulePathRange( + '"getting-started"."subPages"."installation"', + modulePathMap, + ), + ).toEqual({ + end: { character: 18, line: 10 }, + start: { character: 6, line: 10 }, + }); + }); +}); diff --git a/packages/tooling/src/modulePathMap.ts b/packages/tooling/src/modulePathMap.ts new file mode 100644 index 000000000..3761f1b3d --- /dev/null +++ b/packages/tooling/src/modulePathMap.ts @@ -0,0 +1,227 @@ +import * as ts from "typescript"; + +export type ModulePathMap = { + [modulePath: string]: { + children: ModulePathMap; + start: { + line: number; + character: number; + }; + end: { + line: number; + character: number; + }; + }; +}; + +export function getModulePathRange( + modulePath: string, + modulePathMap: ModulePathMap, +) { + const segments = modulePath.split(".").map((segment) => JSON.parse(segment)); // TODO: this is not entirely correct, but works for now. We have a function I think that does this so replace this with it + let range = modulePathMap[segments[0]]; + for (const pathSegment of segments.slice(1)) { + if (!range) { + break; + } + range = range?.children?.[pathSegment]; + } + return ( + range?.start && + range?.end && { + start: range.start, + end: range.end, + } + ); +} + +export function createModulePathMap( + sourceFile: ts.SourceFile, +): ModulePathMap | undefined { + for (const child of sourceFile + .getChildren() + .flatMap((child) => child.getChildren())) { + if (ts.isExportAssignment(child)) { + const contentNode = + child.expression && + ts.isCallExpression(child.expression) && + child.expression.arguments[2]; + + if (contentNode) { + return traverse(contentNode, sourceFile); + } + } + } +} + +function traverse( + node: ts.Expression, + sourceFile: ts.SourceFile, +): ModulePathMap | undefined { + if (ts.isStringLiteral(node) || ts.isNumericLiteral(node)) { + const tsEnd = sourceFile.getLineAndCharacterOfPosition(node.end); + const start = { + line: tsEnd.line, + character: tsEnd.character - node.getWidth(sourceFile), + }; + const end = { + line: tsEnd.line, + character: tsEnd.character, + }; + return { + "": { + children: {}, + start, + end, + }, + }; + } + if (ts.isObjectLiteralExpression(node)) { + return traverseObjectLiteral(node, sourceFile); + } + if (ts.isArrayLiteralExpression(node)) { + return traverseArrayLiteral(node, sourceFile); + } + if (ts.isCallExpression(node)) { + return traverseCallExpression(node, sourceFile); + } +} + +function traverseCallExpression( + node: ts.CallExpression, + sourceFile: ts.SourceFile, +): ModulePathMap | undefined { + if (ts.isPropertyAccessExpression(node.expression)) { + if ( + node.expression.expression.getText(sourceFile) === "c" && + node.expression.name.getText(sourceFile) === "file" + ) { + const val = { + children: {}, + start: sourceFile.getLineAndCharacterOfPosition( + node.getStart(sourceFile), + ), // TODO: We do + 1 to line up the diagnostics error exactly below a normal + end: sourceFile.getLineAndCharacterOfPosition(node.getEnd()), + }; + if (node.arguments[0]) { + const firstArgEnd = sourceFile.getLineAndCharacterOfPosition( + node.arguments[0].end, + ); + const _ref = { + children: {}, + start: { + line: firstArgEnd.line, + character: + firstArgEnd.character - node.arguments[0].getWidth(sourceFile), + }, + end: { + line: firstArgEnd.line, + character: firstArgEnd.character, + }, + }; + if (!node.arguments[1]) { + return { + val, + _ref, + }; + } + const metadataEnd = sourceFile.getLineAndCharacterOfPosition( + node.arguments[1].end, + ); + return { + val, + _ref, + metadata: { + children: {}, + start: { + line: metadataEnd.line, + character: + metadataEnd.character - node.arguments[1].getWidth(sourceFile), + }, + end: { + line: metadataEnd.line, + character: metadataEnd.character, + }, + }, + }; + } + } + } +} + +function traverseArrayLiteral( + node: ts.ArrayLiteralExpression, + sourceFile: ts.SourceFile, +): ModulePathMap { + return node.elements.reduce((acc, element, index) => { + if (ts.isExpression(element)) { + const tsEnd = sourceFile.getLineAndCharacterOfPosition(element.end); + const start = { + line: tsEnd.line, + character: tsEnd.character - element.getWidth(sourceFile), + }; + const end = { + line: tsEnd.line, + character: tsEnd.character, + }; + return { + ...acc, + [index]: { + children: traverse(element, sourceFile), + start, + end, + }, + }; + } + return acc; + }, {}); +} + +function traverseObjectLiteral( + node: ts.ObjectLiteralExpression, + sourceFile: ts.SourceFile, +): ModulePathMap { + return node.properties.reduce((acc, property) => { + if (ts.isPropertyAssignment(property)) { + const key = + property.name && + (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) && + property.name.text; + const value = property.initializer; + if (key) { + const tsEnd = sourceFile.getLineAndCharacterOfPosition( + property.name.getEnd(), + ); + const start = { + line: tsEnd.line, + character: tsEnd.character - property.name.getWidth(sourceFile), + }; + const end = { + line: tsEnd.line, + character: tsEnd.character, + }; + const val = { + children: {}, + start: sourceFile.getLineAndCharacterOfPosition( + property.initializer.getStart(sourceFile), + ), + end: sourceFile.getLineAndCharacterOfPosition( + property.initializer.getEnd(), + ), + }; + return { + ...acc, + [key]: { + children: { + val, + ...traverse(value, sourceFile), + }, + start, + end, + }, + }; + } + } + return acc; + }, {}); +} From 83b456430c4ce58e3db164543bff7b45e8aadef1 Mon Sep 17 00:00:00 2001 From: Fredrik Ekholdt Date: Mon, 9 Sep 2024 16:59:04 +0200 Subject: [PATCH 3/6] Add typescript and core dependencies --- packages/tooling/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/tooling/package.json b/packages/tooling/package.json index b9277ad94..743cf58b0 100644 --- a/packages/tooling/package.json +++ b/packages/tooling/package.json @@ -21,7 +21,8 @@ ] }, "dependencies": { - "@valbuild/core": "~0.63.5" + "@valbuild/core": "~0.63.5", + "typescript": "5" }, "devDependencies": {} } From 609f0b921d3cf6dbebca1c1a4d096e198d91996e Mon Sep 17 00:00:00 2001 From: Fredrik Ekholdt Date: Tue, 10 Sep 2024 21:56:44 +0200 Subject: [PATCH 4/6] Fix validation error message for file / image metadata --- packages/core/src/schema/file.ts | 2 +- packages/core/src/schema/image.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/schema/file.ts b/packages/core/src/schema/file.ts index 617a1770b..dcb1f1a05 100644 --- a/packages/core/src/schema/file.ts +++ b/packages/core/src/schema/file.ts @@ -136,7 +136,7 @@ export class FileSchema< return { [path]: [ { - message: `Found metadata, but it could not be validated. File metadata must be an object with the required props: width (positive number), height (positive number) and sha256 (string of length 64 of the base16 hash).`, // These validation errors will have to be picked up by logic outside of this package and revalidated. Reasons: 1) we have to read files to verify the metadata, which is handled differently in different runtimes (Browser, QuickJS, Node.js); 2) we want to keep this package dependency free. + message: `Found metadata, but it could not be validated. File metadata must be an object with the required props: width (positive number), height (positive number) and sha256 (string of length 64 of the sha256 hash of the base64 encoded data).`, // These validation errors will have to be picked up by logic outside of this package and revalidated. Reasons: 1) we have to read files to verify the metadata, which is handled differently in different runtimes (Browser, QuickJS, Node.js); 2) we want to keep this package dependency free. value: src, fixes: ["file:check-metadata"], }, diff --git a/packages/core/src/schema/image.ts b/packages/core/src/schema/image.ts index bcacf50b5..1536e19d4 100644 --- a/packages/core/src/schema/image.ts +++ b/packages/core/src/schema/image.ts @@ -147,7 +147,7 @@ export class ImageSchema< return { [path]: [ { - message: `Found metadata, but it could not be validated. Image metadata must be an object with the required props: width (positive number), height (positive number) and sha256 (string of length 64 of the base16 hash).`, // These validation errors will have to be picked up by logic outside of this package and revalidated. Reasons: 1) we have to read files to verify the metadata, which is handled differently in different runtimes (Browser, QuickJS, Node.js); 2) we want to keep this package dependency free. + message: `Found metadata, but it could not be validated. Image metadata must be an object with the required props: width (positive number), height (positive number) and sha256 (string of length 64 of the sha256 hash of the base64 encoded data).`, // These validation errors will have to be picked up by logic outside of this package and revalidated. Reasons: 1) we have to read files to verify the metadata, which is handled differently in different runtimes (Browser, QuickJS, Node.js); 2) we want to keep this package dependency free. value: src, fixes: ["image:replace-metadata"], }, From b1d23b4021cf1d2ebed21b36939e2c6d0ecee1e0 Mon Sep 17 00:00:00 2001 From: Fredrik Ekholdt Date: Tue, 10 Sep 2024 21:57:11 +0200 Subject: [PATCH 5/6] Add missing jest config --- packages/tooling/jest.config.js | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 packages/tooling/jest.config.js diff --git a/packages/tooling/jest.config.js b/packages/tooling/jest.config.js new file mode 100644 index 000000000..6458500a7 --- /dev/null +++ b/packages/tooling/jest.config.js @@ -0,0 +1,4 @@ +/** @type {import("jest").Config} */ +module.exports = { + preset: "../../jest.preset", +}; From 7fa9bbe3de3e25ff4887a90147eef22161de40d9 Mon Sep 17 00:00:00 2001 From: Fredrik Ekholdt Date: Tue, 10 Sep 2024 21:57:36 +0200 Subject: [PATCH 6/6] Update package lock with tooling package --- package-lock.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/package-lock.json b/package-lock.json index 68897f440..1ff56a6ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12341,6 +12341,10 @@ "resolved": "packages/shared", "link": true }, + "node_modules/@valbuild/tooling": { + "resolved": "packages/tooling", + "link": true + }, "node_modules/@valbuild/ui": { "resolved": "packages/ui", "link": true @@ -29355,6 +29359,14 @@ "zod-validation-error": "^3.3.0" } }, + "packages/tooling": { + "version": "0.63.5", + "dependencies": { + "@valbuild/core": "~0.63.5", + "typescript": "5" + }, + "devDependencies": {} + }, "packages/ui": { "name": "@valbuild/ui", "version": "0.63.6",