@autometa/playwright-loader 1.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/loader.ts","../src/bridge-generator.ts"],"names":["existsSync","dirname","require","resolve"],"mappings":"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","sourcesContent":["/**\n * Node.js Module Loader Hooks for .feature files.\n *\n * These hooks intercept imports of .feature files and transform them into\n * executable Playwright test modules on-the-fly.\n *\n * @see https://nodejs.org/api/module.html#customization-hooks\n */\n\nimport { readFileSync, existsSync } from \"node:fs\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { resolve as resolvePath, dirname } from \"node:path\";\n\nimport { generateBridgeCode } from \"./bridge-generator.js\";\n\ninterface ResolveContext {\n conditions: string[];\n importAttributes: Record<string, string>;\n parentURL?: string;\n}\n\ninterface ResolveResult {\n format?: string;\n shortCircuit?: boolean;\n url: string;\n}\n\ntype NextResolve = (\n specifier: string,\n context: ResolveContext\n) => Promise<ResolveResult>;\n\ninterface LoadContext {\n conditions: string[];\n format?: string;\n importAttributes: Record<string, string>;\n}\n\ninterface LoadResult {\n format: string;\n shortCircuit?: boolean;\n source: string | ArrayBuffer | SharedArrayBuffer;\n}\n\ntype NextLoad = (url: string, context: LoadContext) => Promise<LoadResult>;\n\n/**\n * Resolve hook - intercepts .feature file specifiers.\n *\n * When a .feature file is imported, we resolve it ourselves since\n * Node.js doesn't know how to handle .feature files.\n */\nexport async function resolve(\n specifier: string,\n context: ResolveContext,\n nextResolve: NextResolve\n): Promise<ResolveResult> {\n // Check if this is a .feature file import\n if (specifier.endsWith(\".feature\")) {\n // Resolve the path ourselves since Node.js doesn't handle .feature files\n let resolvedPath: string;\n\n if (specifier.startsWith(\"file://\")) {\n // Already a file URL\n resolvedPath = fileURLToPath(specifier);\n } else if (specifier.startsWith(\"/\")) {\n // Absolute path\n resolvedPath = specifier;\n } else if (specifier.startsWith(\".\")) {\n // Relative path - resolve from parent\n if (context.parentURL) {\n const parentPath = fileURLToPath(context.parentURL);\n const parentDir = dirname(parentPath);\n resolvedPath = resolvePath(parentDir, specifier);\n } else {\n resolvedPath = resolvePath(process.cwd(), specifier);\n }\n } else {\n // Bare specifier - try from cwd\n resolvedPath = resolvePath(process.cwd(), specifier);\n }\n\n // Verify the file exists\n if (!existsSync(resolvedPath)) {\n throw new Error(`Cannot find .feature file: ${resolvedPath}`);\n }\n\n return {\n url: pathToFileURL(resolvedPath).href,\n format: \"autometa-feature\",\n shortCircuit: true,\n };\n }\n\n // Not a .feature file, use default resolution\n return nextResolve(specifier, context);\n}\n\n/**\n * Load hook - transforms .feature files into Playwright test modules.\n *\n * When a file with format \"autometa-feature\" is loaded, we:\n * 1. Read the .feature file content\n * 2. Generate bridge code that imports Playwright and executes the feature\n * 3. Return the generated code as an ESM module\n */\nexport async function load(\n url: string,\n context: LoadContext,\n nextLoad: NextLoad\n): Promise<LoadResult> {\n // Check if this is a feature file we need to transform\n if (context.format === \"autometa-feature\") {\n const filePath = fileURLToPath(url);\n const featureContent = readFileSync(filePath, \"utf-8\");\n\n // Use process.cwd() as the project root for package resolution.\n // This should be the directory where Playwright is running from,\n // which has the necessary dependencies installed.\n const projectRoot = process.cwd();\n\n // Generate Playwright bridge code\n const bridgeCode = generateBridgeCode(filePath, featureContent, projectRoot);\n\n return {\n format: \"module\",\n shortCircuit: true,\n source: bridgeCode,\n };\n }\n\n // Not a feature file, use default loading\n return nextLoad(url, context);\n}\n","/**\n * Bridge Code Generator for Playwright.\n *\n * Generates JavaScript/TypeScript code that transforms a Gherkin feature file\n * into a Playwright test suite using `test.describe` and `test()`.\n *\n * This is heavily based on the vitest-plugins transform but adapted for\n * Playwright's test runner and Node.js module loader hooks.\n */\n\nimport { dirname, extname, isAbsolute, resolve } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport type { Config } from \"@autometa/config\";\nimport jiti from \"jiti\";\nimport fg from \"fast-glob\";\n\nconst STEP_FALLBACK_GLOB = \"**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}\";\n\nexport interface BridgeGeneratorOptions {\n /** The absolute path to the .feature file */\n featurePath: string;\n /** The raw content of the .feature file */\n featureContent: string;\n /** The resolved autometa config (optional, will be loaded if not provided) */\n config?: Config;\n /** The path to the autometa config file */\n configPath?: string;\n /** The project root directory */\n projectRoot?: string;\n}\n\n/**\n * Resolve a package to its absolute file path.\n * This is needed because the generated code runs from the .feature file's location\n * which may not have access to node_modules.\n */\nfunction resolvePackage(packageName: string, fromDir: string): string {\n try {\n const require = createRequire(resolve(fromDir, \"package.json\"));\n const resolved = require.resolve(packageName);\n \n // Check if we need to use ESM version\n // If the resolved path ends with .js, check if there's an .mjs version\n if (resolved.endsWith(\".js\") || resolved.endsWith(\"/index.js\")) {\n const mjs = resolved.replace(/\\.js$/, \".mjs\");\n if (existsSync(mjs)) {\n return mjs;\n }\n // Also check for index.mjs in the same directory\n const indexMjs = resolved.replace(/index\\.js$/, \"index.mjs\");\n if (indexMjs !== mjs && existsSync(indexMjs)) {\n return indexMjs;\n }\n }\n \n return resolved;\n } catch {\n // Fallback to bare specifier if resolution fails\n return packageName;\n }\n}\n\n/**\n * Generate Playwright bridge code for a .feature file.\n *\n * The generated code:\n * 1. Imports Playwright's test runner and Autometa executors\n * 2. Uses glob imports to load step definition modules\n * 3. Parses the feature file at runtime\n * 4. Coordinates the runner and executes tests\n *\n * @param featurePath - Absolute path to the .feature file\n * @param featureContent - Raw content of the .feature file\n * @param runtimeProjectRoot - The project root where packages should be resolved from (usually process.cwd())\n * @returns Generated ESM module code\n */\nexport function generateBridgeCode(\n featurePath: string,\n featureContent: string,\n runtimeProjectRoot?: string\n): string {\n // Use runtime project root if provided, otherwise try to find it from feature path\n const projectRoot = runtimeProjectRoot ?? findProjectRoot(dirname(featurePath));\n\n // Find and load the autometa config to get step roots\n const { configPath, stepRoots, events } = loadConfigSync(projectRoot);\n const configDir = configPath ? dirname(configPath) : projectRoot;\n\n // Build step file patterns for dynamic import\n const stepPatterns = buildStepPatterns(stepRoots, {\n configDir,\n projectRoot,\n });\n\n const eventPatterns = buildStepPatterns(events, {\n configDir,\n projectRoot,\n });\n\n const eventImports = generateEventImports(eventPatterns);\n\n // Generate import statements for step modules\n const stepImports = generateStepImports(stepPatterns, projectRoot);\n\n // Resolve package paths from project root so imports work from any location\n const playwrightPath = resolvePackage(\"@playwright/test\", projectRoot);\n const executorPath = resolvePackage(\"@autometa/playwright-executor\", projectRoot);\n const runnerPath = resolvePackage(\"@autometa/runner\", projectRoot);\n const gherkinPath = resolvePackage(\"@autometa/gherkin\", projectRoot);\n\n // Log the step patterns for debugging\n const debugInfo = JSON.stringify({\n projectRoot,\n configPath,\n stepRoots,\n stepPatterns,\n }, null, 2);\n\n // Generate the bridge code that Playwright will execute\n return `\nimport { test } from ${JSON.stringify(playwrightPath)};\nimport { execute } from ${JSON.stringify(executorPath)};\nimport { coordinateRunnerFeature, CucumberRunner } from ${JSON.stringify(runnerPath)};\nimport { parseGherkin } from ${JSON.stringify(gherkinPath)};\n\nconst debugFlagValue = typeof process !== 'undefined'\n ? (process.env.AUTOMETA_BRIDGE_DEBUG ?? process.env.AUTOMETA_DEBUG ?? '')\n : '';\nconst debugEnabled = (() => {\n if (!debugFlagValue) {\n return false;\n }\n const normalized = String(debugFlagValue).trim().toLowerCase();\n return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';\n})();\nconst debugLog = (...args) => {\n if (!debugEnabled) {\n return;\n }\n console.log(...args);\n};\n\n// Debug: Step discovery configuration\ndebugLog(\"[Autometa Bridge] Step discovery info:\", ${JSON.stringify(debugInfo)});\n\n// Dynamic imports for event listener modules (side effects)\n${eventImports}\n\n// Dynamic imports for step definition modules\n${stepImports}\n\nfunction collectCandidateModules(imported) {\n if (!imported || typeof imported !== 'object') {\n return [];\n }\n\n const modules = new Set();\n modules.add(imported);\n\n const exportedModules = imported.modules;\n if (Array.isArray(exportedModules)) {\n for (const entry of exportedModules) {\n if (entry && typeof entry === 'object') {\n modules.add(entry);\n }\n }\n }\n\n const defaultExport = imported.default;\n if (Array.isArray(defaultExport)) {\n for (const entry of defaultExport) {\n if (entry && typeof entry === 'object') {\n modules.add(entry);\n }\n }\n } else if (defaultExport && typeof defaultExport === 'object') {\n modules.add(defaultExport);\n }\n\n return Array.from(modules);\n}\n\nfunction isStepsEnvironment(candidate) {\n return Boolean(\n candidate &&\n typeof candidate === 'object' &&\n typeof candidate.coordinateFeature === 'function' &&\n typeof candidate.getPlan === 'function' &&\n typeof candidate.Given === 'function' &&\n typeof candidate.When === 'function' &&\n typeof candidate.Then === 'function'\n );\n}\n\nfunction extractStepsEnvironment(candidate) {\n if (!candidate || typeof candidate !== 'object') {\n return undefined;\n }\n\n if (isStepsEnvironment(candidate)) {\n return candidate;\n }\n\n const stepsEnv = candidate.stepsEnvironment;\n if (isStepsEnvironment(stepsEnv)) {\n return stepsEnv;\n }\n\n const defaultExport = candidate.default;\n if (isStepsEnvironment(defaultExport)) {\n return defaultExport;\n }\n\n return undefined;\n}\n\nfunction resolveStepsEnvironment(modules) {\n for (const moduleExports of Object.values(modules)) {\n for (const candidate of collectCandidateModules(moduleExports)) {\n const environment = extractStepsEnvironment(candidate);\n if (environment) {\n return environment;\n }\n }\n }\n return undefined;\n}\n\nfunction isScenario(element) {\n return Boolean(\n element &&\n typeof element === 'object' &&\n 'steps' in element &&\n !('exampleGroups' in element) &&\n !('elements' in element)\n );\n}\n\nfunction isScenarioOutline(element) {\n return Boolean(\n element &&\n typeof element === 'object' &&\n 'steps' in element &&\n 'exampleGroups' in element\n );\n}\n\nfunction isRule(element) {\n return Boolean(\n element &&\n typeof element === 'object' &&\n 'elements' in element &&\n Array.isArray(element.elements)\n );\n}\n\nfunction createFeatureScopePlan(feature, basePlan) {\n const allSteps = Array.from(basePlan.stepsById.values());\n const featureChildren = [];\n const scopesById = new Map(basePlan.scopesById);\n\n for (const element of feature.elements ?? []) {\n if (isScenario(element) || isScenarioOutline(element)) {\n const scenarioScope = {\n id: element.id ?? element.name,\n kind: isScenarioOutline(element) ? 'scenarioOutline' : 'scenario',\n name: element.name,\n mode: 'default',\n tags: element.tags ?? [],\n steps: allSteps,\n hooks: [],\n children: [],\n pending: false,\n };\n featureChildren.push(scenarioScope);\n scopesById.set(scenarioScope.id, scenarioScope);\n continue;\n }\n\n if (isRule(element)) {\n const ruleChildren = [];\n for (const ruleElement of element.elements ?? []) {\n if (isScenario(ruleElement) || isScenarioOutline(ruleElement)) {\n const scenarioScope = {\n id: ruleElement.id ?? ruleElement.name,\n kind: isScenarioOutline(ruleElement) ? 'scenarioOutline' : 'scenario',\n name: ruleElement.name,\n mode: 'default',\n tags: ruleElement.tags ?? [],\n steps: allSteps,\n hooks: [],\n children: [],\n pending: false,\n };\n ruleChildren.push(scenarioScope);\n scopesById.set(scenarioScope.id, scenarioScope);\n }\n }\n\n const ruleScope = {\n id: element.id ?? element.name,\n kind: 'rule',\n name: element.name,\n mode: 'default',\n tags: element.tags ?? [],\n steps: allSteps,\n hooks: [],\n children: ruleChildren,\n pending: false,\n };\n featureChildren.push(ruleScope);\n scopesById.set(ruleScope.id, ruleScope);\n }\n }\n\n const featureScope = {\n id: feature.uri ?? feature.name,\n kind: 'feature',\n name: feature.name,\n mode: 'default',\n tags: feature.tags ?? [],\n steps: allSteps,\n hooks: [],\n children: featureChildren,\n pending: false,\n };\n\n const existingRoot = basePlan.root;\n const updatedRoot = {\n ...existingRoot,\n children: [...existingRoot.children, featureScope],\n };\n\n scopesById.set(featureScope.id, featureScope);\n scopesById.set(updatedRoot.id, updatedRoot);\n\n const scopePlan = {\n root: updatedRoot,\n stepsById: basePlan.stepsById,\n hooksById: basePlan.hooksById,\n scopesById,\n };\n\n if (basePlan.worldFactory) {\n scopePlan.worldFactory = basePlan.worldFactory;\n }\n\n if (basePlan.parameterRegistry) {\n scopePlan.parameterRegistry = basePlan.parameterRegistry;\n }\n\n return scopePlan;\n}\n\nconst gherkin = ${JSON.stringify(featureContent)};\nconst feature = parseGherkin(gherkin);\nawait loadEventModules();\nconst stepModules = await loadStepModules();\nconst steps = resolveStepsEnvironment(stepModules);\n\nif (!steps) {\n throw new Error(\n 'Autometa could not find an exported steps environment for the configured step roots. ' +\n 'Export your runner environment as \"stepsEnvironment\" or default.'\n );\n}\n\n// Debug: Check the steps environment\ndebugLog(\"[Autometa Bridge] Steps environment found:\", {\n hasGiven: typeof steps.Given === 'function',\n hasWhen: typeof steps.When === 'function', \n hasThen: typeof steps.Then === 'function',\n hasGetPlan: typeof steps.getPlan === 'function',\n});\n\nCucumberRunner.setSteps(steps);\n\nconst runtimeConfig = steps.getConfig?.() ?? {\n test: {},\n shim: {},\n globals: {}\n};\n\n// Debug: Check the plan\nconst basePlan = steps.getPlan();\nconst paramRegistry = basePlan.parameterRegistry;\nconst actualRegistry = paramRegistry?.registry ?? paramRegistry;\nconst paramTypeNames = actualRegistry?.parameterTypes \n ? Array.from(actualRegistry.parameterTypes).map(pt => pt.name)\n : [];\ndebugLog(\"[Autometa Bridge] Base plan:\", {\n stepsCount: basePlan.stepsById?.size ?? 0,\n hooksCount: basePlan.hooksById?.size ?? 0,\n hasParameterRegistry: Boolean(paramRegistry),\n parameterTypes: paramTypeNames,\n hasHttpMethod: actualRegistry?.lookupByTypeName?.('httpMethod') !== undefined,\n stepIds: Array.from(basePlan.stepsById?.keys() ?? []).slice(0, 5),\n});\n\n// Debug: Check step module contents\ndebugLog(\"[Autometa Bridge] Step modules keys:\", Object.keys(stepModules));\nconst firstModKey = Object.keys(stepModules)[0];\nif (firstModKey) {\n const firstMod = stepModules[firstModKey];\n debugLog(\"[Autometa Bridge] First module exports:\", Object.keys(firstMod ?? {}));\n}\n\ntest.describe(feature.name, () => {\n const scopedPlan = createFeatureScopePlan(feature, basePlan);\n \n // Debug: Check the scoped plan\n const scopedRegistry = scopedPlan.parameterRegistry?.registry ?? scopedPlan.parameterRegistry;\n debugLog(\"[Autometa Bridge] Scoped plan:\", {\n hasParameterRegistry: Boolean(scopedPlan.parameterRegistry),\n hasHttpMethod: scopedRegistry?.lookupByTypeName?.('httpMethod') !== undefined,\n rootChildren: scopedPlan.root?.children?.length ?? 0,\n stepsCount: scopedPlan.stepsById?.size ?? 0,\n });\n\n // Debug: Feature name matching\n debugLog(\"[Autometa Bridge] Feature matching:\", {\n gherkinName: feature.name,\n featureScopeNames: scopedPlan.root?.children?.filter(c => c.kind === 'feature').map(c => c.name) ?? [],\n });\n \n const { plan, adapter } = coordinateRunnerFeature({\n feature,\n environment: steps,\n config: runtimeConfig,\n plan: scopedPlan\n });\n\n // Debug: Check what adapter sees\n const adapterRegistry = adapter.getParameterRegistry?.();\n const actualAdapterReg = adapterRegistry?.registry ?? adapterRegistry;\n debugLog(\"[Autometa Bridge] Adapter:\", {\n hasParameterRegistry: Boolean(adapterRegistry),\n hasHttpMethod: actualAdapterReg?.lookupByTypeName?.('httpMethod') !== undefined,\n });\n\n execute({ plan, adapter, config: runtimeConfig });\n});\n`;\n}\n\n/**\n * Find the project root by looking for package.json\n */\nfunction findProjectRoot(startDir: string): string {\n let dir = startDir;\n while (dir !== \"/\") {\n if (existsSync(resolve(dir, \"package.json\"))) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return startDir;\n}\n\n/**\n * Synchronously load autometa config to find step roots.\n * Note: This is synchronous because Node.js loader hooks are synchronous.\n */\nfunction loadConfigSync(\n root: string\n): { configPath: string | undefined; stepRoots: string[]; events: string[] } {\n const candidates = [\n \"autometa.config.ts\",\n \"autometa.config.js\",\n \"autometa.config.mts\",\n \"autometa.config.mjs\",\n \"autometa.config.cts\",\n \"autometa.config.cjs\",\n ];\n\n const _jiti = jiti(root, { interopDefault: true });\n\n\tfor (const candidate of candidates) {\n\t\tconst configPath = resolve(root, candidate);\n\t\tif (!existsSync(configPath)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst mod = _jiti(configPath) as unknown;\n\t\tconst config =\n\t\t\tmod && typeof mod === \"object\" && \"default\" in mod\n\t\t\t\t? (mod as { default?: unknown }).default ?? mod\n\t\t\t\t: mod;\n\n\t\tif (!isConfig(config)) {\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to load Autometa config from \"${configPath}\". ` +\n\t\t\t\t\t'Ensure the module exports a Config instance (e.g. export default defineConfig({...})).'\n );\n }\n\n const resolved = config.resolve();\n const stepRoots = resolved.config?.roots?.steps ?? [];\n const events = resolved.config?.events ?? [];\n\n return {\n configPath,\n stepRoots: Array.isArray(stepRoots) && stepRoots.length > 0\n ? stepRoots\n : findDefaultStepRoots(root),\n events: Array.isArray(events) ? events : [],\n };\n }\n\n return { configPath: undefined, stepRoots: findDefaultStepRoots(root), events: [] };\n}\n\nfunction isConfig(config: unknown): config is Config {\n return (\n typeof config === \"object\" &&\n config !== null &&\n \"resolve\" in config &&\n typeof (config as Config).resolve === \"function\" &&\n \"current\" in config &&\n typeof (config as Config).current === \"function\"\n );\n}\n\n/**\n * Find default step definition directories.\n */\nfunction findDefaultStepRoots(root: string): string[] {\n const candidates = [\n \"src/steps\",\n \"src/step-definitions\",\n \"steps\",\n \"step-definitions\",\n \"src\",\n ];\n\n const found: string[] = [];\n for (const candidate of candidates) {\n const path = resolve(root, candidate);\n if (existsSync(path)) {\n found.push(candidate);\n break; // Use first match\n }\n }\n\n // Also check for a main step-definitions.ts file\n const stepDefFile = resolve(root, \"src/step-definitions.ts\");\n if (existsSync(stepDefFile)) {\n found.push(\"src/step-definitions.ts\");\n }\n\n return found.length > 0 ? found : [\"src\"];\n}\n\ninterface StepPatternOptions {\n readonly configDir: string;\n readonly projectRoot: string;\n}\n\n/**\n * Build step file patterns from step roots.\n */\nfunction buildStepPatterns(\n entries: readonly string[],\n options: StepPatternOptions\n): string[] {\n if (!entries || entries.length === 0) {\n return [];\n }\n\n const patterns = new Set<string>();\n for (const entry of entries) {\n const normalized = entry.trim();\n if (!normalized) {\n continue;\n }\n\n for (const candidate of toPatterns(normalized, STEP_FALLBACK_GLOB)) {\n const absolute = isAbsolute(candidate)\n ? normalizeSlashes(candidate)\n : normalizeSlashes(resolve(options.configDir, candidate));\n\n // For Playwright loader, we need actual file paths not globs\n // Resolve the glob to actual files\n const files = resolveGlobToFiles(absolute, options.projectRoot);\n for (const file of files) {\n patterns.add(file);\n }\n }\n }\n\n return Array.from(patterns);\n}\n\n/**\n * Resolve a glob pattern to actual file paths.\n */\nfunction resolveGlobToFiles(pattern: string, projectRoot: string): string[] {\n // If it's a specific file (not a glob pattern), just return it if it exists.\n const isSpecificFile = hasFileExtension(pattern) && !hasGlobMagic(pattern);\n if (isSpecificFile) {\n return existsSync(pattern) ? [pattern] : [];\n }\n\n const matches = fg.sync(pattern, {\n cwd: projectRoot,\n absolute: true,\n onlyFiles: true,\n unique: true,\n followSymbolicLinks: true,\n ignore: [\n \"**/node_modules/**\",\n \"**/*.test.*\",\n \"**/*.spec.*\",\n ],\n }) as string[];\n\n return matches.sort((a, b) => a.localeCompare(b));\n}\n\nfunction toPatterns(entry: string, fallbackGlob: string): string[] {\n if (hasGlobMagic(entry) || hasFileExtension(entry)) {\n return [entry];\n }\n return [appendGlob(entry, fallbackGlob)];\n}\n\nfunction hasGlobMagic(input: string): boolean {\n return /[*?{}()[\\]!,@+]/u.test(input);\n}\n\nfunction hasFileExtension(input: string): boolean {\n const normalized = normalizeSlashes(input);\n const trimmed =\n normalized === \"/\" ? normalized : normalized.replace(/\\/+$/u, \"\");\n if (!trimmed || trimmed === \".\" || trimmed === \"..\") {\n return false;\n }\n return Boolean(extname(trimmed));\n}\n\nfunction appendGlob(entry: string, glob: string): string {\n const normalized = normalizeSlashes(entry);\n const trimmed =\n normalized === \"/\" ? normalized : normalized.replace(/\\/+$/u, \"\");\n if (!trimmed || trimmed === \".\") {\n return glob;\n }\n if (trimmed === \"/\") {\n return `/${glob}`;\n }\n return `${trimmed}/${glob}`;\n}\n\nfunction normalizeSlashes(pathname: string): string {\n return pathname.replace(/\\\\/gu, \"/\");\n}\n\n/**\n * Generate dynamic import statements for step modules.\n */\nfunction generateStepImports(\n stepPatterns: string[],\n _projectRoot: string\n): string {\n if (stepPatterns.length === 0) {\n return `\nasync function loadStepModules() {\n return {};\n}\n`;\n }\n\n // Sort patterns to ensure main step-definitions files are loaded first\n // Files named \"step-definitions.ts\" or similar should come first\n // Then index files, then everything else\n const sortedPatterns = [...stepPatterns].sort((a, b) => {\n const aName = a.toLowerCase();\n const bName = b.toLowerCase();\n \n // Main step definition files first\n const aIsMain = aName.includes('step-definitions.ts') || aName.includes('step-definitions.js');\n const bIsMain = bName.includes('step-definitions.ts') || bName.includes('step-definitions.js');\n if (aIsMain && !bIsMain) return -1;\n if (!aIsMain && bIsMain) return 1;\n \n // Index files next\n const aIsIndex = aName.endsWith('/index.ts') || aName.endsWith('/index.js');\n const bIsIndex = bName.endsWith('/index.ts') || bName.endsWith('/index.js');\n if (aIsIndex && !bIsIndex) return -1;\n if (!aIsIndex && bIsIndex) return 1;\n \n return a.localeCompare(b);\n });\n\n const imports = sortedPatterns\n .map((file, index) => {\n const importPath = normalizeSlashes(file);\n return ` const mod${index} = await import(${JSON.stringify(importPath)});`;\n })\n .join(\"\\n\");\n\n const moduleMap = sortedPatterns\n .map((file, index) => {\n const key = normalizeSlashes(file);\n return ` ${JSON.stringify(key)}: mod${index},`;\n })\n .join(\"\\n\");\n\n return `\nasync function loadStepModules() {\n${imports}\n return {\n${moduleMap}\n };\n}\n`;\n}\n\nfunction generateEventImports(eventPatterns: string[]): string {\n if (eventPatterns.length === 0) {\n return `\nasync function loadEventModules() {\n return;\n}\n`;\n }\n\n const imports = eventPatterns\n .map((file) => {\n const importPath = normalizeSlashes(file);\n return ` await import(${JSON.stringify(importPath)});`;\n })\n .join(\"\\n\");\n\n return `\nasync function loadEventModules() {\n${imports}\n}\n`;\n}\n\n/**\n * Escape a string for use in generated code.\n */\nexport function escapeString(str: string): string {\n return str\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/'/g, \"\\\\'\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, \"\\\\n\")\n .replace(/\\r/g, \"\\\\r\")\n .replace(/\\t/g, \"\\\\t\");\n}\n"]}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @autometa/playwright-loader
3
+ *
4
+ * Node.js module loader hooks for transforming .feature files into Playwright test suites.
5
+ *
6
+ * This package provides the infrastructure to run Gherkin feature files directly
7
+ * with Playwright, leveraging Node.js customization hooks to transform .feature
8
+ * imports on-the-fly.
9
+ *
10
+ * ## Usage
11
+ *
12
+ * ### Option 1: Import in playwright.config.ts
13
+ * ```typescript
14
+ * import '@autometa/playwright-loader/register';
15
+ * import { defineConfig } from '@playwright/test';
16
+ *
17
+ * export default defineConfig({
18
+ * testMatch: '**\/*.feature',
19
+ * });
20
+ * ```
21
+ *
22
+ * ### Option 2: Use with NODE_OPTIONS
23
+ * ```bash
24
+ * NODE_OPTIONS="--import @autometa/playwright-loader/register" npx playwright test
25
+ * ```
26
+ *
27
+ * @packageDocumentation
28
+ */
29
+ export { resolve, load } from "./loader.js";
30
+ export { generateBridgeCode } from "./bridge-generator.js";