diff --git a/package.json b/package.json index 69bd99f..c377de8 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,9 @@ "build:tailwind": "yarn --cwd packages/ink-tailwind build", "build:ui": "yarn --cwd packages/ink-ui-src build", "build:web": "yarn --cwd packages/ink-web build", - "test": "yarn --cwd packages/ink test", - "test:web": "yarn --cwd packages/ink-web test", + "test": "yarn test:ink && yarn test:css", + "test:ink":"yarn --cwd packages/ink test", + "test:css": "yarn --cwd packages/ink-css test", "dev:http": "yarn --cwd examples/with-http dev", "dev:express": "yarn --cwd examples/with-express dev", "dev:fastify": "yarn --cwd examples/with-fastify dev", diff --git a/packages/ink-css/package.json b/packages/ink-css/package.json index 9692d79..e0f2595 100644 --- a/packages/ink-css/package.json +++ b/packages/ink-css/package.json @@ -17,14 +17,20 @@ "tsconfig.json" ], "scripts": { - "build": "tsc" + "build": "tsc", + "test": "nyc ts-mocha tests/*.test.ts" }, "dependencies": { "@stackpress/ink": "0.3.10" }, "devDependencies": { + "@types/chai": "4.3.20", + "@types/mocha": "10.0.10", "@types/node": "22.9.3", - "ts-node": "10.9.2", - "typescript": "5.7.2" + "chai": "4.5.0", + "mocha": "10.8.2", + "nyc": "17.1.0", + "ts-mocha": "10.0.0", + "ts-node": "10.9.2" } } \ No newline at end of file diff --git a/packages/ink-css/tests/StyleParser.test.ts b/packages/ink-css/tests/StyleParser.test.ts new file mode 100644 index 0000000..12dd65e --- /dev/null +++ b/packages/ink-css/tests/StyleParser.test.ts @@ -0,0 +1,161 @@ +import { expect } from 'chai'; +import { default as StyleParser, pattern } from '../src/StyleParser'; +import NodeFS from '@stackpress/types/dist/system/NodeFS'; +import path from 'path'; +import fs from 'fs'; + +describe('StyleParser', () => { + describe('pattern', () => { + it('should match valid class names', () => { + const validClasses = [ + 'abc', + 'abc-def', + 'abc-1def', + 'abc-1def-2ghi' + ]; + + validClasses.forEach(className => { + const matches = className.match(pattern); + expect(matches).to.not.be.null; + expect(matches![0]).to.equal(className); + }); + }); + + it('should not match invalid class names', () => { + const invalidClasses = [ + 'ABC', // uppercase not allowed + '@abc', // special characters not allowed + 'abc_def' // underscore not allowed + ]; + + invalidClasses.forEach(className => { + // Test each invalid class name individually against the pattern + const regex = new RegExp('^' + pattern.source + '$'); + const matches = className.match(regex); + expect(matches, `${className} should not match`).to.be.null; + }); + }); + }); + + describe('StyleParser class', () => { + let parser: StyleParser; + let tempDir: string; + let testFile: string; + + beforeEach(() => { + parser = new StyleParser({ + cwd: process.cwd() + }); + + // Create a temporary test directory and file + tempDir = path.join(process.cwd(), 'temp_test_dir'); + testFile = path.join(tempDir, 'test.css'); + fs.mkdirSync(tempDir, { recursive: true }); + }); + + afterEach(() => { + // Clean up temporary files + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + describe('static match()', () => { + it('should return unique class names from content', () => { + const content = 'some-class other-class some-class third-class'; + const matches = StyleParser.match(content); + expect(matches).to.deep.equal(['some-class', 'other-class', 'third-class']); + }); + + it('should return empty array for content with no matches', () => { + const content = 'NO_VALID_CLASSES_HERE'; + const matches = StyleParser.match(content); + expect(matches).to.deep.equal([]); + }); + }); + + describe('instance methods', () => { + it('should initialize with default options', () => { + const defaultParser = new StyleParser(); + expect(defaultParser.cwd).to.equal(process.cwd()); + expect(defaultParser.fs).to.be.instanceOf(NodeFS); + }); + + it('should initialize with custom options', () => { + const customFs = new NodeFS(); + const customCwd = process.cwd() + '/custom'; + const customParser = new StyleParser({ fs: customFs, cwd: customCwd }); + + expect(customParser.cwd).to.equal(customCwd); + expect(customParser.fs).to.equal(customFs); + }); + + it('should throw exception for non-existent file', () => { + const nonExistentFile = 'non-existent-file.txt'; + const absolutePath = parser.loader.absolute(nonExistentFile); + // First add the file to the cache using the absolute path + parser.vfs.set(absolutePath, 'some content'); + // Now try to add the non-existent file, which should throw + expect(() => parser.add(nonExistentFile)).to.throw('File not found'); + }); + + it('should successfully read and cache file content', () => { + // Create a test file + const fileContent = 'test-class another-class'; + fs.writeFileSync(testFile, fileContent); + + // Add the file to the parser + const absolutePath = parser.loader.absolute(testFile); + parser.vfs.clear(); // Clear the cache first + + // First set it in the cache (required by implementation) + parser.vfs.set(absolutePath, ''); + parser.add(testFile); + + // Verify the file was cached with updated content + expect(parser.vfs.has(absolutePath)).to.be.true; + expect(parser.vfs.get(absolutePath)).to.equal(fileContent); + }); + + it('should skip adding file if not in cache', () => { + // Create a test file + const fileContent = 'test-class'; + fs.writeFileSync(testFile, fileContent); + + // Add the file without setting it in cache first + const absolutePath = parser.loader.absolute(testFile); + parser.vfs.clear(); // Clear the cache first + parser.add(testFile); + + // Verify the file was not added to cache + expect(parser.vfs.has(absolutePath)).to.be.false; + }); + + it('should parse all cached files and return unique class names', () => { + // Add multiple files to the cache + parser.vfs.clear(); // Clear the cache first + parser.set('file1.css', 'class1 shared-class'); + parser.set('file2.css', 'class2 shared-class'); + + const classNames = parser.parse(); + expect(classNames).to.deep.equal(['class1', 'shared-class', 'class2', 'shared-class']); + }); + + it('should walk through cached files and yield class names', () => { + // Add multiple files to the cache + parser.vfs.clear(); // Clear the cache first + parser.set('file1.css', 'class1 shared-class'); + parser.set('file2.css', 'class2 shared-class'); + + const classNames = Array.from(parser.walk()); + expect(classNames).to.deep.equal(['class1', 'shared-class', 'class2', 'shared-class']); + }); + + it('should handle empty cache in parse and walk', () => { + parser.vfs.clear(); // Clear the cache first + expect(parser.parse()).to.deep.equal([]); + expect(Array.from(parser.walk())).to.deep.equal([]); + }); + }); + }); +}); diff --git a/packages/ink/package.json b/packages/ink/package.json index df0880c..b283d46 100644 --- a/packages/ink/package.json +++ b/packages/ink/package.json @@ -30,7 +30,7 @@ ], "scripts": { "build": "tsc", - "test": "ts-mocha tests/*.test.ts" + "test": "nyc ts-mocha tests/*.test.ts" }, "dependencies": { "@stackpress/types": "0.3.10", diff --git a/packages/ink/src/directives/IteratorDirective.ts b/packages/ink/src/directives/IteratorDirective.ts index 159127a..1dc85b3 100644 --- a/packages/ink/src/directives/IteratorDirective.ts +++ b/packages/ink/src/directives/IteratorDirective.ts @@ -12,16 +12,30 @@ import Parser from '../compiler/Parser'; //local import AbstractDirective from './AbstractDirective'; +/** + * IteratorDirective implements the 'each' directive for iterating over arrays and objects + * in templates. It supports the following syntax: + * + * Arrays: ... + * Objects: ... + * Expressions: ... + */ export default class IteratorDirective extends AbstractDirective { /** - * Returns the directive name + * Returns the directive name used in templates */ public get name() { return 'each'; } /** - * Saves the compiler instance + * Processes the markup token and generates the iteration code + * @param parent - The parent markup token (if any) + * @param token - The current markup token being processed + * @param components - List of available components + * @param next - Function to process child elements + * @returns Generated JavaScript code for iteration + * @throws Exception if the iteration syntax is invalid */ public markup( parent: MarkupToken|null, @@ -30,12 +44,15 @@ export default class IteratorDirective extends AbstractDirective { next: NextDirective ) { let expression = ''; - //syntax ... + + // Validate that attributes are present if (!token.attributes || token.attributes.properties.length === 0 ) { throw Exception.for('Invalid each statement'); } + + // Extract key, value, and from attributes const key = token.attributes.properties.find( property => property.key.name === 'key' ); @@ -45,6 +62,8 @@ export default class IteratorDirective extends AbstractDirective { const from = token.attributes.properties.find( property => property.key.name === 'from' ); + + // Validate required attributes if (!from || (!key && !value)) { throw Exception.for('Invalid each statement'); } else if (key && key.value.type !== 'Identifier') { @@ -52,31 +71,46 @@ export default class IteratorDirective extends AbstractDirective { } else if (value && value.value.type !== 'Identifier') { throw Exception.for('Invalid value in each'); } + + // Use default '_' for unused key/value names const keyName = (key?.value as IdentifierToken)?.name || '_'; const valueName = (value?.value as IdentifierToken)?.name || '_'; + expression += `...`; + + // Handle different types of 'from' values if (from.value.type === 'ProgramExpression') { + // For JavaScript expressions (e.g., array.filter()) const script = from.value as ScriptToken; expression += `Object.entries(${script.source})`; } else if (from.value.type === 'ArrayExpression') { + // For literal arrays (e.g., ['a', 'b', 'c']) expression += `Object.entries(${ JSON.stringify(Parser.array(from.value)) })`; } else if (from.value.type === 'ObjectExpression') { + // For literal objects (e.g., {a: 1, b: 2}) expression += `Object.entries(${ JSON.stringify(Parser.object(from.value)) })`; } else if (from.value.type === 'Identifier') { + // For variables (e.g., items) expression += `Object.entries(${from.value.name})`; } else { throw Exception.for('Invalid from value in each'); } + + // Generate map function with destructuring for key/value pairs expression += `.map(([${keyName}, ${valueName}]) => `; + + // Process child elements if they exist if (token.children) { expression += next(token, token.children, components); } else { expression += '[]'; } + + // Flatten the results to handle nested arrays expression += ').flat()'; return expression; } diff --git a/packages/ink/tests/CompilerComponent.test.ts b/packages/ink/tests/CompilerComponent.test.ts index b1f60cc..61fce24 100644 --- a/packages/ink/tests/CompilerComponent.test.ts +++ b/packages/ink/tests/CompilerComponent.test.ts @@ -1,8 +1,141 @@ import { describe, it } from 'mocha'; import { expect } from 'chai'; +import path from 'node:path'; +import Component from '../src/compiler/Component'; +import NodeFS from '@stackpress/types/dist/system/NodeFS'; +import type { ComponentOptions } from '../src/types'; +/** + * Test suite for the Ink Compiler Component class + * This class is responsible for parsing and processing Ink components, + * including handling component initialization, AST generation, + * and path resolution. + */ describe('Ink Compiler Component', () => { - it('Should parse component', () => { - expect(true).to.equal(true); + const fs = new NodeFS(); + const cwd = path.resolve(__dirname); + const samplePath = path.join(__dirname, 'fixtures', 'sample.ink'); + const externalPath = path.join(__dirname, 'fixtures', 'external.js'); + + /** + * Helper function to create a component with options + * @param source - Path to the component source file + * @param options - Optional component configuration + * @returns A new Component instance + */ + const createComponent = ( + source: string, options: Partial = {}) => { + return new Component(source, { cwd, fs, ...options }); + }; + + /** + * Tests for component initialization and basic property access + * Verifies that components are created with correct default values + * and that custom options are properly applied + */ + describe('Constructor and Basic Properties', () => { + it('Should initialize with default options', () => { + const component = createComponent(samplePath); + expect(component.brand).to.equal('ink'); + expect(component.type).to.equal('component'); + expect(component.parent).to.be.null; + }); + + it('Should initialize with custom options', () => { + const component = createComponent(samplePath, { + brand: 'custom', + type: 'template', + name: 'custom-name' + }); + expect(component.brand).to.equal('custom'); + expect(component.type).to.equal('template'); + expect(component.tagname).to.equal('custom-name'); + }); + + it('Should handle numeric component names', () => { + const component = createComponent(samplePath, { name: '123component' }); + expect(component.classname).to.match(/^N123component_/); + }); + }); + + /** + * Tests for AST (Abstract Syntax Tree) generation and caching + * Verifies that components can generate and cache their AST correctly, + * and that external components throw appropriate errors + */ + describe('AST and Tokenization', () => { + it('Should get AST for a valid component', () => { + const component = createComponent(samplePath); + const ast = component.ast; + expect(ast).to.be.an('object'); + // Get AST again to test caching + const cachedAst = component.ast; + expect(cachedAst).to.equal(ast); + }); + + it('Should throw error when trying to get AST for external component', + () => { + const component = createComponent(externalPath, { type: 'external' }); + expect(() => component.ast).to.throw('No tokenizer for external'); + }); + + it('Should get fresh AST when cache is disabled', () => { + const component = createComponent(samplePath); + const ast1 = component.tokenize(false); + const ast2 = component.tokenize(false); + expect(ast1).to.not.equal(ast2); + expect(ast1).to.deep.equal(ast2); + }); + }); + + /** + * Tests for special component properties and attributes + * Verifies handling of external components, form fields, + * and observable attributes + */ + describe('Component Properties', () => { + it('Should handle external component properties', () => { + const component = createComponent(externalPath, { type: 'external' }); + expect(component.components).to.deep.equal([]); + expect(component.dependencies).to.deep.equal([]); + expect(component.markup).to.deep.equal([]); + expect(component.scripts).to.deep.equal([]); + expect(component.styles).to.deep.equal([]); + expect(component.field).to.be.false; + }); + + it('Should handle component with form field', () => { + const component = + createComponent(path.join(__dirname, 'fixtures', 'form.ink')); + expect(component.field).to.be.true; + }); + + it('Should handle component with observe attributes', () => { + const component = + createComponent(path.join(__dirname, 'fixtures', 'observe.ink')); + expect(component.observe).to.have.length.greaterThan(0); + expect(component.observe).to.include('value'); + }); + }); + + /** + * Tests for component path resolution + * Verifies that components can correctly resolve absolute + * and relative paths, and handle parent-child component relationships + */ + describe('Path Resolution', () => { + it('Should resolve absolute and relative paths', () => { + const component = createComponent(samplePath); + expect(component.absolute).to.equal(path.resolve(samplePath)); + expect(component.relative).to.match(/^\.\//); + expect(component.dirname).to.equal(path.dirname(path.resolve(samplePath))); + }); + + it('Should handle parent component path resolution', () => { + const parent = createComponent(samplePath); + const child = new Component(externalPath, { cwd, fs }, parent); + expect(child.parent).to.equal(parent); + expect(child.absolute).to.equal(path.resolve(externalPath)); + }); }); }); \ No newline at end of file diff --git a/packages/ink/tests/CompilerLexer.test.ts b/packages/ink/tests/CompilerLexer.test.ts index a7d0e5a..49a6800 100644 --- a/packages/ink/tests/CompilerLexer.test.ts +++ b/packages/ink/tests/CompilerLexer.test.ts @@ -5,12 +5,21 @@ import { expect } from 'chai'; import Lexer from '../src/compiler/Lexer'; import definitions, { data } from '../src/compiler/definitions'; +/** + * Test suite for the Ink Compiler Lexer. + */ describe('Ink Compiler Lexer', () => { + // Create a new lexer instance const lexer = new Lexer(); + + // Define all the built-in definitions Object.keys(definitions).forEach((key) => { lexer.define(key, definitions[key]); }); - + + /** + * Test that the lexer can parse a float. + */ it('Should parse float', () => { lexer.load('4.4'); const token = lexer.expect(data); @@ -20,6 +29,9 @@ describe('Ink Compiler Lexer', () => { expect(token.end).to.equal(3); }); + /** + * Test that the lexer can parse an integer. + */ it('Should parse integer', () => { lexer.load('44'); const token = lexer.expect(data); @@ -29,6 +41,9 @@ describe('Ink Compiler Lexer', () => { expect(token.end).to.equal(2); }); + /** + * Test that the lexer can parse null. + */ it('Should parse null', () => { lexer.load('null'); const token = lexer.expect(data); @@ -38,8 +53,11 @@ describe('Ink Compiler Lexer', () => { expect(token.end).to.equal(4); }); + /** + * Test that the lexer can parse a boolean. + */ it('Should parse boolean', () => { - //true + // Test true (() => { lexer.load('true'); const token = lexer.expect(data); @@ -48,7 +66,8 @@ describe('Ink Compiler Lexer', () => { expect(token.start).to.equal(0); expect(token.end).to.equal(4); })(); - //false + + // Test false (() => { lexer.load('false'); const token = lexer.expect(data); @@ -59,6 +78,9 @@ describe('Ink Compiler Lexer', () => { })(); }); + /** + * Test that the lexer can parse a string. + */ it('Should parse string', () => { lexer.load('"foobar"'); const token = lexer.expect(data); @@ -67,4 +89,144 @@ describe('Ink Compiler Lexer', () => { expect(token.start).to.equal(0); expect(token.end).to.equal(8); }); + + /** + * Test that the lexer can clone itself. + */ + it('Should clone the lexer', () => { + const lexerClone = lexer.clone(); + expect(lexerClone).to.not.equal(lexer); + expect(lexerClone.code).to.equal(lexer.code); + expect(lexerClone.index).to.equal(lexer.index); + }); + + /** + * Test that the lexer can define a new token. + */ + it('Should define a new token', () => { + // Define a new token with the required properties: + //type, value, start, and end + lexer.define('testToken', () => ({ type: 'Test', value: 'test', start: 0, end: 8 })); + lexer.load('testToken'); // Load the token into the lexer + const token = lexer.expect('testToken'); + // Ensure the token has the correct properties + expect(token).to.not.be.undefined; + // Use non-null assertion + expect(token!.type).to.equal('Test'); + expect(token!.value).to.equal('test'); + expect(token!.start).to.equal(0); + expect(token!.end).to.equal(8); + }); + + /** + * Test that the lexer can find the next token. + */ + it('Should find the next token', () => { + lexer.load('testToken'); + // Define the token with the required properties + lexer.define('testToken', () => + ({ type: 'Test', value: 'test', start: 0, end: 8 })); + const token = lexer.find('testToken'); + // Ensure the token is found and has the correct properties + expect(token).to.not.be.undefined; + // Use non-null assertion + expect(token!.type).to.equal('Test'); + expect(token!.value).to.equal('test'); + expect(token!.start).to.equal(0); + expect(token!.end).to.equal(8); + }); + + /** + * Test that the lexer can match definitions correctly. + */ + it('Should match definitions correctly', () => { + lexer.load('testToken'); + lexer.define('testToken', () => + ({ type: 'Test', value: 'test', start: 0, end: 8 })); + const match = lexer.match(0, ['testToken']); + // Ensure a match is found + expect(match).to.not.be.null; + // Use non-null assertion + expect(match!.type).to.equal('Test'); + expect(match!.value).to.equal('test'); + expect(match!.start).to.equal(0); + expect(match!.end).to.equal(8); + }); + + /** + * Test that the lexer can check the next token correctly. + */ + it('Should check next correctly', () => { + lexer.load('testToken'); + lexer.define('testToken', () => + ({ type: 'Test', value: 'test', start: 0, end: 8 })); + expect(lexer.next('testToken')).to.be.true; + }); + + /** + * Test that the lexer can optionally return a token. + */ + it('Should optionally return a token', () => { + lexer.load('optionalToken'); + lexer.define('optionalToken', () => + ({ type: 'Optional', value: 'optional', start: 0, end: 12 })); + const token = lexer.optional('optionalToken'); + // Ensure the token is found and has the correct properties + expect(token).to.not.be.undefined; + // Use non-null assertion + expect(token!.type).to.equal('Optional'); + expect(token!.value).to.equal('optional'); + expect(token!.start).to.equal(0); + expect(token!.end).to.equal(12); + }); + + /** + * Test that the lexer can load code correctly. + */ + it('Should load code correctly', () => { + lexer.load('test'); + expect(lexer.code).to.equal('test'); + expect(lexer.index).to.equal(0); + }); + + /** + * Test that the lexer can return a substring. + */ + it('Should return a substring', () => { + lexer.load('Hello, World!'); + const substring = lexer.substring(0, 5); + expect(substring).to.equal('Hello'); + }); + + /** + * Test that the lexer returns undefined for an unknown token. + */ + it('Should return undefined for unknown token', () => { + lexer.load('unknown'); + expect(lexer.find('unknownToken')).to.be.undefined; + }); + + /** + * Test that the lexer returns undefined for an optional unknown token. + */ + it('Should return undefined for optional unknown token', () => { + lexer.load('unknown'); + expect(lexer.optional('unknownToken')).to.be.undefined; + }); + + /** + * Test that the lexer returns null for a match unknown token. + */ + it('Should return null for match unknown token', () => { + lexer.load('unknown'); + expect(() => lexer.match(0, ['unknownToken'])).to.throw(); + }); + + /** + * Test that the lexer returns false for next unknown token. + */ + it('Should return false for next unknown token', () => { + lexer.load('unknown'); + expect(lexer.next('unknownToken')).to.be.false; + }); }); \ No newline at end of file diff --git a/packages/ink/tests/ConditionalDirective.test.ts b/packages/ink/tests/ConditionalDirective.test.ts new file mode 100644 index 0000000..43fd48e --- /dev/null +++ b/packages/ink/tests/ConditionalDirective.test.ts @@ -0,0 +1,528 @@ +import { expect } from 'chai'; +import { describe, it, beforeEach } from 'mocha'; +import { IfDirective, ElifDirective, ElseDirective } +from '../src/directives/ConditionalDirective'; +import type { MarkupToken, PropertyToken, ScriptToken, + ObjectToken, NextDirective, MarkupChildToken } from '../src/types'; +import Component from '../src/compiler/Component'; +import type Transpiler from '../src/compiler/Transpiler'; + +describe('ConditionalDirective', () => { + // Create a mock transpiler + const mockTranspiler = { + // Add any required transpiler methods here + } as Transpiler; + + // Helper function to create a mock next directive function + const mockNext: NextDirective = (parent: MarkupToken|null, + token: MarkupChildToken[], components: Component[]) => { + return '[child content]'; + }; + + describe('IfDirective', () => { + let ifDirective: IfDirective; + + beforeEach(() => { + ifDirective = new IfDirective(mockTranspiler); + }); + + // Test directive name + it('should have correct directive name', () => { + expect(ifDirective.name).to.equal('if'); + }); + + // Test invalid if statement (no attributes) + it('should throw error for if statement without attributes', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + attributes: undefined, + children: [] + }; + expect(() => { + ifDirective.markup(null, token, [], mockNext); + }).to.throw('Invalid if statement'); + }); + + // Test if statement with boolean literal + it('should handle boolean literal true condition', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { type: 'Literal', value: true, raw: 'true', + start: 0, end: 4, escape: false }, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + const result = ifDirective.markup(null, token, [], mockNext); + expect(result).to.equal('...(!!(true) ? [child content] : [])'); + }); + + // Test if statement with string literal + it('should handle string literal condition', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { type: 'Literal', value: 'hello', raw: "'hello'", + start: 0, end: 4, escape: false }, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + const result = ifDirective.markup(null, token, [], mockNext); + expect(result).to.equal("...(!!('hello') ? [child content] : [])"); + }); + + // Test if statement with program expression + it('should handle program expression condition', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { + type: 'ProgramExpression', + start: 0, + end: 10, + inline: true, + source: 'count > 1', + runtime: false + } as ScriptToken, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + const result = ifDirective.markup(null, token, [], mockNext); + expect(result).to.equal('...(!!(count > 1) ? [child content] : [])'); + }); + + // Test if statement with children + it('should handle if statement with children', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { type: 'Identifier', name: 'isVisible', start: 0, end: 4 }, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [{ + type: 'MarkupExpression', + name: 'div', + kind: 'block', + start: 0, + end: 10, + children: [] + }] + }; + const result = ifDirective.markup(null, token, [], mockNext); + expect(result).to.equal('...(!!(isVisible) ? [child content] : [])'); + }); + + // Test if statement with identifier + it('should handle identifier condition', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { type: 'Identifier', name: 'isVisible', start: 0, end: 4 }, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + const result = ifDirective.markup(null, token, [], mockNext); + expect(result).to.equal('...(!!(isVisible) ? [child content] : [])'); + }); + + // Test if statement with false condition + it('should handle false condition', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'false', start: 0, end: 4 }, + value: { type: 'Identifier', name: 'isHidden', start: 0, end: 4 }, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + const result = ifDirective.markup(null, token, [], mockNext); + expect(result).to.equal('...(!(isHidden) ? [child content] : [])'); + }); + + // Test invalid property type + it('should throw error for invalid property type', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { type: 'ArrayExpression', elements: [], start: 0, end: 4 }, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + expect(() => { + ifDirective.markup(null, token, [], mockNext); + }).to.throw('Invalid if statement'); + }); + }); + + describe('ElifDirective', () => { + let elifDirective: ElifDirective; + + beforeEach(() => { + elifDirective = new ElifDirective(mockTranspiler); + }); + + it('should have correct directive name', () => { + expect(elifDirective.name).to.equal('elif'); + }); + + // Test elif without parent + it('should throw error when no parent', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'elif', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { type: 'Literal', value: true, raw: 'true', + start: 0, end: 4, escape: false }, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + expect(() => { + elifDirective.markup(null, token); + }).to.throw('Invalid elif statement'); + }); + + // Test elif with wrong parent type + it('should throw error when parent is not if', () => { + const parent: MarkupToken = { + type: 'MarkupExpression', + name: 'div', + kind: 'block', + start: 0, + end: 10, + children: [] + }; + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'elif', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { type: 'Literal', value: true, raw: 'true', + start: 0, end: 4, escape: false }, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + expect(() => { + elifDirective.markup(parent, token); + }).to.throw('Invalid elif statement'); + }); + + // Test valid elif with program expression + it('should handle program expression condition', () => { + const parent: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + children: [] + }; + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'elif', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { + type: 'ProgramExpression', + start: 0, + end: 10, + inline: true, + source: 'count === 2', + runtime: false + } as ScriptToken, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + const result = elifDirective.markup(parent, token); + expect(result).to.equal(']: !!(count === 2) ? [ '); + }); + }); + + describe('ElseDirective', () => { + let elseDirective: ElseDirective; + + beforeEach(() => { + elseDirective = new ElseDirective(mockTranspiler); + }); + + it('should have correct directive name', () => { + expect(elseDirective.name).to.equal('else'); + }); + + // Test else without parent + it('should throw error when no parent', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'else', + kind: 'block', + start: 0, + end: 10, + children: [] + }; + expect(() => { + elseDirective.markup(null, token); + }).to.throw('Invalid else statement'); + }); + + // Test else with wrong parent type + it('should throw error when parent is not if', () => { + const parent: MarkupToken = { + type: 'MarkupExpression', + name: 'div', + kind: 'block', + start: 0, + end: 10, + children: [] + }; + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'else', + kind: 'block', + start: 0, + end: 10, + children: [] + }; + expect(() => { + elseDirective.markup(parent, token); + }).to.throw('Invalid else statement'); + }); + + // Test valid else directive + it('should handle valid else statement', () => { + const parent: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + children: [] + }; + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'else', + kind: 'block', + start: 0, + end: 10, + children: [] + }; + const result = elseDirective.markup(parent, token); + expect(result).to.equal(']: true ? ['); + }); + + // Test else ignores attributes + it('should ignore attributes in else statement', () => { + const parent: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + children: [] + }; + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'else', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { type: 'Literal', value: true, raw: 'true', + start: 0, end: 4, escape: false }, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + const result = elseDirective.markup(parent, token); + expect(result).to.equal(']: true ? ['); + }); + }); +}); \ No newline at end of file diff --git a/packages/ink/tests/DocumentBuilder.test.ts b/packages/ink/tests/DocumentBuilder.test.ts index 7526251..f9ab4d3 100644 --- a/packages/ink/tests/DocumentBuilder.test.ts +++ b/packages/ink/tests/DocumentBuilder.test.ts @@ -1,32 +1,231 @@ import path from 'path'; -import { describe, it } from 'mocha'; +import { describe, it, beforeEach } from 'mocha'; import { expect } from 'chai'; - +import EventEmitter from '../src/EventEmitter'; import Component from '../src/compiler/Component'; import Builder from '../src/document/Builder'; describe('Ink Document Builder', () => { - //determine the tsconfig path const tsconfig = path.join(__dirname, '../tsconfig.json'); - //make a document component - const component = new Component( - path.join(__dirname, 'fixtures/page.ink'), - { cwd: __dirname } - ); - it('Should build document', async () => { - const builder = new Builder(component, { tsconfig, minify: false }); - const server = await builder.server(); - //console.log('server', server) - expect(server).to.contain('...this._toNodeList(snippet1)'); - expect(server).to.contain('...this._toNodeList(snippet2)'); - const client = await builder.client(); - //console.log('client', client) - expect(client).to.contain('const snippet1 = `//on server:'); - expect(client).to.contain('const snippet2 = ` diff --git a/packages/ink/tests/fixtures/test.js b/packages/ink/tests/fixtures/test.js new file mode 100644 index 0000000..f09fca5 --- /dev/null +++ b/packages/ink/tests/fixtures/test.js @@ -0,0 +1,5 @@ +export function greet(name) { + return `Hello, ${name}!`; +} + +export const VERSION = '1.0.0'; diff --git a/packages/ink/tests/helpers.test.ts b/packages/ink/tests/helpers.test.ts new file mode 100644 index 0000000..792a0d4 --- /dev/null +++ b/packages/ink/tests/helpers.test.ts @@ -0,0 +1,238 @@ +import { describe, it } from 'mocha'; +import { expect } from 'chai'; +import type { SourceFile, OutputFile, EmitOutput } from 'ts-morph'; +import * as vm from 'vm'; +import path from 'path'; +import { + camelize, + capitalize, + lowerlize, + serialize, + slugify, + toJS, + toTS, + load, + build +} from '../src/helpers'; + +describe('helpers', () => { + describe('camelize', () => { + it('should convert string to camelCase', () => { + expect(camelize('some string')).to.equal('SomeString'); + expect(camelize('some-string')).to.equal('SomeString'); + expect(camelize('some_string')).to.equal('SomeString'); + expect(camelize('some__string')).to.equal('SomeString'); + expect(camelize(' some string ')).to.equal('SomeString'); + }); + + it('should convert string to camelCase with lower first letter', () => { + expect(camelize('some string', true)).to.equal('someString'); + expect(camelize('some-string', true)).to.equal('someString'); + expect(camelize('some_string', true)).to.equal('someString'); + }); + }); + + describe('capitalize', () => { + it('should capitalize first letter', () => { + expect(capitalize('word')).to.equal('Word'); + expect(capitalize('Word')).to.equal('Word'); + expect(capitalize('')).to.equal(''); + }); + }); + + describe('lowerlize', () => { + it('should lowercase first letter', () => { + expect(lowerlize('Word')).to.equal('word'); + expect(lowerlize('word')).to.equal('word'); + expect(lowerlize('')).to.equal(''); + }); + }); + + describe('serialize', () => { + it('should create consistent hash for same input', () => { + const input = 'test string'; + const hash1 = serialize(input); + const hash2 = serialize(input); + expect(hash1).to.equal(hash2); + expect(hash1).to.have.lengthOf(20); // shake256 with outputLength 10 produces 20 hex chars + }); + + it('should create different hashes for different inputs', () => { + const hash1 = serialize('test1'); + const hash2 = serialize('test2'); + expect(hash1).to.not.equal(hash2); + }); + }); + + describe('slugify', () => { + it('should convert string to slug format', () => { + expect(slugify('Some Title')).to.equal('some-title'); + expect(slugify('SomeTitle')).to.equal('sometitle'); + expect(slugify(' Some Title ')).to.equal('some-title'); + expect(slugify('Some-Title')).to.equal('some-title'); + expect(slugify('Some__Title')).to.equal('some-title'); + }); + }); + + describe('toJS', () => { + it('should convert source file to javascript', () => { + // Mock SourceFile and its methods + const mockOutputFile: Partial = { + getFilePath: () => 'test.js' as any, + getText: () => 'const test = "hello";' + }; + + const mockEmitOutput: Partial = { + getOutputFiles: () => [mockOutputFile as OutputFile], + compilerObject: { + outputFiles: [], + emitSkipped: false, + diagnostics: [] + } as any, + getDiagnostics: () => [], + getEmitSkipped: () => false + }; + + const mockSourceFile: Partial = { + getEmitOutput: () => mockEmitOutput as EmitOutput + }; + + const result = toJS(mockSourceFile as SourceFile); + expect(result).to.equal('const test = "hello";'); + }); + + it('should handle multiple output files', () => { + const mockOutputFiles: Partial[] = [ + { + getFilePath: () => 'test.d.ts' as any, + getText: () => 'declare const test: string;' + }, + { + getFilePath: () => 'test.js' as any, + getText: () => 'const test = "hello";' + } + ]; + + const mockEmitOutput: Partial = { + getOutputFiles: () => mockOutputFiles as OutputFile[], + compilerObject: { + outputFiles: [], + emitSkipped: false, + diagnostics: [] + } as any, + getDiagnostics: () => [], + getEmitSkipped: () => false + }; + + const mockSourceFile: Partial = { + getEmitOutput: () => mockEmitOutput as EmitOutput + }; + + const result = toJS(mockSourceFile as SourceFile); + expect(result).to.equal('const test = "hello";'); + }); + }); + + describe('toTS', () => { + it('should get full text from source file', () => { + const mockSourceFile: Partial = { + getFullText: () => 'const test: string = "hello";' + }; + + const result = toTS(mockSourceFile as SourceFile); + expect(result).to.equal('const test: string = "hello";'); + }); + }); + + describe('load', () => { + it('should load and execute javascript in vm context', () => { + const source = ` + exports.testValue = "hello"; + exports.testFunc = function() { return "world"; }; + `; + + const context = load(source); + expect(context.exports).to.have.property('testValue', 'hello'); + expect(context.exports).to.have.property('testFunc'); + expect(context.exports.testFunc()).to.equal('world'); + }); + + it('should provide necessary global objects', () => { + const source = ` + exports.hasConsole = typeof console !== 'undefined'; + exports.hasModule = typeof module !== 'undefined'; + exports.hasRequire = typeof require !== 'undefined'; + exports.hasProcess = typeof process !== 'undefined'; + exports.hasBtoa = typeof btoa !== 'undefined'; + exports.hasAtob = typeof atob !== 'undefined'; + `; + + const context = load(source); + expect(context.exports.hasConsole).to.be.true; + expect(context.exports.hasModule).to.be.true; + expect(context.exports.hasRequire).to.be.true; + expect(context.exports.hasProcess).to.be.true; + expect(context.exports.hasBtoa).to.be.true; + expect(context.exports.hasAtob).to.be.true; + }); + + it('should handle errors in script execution', () => { + const source = 'throw new Error("test error");'; + expect(() => load(source)).to.throw('test error'); + }); + }); + + describe('build', () => { + const testFilePath = path.join(__dirname, 'fixtures', 'test.js'); + + it('should build with default options', async () => { + const result = await build(testFilePath); + expect(result).to.be.a('string'); + }); + + it('should build with custom format', async () => { + const result = await build(testFilePath, { + format: 'esm', + minify: false + }); + expect(result).to.be.a('string'); + expect(result).to.include('export'); // ESM format should have export statements + }); + + it('should build with global name', async () => { + const result = await build(testFilePath, { + format: 'iife', + globalName: 'TestModule' + }); + expect(result).to.be.a('string'); + expect(result).to.include('TestModule'); + }); + + it('should build without bundling', async () => { + const result = await build(testFilePath, { + bundle: false, + minify: false + }); + expect(result).to.be.a('string'); + }); + + it('should build with custom platform', async () => { + const result = await build(testFilePath, { + platform: 'node' + }); + expect(result).to.be.a('string'); + }); + + it('should build with custom plugins', async () => { + const mockPlugin = { + name: 'test-plugin', + setup: () => {} + }; + + const result = await build(testFilePath, { + plugins: [mockPlugin] + }); + expect(result).to.be.a('string'); + }); + }); +}); diff --git a/packages/ink/tests/plugins.test.ts b/packages/ink/tests/plugins.test.ts new file mode 100644 index 0000000..c5c4267 --- /dev/null +++ b/packages/ink/tests/plugins.test.ts @@ -0,0 +1,152 @@ +import { describe, it } from 'mocha'; +import { expect } from 'chai'; +import path from 'node:path'; +import type { Stats } from 'fs'; +import type { PluginBuild } from 'esbuild'; +import NodeFS from '@stackpress/types/dist/system/NodeFS'; +import { + esAliasPlugin, + esComponentPlugin, + esDocumentPlugin, + esWorkspacePlugin, + esInkPlugin +} from '../src/plugins'; + +class MockStats implements Stats { + isFile(): boolean { return true; } + isDirectory(): boolean { return false; } + isBlockDevice(): boolean { return false; } + isCharacterDevice(): boolean { return false; } + isSymbolicLink(): boolean { return false; } + isFIFO(): boolean { return false; } + isSocket(): boolean { return false; } + dev: number = 0; + ino: number = 0; + mode: number = 0; + nlink: number = 1; + uid: number = 0; + gid: number = 0; + rdev: number = 0; + size: number = 0; + blksize: number = 0; + blocks: number = 0; + atimeMs: number = 0; + mtimeMs: number = 0; + ctimeMs: number = 0; + birthtimeMs: number = 0; + atime: Date = new Date(); + mtime: Date = new Date(); + ctime: Date = new Date(); + birthtime: Date = new Date(); +} + +class MockFS extends NodeFS { + private files: Map = new Map(); + + constructor(cwd: string = '/test') { + super(); + // Add some mock files with absolute paths + this.files.set(path.resolve(cwd, 'component.ink'), { content: '', isFile: true }); + this.files.set(path.resolve(cwd, 'script.ts'), { content: '', isFile: true }); + this.files.set(path.resolve(cwd, 'index.ts'), { content: '', isFile: true }); + } + + existsSync(filepath: string): boolean { + return this.files.has(path.resolve(filepath)); + } + + lstatSync(filepath: string): Stats { + const file = this.files.get(path.resolve(filepath)); + const stats = new MockStats(); + Object.defineProperty(stats, 'isFile', { + value: () => file?.isFile ?? false + }); + return stats; + } +} + +describe('plugins', () => { + describe('esAliasPlugin', () => { + it('should resolve @/ paths correctly', () => { + const cwd = process.platform === 'win32' ? 'c:\\test' : '/test'; + const fs = new MockFS(cwd); + const plugin = esAliasPlugin({ cwd, fs }); + + let resolveResult: any; + const build: Partial = { + onResolve: ({ filter }: any, callback: any) => { + expect(filter.toString()).to.equal('/^@\\//'); + resolveResult = callback({ + path: '@/component.ink', + resolveDir: cwd + }); + } + }; + + plugin.setup(build as PluginBuild); + const expectedPath = process.platform === 'win32' + ? 'c:\\test\\component.ink' + : '/test/component.ink'; + expect(resolveResult).to.deep.equal({ + path: expectedPath, + namespace: 'ink-component-plugin' + }); + }); + + it('should handle different file extensions', () => { + const cwd = process.platform === 'win32' ? 'c:\\test' : '/test'; + const fs = new MockFS(cwd); + const plugin = esAliasPlugin({ cwd, fs }); + + let resolveResult: any; + const build: Partial = { + onResolve: ({ filter }: any, callback: any) => { + expect(filter.toString()).to.equal('/^@\\//'); + resolveResult = callback({ + path: '@/script.ts', + resolveDir: cwd + }); + } + }; + + plugin.setup(build as PluginBuild); + const expectedPath = process.platform === 'win32' + ? 'c:\\test\\script.ts' + : '/test/script.ts'; + expect(resolveResult).to.deep.equal({ + path: expectedPath, + loader: 'ts' + }); + }); + }); + + describe('esComponentPlugin', () => { + it('should initialize with default options', () => { + const plugin = esComponentPlugin(); + expect(plugin.name).to.equal('ink-component-plugin'); + }); + }); + + describe('esDocumentPlugin', () => { + it('should initialize with default options', () => { + const plugin = esDocumentPlugin(); + expect(plugin.server.name).to.equal('ink-document-server-plugin'); + expect(plugin.client.name).to.equal('ink-document-client-plugin'); + }); + }); + + describe('esWorkspacePlugin', () => { + it('should initialize correctly', () => { + const plugin = esWorkspacePlugin(); + expect(plugin.name).to.equal('resolve-workspace-packages'); + }); + }); + + describe('esInkPlugin', () => { + it('should combine all plugins', () => { + const plugin = esInkPlugin(); + expect(plugin).to.have.property('name', 'ink-plugin'); + expect(plugin).to.have.property('setup').that.is.a('function'); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 43d0123..3e0a881 100644 --- a/yarn.lock +++ b/yarn.lock @@ -331,21 +331,6 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@codexteam/icons@^0.0.4": - version "0.0.4" - resolved "https://registry.yarnpkg.com/@codexteam/icons/-/icons-0.0.4.tgz#8b72dcd3f3a1b0d880bdceb2abebd74b46d3ae13" - integrity sha512-V8N/TY2TGyas4wLrPIFq7bcow68b3gu8DfDt1+rrHPtXxcexadKauRJL6eQgfG7Z0LCrN4boLRawR4S9gjIh/Q== - -"@codexteam/icons@^0.0.5": - version "0.0.5" - resolved "https://registry.yarnpkg.com/@codexteam/icons/-/icons-0.0.5.tgz#d17f39b6a0497c6439f57dd42711817a3dd3679c" - integrity sha512-s6H2KXhLz2rgbMZSkRm8dsMJvyUNZsEjxobBEg9ztdrb1B2H3pEzY6iTwI4XUPJWJ3c3qRKwV4TrO3J5jUdoQA== - -"@codexteam/icons@^0.3.0": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@codexteam/icons/-/icons-0.3.2.tgz#b7aed0ba7b344e07953101f5476cded570d4f150" - integrity sha512-P1ep2fHoy0tv4wx85eic+uee5plDnZQ1Qa6gDfv7eHPkCXorMtVqJhzMb75o1izogh6G7380PqmFDXV3bW3Pig== - "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -368,45 +353,6 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@editorjs/editorjs@2.30.6": - version "2.30.6" - resolved "https://registry.yarnpkg.com/@editorjs/editorjs/-/editorjs-2.30.6.tgz#a77292da7433bc912e4beaf359b13812cab89c4d" - integrity sha512-6eQMc4Di3Hz9p4o+NGRgKaCeAF7eAk106m+bsDLc4eo94VGYO1M163OiGFdmanE+w503qTmXOzycWff5blEOAQ== - -"@editorjs/editorjs@^2.29.1": - version "2.30.7" - resolved "https://registry.yarnpkg.com/@editorjs/editorjs/-/editorjs-2.30.7.tgz#6ba210490c1040c55ef7e5ef040c4c6e3dc722e7" - integrity sha512-FfdeUqrgcKWC+Cy2GW6Dxup6s2TaRKokR4FL+HKXshu6h9Y//rrx4SQkURgkZOCSbV77t9btbmAXdFXWGB+diw== - -"@editorjs/header@2.8.7": - version "2.8.7" - resolved "https://registry.yarnpkg.com/@editorjs/header/-/header-2.8.7.tgz#6aa34e01638d18fbbc6d3bd75f1844869eca9193" - integrity sha512-rfxzYFR/Jhaocj3Xxx8XjEjyzfPbBIVkcPZ9Uy3rEz1n3ewhV0V4zwuxCjVfFhLUVgQQExq43BxJnTNlLOzqDQ== - dependencies: - "@codexteam/icons" "^0.0.5" - "@editorjs/editorjs" "^2.29.1" - -"@editorjs/image@2.9.3": - version "2.9.3" - resolved "https://registry.yarnpkg.com/@editorjs/image/-/image-2.9.3.tgz#d0e2b1add332fd16c2e2f4cf4b12f36e07b4b4d6" - integrity sha512-hBOHuqvL/ovjrns+xLuBh/b3kqABDlLxlByWnSuKnE31O351NDrg9AXrB1yYo0yZerw5V591rP0US3PEzp7CzQ== - dependencies: - "@codexteam/icons" "^0.3.0" - -"@editorjs/list@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@editorjs/list/-/list-1.10.0.tgz#5292ccc44d07effb2bca5e3206e7a647bf1fcbc1" - integrity sha512-zXCHaNcIscpefnteBOS3x+98f/qBgEVsv+OvtKoTDZipMNqck2uVG+X2qMQr8xcwtJrj9ySX54lUac9FDlAHnA== - dependencies: - "@codexteam/icons" "^0.0.4" - -"@editorjs/paragraph@2.11.6": - version "2.11.6" - resolved "https://registry.yarnpkg.com/@editorjs/paragraph/-/paragraph-2.11.6.tgz#011444187a74dc603201dce37d2fc6d054022407" - integrity sha512-i9B50Ylvh+0ZzUGWIba09PfUXsA00Y//zFZMwqsyaXXKxMluSIJ6ADFJbbK0zaV9Ijx49Xocrlg+CEPRqATk9w== - dependencies: - "@codexteam/icons" "^0.0.4" - "@esbuild/aix-ppc64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz#b57697945b50e99007b4c2521507dc613d4a648c"