diff --git a/.gitignore b/.gitignore index 0b38f535..8f025dc9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,13 @@ node_modules/ dist/ .vite/ coverage/ + +# Build artifacts +dev/ +min/ + +# Generated test files +test/extracted/ + +# Temporary test files +test-build.html diff --git a/package-lock.json b/package-lock.json index 3fa0415c..331627ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "postcss-import": "^16.0.0", "postcss-nesting": "^12.0.2", "rollup": "^4.9.5", + "terser": "^5.26.0", "vite": "^5.0.11", "vitest": "^1.2.1" } @@ -2241,7 +2242,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -2998,8 +2998,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/cac": { "version": "6.7.14", @@ -3107,8 +3106,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/concat-map": { "version": "0.0.1", @@ -4972,7 +4970,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -4991,7 +4988,6 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -5182,7 +5178,6 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", diff --git a/package.json b/package.json index 6c4ab0f3..9e05f01d 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ }, "scripts": { "dev": "vite", - "build": "vite build", + "build": "node scripts/build.js", + "build:vite": "vite build --config vite.config.build.js", "test": "vitest", "test:run": "vitest run", "extract-tests": "node scripts/extract-tests.js", @@ -45,6 +46,7 @@ "postcss-import": "^16.0.0", "postcss-nesting": "^12.0.2", "rollup": "^4.9.5", + "terser": "^5.26.0", "vite": "^5.0.11", "vitest": "^1.2.1" } diff --git a/scripts/build.js b/scripts/build.js new file mode 100644 index 00000000..59f0a518 --- /dev/null +++ b/scripts/build.js @@ -0,0 +1,105 @@ +#!/usr/bin/env node + +/** + * Build script for OxJS + * Generates ESM, UMD, and minified builds + */ + +const { build } = require('vite'); +const fs = require('fs'); +const path = require('path'); +const { minify } = require('terser'); + +async function buildOx() { + console.log('Building OxJS...\n'); + + // Step 1: Build ESM and UMD formats using Vite + console.log('1. Building ES modules and UMD...'); + await build({ + configFile: path.resolve(__dirname, '../vite.config.build.js') + }); + + // Step 2: Create minified version for script tag usage (min/Ox.js) + console.log('\n2. Creating minified build...'); + + // Read the UMD build + const umdPath = path.resolve(__dirname, '../dist/ox.umd.js'); + const umdCode = fs.readFileSync(umdPath, 'utf-8'); + + // Minify with Terser + const minified = await minify(umdCode, { + compress: { + drop_console: false, // Keep console for debugging + drop_debugger: true, + pure_funcs: ['console.log'] + }, + mangle: { + reserved: ['Ox'] // Don't mangle the main Ox object + }, + format: { + comments: false, + preamble: '/* OxJS v0.2.0 | (c) 2024 0x2620 | MIT License | oxjs.org */' + } + }); + + // Ensure min directory exists + const minDir = path.resolve(__dirname, '../min'); + if (!fs.existsSync(minDir)) { + fs.mkdirSync(minDir, { recursive: true }); + } + + // Write minified file + fs.writeFileSync(path.join(minDir, 'Ox.js'), minified.code); + + // Step 3: Copy the minified file to be compatible with old path structure + console.log('\n3. Creating backward compatible structure...'); + + // Create dev symlink if it doesn't exist + const devPath = path.resolve(__dirname, '../dev'); + if (!fs.existsSync(devPath)) { + fs.symlinkSync('source', devPath, 'dir'); + } + + // Step 4: Generate build info + const buildInfo = { + version: '0.2.0', + date: new Date().toISOString(), + files: { + 'dist/ox.esm.js': getFileSize('../dist/ox.esm.js'), + 'dist/ox.umd.js': getFileSize('../dist/ox.umd.js'), + 'min/Ox.js': getFileSize('../min/Ox.js') + } + }; + + fs.writeFileSync( + path.resolve(__dirname, '../dist/build-info.json'), + JSON.stringify(buildInfo, null, 2) + ); + + console.log('\n✅ Build complete!\n'); + console.log('Generated files:'); + console.log(` dist/ox.esm.js (${buildInfo.files['dist/ox.esm.js']})`); + console.log(` dist/ox.umd.js (${buildInfo.files['dist/ox.umd.js']})`); + console.log(` min/Ox.js (${buildInfo.files['min/Ox.js']})`); +} + +function getFileSize(relativePath) { + const filePath = path.resolve(__dirname, relativePath); + if (fs.existsSync(filePath)) { + const stats = fs.statSync(filePath); + return formatBytes(stats.size); + } + return 'N/A'; +} + +function formatBytes(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; +} + +// Run build +buildOx().catch(error => { + console.error('Build failed:', error); + process.exit(1); +}); \ No newline at end of file diff --git a/scripts/extract-tests.js b/scripts/extract-tests.js index 0f627799..ae5baecf 100644 --- a/scripts/extract-tests.js +++ b/scripts/extract-tests.js @@ -47,10 +47,26 @@ function parseDocComments(source, filename) { */ function parseDocContent(content, filename) { const lines = content.split('\n'); - const firstLine = lines[0].trim(); - const itemMatch = firstLine.match(re.item); - if (!itemMatch) return null; + // Find the first non-empty line that matches the item pattern + let itemMatch = null; + let itemName = 'Unknown'; + + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed) { + itemMatch = trimmed.match(re.item); + if (itemMatch) { + break; + } + } + } + + if (!itemMatch) { + // If no item match, still try to extract tests with a generic name + // This handles cases where tests are in script blocks or without proper headers + itemMatch = ['', filename.replace(/.*\//, '').replace('.js', ''), 'tests', '']; + } const doc = { name: itemMatch[1], diff --git a/src/ox/core/Date.js b/src/ox/core/Date.js new file mode 100644 index 00000000..5605936f --- /dev/null +++ b/src/ox/core/Date.js @@ -0,0 +1,333 @@ +/** + * Date utilities - ES Module Version + */ + +import { isDate, isNumber, isString, isUndefined } from './Type.js'; +import { mod } from './Math.js'; + +/** + * Get the name of the day of the week for a given date + */ +export function getDayName(date, utc) { + const names = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + return names[getDayOfWeek(date, utc)]; +} + +/** + * Get the day of the week (0-6) for a given date + */ +export function getDayOfWeek(date, utc) { + date = makeDate(date); + return utc ? date.getUTCDay() : date.getDay(); +} + +/** + * Get the day of the year (1-366) for a given date + */ +export function getDayOfYear(date, utc) { + date = makeDate(date); + const startOfYear = new Date(Date.UTC( + getFullYear(date, utc), + 0, 1 + )); + const diff = date - startOfYear; + return Math.floor(diff / 86400000) + 1; +} + +/** + * Get the number of days in a month + */ +export function getDaysInMonth(year, month) { + return new Date(year, month + 1, 0).getDate(); +} + +/** + * Get the number of days in a year + */ +export function getDaysInYear(year) { + return isLeapYear(year) ? 366 : 365; +} + +/** + * Get the first day of the week for a given date + */ +export function getFirstDayOfWeek(date, utc) { + date = makeDate(date); + const day = getDayOfWeek(date, utc); + return new Date(date.getTime() - day * 86400000); +} + +/** + * Get the full year from a date + */ +export function getFullYear(date, utc) { + date = makeDate(date); + return utc ? date.getUTCFullYear() : date.getFullYear(); +} + +/** + * Get hours from date + */ +export function getHours(date, utc) { + date = makeDate(date); + return utc ? date.getUTCHours() : date.getHours(); +} + +/** + * Get the ISO date string (YYYY-MM-DD) + */ +export function getISODate(date, utc) { + return formatDate(date, '%Y-%m-%d', utc); +} + +/** + * Get the ISO week number + */ +export function getISOWeek(date, utc) { + date = makeDate(date); + const year = getFullYear(date, utc); + const firstThursday = getFirstThursday(year, utc); + const week = Math.floor((date - firstThursday) / 604800000) + 1; + + if (week < 1) { + return getISOWeek(new Date(year - 1, 11, 31), utc); + } else if (week > 52) { + const nextFirstThursday = getFirstThursday(year + 1, utc); + if (date >= nextFirstThursday) { + return 1; + } + } + + return week; +} + +/** + * Get the ISO year + */ +export function getISOYear(date, utc) { + date = makeDate(date); + const year = getFullYear(date, utc); + const week = getISOWeek(date, utc); + + if (week === 1 && getMonth(date, utc) === 11) { + return year + 1; + } else if (week >= 52 && getMonth(date, utc) === 0) { + return year - 1; + } + + return year; +} + +/** + * Get minutes from date + */ +export function getMinutes(date, utc) { + date = makeDate(date); + return utc ? date.getUTCMinutes() : date.getMinutes(); +} + +/** + * Get month from date (0-11) + */ +export function getMonth(date, utc) { + date = makeDate(date); + return utc ? date.getUTCMonth() : date.getMonth(); +} + +/** + * Get month name + */ +export function getMonthName(date, utc) { + const names = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ]; + return names[getMonth(date, utc)]; +} + +/** + * Get seconds from date + */ +export function getSeconds(date, utc) { + date = makeDate(date); + return utc ? date.getUTCSeconds() : date.getSeconds(); +} + +/** + * Get milliseconds from date + */ +export function getMilliseconds(date, utc) { + date = makeDate(date); + return utc ? date.getUTCMilliseconds() : date.getMilliseconds(); +} + +/** + * Get timezone offset in minutes + */ +export function getTimezoneOffset(date) { + return makeDate(date).getTimezoneOffset(); +} + +/** + * Get timezone offset string (+HH:MM or -HH:MM) + */ +export function getTimezoneOffsetString(date) { + const offset = getTimezoneOffset(date); + const sign = offset <= 0 ? '+' : '-'; + const hours = Math.floor(Math.abs(offset) / 60); + const minutes = Math.abs(offset) % 60; + return sign + pad(hours, 2) + ':' + pad(minutes, 2); +} + +/** + * Get Unix timestamp (seconds since epoch) + */ +export function getUnixTime(date) { + return Math.floor(makeDate(date).getTime() / 1000); +} + +/** + * Get week number (1-53) + */ +export function getWeek(date, utc) { + date = makeDate(date); + const firstDayOfYear = new Date(Date.UTC( + getFullYear(date, utc), 0, 1 + )); + const days = Math.floor((date - firstDayOfYear) / 86400000); + return Math.ceil((days + getDayOfWeek(firstDayOfYear, utc) + 1) / 7); +} + +/** + * Check if a year is a leap year + */ +export function isLeapYear(year) { + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); +} + +/** + * Check if a date is valid + */ +export function isValidDate(date) { + date = makeDate(date); + return !isNaN(date.getTime()); +} + +/** + * Make a date object from various inputs + */ +export function makeDate(date) { + if (isDate(date)) { + return date; + } else if (isString(date)) { + return new Date(date); + } else if (isNumber(date)) { + return new Date(date); + } else if (isUndefined(date)) { + return new Date(); + } + return new Date(date); +} + +/** + * Format a date according to a format string + */ +export function formatDate(date, format, utc) { + date = makeDate(date); + format = format || '%Y-%m-%d %H:%M:%S'; + + const replacements = { + '%a': () => getDayName(date, utc).substr(0, 3), + '%A': () => getDayName(date, utc), + '%b': () => getMonthName(date, utc).substr(0, 3), + '%B': () => getMonthName(date, utc), + '%c': () => date.toLocaleString(), + '%d': () => pad(getDate(date, utc), 2), + '%e': () => pad(getDate(date, utc), 2, ' '), + '%H': () => pad(getHours(date, utc), 2), + '%I': () => pad(((getHours(date, utc) + 11) % 12) + 1, 2), + '%j': () => pad(getDayOfYear(date, utc), 3), + '%k': () => pad(getHours(date, utc), 2, ' '), + '%l': () => pad(((getHours(date, utc) + 11) % 12) + 1, 2, ' '), + '%m': () => pad(getMonth(date, utc) + 1, 2), + '%M': () => pad(getMinutes(date, utc), 2), + '%p': () => getHours(date, utc) < 12 ? 'AM' : 'PM', + '%S': () => pad(getSeconds(date, utc), 2), + '%u': () => getDayOfWeek(date, utc) || 7, + '%U': () => pad(getWeek(date, utc), 2), + '%V': () => pad(getISOWeek(date, utc), 2), + '%w': () => getDayOfWeek(date, utc), + '%W': () => pad(getWeek(date, utc), 2), + '%x': () => date.toLocaleDateString(), + '%X': () => date.toLocaleTimeString(), + '%y': () => pad(getFullYear(date, utc) % 100, 2), + '%Y': () => getFullYear(date, utc), + '%z': () => getTimezoneOffsetString(date), + '%Z': () => '', // Timezone abbreviation not easily available + '%%': () => '%' + }; + + return format.replace(/%[a-zA-Z%]/g, (match) => { + return replacements[match] ? replacements[match]() : match; + }); +} + +/** + * Parse a date string + */ +export function parseDate(string, format, utc) { + // Basic implementation - can be enhanced + return new Date(string); +} + +/** + * Get date (day of month) + */ +export function getDate(date, utc) { + date = makeDate(date); + return utc ? date.getUTCDate() : date.getDate(); +} + +// Helper functions +function getFirstThursday(year, utc) { + const jan1 = new Date(Date.UTC(year, 0, 1)); + const dayOfWeek = getDayOfWeek(jan1, utc); + const daysToThursday = (11 - dayOfWeek) % 7; + return new Date(jan1.getTime() + daysToThursday * 86400000); +} + +function pad(number, length, padding) { + padding = padding || '0'; + const str = String(number); + return padding.repeat(Math.max(0, length - str.length)) + str; +} + +// Export all functions +export default { + getDayName, + getDayOfWeek, + getDayOfYear, + getDaysInMonth, + getDaysInYear, + getFirstDayOfWeek, + getFullYear, + getHours, + getISODate, + getISOWeek, + getISOYear, + getMinutes, + getMonth, + getMonthName, + getSeconds, + getMilliseconds, + getTimezoneOffset, + getTimezoneOffsetString, + getUnixTime, + getWeek, + isLeapYear, + isValidDate, + makeDate, + formatDate, + parseDate, + getDate +}; \ No newline at end of file diff --git a/src/ox/core/stubs.js b/src/ox/core/stubs.js new file mode 100644 index 00000000..2c8abee1 --- /dev/null +++ b/src/ox/core/stubs.js @@ -0,0 +1,57 @@ +/** + * Stub implementations for modules not yet converted + * These will be replaced with full implementations + */ + +// Format utilities stub +export const FormatUtils = { + formatNumber: (n) => n.toString(), + formatDuration: (ms) => `${ms}ms`, + formatBytes: (b) => `${b}B` +}; + +// Color utilities stub +export const ColorUtils = { + rgb: (r, g, b) => `rgb(${r}, ${g}, ${b})`, + hex: (color) => color +}; + +// Encoding utilities stub +export const EncodingUtils = { + encodeBase64: (str) => btoa(str), + decodeBase64: (str) => atob(str) +}; + +// RegExp utilities stub +export const RegExpUtils = { + escape: (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +}; + +// HTML utilities stub +export const HTMLUtils = { + encode: (str) => str.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`), + decode: (str) => str +}; + +// Async utilities stub +export const AsyncUtils = { + sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms)), + series: async (tasks) => { + const results = []; + for (const task of tasks) { + results.push(await task()); + } + return results; + } +}; + +// Geo utilities stub (enhance existing) +export const GeoUtils = { + getDistance: (a, b) => Math.sqrt(Math.pow(b.lat - a.lat, 2) + Math.pow(b.lng - a.lng, 2)) +}; + +// JavaScript utilities stub +export const JavaScriptUtils = { + minify: (code) => code, + tokenize: (code) => [] +}; \ No newline at end of file diff --git a/src/ox/index.js b/src/ox/index.js index 34c747f8..204feee3 100644 --- a/src/ox/index.js +++ b/src/ox/index.js @@ -16,19 +16,14 @@ import * as CollectionUtils from './core/Collection.js'; import * as MathUtils from './core/Math.js'; import * as ObjectUtils from './core/Object.js'; import * as DateUtils from './core/Date.js'; -import * as FormatUtils from './core/Format.js'; -import * as ColorUtils from './core/Color.js'; -import * as EncodingUtils from './core/Encoding.js'; -import * as RegExpUtils from './core/RegExp.js'; -import * as HTMLUtils from './core/HTML.js'; import * as DOMUtils from './core/DOM.js'; import * as RequestUtils from './core/Request.js'; -import * as AsyncUtils from './core/Async.js'; -import * as GeoUtils from './core/Geo.js'; -import * as JavaScriptUtils from './core/JavaScript.js'; import * as LocaleUtils from './core/Locale.js'; import * as Constants from './core/Constants.js'; +// Import stubs for modules not yet converted +import { FormatUtils, ColorUtils, EncodingUtils, RegExpUtils, HTMLUtils, AsyncUtils, GeoUtils, JavaScriptUtils } from './core/stubs.js'; + // Create the main Ox object const Ox = function(value) { return wrap(value); diff --git a/test/test-setup.js b/test/test-setup.js new file mode 100644 index 00000000..e71fa6c8 --- /dev/null +++ b/test/test-setup.js @@ -0,0 +1,22 @@ +/** + * Test setup for running extracted OxJS inline tests + */ + +// Load OxJS ES modules +import Ox from '../src/ox/index.js'; + +// Helper function to evaluate test statements in context +global.evaluateInContext = async function(statement) { + try { + // This will need to be enhanced to handle async tests + // For now, we'll use eval which isn't ideal but matches the original test system + return eval(statement); + } catch (error) { + console.error('Error evaluating:', statement, error); + throw error; + } +}; + +// Make Ox available globally for tests +global.Ox = Ox; +console.log('Test environment setup complete'); diff --git a/vite.config.build.js b/vite.config.build.js new file mode 100644 index 00000000..5f002005 --- /dev/null +++ b/vite.config.build.js @@ -0,0 +1,41 @@ +import { defineConfig } from 'vite'; +import { resolve } from 'path'; +import fs from 'fs'; +import path from 'path'; + +export default defineConfig({ + build: { + lib: { + entry: resolve(__dirname, 'src/ox/index.js'), + name: 'Ox', + formats: ['es', 'umd'], + fileName: (format) => { + if (format === 'es') return 'ox.esm.js'; + if (format === 'umd') return 'ox.umd.js'; + return `ox.${format}.js`; + } + }, + rollupOptions: { + output: { + globals: { + // Any external dependencies would go here + }, + // Keep all exports at top level + preserveModules: false, + // Ensure compatibility with older environments + generatedCode: { + constBindings: false + } + } + }, + sourcemap: true, + minify: false, // We'll minify separately for min/Ox.js + outDir: 'dist', + emptyOutDir: false + }, + resolve: { + alias: { + '@': resolve(__dirname, './src'), + } + } +}); \ No newline at end of file diff --git a/vitest.config.js b/vitest.config.js index 8ebfc305..22c95bab 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -5,7 +5,7 @@ export default defineConfig({ test: { globals: true, environment: 'node', // Use node for now to avoid jsdom issues - setupFiles: './test/setup.js' + setupFiles: './test/test-setup.js' }, resolve: { alias: {