From c5cacb43dccc81c4f99fb774bbb9ecf61ed944cd Mon Sep 17 00:00:00 2001 From: Gustave Date: Mon, 26 Feb 2024 09:27:04 +0100 Subject: [PATCH] fix: finally real obj parsing in the prebuilder codebase --- .eslintrc.js | 3 +- doc/i18n/00.quirks.md | 4 +- packages/prebuilder/package.json | 8 +- .../prebuild.blogArchitectureType.test.ts | 1 + .../blog/__tests__/prebuild.blogType.test.ts | 1 + .../prebuild.i18nBlogCategories.test.ts | 1 + .../prebuild.i18nPagesTitles.test.ts | 1 + .../prebuild.defaultLanguageTokenType.test.ts | 1 + .../__tests__/postbuild.indexFile.test.ts | 1 + .../lp/__tests__/prebuild.lpType.test.ts | 1 + .../__tests__/prebuild.pagesType.test.ts | 1 + .../src/lib/__tests__/prebuild.etc.test.ts | 87 ++++++++++++++++++- packages/prebuilder/src/lib/etc.ts | 51 ++++++++++- .../__tests__/prebuild.sysPages.test.ts | 4 +- pnpm-lock.yaml | 6 ++ src/i18n/locales/schema.ts | 8 +- 16 files changed, 162 insertions(+), 17 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index ec71376a1..926d5a319 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,7 +20,8 @@ module.exports = { 'no-unreachable': ERROR, 'require-await': ERROR, 'no-unused-vars': OFF, - 'import/first': ERROR + 'import/first': ERROR, + 'no-eval': ERROR }, extends: [ diff --git a/doc/i18n/00.quirks.md b/doc/i18n/00.quirks.md index 5011ef168..124b1d624 100644 --- a/doc/i18n/00.quirks.md +++ b/doc/i18n/00.quirks.md @@ -15,7 +15,7 @@ vocabulary, since avoiding hard-coded duplicate values is essential in such data Another motivation was also to maintain a _prebuilder_ code that is as simple and predictable as possible. The _prebuilder_ is a standalone NPM package. It does not "import" any scripts from the codebase. Instead, it opens them as files via the _filesystem_ -and juggles between parsing and raw `eval` function calls (which could be also `JSON.parse` calls... but still). +and parses them. The ability to dynamically define values based on variables scattered in different parts of the code made implementing this feature nightmarish. In my opinion, this indicated a poor design choice rather than a flawed implementation of the _prebuilder_. @@ -69,7 +69,7 @@ Likewise: export default { // * ... _infos: { - lng: '__SCANNED_ON_PREBUILD_FIELD__' + lng: '__SCANNED__' }, navbar: { diff --git a/packages/prebuilder/package.json b/packages/prebuilder/package.json index d535dc8cd..ab0109a9c 100644 --- a/packages/prebuilder/package.json +++ b/packages/prebuilder/package.json @@ -9,11 +9,13 @@ "start": "tsc --build && node prebuilder-dist/src/main.js" }, "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", "arg": "^5.0.2", - "inflection": "^3.0.0", + "chokidar": "^3.6.0", "format-message": "6.2.4", - "ts-morph": "21.0.1", - "chokidar": "^3.6.0" + "inflection": "^3.0.0", + "ts-morph": "21.0.1" }, "devDependencies": { "@rtm/shared-types": "*", diff --git a/packages/prebuilder/src/generators/blog/__tests__/prebuild.blogArchitectureType.test.ts b/packages/prebuilder/src/generators/blog/__tests__/prebuild.blogArchitectureType.test.ts index e891e99c7..0943899b9 100644 --- a/packages/prebuilder/src/generators/blog/__tests__/prebuild.blogArchitectureType.test.ts +++ b/packages/prebuilder/src/generators/blog/__tests__/prebuild.blogArchitectureType.test.ts @@ -3,6 +3,7 @@ import { afterAll, describe, expect, it } from 'vitest'; import generateBlogArchitectureType from '../blogArchitectureType'; +// https://github.com/vitest-dev/vitest/discussions/2484 const fs = require('fs/promises'); const __TARGET_FOLDER_ROOT = './packages/prebuilder/src/generators/blog/__tests__/FAKE_CODEGEN'; diff --git a/packages/prebuilder/src/generators/blog/__tests__/prebuild.blogType.test.ts b/packages/prebuilder/src/generators/blog/__tests__/prebuild.blogType.test.ts index 4c8e4b659..6058efbc4 100644 --- a/packages/prebuilder/src/generators/blog/__tests__/prebuild.blogType.test.ts +++ b/packages/prebuilder/src/generators/blog/__tests__/prebuild.blogType.test.ts @@ -5,6 +5,7 @@ import { afterAll, describe, expect, it } from 'vitest'; import generateBlogType from '../blogType'; +// https://github.com/vitest-dev/vitest/discussions/2484 const fs = require('fs/promises'); const __TARGET_FOLDER_ROOT = './packages/prebuilder/src/generators/blog/__tests__/FAKE_CODEGEN'; diff --git a/packages/prebuilder/src/generators/blog/__tests__/prebuild.i18nBlogCategories.test.ts b/packages/prebuilder/src/generators/blog/__tests__/prebuild.i18nBlogCategories.test.ts index f8ac495a9..4e9688fa7 100644 --- a/packages/prebuilder/src/generators/blog/__tests__/prebuild.i18nBlogCategories.test.ts +++ b/packages/prebuilder/src/generators/blog/__tests__/prebuild.i18nBlogCategories.test.ts @@ -5,6 +5,7 @@ import { afterAll, describe, expect, it } from 'vitest'; import generateI18nBlogCategories from '../i18nBlogCategories'; +// https://github.com/vitest-dev/vitest/discussions/2484 const fs = require('fs/promises'); const __TARGET_FOLDER_ROOT = './packages/prebuilder/src/generators/blog/__tests__/FAKE_CODEGEN'; diff --git a/packages/prebuilder/src/generators/blog/__tests__/prebuild.i18nPagesTitles.test.ts b/packages/prebuilder/src/generators/blog/__tests__/prebuild.i18nPagesTitles.test.ts index ede970399..3321ba2bc 100644 --- a/packages/prebuilder/src/generators/blog/__tests__/prebuild.i18nPagesTitles.test.ts +++ b/packages/prebuilder/src/generators/blog/__tests__/prebuild.i18nPagesTitles.test.ts @@ -5,6 +5,7 @@ import { afterAll, describe, expect, it } from 'vitest'; import generateI18nPagesTitles from '../i18nPagesTitles'; +// https://github.com/vitest-dev/vitest/discussions/2484 const fs = require('fs/promises'); const __TARGET_FOLDER_ROOT = './packages/prebuilder/src/generators/blog/__tests__/FAKE_CODEGEN'; diff --git a/packages/prebuilder/src/generators/defaultLanguageToken/__tests__/prebuild.defaultLanguageTokenType.test.ts b/packages/prebuilder/src/generators/defaultLanguageToken/__tests__/prebuild.defaultLanguageTokenType.test.ts index a8a9b579b..e48b6478b 100644 --- a/packages/prebuilder/src/generators/defaultLanguageToken/__tests__/prebuild.defaultLanguageTokenType.test.ts +++ b/packages/prebuilder/src/generators/defaultLanguageToken/__tests__/prebuild.defaultLanguageTokenType.test.ts @@ -3,6 +3,7 @@ import { afterAll, describe, expect, it } from 'vitest'; import generateDefaultLanguageTokenType from '../defaultLanguageTokenType'; +// https://github.com/vitest-dev/vitest/discussions/2484 const fs = require('fs/promises'); const __TARGET_FOLDER_ROOT = './packages/prebuilder/src/generators/defaultLanguageToken/__tests__/FAKE_CODEGEN'; diff --git a/packages/prebuilder/src/generators/index/__tests__/postbuild.indexFile.test.ts b/packages/prebuilder/src/generators/index/__tests__/postbuild.indexFile.test.ts index 9c799efa8..2e41d184e 100644 --- a/packages/prebuilder/src/generators/index/__tests__/postbuild.indexFile.test.ts +++ b/packages/prebuilder/src/generators/index/__tests__/postbuild.indexFile.test.ts @@ -3,6 +3,7 @@ import { afterAll, describe, expect, it } from 'vitest'; import generateIndexFile from '../indexFile'; +// https://github.com/vitest-dev/vitest/discussions/2484 const fs = require('fs/promises'); const __TARGET_FOLDER_ROOT = './packages/prebuilder/src/generators/index/__tests__/FAKE_CODEGEN'; diff --git a/packages/prebuilder/src/generators/lp/__tests__/prebuild.lpType.test.ts b/packages/prebuilder/src/generators/lp/__tests__/prebuild.lpType.test.ts index 72aac5cc2..09a27a719 100644 --- a/packages/prebuilder/src/generators/lp/__tests__/prebuild.lpType.test.ts +++ b/packages/prebuilder/src/generators/lp/__tests__/prebuild.lpType.test.ts @@ -5,6 +5,7 @@ import { afterAll, describe, expect, it } from 'vitest'; import generateLandingPagesType from '../lpType'; +// https://github.com/vitest-dev/vitest/discussions/2484 const fs = require('fs/promises'); const __TARGET_FOLDER_ROOT = './packages/prebuilder/src/generators/lp/__tests__/FAKE_CODEGEN'; diff --git a/packages/prebuilder/src/generators/pages/__tests__/prebuild.pagesType.test.ts b/packages/prebuilder/src/generators/pages/__tests__/prebuild.pagesType.test.ts index 1a4f124f6..e119fa984 100644 --- a/packages/prebuilder/src/generators/pages/__tests__/prebuild.pagesType.test.ts +++ b/packages/prebuilder/src/generators/pages/__tests__/prebuild.pagesType.test.ts @@ -5,6 +5,7 @@ import type { Page } from '../../../types/Metadatas'; import generatePagesType, { HARDCODED_FALLBACK_TYPE_FIELDS } from '../pagesType'; +// https://github.com/vitest-dev/vitest/discussions/2484 const fs = require('fs/promises'); const __TARGET_FOLDER_ROOT = './packages/prebuilder/src/generators/pages/__tests__/FAKE_CODEGEN'; diff --git a/packages/prebuilder/src/lib/__tests__/prebuild.etc.test.ts b/packages/prebuilder/src/lib/__tests__/prebuild.etc.test.ts index 7e9565a0b..b0e094a8a 100644 --- a/packages/prebuilder/src/lib/__tests__/prebuild.etc.test.ts +++ b/packages/prebuilder/src/lib/__tests__/prebuild.etc.test.ts @@ -1,14 +1,93 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { describe, expect, it } from 'vitest'; +import getRawDataFromBracesDeclaration from '../getRawDataFromBracesDeclaration'; import { objInnerToObj } from '../etc'; describe('objInnerToObj', () => { + it('should throw when Babel parsing fails', () => { + const objInner = 'foo: bar'; + expect(() => objInnerToObj(objInner as string)).toThrow(); + }); + + it('should return an obj when parsing succeeds', () => { + const objInner = '"foo": "bar"'; + expect(objInnerToObj(objInner as string)).toStrictEqual({ foo: 'bar' }); + }); + it('should return an obj, given a valid obj inner', () => { - const objInner = ` + const initialObj = { foo: 'bar', bar: 'foo' }; + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + const objInner = getRawDataFromBracesDeclaration(JSON.stringify(initialObj), 0); + expect(objInner).not.toBe(null); + expect(objInnerToObj(objInner as string)).toStrictEqual(initialObj); + }); + + it('should return an obj, given a valid obj inner (nested)', () => { + const initialObj = { + baz: { + baz: { + bar: 'foo' + }, + foo: 'bar', + bar: 'foo' + }, foo: 'bar', - bar: 'foo', - `; - expect(objInnerToObj(objInner)).toStrictEqual({ foo: 'bar', bar: 'foo' }); + bar: 'foo' + }; + + const objInner = getRawDataFromBracesDeclaration( + JSON.stringify(initialObj), + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + 0 + ); + expect(objInner).not.toBe(null); + expect(objInnerToObj(objInner as string)).toStrictEqual(initialObj); + }); + + it('should return an obj, given a valid obj inner (literals)', () => { + const objInner = "foo: 'bar', bar: 'foo'"; + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect(objInnerToObj(objInner as string)).toStrictEqual({ foo: 'bar', bar: 'foo' }); + }); + + it('should return an obj, given a valid obj inner (string literals)', () => { + const objInner = "'foo': 'bar', 'bar': 'foo'"; + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect(objInnerToObj(objInner as string)).toStrictEqual({ foo: 'bar', bar: 'foo' }); + }); + + it('should throw, given an invalid obj inner (numeric literals)', () => { + const objInner = "2172183: 'bar', 211838173: 'foo'"; + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect(() => objInnerToObj(objInner as string)).toThrow(); + }); + + it('should throw, given an invalid obj inner (int values)', () => { + const objInner = 'foo: 45, bar: 12'; + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect(() => objInnerToObj(objInner as string)).toThrow(); + }); + + it('should throw when encountering unsupported value type', () => { + const objInner = 'foo: /regex/'; + expect(() => objInnerToObj(objInner as string)).toThrow(); + }); + + it('should throw, given stupid input (random number)', () => { + const objInner = '88909988799'; + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect(() => objInnerToObj(objInner as string)).toThrow(); + }); + + it('should return empty object, given empty string input', () => { + const objInner = ''; + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect(objInnerToObj(objInner as string)).toStrictEqual({}); + }); + + it('should throw when JSON parsing fails', () => { + const objInner = 'invalid json'; + expect(() => objInnerToObj(objInner as string)).toThrow(); }); }); diff --git a/packages/prebuilder/src/lib/etc.ts b/packages/prebuilder/src/lib/etc.ts index 33054c5a0..06f0b7805 100644 --- a/packages/prebuilder/src/lib/etc.ts +++ b/packages/prebuilder/src/lib/etc.ts @@ -1,2 +1,49 @@ -// eslint-disable-next-line -export const objInnerToObj = (objInner: string): any => eval('({\n' + objInner + '\n})'); +import { parse } from '@babel/parser'; + +import type { I18nJSONPart } from '../types/Metadatas'; + +export function objInnerToObj(objInner: string): I18nJSONPart { + let res = {}; + try { + const obj = JSON.parse('{\n' + objInner + '\n}'); + return obj; + } catch { + try { + const parsedObject = parse(`({${objInner}})`, { + sourceType: 'unambiguous', + plugins: [] + }); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + if (parsedObject.program.body[0]?.type === 'ExpressionStatement' && parsedObject.program.body[0].expression.type === 'ObjectExpression') { + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + const objExpression = parsedObject.program.body[0].expression; + const obj: I18nJSONPart = objExpression.properties.reduce((accumulator: I18nJSONPart, prop) => { + if (prop.type === 'ObjectProperty') { + let key: string; + if (prop.key.type === 'Identifier') { + key = prop.key.name; + } else if (prop.key.type === 'StringLiteral') { + key = prop.key.value; + } else { + throw new Error(`Unsupported key type: ${prop.key.type}`); + } + + let value: string | null = null; + if (prop.value.type === 'StringLiteral') { + value = prop.value.value; + } else { + throw new Error(`Unsupported value type: ${prop.value.type}`); + } + accumulator[key] = value; + } + return accumulator; + }, {}); + res = obj; + } + } catch (babelError) { + throw babelError; + } + } + return res; +} diff --git a/packages/prebuilder/src/validators/__tests__/prebuild.sysPages.test.ts b/packages/prebuilder/src/validators/__tests__/prebuild.sysPages.test.ts index 1998e2e01..3db5aa58c 100644 --- a/packages/prebuilder/src/validators/__tests__/prebuild.sysPages.test.ts +++ b/packages/prebuilder/src/validators/__tests__/prebuild.sysPages.test.ts @@ -4,13 +4,15 @@ import { INVALID_NESTINGS_NEEDLE, INVALID_NESTING_NEEDLE, INVALID_SLUGS_NEEDLE, import { describe, expect, it, vi } from 'vitest'; // eslint-disable-next-line import/no-extraneous-dependencies import { INVALID_PATH } from '𝕍/commons'; -import path from 'path'; import type { VocabKey } from '../../config/translations'; import formatMessage from '../../config/formatMessage'; import sysPagesValidator from '../sysPages'; +// https://github.com/vitest-dev/vitest/discussions/2484 +const path = require('path'); + const VALID_PAGES_FOLDER = path.normalize('./packages/prebuilder/src/validators/__tests__/fake_pages_folders/valid_fake_pages_folder'); const VALID_PAGES_FOLDER_WITH_ONE_UGLY_INDEX_STRATEGY = path.normalize( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 583eefdbe..66d3a431b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -264,6 +264,12 @@ importers: packages/prebuilder: dependencies: + '@babel/core': + specifier: ^7.23.9 + version: 7.23.9 + '@babel/parser': + specifier: ^7.23.9 + version: 7.23.9 arg: specifier: ^5.0.2 version: 5.0.2 diff --git a/src/i18n/locales/schema.ts b/src/i18n/locales/schema.ts index 3354340af..1b58266bd 100644 --- a/src/i18n/locales/schema.ts +++ b/src/i18n/locales/schema.ts @@ -76,17 +76,17 @@ export default { login: _ }, + 'pages-titles': SHARED_VOCAB_SCHEMA['pages-titles'], + _infos: { - lng: '__SCANNED_ON_PREBUILD_FIELD__' + lng: '__SCANNED__' }, - 'pages-titles': SHARED_VOCAB_SCHEMA['pages-titles'], - 'blog-categories': blogCategories } as const satisfies TypedLeafsJSONData; type NotScanned = ''; -type Scanned = '__SCANNED_ON_PREBUILD_FIELD__'; +type Scanned = '__SCANNED__'; type MaybeScanned = NotScanned | Scanned; // Stryker restore all