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"