@absolutejs/absolute 0.20.0-beta.3 → 0.20.0-beta.4
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.
- package/dist/angular/components/core/streamingSlotRegistrar.js +1 -1
- package/dist/angular/components/core/streamingSlotRegistry.js +2 -2
- package/dist/build.js.map +1 -1
- package/dist/cli/index.js +1081 -415
- package/dist/index.js.map +1 -1
- package/dist/mobile/index.js +685 -101
- package/dist/mobile/index.js.map +5 -3
- package/dist/mobile/remoteMacAgentEntry.js +29 -0
- package/dist/src/mobile/index.d.ts +1 -0
- package/dist/src/mobile/remoteMacAgent.d.ts +2 -0
- package/dist/src/mobile/remoteMacAgentEntry.d.ts +1 -0
- package/dist/src/mobile/remoteMacProtocol.d.ts +114 -0
- package/dist/src/mobile/remoteMacWire.d.ts +2 -0
- package/package.json +7 -7
package/dist/index.js.map
CHANGED
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
"import { existsSync, readFileSync } from 'node:fs';\nimport { Glob } from 'bun';\nimport { resolve } from 'node:path';\nimport { stripStringsAndComments } from '../utils/stripStringsAndComments';\n\n/* Dependency graph for tracking file relationships\n This handles the \"what depends on what\" problem for incremental HMR */\nexport type DependencyGraph = {\n\t// filePath -> Set of files that depend on this file\n\tdependents: Map<string, Set<string>>;\n\t// filePath -> Set of files this file depends on\n\tdependencies: Map<string, Set<string>>;\n};\n\nexport const emptyDependencyGraph: DependencyGraph = {\n\tdependencies: new Map(),\n\tdependents: new Map()\n};\n\n/* Shared transpiler instance for scanImports(). Bun.Transpiler\n is a native Zig parser — much faster than regex for extracting\n imports from TS/TSX/JS/JSX files. */\nconst tsTranspiler = new Bun.Transpiler({ loader: 'tsx' });\nconst jsTranspiler = new Bun.Transpiler({ loader: 'js' });\n\nconst loaderForFile = (filePath: string) => {\n\tconst lower = filePath.toLowerCase();\n\tif (\n\t\tlower.endsWith('.ts') ||\n\t\tlower.endsWith('.tsx') ||\n\t\tlower.endsWith('.jsx')\n\t)\n\t\treturn 'tsx';\n\tif (lower.endsWith('.js') || lower.endsWith('.mjs')) return 'js';\n\tif (lower.endsWith('.html') || lower.endsWith('.htm')) return 'html';\n\n\treturn null;\n};\n\n/* Resolve relative import paths to absolute paths using existsSync\n instead of readFileSync — avoids reading file content just to check\n existence. */\nconst resolveImportPath = (importPath: string, fromFile: string) => {\n\t// Skip external packages\n\tif (!importPath.startsWith('.') && !importPath.startsWith('/')) {\n\t\treturn null;\n\t}\n\n\tconst fromDir = resolve(fromFile, '..');\n\tconst normalized = resolve(fromDir, importPath);\n\n\t// Try common extensions\n\tconst extensions = [\n\t\t'.ts',\n\t\t'.tsx',\n\t\t'.js',\n\t\t'.jsx',\n\t\t'.vue',\n\t\t'.svelte',\n\t\t'.css',\n\t\t'.html'\n\t];\n\n\tfor (const ext of extensions) {\n\t\tconst withExt = normalized + ext;\n\t\tif (existsSync(withExt)) return withExt;\n\t}\n\n\t// Try without extension (already has one, or is extensionless)\n\tif (existsSync(normalized)) return normalized;\n\n\treturn null;\n};\n\nconst clearExistingDependents = (\n\tgraph: DependencyGraph,\n\tnormalizedPath: string\n) => {\n\tconst existingDeps = graph.dependencies.get(normalizedPath);\n\tif (!existingDeps) return;\n\n\tfor (const dep of existingDeps) {\n\t\tconst dependents = graph.dependents.get(dep);\n\t\tif (!dependents) continue;\n\t\tdependents.delete(normalizedPath);\n\t}\n};\n\n/* Extract import/require statements from a file.\n Uses Bun.Transpiler.scanImports() for JS/TS files (native Zig parser)\n and falls back to regex for HTML (stylesheet links) and .vue/.svelte. */\nexport const addFileToGraph = (graph: DependencyGraph, filePath: string) => {\n\tconst normalizedPath = resolve(filePath);\n\n\tif (!existsSync(normalizedPath)) return;\n\n\tconst dependencies = extractDependencies(normalizedPath);\n\n\tclearExistingDependents(graph, normalizedPath);\n\n\tconst newDeps = new Set(dependencies);\n\tgraph.dependencies.set(normalizedPath, newDeps);\n\n\tconst addDependent = (dep: string) => {\n\t\tif (!graph.dependents.has(dep)) {\n\t\t\tgraph.dependents.set(dep, new Set());\n\t\t}\n\t\tgraph.dependents.get(dep)?.add(normalizedPath);\n\t};\n\n\tdependencies.forEach(addDependent);\n\n\t// Rebuild inverse links. When this file was previously deleted,\n\t// `removeDependentsForFile` cleared the dependents cache for it\n\t// but left other files' deps lists intact (they still reflect\n\t// \"I import this path\"). On (re)create, walk those deps lists and\n\t// re-register this path's dependents so subsequent edits propagate\n\t// correctly. Without this, recreating a deleted file leaves\n\t// importers stuck with stale `?t=` cache-bust queries.\n\tfor (const [otherFile, otherDeps] of graph.dependencies) {\n\t\tif (otherFile === normalizedPath) continue;\n\t\tif (otherDeps.has(normalizedPath)) {\n\t\t\tif (!graph.dependents.has(normalizedPath)) {\n\t\t\t\tgraph.dependents.set(normalizedPath, new Set());\n\t\t\t}\n\t\t\tgraph.dependents.get(normalizedPath)?.add(otherFile);\n\t\t}\n\t}\n};\n\nconst IGNORED_SEGMENTS = [\n\t'/node_modules/',\n\t'/.git/',\n\t'/build/',\n\t'/compiled/',\n\t'/indexes/',\n\t'/server/',\n\t'/client/'\n];\n\nexport const buildInitialDependencyGraph = (\n\tgraph: DependencyGraph,\n\tdirectories: string[]\n) => {\n\t// Use Bun.Glob for fast recursive file scanning, then process\n\t// files in parallel batches. ~50-100ms faster than sync readdirSync.\n\tconst processedFiles = new Set<string>();\n\tconst glob = new Glob('**/*.{ts,tsx,js,jsx,vue,svelte,html,htm}');\n\n\tconst resolvedDirs = directories\n\t\t.map((dir) => resolve(dir))\n\t\t.filter((dir) => existsSync(dir));\n\n\tconst allFiles = resolvedDirs.flatMap((dir) =>\n\t\tArray.from(glob.scanSync({ absolute: true, cwd: dir }))\n\t);\n\n\tfor (const file of allFiles) {\n\t\tconst fullPath = resolve(file);\n\t\tif (IGNORED_SEGMENTS.some((seg) => fullPath.includes(seg))) continue;\n\t\tif (processedFiles.has(fullPath)) continue;\n\n\t\taddFileToGraph(graph, fullPath);\n\t\tprocessedFiles.add(fullPath);\n\t}\n};\n\nconst extractHtmlDependencies = (filePath: string, content: string) => {\n\tconst dependencies: string[] = [];\n\tconst linkRegex =\n\t\t/<link\\s+[^>]*rel=[\"']stylesheet[\"'][^>]*href=[\"']([^\"']+)[\"'][^>]*>/gi;\n\tlet matchLink;\n\twhile ((matchLink = linkRegex.exec(content)) !== null) {\n\t\tconst [, href] = matchLink;\n\t\tif (!href) continue;\n\t\tconst resolvedHref = resolveImportPath(href, filePath);\n\t\tif (resolvedHref) dependencies.push(resolvedHref);\n\t}\n\n\treturn dependencies;\n};\n\nconst resolveRegexMatches = (\n\tregex: RegExp,\n\tcontent: string,\n\tfilePath: string,\n\tdependencies: string[]\n) => {\n\tlet match;\n\twhile ((match = regex.exec(content)) !== null) {\n\t\tif (!match[1]) continue;\n\t\tconst resolved = resolveImportPath(match[1], filePath);\n\t\tif (resolved) dependencies.push(resolved);\n\t}\n};\n\nconst resolveStyleUrls = (\n\tmatchContent: string,\n\tfilePath: string,\n\tdependencies: string[]\n) => {\n\tconst stringLiteralRegex = /['\"]([^'\"]+)['\"]/g;\n\tlet urlMatch;\n\twhile ((urlMatch = stringLiteralRegex.exec(matchContent)) !== null) {\n\t\tif (!urlMatch[1]) continue;\n\t\tconst resolved = resolveImportPath(urlMatch[1], filePath);\n\t\tif (resolved) dependencies.push(resolved);\n\t}\n};\n\nconst extractStyleUrlsDependencies = (\n\tcontent: string,\n\tfilePath: string,\n\tdependencies: string[]\n) => {\n\tconst styleUrlsRegex = /styleUrls\\s*:\\s*\\[([^\\]]*)\\]/g;\n\n\tlet match;\n\twhile ((match = styleUrlsRegex.exec(content)) !== null) {\n\t\tif (!match[1]) continue;\n\t\tresolveStyleUrls(match[1], filePath, dependencies);\n\t}\n};\n\nconst extractAngularDependencies = (\n\tcontent: string,\n\tfilePath: string,\n\tdependencies: string[]\n) => {\n\tconst templateUrlRegex = /templateUrl\\s*:\\s*['\"]([^'\"]+)['\"]/g;\n\tconst styleUrlSingularRegex = /styleUrl\\s*:\\s*['\"]([^'\"]+)['\"]/g;\n\n\tresolveRegexMatches(templateUrlRegex, content, filePath, dependencies);\n\tresolveRegexMatches(styleUrlSingularRegex, content, filePath, dependencies);\n\textractStyleUrlsDependencies(content, filePath, dependencies);\n};\n\nconst extractJsDependencies = (\n\tfilePath: string,\n\tcontent: string,\n\tloader: 'tsx' | 'js'\n) => {\n\tconst transpiler = loader === 'tsx' ? tsTranspiler : jsTranspiler;\n\tconst imports = transpiler.scanImports(content);\n\tconst dependencies: string[] = [];\n\n\tfor (const imp of imports) {\n\t\tconst resolved = resolveImportPath(imp.path, filePath);\n\t\tif (resolved) dependencies.push(resolved);\n\t}\n\n\t// Only treat as Angular when `@Component` survives stripping strings +\n\t// comments — otherwise docs/examples that mention it in text get\n\t// misclassified. The raw check stays as a cheap fast-path.\n\tif (\n\t\tcontent.includes('@Component') &&\n\t\tstripStringsAndComments(content).includes('@Component')\n\t) {\n\t\textractAngularDependencies(content, filePath, dependencies);\n\t}\n\n\treturn dependencies;\n};\n\nconst resolveScannedImports = (\n\timports: ReturnType<typeof tsTranspiler.scanImports>,\n\tfilePath: string,\n\tdependencies: string[]\n) => {\n\tfor (const imp of imports) {\n\t\tconst resolved = resolveImportPath(imp.path, filePath);\n\t\tif (resolved) dependencies.push(resolved);\n\t}\n};\n\nconst extractScriptImports = (\n\tscriptContent: string,\n\tfilePath: string,\n\tdependencies: string[]\n) => {\n\ttry {\n\t\tconst imports = tsTranspiler.scanImports(scriptContent);\n\t\tresolveScannedImports(imports, filePath, dependencies);\n\t} catch {\n\t\t/* ignored */\n\t}\n};\n\nconst extractSvelteVueDependencies = (filePath: string, content: string) => {\n\tconst dependencies: string[] = [];\n\tconst scriptRegex = /<script[^>]*>([\\s\\S]*?)<\\/script>/gi;\n\tlet scriptMatch;\n\twhile ((scriptMatch = scriptRegex.exec(content)) !== null) {\n\t\tconst [, scriptContent] = scriptMatch;\n\t\tif (!scriptContent?.trim()) continue;\n\t\textractScriptImports(scriptContent, filePath, dependencies);\n\t}\n\n\treturn dependencies;\n};\n\nconst extractDependenciesForFile = (filePath: string) => {\n\tconst loader = loaderForFile(filePath);\n\tconst lowerPath = filePath.toLowerCase();\n\tconst isSvelteOrVue =\n\t\tlowerPath.endsWith('.svelte') || lowerPath.endsWith('.vue');\n\n\tif (loader === 'html') {\n\t\tconst content = readFileSync(filePath, 'utf-8');\n\n\t\treturn extractHtmlDependencies(filePath, content);\n\t}\n\n\tif (loader === 'tsx' || loader === 'js') {\n\t\tconst content = readFileSync(filePath, 'utf-8');\n\n\t\treturn extractJsDependencies(filePath, content, loader);\n\t}\n\n\tif (isSvelteOrVue) {\n\t\tconst content = readFileSync(filePath, 'utf-8');\n\n\t\treturn extractSvelteVueDependencies(filePath, content);\n\t}\n\n\treturn [];\n};\n\nexport const extractDependencies = (filePath: string) => {\n\ttry {\n\t\treturn extractDependenciesForFile(filePath);\n\t} catch {\n\t\treturn [];\n\t}\n};\n\nexport const getAffectedFiles = (\n\tgraph: DependencyGraph,\n\tchangedFile: string\n) => {\n\tconst normalizedPath = resolve(changedFile);\n\tconst affected = new Set<string>();\n\tconst toProcess = [normalizedPath];\n\n\tconst processNode = (current: string) => {\n\t\tif (affected.has(current)) return;\n\n\t\taffected.add(current);\n\n\t\tconst dependents = graph.dependents.get(current);\n\t\tif (!dependents) return;\n\n\t\tdependents.forEach((dependent) => toProcess.push(dependent));\n\t};\n\n\twhile (toProcess.length > 0) {\n\t\tconst current = toProcess.pop() ?? normalizedPath;\n\t\tprocessNode(current);\n\t}\n\n\treturn Array.from(affected);\n};\n\nconst removeDepsForFile = (graph: DependencyGraph, normalizedPath: string) => {\n\tconst deps = graph.dependencies.get(normalizedPath);\n\tif (!deps) return;\n\n\tfor (const dep of deps) {\n\t\tconst dependents = graph.dependents.get(dep);\n\t\tif (!dependents) continue;\n\t\tdependents.delete(normalizedPath);\n\t}\n\tgraph.dependencies.delete(normalizedPath);\n};\n\nconst removeDependentsForFile = (\n\tgraph: DependencyGraph,\n\tnormalizedPath: string\n) => {\n\t// Drop the dependents-side cache entry. Do NOT mutate dependents'\n\t// own deps lists — those reflect what their source code imports,\n\t// which doesn't change when the imported file is deleted.\n\t// Preserving them lets `addFileToGraph` re-link on recreate (see\n\t// the inverse-rebuild loop there) so dependent pages auto-recover\n\t// when a file goes missing then comes back.\n\tgraph.dependents.delete(normalizedPath);\n};\n\nexport const removeFileFromGraph = (\n\tgraph: DependencyGraph,\n\tfilePath: string\n) => {\n\tconst normalizedPath = resolve(filePath);\n\n\tremoveDepsForFile(graph, normalizedPath);\n\tremoveDependentsForFile(graph, normalizedPath);\n};\n",
|
|
128
128
|
"/* Module Version Tracker for Server-Client Synchronization\n Tracks module versions to ensure server and client stay in sync */\n\n/* Module version: increments each time a module is updated */\nexport type ModuleVersion = number;\n\n/* Module version map: module path -> version */\nexport type ModuleVersions = Map<string, ModuleVersion>;\n\n/* Global module version counter */\nlet globalVersionCounter = 0;\n\n/* Get next version number */\nexport const createModuleVersionTracker = () =>\n\tnew Map<string, ModuleVersion>();\nexport const getNextVersion = () => ++globalVersionCounter;\nexport const incrementModuleVersion = (\n\tversions: ModuleVersions,\n\tmodulePath: string\n) => {\n\tconst newVersion = getNextVersion();\n\tversions.set(modulePath, newVersion);\n\n\treturn newVersion;\n};\nexport const incrementModuleVersions = (\n\tversions: ModuleVersions,\n\tmodulePaths: string[]\n) => {\n\tconst updated = new Map<string, ModuleVersion>();\n\tfor (const path of modulePaths) {\n\t\tconst version = incrementModuleVersion(versions, path);\n\t\tupdated.set(path, version);\n\t}\n\n\treturn updated;\n};\nexport const serializeModuleVersions = (versions: ModuleVersions) => {\n\tconst serialized: Record<string, number> = {};\n\tfor (const [path, version] of versions.entries()) {\n\t\tserialized[path] = version;\n\t}\n\n\treturn serialized;\n};\n",
|
|
129
129
|
"import { resolve } from 'node:path';\nimport type { BuildConfig } from '../../types/build';\n\n/** Normalize and default build paths so HMR works outside the example app. */\nexport const resolveBuildPaths = (config: BuildConfig) => {\n\tconst cwd = process.cwd();\n\t// Normalize to forward slashes for cross-platform compatibility (Windows uses backslashes)\n\tconst normalize = (path: string) => path.replace(/\\\\/g, '/');\n\tconst withDefault = (value: string | undefined, fallback: string) =>\n\t\tnormalize(resolve(cwd, value ?? fallback));\n\tconst optional = (value: string | undefined) =>\n\t\tvalue ? normalize(resolve(cwd, value)) : undefined;\n\n\treturn {\n\t\tangularDir: optional(config.angularDirectory),\n\t\tassetsDir: optional(config.assetsDirectory),\n\t\tbuildDir: withDefault(config.buildDirectory, 'build'),\n\t\temberDir: optional(config.emberDirectory),\n\t\thtmlDir: optional(config.htmlDirectory),\n\t\thtmxDir: optional(config.htmxDirectory),\n\t\tmobileBundleDir: optional(\n\t\t\tconfig.mobile?.bundleDirectory ??\n\t\t\t\t(config.mobile ? '.absolutejs/mobile/web' : undefined)\n\t\t),\n\t\tmobileNativeDir: optional(\n\t\t\tconfig.mobile?.nativeProject?.directory ??\n\t\t\t\t(config.mobile ? 'mobile' : undefined)\n\t\t),\n\t\tpublicDir: optional(config.publicDirectory),\n\t\treactDir: optional(config.reactDirectory),\n\t\tstylesDir: optional(\n\t\t\ttypeof config.stylesConfig === 'string'\n\t\t\t\t? config.stylesConfig\n\t\t\t\t: config.stylesConfig?.path\n\t\t),\n\t\tsvelteDir: optional(config.svelteDirectory),\n\t\tvueDir: optional(config.vueDirectory)\n\t};\n};\n\nexport type ResolvedBuildPaths = ReturnType<typeof resolveBuildPaths>;\n",
|
|
130
|
-
"import type { FSWatcher } from 'fs';\nimport { emptyDependencyGraph, type DependencyGraph } from './dependencyGraph';\nimport {\n\tcreateModuleVersionTracker,\n\ttype ModuleVersions\n} from './moduleVersionTracker';\nimport type { HMRWebSocket } from '../../types/websocket';\nimport type { HMRClientTarget } from '../../types/messages';\nimport type { BuildConfig, BuildPassError } from '../../types/build';\nimport { resolveBuildPaths, type ResolvedBuildPaths } from './configResolver';\n\ntype HMRUpdateMetadata = {\n\tframework?: string;\n\tpath?: string;\n};\n\n/* This handles the \"tracking clients\" problem */\nexport type HMRState = {\n\tconnectedClients: Set<HMRWebSocket>;\n\tclientTargets: Map<HMRWebSocket, HMRClientTarget>;\n\tactiveFrameworks: Set<string>; // Frameworks with active browser clients\n\tdependencyGraph: DependencyGraph;\n\tisRebuilding: boolean;\n\trebuildQueue: Set<string>;\n\trebuildTimeout: NodeJS.Timeout | null;\n\tfileChangeQueue: Map<string, string[]>;\n\tdebounceTimeout: NodeJS.Timeout | null;\n\tfileHashes: Map<string, number>; // filename -> Bun.hash (Wyhash) value\n\twatchers: FSWatcher[];\n\tmoduleVersions: ModuleVersions; // module path -> version number (for client-server sync)\n\tsourceFileVersions: Map<string, number>; // source file path -> version number (for cache busting)\n\tconfig: BuildConfig; // Build configuration for path resolution\n\tresolvedPaths: ResolvedBuildPaths; // Normalized paths derived from config\n\tvueChangeTypes: Map<string, 'template-only' | 'script' | 'full'>; // Vue HMR change type tracking\n\tassetStore: Map<string, Uint8Array>; // In-memory client asset store for dev mode\n\tmanifest: Record<string, string>; // Current build manifest (for Angular fast-path HMR)\n\trebuildCount: number; // Incremented after each successful rebuild\n\tlastHmrPath?: string;\n\tlastHmrFramework?: string;\n\tlastBroadcastTimestamp: number;\n\thmrUpdates: Map<number, HMRUpdateMetadata>;\n\t// Set captured at the start of each rebuild cycle: the user's actual\n\t// edited files BEFORE the dependency graph adds transitive dependents\n\t// to `filesToRebuild`. Consumed by Angular's HMR classifier so it\n\t// classifies the real edit (e.g. a `.component.css` file) instead of\n\t// a page bundle that the graph dragged in.\n\tlastUserEditedFiles?: Set<string>;\n\t/* Set to `true` by `devBuild` when the initial cold-start\n\t * `build()` throws on a user-source error. The next file change\n\t * routes to a full `build()` instead of the fast-path\n\t * `queueFileChange` so all manifest entries (page, index, CSS,\n\t * vendor) get populated from scratch — the fast-path only\n\t * updates the directly-edited file's entry. Cleared once a\n\t * recovery build succeeds. */\n\tinitialBuildFailed?: boolean;\n\t/* Bundling passes that failed in the most recent dev build. In dev a\n\t * single unresolvable reference degrades to a partial manifest rather\n\t * than aborting the build; the failures land here so a freshly\n\t * connecting browser (cold start) can be shown the same error overlay\n\t * that mid-session `rebuild-error` broadcasts produce. Cleared on the\n\t * next fully-successful rebuild. */\n\tlastBuildErrors?: BuildPassError[];\n\t/* Resolved paths of `.svelte` files the surgical fast path already\n\t * broadcast a `svelte-update` for in the current rebuild cycle.\n\t * Consumed by `handleSvelteHMR` to suppress the redundant\n\t * page-update broadcast (which re-bootstraps the page and discards\n\t * component-local state) for files the fast path already swapped in\n\t * place. Reset at the start of each rebuild. */\n\tsvelteSurgicallyHandled?: Set<string>;\n};\n\n/* Initialize HMR state */\nexport const createHMRState = (config: BuildConfig): HMRState => ({\n\tactiveFrameworks: new Set(), // Frameworks with active browser clients
|
|
130
|
+
"import type { FSWatcher } from 'fs';\nimport { emptyDependencyGraph, type DependencyGraph } from './dependencyGraph';\nimport {\n\tcreateModuleVersionTracker,\n\ttype ModuleVersions\n} from './moduleVersionTracker';\nimport type { HMRWebSocket } from '../../types/websocket';\nimport type { HMRClientTarget } from '../../types/messages';\nimport type { BuildConfig, BuildPassError } from '../../types/build';\nimport { resolveBuildPaths, type ResolvedBuildPaths } from './configResolver';\n\ntype HMRUpdateMetadata = {\n\tframework?: string;\n\tpath?: string;\n};\n\n/* This handles the \"tracking clients\" problem */\nexport type HMRState = {\n\tconnectedClients: Set<HMRWebSocket>;\n\tclientTargets: Map<HMRWebSocket, HMRClientTarget>;\n\tactiveFrameworks: Set<string>; // Frameworks with active browser clients\n\tdependencyGraph: DependencyGraph;\n\tisRebuilding: boolean;\n\trebuildQueue: Set<string>;\n\trebuildTimeout: NodeJS.Timeout | null;\n\tfileChangeQueue: Map<string, string[]>;\n\tdebounceTimeout: NodeJS.Timeout | null;\n\tfileHashes: Map<string, number>; // filename -> Bun.hash (Wyhash) value\n\twatchers: FSWatcher[];\n\tmoduleVersions: ModuleVersions; // module path -> version number (for client-server sync)\n\tsourceFileVersions: Map<string, number>; // source file path -> version number (for cache busting)\n\tconfig: BuildConfig; // Build configuration for path resolution\n\tresolvedPaths: ResolvedBuildPaths; // Normalized paths derived from config\n\tvueChangeTypes: Map<string, 'template-only' | 'script' | 'full'>; // Vue HMR change type tracking\n\tassetStore: Map<string, Uint8Array>; // In-memory client asset store for dev mode\n\tmanifest: Record<string, string>; // Current build manifest (for Angular fast-path HMR)\n\trebuildCount: number; // Incremented after each successful rebuild\n\tlastHmrPath?: string;\n\tlastHmrFramework?: string;\n\tlastBroadcastTimestamp: number;\n\thmrUpdates: Map<number, HMRUpdateMetadata>;\n\t// Set captured at the start of each rebuild cycle: the user's actual\n\t// edited files BEFORE the dependency graph adds transitive dependents\n\t// to `filesToRebuild`. Consumed by Angular's HMR classifier so it\n\t// classifies the real edit (e.g. a `.component.css` file) instead of\n\t// a page bundle that the graph dragged in.\n\tlastUserEditedFiles?: Set<string>;\n\t/* Set to `true` by `devBuild` when the initial cold-start\n\t * `build()` throws on a user-source error. The next file change\n\t * routes to a full `build()` instead of the fast-path\n\t * `queueFileChange` so all manifest entries (page, index, CSS,\n\t * vendor) get populated from scratch — the fast-path only\n\t * updates the directly-edited file's entry. Cleared once a\n\t * recovery build succeeds. */\n\tinitialBuildFailed?: boolean;\n\t/* Bundling passes that failed in the most recent dev build. In dev a\n\t * single unresolvable reference degrades to a partial manifest rather\n\t * than aborting the build; the failures land here so a freshly\n\t * connecting browser (cold start) can be shown the same error overlay\n\t * that mid-session `rebuild-error` broadcasts produce. Cleared on the\n\t * next fully-successful rebuild. */\n\tlastBuildErrors?: BuildPassError[];\n\t/* Resolved paths of `.svelte` files the surgical fast path already\n\t * broadcast a `svelte-update` for in the current rebuild cycle.\n\t * Consumed by `handleSvelteHMR` to suppress the redundant\n\t * page-update broadcast (which re-bootstraps the page and discards\n\t * component-local state) for files the fast path already swapped in\n\t * place. Reset at the start of each rebuild. */\n\tsvelteSurgicallyHandled?: Set<string>;\n};\n\n/* Initialize HMR state */\nexport const createHMRState = (config: BuildConfig): HMRState => ({\n\tactiveFrameworks: new Set(), // Frameworks with active browser clients\n\tassetStore: new Map(), // In-memory client asset store for dev mode\n\tclientTargets: new Map<HMRWebSocket, HMRClientTarget>(),\n\tconfig,\n\tconnectedClients: new Set<HMRWebSocket>(),\n\tdebounceTimeout: null,\n\tdependencyGraph: emptyDependencyGraph,\n\tfileChangeQueue: new Map(),\n\tfileHashes: new Map(),\n\thmrUpdates: new Map(),\n\tisRebuilding: false,\n\tlastBroadcastTimestamp: 0,\n\tmanifest: {}, // Current build manifest (populated after initial build)\n\tmoduleVersions: createModuleVersionTracker(),\n\trebuildCount: 0,\n\trebuildQueue: new Set(),\n\trebuildTimeout: null,\n\tresolvedPaths: resolveBuildPaths(config), // Track versions for source files to bypass Bun's cache\n\tsourceFileVersions: new Map(),\n\tvueChangeTypes: new Map(), // Vue HMR change type tracking\n\twatchers: []\n});\n\n/* Increment version for a source file (forces Bun to treat it as a new module) */\nexport const incrementSourceFileVersion = (\n\tstate: HMRState,\n\tfilePath: string\n) => {\n\tconst currentVersion = state.sourceFileVersions.get(filePath) || 0;\n\tconst newVersion = currentVersion + 1;\n\tstate.sourceFileVersions.set(filePath, newVersion);\n\n\treturn newVersion;\n};\n\n/* Increment versions for multiple source files */\nexport const incrementSourceFileVersions = (\n\tstate: HMRState,\n\tfilePaths: string[]\n) => {\n\tfor (const filePath of filePaths) {\n\t\tincrementSourceFileVersion(state, filePath);\n\t}\n};\n",
|
|
131
131
|
"import { type Dirent, existsSync, readdirSync, readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { BuildConfig } from '../../types/build';\nimport { normalizePath } from '../utils/normalizePath';\nimport type { ResolvedBuildPaths } from './configResolver';\n\nconst STYLE_EXTENSION_PATTERN = /\\.(css|s[ac]ss|less|styl(?:us)?)$/i;\n\n/* Get the directories we should watch based on our config\n This handles the \"where to watch\" problem */\nexport const detectFramework = (\n\tfilePath: string,\n\tresolved?: ResolvedBuildPaths\n) => {\n\t// Check if this is an ignored file first\n\tif (shouldIgnorePath(filePath, resolved)) {\n\t\treturn 'ignored';\n\t}\n\n\tconst normalized = filePath.replace(/\\\\/g, '/');\n\n\tconst startsWithDir = (dir?: string) =>\n\t\tdir ? normalized.startsWith(dir.replace(/\\\\/g, '/')) : false;\n\n\t// Prefer resolved directory prefixes when available\n\tif (resolved) {\n\t\tif (startsWithDir(resolved.stylesDir)) return 'styles';\n\t\tif (startsWithDir(resolved.htmxDir)) return 'htmx';\n\t\tif (startsWithDir(resolved.reactDir)) return 'react';\n\t\tif (startsWithDir(resolved.svelteDir)) return 'svelte';\n\t\tif (startsWithDir(resolved.vueDir)) return 'vue';\n\t\tif (startsWithDir(resolved.angularDir)) return 'angular';\n\t\tif (startsWithDir(resolved.emberDir)) return 'ember';\n\t\tif (startsWithDir(resolved.htmlDir)) return 'html';\n\t\tif (startsWithDir(resolved.assetsDir)) return 'assets';\n\t} else {\n\t\t// Fallback heuristics when resolved paths are not provided\n\t\tif (normalized.includes('/htmx/')) return 'htmx';\n\t\tif (normalized.includes('/react/')) return 'react';\n\t\tif (normalized.includes('/svelte/')) return 'svelte';\n\t\tif (normalized.includes('/vue/')) return 'vue';\n\t\tif (normalized.includes('/angular/')) return 'angular';\n\t\tif (normalized.includes('/ember/')) return 'ember';\n\t\tif (normalized.includes('/html/')) return 'html';\n\t}\n\n\t// Then check file extensions for files not in framework directories\n\tif (normalized.endsWith('.tsx') || normalized.endsWith('.jsx'))\n\t\treturn 'react';\n\tif (normalized.endsWith('.svelte')) return 'svelte';\n\tif (normalized.endsWith('.vue')) return 'vue';\n\tif (normalized.endsWith('.gjs') || normalized.endsWith('.gts'))\n\t\treturn 'ember';\n\tif (normalized.endsWith('.html')) return 'html';\n\tif (normalized.endsWith('.ts') && normalized.includes('angular'))\n\t\treturn 'angular';\n\tif (normalized.endsWith('.ts') && normalized.includes('ember'))\n\t\treturn 'ember';\n\n\t// Generic assets (styles in root /assets/, images, etc.)\n\tif (normalized.includes('/assets/')) return 'assets';\n\n\t// For style files not caught by framework directory checks, check one more time\n\tif (STYLE_EXTENSION_PATTERN.test(normalized)) {\n\t\tif (normalized.includes('/vue/') || normalized.includes('/vue-'))\n\t\t\treturn 'vue';\n\t\tif (normalized.includes('/svelte/') || normalized.includes('/svelte-'))\n\t\t\treturn 'svelte';\n\t\tif (normalized.includes('/react/') || normalized.includes('/react-'))\n\t\t\treturn 'react';\n\t\tif (\n\t\t\tnormalized.includes('/angular/') ||\n\t\t\tnormalized.includes('/angular-')\n\t\t)\n\t\t\treturn 'angular';\n\t\tif (normalized.includes('/html/') || normalized.includes('/html-'))\n\t\t\treturn 'html';\n\t\tif (normalized.includes('/htmx/') || normalized.includes('/htmx-'))\n\t\t\treturn 'htmx';\n\n\t\treturn 'assets';\n\t}\n\n\treturn 'unknown';\n};\n\n/** Resolve every directory the watcher is allowed to walk into. The\n * returned set is an absolute, normalized include-list — anything\n * outside it is implicitly ignored. This replaces the old approach\n * of listing the *whole* project root and filtering with an exclude\n * pattern, which silently caught (and re-built on) files inside\n * framework-managed paths like `<frameworkDir>/generated/`,\n * `.absolutejs/`, and the build directory. */\n/* Walk `<angularDir>/**` for `*.component.ts` files and return the\n * unique parent directories of any `templateUrl` / `styleUrl` /\n * `styleUrls` reference that resolves OUTSIDE `angularDir`.\n *\n * Without this, components like\n * `@Component({ styleUrl: '../../styles/foo.css', ... })`\n * never get re-bundled on CSS edits because the CSS file lives at\n * `example/styles/` but the watcher's positive roots are\n * `example/angular/` (recursive) + `<stylesConfig>` (scoped to\n * global stylesheet indexes, NOT per-component CSS). The dep graph\n * already records the styleUrl link, so the rebuild trigger does\n * fire correctly once the watcher reports the event — the gap is\n * purely \"is this dir watched?\".\n *\n * Cheap to do once at startup: a small angular project has <100\n * `.component.ts` files, each ~1ms to read+regex-scan. */\nconst collectAngularResourceDirs = (angularDir: string) => {\n\tconst out = new Set<string>();\n\tconst angularRoot = resolve(angularDir);\n\tconst angularRootNormalized = normalizePath(angularRoot);\n\n\tconst walk = (dir: string) => {\n\t\tlet entries: Dirent[];\n\t\ttry {\n\t\t\tentries = readdirSync(dir, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.name.startsWith('.') || entry.name === 'node_modules') {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst full = resolve(dir, entry.name);\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\twalk(full);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!entry.isFile() || !entry.name.endsWith('.component.ts')) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet source: string;\n\t\t\ttry {\n\t\t\t\tsource = readFileSync(full, 'utf8');\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst refs: string[] = [];\n\t\t\tconst tplRe = /templateUrl\\s*:\\s*['\"]([^'\"]+)['\"]/g;\n\t\t\tconst styleRe = /styleUrl\\s*:\\s*['\"]([^'\"]+)['\"]/g;\n\t\t\tconst stylesArrRe = /styleUrls\\s*:\\s*\\[([^\\]]*)\\]/g;\n\t\t\tconst literalRe = /['\"]([^'\"]+)['\"]/g;\n\t\t\tlet match: RegExpExecArray | null;\n\t\t\twhile ((match = tplRe.exec(source)) !== null) {\n\t\t\t\tif (match[1]) refs.push(match[1]);\n\t\t\t}\n\t\t\twhile ((match = styleRe.exec(source)) !== null) {\n\t\t\t\tif (match[1]) refs.push(match[1]);\n\t\t\t}\n\t\t\twhile ((match = stylesArrRe.exec(source)) !== null) {\n\t\t\t\tconst [inner] = match.slice(1);\n\t\t\t\tif (!inner) continue;\n\t\t\t\tlet strMatch: RegExpExecArray | null;\n\t\t\t\tconst innerRe = new RegExp(literalRe.source, literalRe.flags);\n\t\t\t\twhile ((strMatch = innerRe.exec(inner)) !== null) {\n\t\t\t\t\tif (strMatch[1]) refs.push(strMatch[1]);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst componentDir = dirname(full);\n\t\t\tfor (const ref of refs) {\n\t\t\t\tconst refAbs = normalizePath(resolve(componentDir, ref));\n\t\t\t\tconst refDir = normalizePath(dirname(refAbs));\n\t\t\t\t// Skip if already under angularDir (recursive watch covers it).\n\t\t\t\tif (\n\t\t\t\t\trefDir === angularRootNormalized ||\n\t\t\t\t\trefDir.startsWith(`${angularRootNormalized}/`)\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tout.add(refDir);\n\t\t\t}\n\t\t}\n\t};\n\n\twalk(angularRoot);\n\n\treturn Array.from(out);\n};\n\nconst collectPositiveWatchRoots = (\n\tconfig: BuildConfig,\n\tresolved?: ResolvedBuildPaths\n) => {\n\tconst cwd = process.cwd();\n\tconst roots: string[] = [];\n\tconst push = (path: string | undefined) => {\n\t\tif (!path) return;\n\t\tconst abs = normalizePath(resolve(cwd, path));\n\t\tif (!roots.includes(abs)) roots.push(abs);\n\t};\n\n\tconst cfg = resolved ?? {\n\t\tangularDir: config.angularDirectory,\n\t\tassetsDir: config.assetsDirectory,\n\t\temberDir: config.emberDirectory,\n\t\thtmlDir: config.htmlDirectory,\n\t\thtmxDir: config.htmxDirectory,\n\t\treactDir: config.reactDirectory,\n\t\tstylesDir:\n\t\t\ttypeof config.stylesConfig === 'string'\n\t\t\t\t? config.stylesConfig\n\t\t\t\t: config.stylesConfig?.path,\n\t\tsvelteDir: config.svelteDirectory,\n\t\tvueDir: config.vueDirectory\n\t};\n\n\t// Configured framework directories.\n\tpush(cfg.reactDir);\n\tpush(cfg.svelteDir);\n\tpush(cfg.vueDir);\n\tpush(cfg.emberDir);\n\tpush(cfg.angularDir);\n\tpush(cfg.htmlDir);\n\tpush(cfg.htmxDir);\n\tpush(cfg.assetsDir);\n\tpush(cfg.stylesDir);\n\n\t// Common shared-source directories. We only include them when they\n\t// actually exist on disk so missing dirs don't pollute the watcher\n\t// or short-circuit shouldIgnorePath checks. These are the canonical\n\t// places framework-agnostic source lives in real projects.\n\tfor (const candidate of ['src', 'db', 'assets', 'styles']) {\n\t\tconst abs = normalizePath(resolve(cwd, candidate));\n\t\tif (existsSync(abs) && !roots.includes(abs)) roots.push(abs);\n\t}\n\n\t// Cover the rest: any other directory at the project root that\n\t// isn't ignored. This catches helpers under `utils/`, `lib/`,\n\t// `shared/`, `config/`, `core/`, or any other non-canonical name\n\t// the user picked, without hardcoding a list. `shouldIgnorePath`\n\t// already gates against `node_modules`, `build`, `.absolutejs`,\n\t// `.git`, etc.; we additionally skip dot-directories and the\n\t// already-included framework roots.\n\ttry {\n\t\tconst entries = readdirSync(cwd, { withFileTypes: true });\n\t\tfor (const entry of entries) {\n\t\t\tif (!entry.isDirectory()) continue;\n\t\t\tif (entry.name.startsWith('.')) continue;\n\t\t\tconst abs = normalizePath(resolve(cwd, entry.name));\n\t\t\tif (roots.includes(abs)) continue;\n\t\t\tif (shouldIgnorePath(abs, resolved)) continue;\n\t\t\troots.push(abs);\n\t\t}\n\t} catch {\n\t\t// Best-effort — fall back to the canonical list above if\n\t\t// the project root isn't readable for some reason.\n\t}\n\n\t// User-supplied extra dirs from absolute.config.ts → dev.watchDirs.\n\tconst extraDirs = config.dev?.watchDirs ?? [];\n\tfor (const dir of extraDirs) push(dir);\n\n\t// Angular component resource dirs (templateUrl / styleUrl pointing\n\t// outside angularDir). See `collectAngularResourceDirs` above.\n\tif (cfg.angularDir) {\n\t\tconst resourceDirs = collectAngularResourceDirs(cfg.angularDir);\n\t\tfor (const dir of resourceDirs) {\n\t\t\tif (!roots.includes(dir)) roots.push(dir);\n\t\t}\n\t}\n\n\treturn roots;\n};\n\nexport const getWatchPaths = (\n\tconfig: BuildConfig,\n\tresolved?: ResolvedBuildPaths\n) => {\n\tconst roots = collectPositiveWatchRoots(config, resolved);\n\tconst paths: string[] = [];\n\tconst push = (base: string | undefined, sub?: string) => {\n\t\tif (!base) return;\n\t\tconst normalizedBase = normalizePath(base);\n\t\tpaths.push(sub ? `${normalizedBase}/${sub}` : normalizedBase);\n\t};\n\n\tconst cfg = resolved ?? {\n\t\thtmlDir: config.htmlDirectory,\n\t\thtmxDir: config.htmxDirectory\n\t};\n\n\t// HTML/HTMX dirs traditionally watch only specific subpaths to avoid\n\t// noise from co-located fixtures. Preserve that behavior.\n\tif (cfg.htmlDir) {\n\t\tpush(cfg.htmlDir, 'pages');\n\t\tpush(cfg.htmlDir, 'scripts');\n\t\tpush(cfg.htmlDir, 'styles');\n\t}\n\tif (cfg.htmxDir) {\n\t\tpush(cfg.htmxDir, 'pages');\n\t\tpush(cfg.htmxDir, 'scripts');\n\t\tpush(cfg.htmxDir, 'styles');\n\t}\n\n\t// Everything else: watch the directory itself. shouldIgnorePath\n\t// guards against any framework-managed children (build/, generated/,\n\t// .absolutejs/, etc).\n\tfor (const root of roots) {\n\t\tif (root === normalizePath(cfg.htmlDir ?? '')) continue;\n\t\tif (root === normalizePath(cfg.htmxDir ?? '')) continue;\n\t\tpaths.push(root);\n\t}\n\n\treturn paths;\n};\n\n/** Hard-deny segments that ALWAYS get ignored, even inside a watched\n * positive root. These are the build/output paths that AbsoluteJS\n * itself writes into — feeding their events back into the watcher\n * causes the rebuild thrash. */\nconst HARD_DENY_PATTERN =\n\t/(^|\\/)(build|generated|compiled|indexes|\\.absolutejs|node_modules|\\.git|\\.test-builds|dist)(\\/|$)/;\n\n/** A path is ignored when it is NOT inside any of the configured\n * positive watch roots, OR when it falls inside a hard-denied\n * build/output subtree. The styles directory is always allowed. */\nexport const shouldIgnorePath = (\n\tpath: string,\n\tresolved?: ResolvedBuildPaths\n) => {\n\tconst normalized = path.replace(/\\\\/g, '/');\n\n\tif (resolved?.stylesDir) {\n\t\tconst styles = normalized.startsWith(\n\t\t\tresolved.stylesDir.replace(/\\\\/g, '/')\n\t\t);\n\t\tif (styles) return false;\n\t}\n\tconst isInside = (root: string | undefined) => {\n\t\tif (!root) return false;\n\t\tconst normalizedRoot = root.replace(/\\\\/g, '/').replace(/\\/$/, '');\n\n\t\treturn (\n\t\t\tnormalized === normalizedRoot ||\n\t\t\tnormalized.startsWith(`${normalizedRoot}/`)\n\t\t);\n\t};\n\tif (\n\t\tisInside(resolved?.buildDir) ||\n\t\tisInside(resolved?.mobileBundleDir) ||\n\t\tisInside(resolved?.mobileNativeDir)\n\t) {\n\t\treturn true;\n\t}\n\n\tif (HARD_DENY_PATTERN.test(normalized)) return true;\n\tif (normalized.endsWith('.log')) return true;\n\tif (normalized.endsWith('.tmp')) return true;\n\tif (normalized.endsWith('~')) return true;\n\n\treturn false;\n};\n",
|
|
132
132
|
"import { watch } from 'fs';\nimport { existsSync, readdirSync, statSync } from 'node:fs';\nimport { dirname, join, resolve } from 'path';\nimport type { BuildConfig } from '../../types/build';\nimport { sendTelemetryEvent } from '../cli/telemetryEvent';\nimport type { HMRState } from './clientManager';\nimport { addFileToGraph, removeFileFromGraph } from './dependencyGraph';\nimport { getWatchPaths, shouldIgnorePath } from './pathUtils';\n\nconst safeRemoveFromGraph = (\n\tgraph: HMRState['dependencyGraph'],\n\tfullPath: string\n) => {\n\ttry {\n\t\tremoveFileFromGraph(graph, fullPath);\n\t} catch (err) {\n\t\tsendTelemetryEvent('hmr:graph-error', {\n\t\t\tmessage: err instanceof Error ? err.message : String(err),\n\t\t\toperation: 'remove'\n\t\t});\n\t}\n};\n\nconst safeAddToGraph = (\n\tgraph: HMRState['dependencyGraph'],\n\tfullPath: string\n) => {\n\ttry {\n\t\taddFileToGraph(graph, fullPath);\n\t} catch (err) {\n\t\tsendTelemetryEvent('hmr:graph-error', {\n\t\t\tmessage: err instanceof Error ? err.message : String(err),\n\t\t\toperation: 'add'\n\t\t});\n\t}\n};\n\n// Atomic-write temp files created by editors mid-save. These exist for\n// milliseconds before being renamed over the real target — firing the HMR\n// pipeline on them either no-ops (if the temp has no dependents) or\n// emits a spurious `[abs:restart]` marker that triggers a full restart\n// when the actual edit could have been handled in-place.\nconst ATOMIC_WRITE_TEMP_PATTERNS = [\n\t// sed -i: `sed[A-Za-z0-9]+`, no extension\n\t/(^|\\/)sed[A-Za-z0-9]{6,}$/,\n\t// vim's \"4913\" probe file used to test write permissions\n\t/(^|\\/)4913$/\n];\n\nconst shouldSkipFilename = (filename: string, isStylesDir: boolean) =>\n\t(!isStylesDir &&\n\t\t(filename === 'compiled' ||\n\t\t\tfilename === 'generated' ||\n\t\t\tfilename === 'build' ||\n\t\t\tfilename === 'indexes' ||\n\t\t\tfilename === 'server' ||\n\t\t\tfilename === 'client' ||\n\t\t\tfilename.includes('/compiled/') ||\n\t\t\tfilename.includes('/generated/') ||\n\t\t\tfilename.includes('/build/') ||\n\t\t\tfilename.includes('/indexes/') ||\n\t\t\tfilename.includes('/server/') ||\n\t\t\tfilename.includes('/client/') ||\n\t\t\tfilename.startsWith('compiled/') ||\n\t\t\tfilename.startsWith('generated/') ||\n\t\t\tfilename.startsWith('build/') ||\n\t\t\tfilename.startsWith('indexes/') ||\n\t\t\tfilename.startsWith('server/') ||\n\t\t\tfilename.startsWith('client/'))) ||\n\tfilename.endsWith('/') ||\n\tfilename.includes('.tmp.') ||\n\tfilename.endsWith('.tmp') ||\n\tfilename.endsWith('~') ||\n\tfilename.startsWith('.#') ||\n\tfilename.startsWith('.absolutejs-hmr-') ||\n\tATOMIC_WRITE_TEMP_PATTERNS.some((pattern) => pattern.test(filename));\n\nconst setupWatcher = (\n\tabsolutePath: string,\n\tisStylesDir: boolean,\n\tstate: HMRState,\n\tonFileChange: (filePath: string) => void\n) => {\n\t// Atomic-write recovery scan. Linux/Node `fs.watch(recursive: true)`\n\t// reliably delivers IN_MOVED_FROM for the temp filename in an atomic\n\t// rename (sed -i, vim default, prettier, etc.) but drops IN_MOVED_TO\n\t// for the destination when the destination already existed in the\n\t// watched dir. Without recovery, every editor save to an existing\n\t// source file is invisible to the framework's HMR pipeline.\n\t//\n\t// When we observe a temp-file rename event we walk the same parent\n\t// dir for files whose ctime is fresh (last 1s), and synthesize an\n\t// onFileChange for each. The temp file itself is filtered upstream;\n\t// dir entries we already track separately (recursive watch will\n\t// surface them through their own events) are safely deduplicated by\n\t// queueFileChange's content hashes. Do not time-deduplicate by path here:\n\t// two real saves can land within the same rebuild window.\n\t// Watch callbacks share the dev-server event loop with framework\n\t// compilation. A large rebuild can delay delivery well beyond one second;\n\t// content-hash deduplication in queueFileChange makes the wider scan safe.\n\tconst ATOMIC_RECOVERY_WINDOW_MS = 5000;\n\tconst atomicRecoveryScan = (eventDir: string) => {\n\t\tlet entries: string[];\n\t\ttry {\n\t\t\tentries = readdirSync(eventDir);\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tconst now = Date.now();\n\t\tfor (const name of entries) {\n\t\t\tif (shouldSkipFilename(name, isStylesDir)) continue;\n\t\t\tconst child = join(eventDir, name).replace(/\\\\/g, '/');\n\t\t\tlet st: ReturnType<typeof statSync>;\n\t\t\ttry {\n\t\t\t\tst = statSync(child);\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!st.isFile()) continue;\n\t\t\tconst age = now - st.ctimeMs;\n\t\t\tif (age < 0 || age > ATOMIC_RECOVERY_WINDOW_MS) continue;\n\t\t\tonFileChange(child);\n\t\t\tsafeAddToGraph(state.dependencyGraph, child);\n\t\t}\n\t};\n\n\tconst watcher = watch(\n\t\tabsolutePath,\n\t\t{ recursive: true },\n\t\t(event, filename) => {\n\t\t\tif (!filename) return;\n\t\t\tif (shouldSkipFilename(filename, isStylesDir)) {\n\t\t\t\tif (event === 'rename') {\n\t\t\t\t\tconst eventDir = dirname(\n\t\t\t\t\t\tjoin(absolutePath, filename)\n\t\t\t\t\t).replace(/\\\\/g, '/');\n\t\t\t\t\tatomicRecoveryScan(eventDir);\n\t\t\t\t\t// IN_MOVED_FROM can be delivered before the matching\n\t\t\t\t\t// destination rename is visible. Recheck after two short\n\t\t\t\t\t// filesystem turns; queueFileChange content-hash dedupes\n\t\t\t\t\t// the common case where the immediate scan already won.\n\t\t\t\t\tfor (const delay of [25, 100]) {\n\t\t\t\t\t\tconst timer = setTimeout(\n\t\t\t\t\t\t\t() => atomicRecoveryScan(eventDir),\n\t\t\t\t\t\t\tdelay\n\t\t\t\t\t\t);\n\t\t\t\t\t\ttimer.unref();\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst fullPath = join(absolutePath, filename).replace(/\\\\/g, '/');\n\n\t\t\tif (shouldIgnorePath(fullPath, state.resolvedPaths)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (event === 'rename' && !existsSync(fullPath)) {\n\t\t\t\tsafeRemoveFromGraph(state.dependencyGraph, fullPath);\n\t\t\t\tonFileChange(fullPath);\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (existsSync(fullPath)) {\n\t\t\t\tonFileChange(fullPath);\n\t\t\t\tsafeAddToGraph(state.dependencyGraph, fullPath);\n\t\t\t}\n\t\t}\n\t);\n\n\tstate.watchers.push(watcher);\n};\n\n/* Add file watchers for specific paths (used when new framework directories are added at runtime) */\nexport const addFileWatchers = (\n\tstate: HMRState,\n\tpaths: string[],\n\tonFileChange: (filePath: string) => void\n) => {\n\tconst stylesDir = state.resolvedPaths?.stylesDir;\n\n\tpaths.forEach((path) => {\n\t\tconst absolutePath = resolve(path).replace(/\\\\/g, '/');\n\t\tif (!existsSync(absolutePath)) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst isStylesDir = Boolean(\n\t\t\tstylesDir && absolutePath.startsWith(stylesDir)\n\t\t);\n\t\tsetupWatcher(absolutePath, isStylesDir, state, onFileChange);\n\t});\n};\n\n/* Set up file watching for all configured directories\n This handles the \"watch files\" problem */\nexport const startFileWatching = (\n\tstate: HMRState,\n\tconfig: BuildConfig,\n\tonFileChange: (filePath: string) => void\n) => {\n\tconst watchPaths = getWatchPaths(config, state.resolvedPaths);\n\tconst stylesDir = state.resolvedPaths?.stylesDir;\n\n\twatchPaths.forEach((path) => {\n\t\tconst absolutePath = resolve(path).replace(/\\\\/g, '/');\n\t\tif (!existsSync(absolutePath)) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst isStylesDir = Boolean(\n\t\t\tstylesDir && absolutePath.startsWith(stylesDir)\n\t\t);\n\t\tsetupWatcher(absolutePath, isStylesDir, state, onFileChange);\n\t});\n};\n",
|
|
133
133
|
"import { resolve } from 'node:path';\nimport { readdir, unlink } from 'node:fs/promises';\n\nconst mimeTypes: Record<string, string> = {\n\t'.avif': 'image/avif',\n\t'.bmp': 'image/bmp',\n\t'.css': 'text/css',\n\t'.eot': 'application/vnd.ms-fontobject',\n\t'.gif': 'image/gif',\n\t'.html': 'text/html',\n\t'.ico': 'image/x-icon',\n\t'.jpeg': 'image/jpeg',\n\t'.jpg': 'image/jpeg',\n\t'.js': 'application/javascript',\n\t'.json': 'application/json',\n\t'.map': 'application/json',\n\t'.mjs': 'application/javascript',\n\t'.otf': 'font/otf',\n\t'.png': 'image/png',\n\t'.svg': 'image/svg+xml',\n\t'.ttf': 'font/ttf',\n\t'.txt': 'text/plain',\n\t'.wasm': 'application/wasm',\n\t'.webp': 'image/webp',\n\t'.woff': 'font/woff',\n\t'.woff2': 'font/woff2',\n\t'.xml': 'application/xml'\n};\n\n/** Determine Content-Type from a file path extension */\nexport const getMimeType = (filePath: string) => {\n\tconst ext = filePath.slice(filePath.lastIndexOf('.'));\n\n\treturn mimeTypes[ext] ?? 'application/octet-stream';\n};\n\n/** Matches Bun's hashed output naming: name.XXXXXXXX.ext */\nconst HASHED_FILE_RE = /\\.[a-z0-9]{8}\\.(js|css|mjs)$/;\n\n/** Strip the 8-char hash from a hashed path to get its logical identity.\n * e.g. /react/indexes/ReactExample.abc12345.js → /react/indexes/ReactExample.js */\nconst stripHash = (webPath: string) =>\n\twebPath.replace(/\\.[a-z0-9]{8}(\\.(js|css|mjs))$/, '$1');\n\nconst processWalkEntry = (\n\tentry: import('node:fs').Dirent,\n\tdir: string,\n\tliveByIdentity: Map<string, string>,\n\twalkAndClean: (dir: string) => Promise<void>\n) => {\n\tconst fullPath = resolve(dir, entry.name);\n\tif (entry.isDirectory()) {\n\t\treturn walkAndClean(fullPath);\n\t}\n\tif (!HASHED_FILE_RE.test(entry.name)) {\n\t\treturn null;\n\t}\n\tconst identity = stripHash(fullPath);\n\tconst livePath = liveByIdentity.get(identity);\n\t// Delete if: (a) no live entry exists (page was deleted), or\n\t// (b) a different hash is live (stale version of an existing page).\n\t// Only keep the file when it IS the current live version.\n\tif (livePath !== fullPath) {\n\t\treturn unlink(fullPath).catch(() => {\n\t\t\t/* noop */\n\t\t});\n\t}\n\n\treturn null;\n};\n\n/** Upsert build outputs into the in-memory asset store.\n * Evicts previous entries for the same logical asset (same base name,\n * different hash) so stale paths don't accumulate. */\nexport const cleanStaleAssets = async (\n\tstore: Map<string, Uint8Array>,\n\tmanifest: Record<string, string>,\n\tbuildDir: string\n) => {\n\t// Build a map of logical identity → live disk path\n\tconst liveByIdentity = new Map<string, string>();\n\n\t// Client assets from the in-memory store\n\tfor (const webPath of store.keys()) {\n\t\tconst diskPath = resolve(buildDir, webPath.slice(1));\n\t\tliveByIdentity.set(stripHash(diskPath), diskPath);\n\t}\n\n\t// SSR server files from the manifest (absolute disk paths like\n\t// /home/.../build/svelte/.../SvelteExample.hash.js)\n\tconst absBuildDir = resolve(buildDir);\n\tObject.values(manifest).forEach((val) => {\n\t\tif (!HASHED_FILE_RE.test(val)) return;\n\t\tif (val.startsWith(absBuildDir)) {\n\t\t\tliveByIdentity.set(stripHash(val), val);\n\t\t}\n\t});\n\n\ttry {\n\t\tconst walkAndClean = async (dir: string) => {\n\t\t\tconst entries = await readdir(dir, { withFileTypes: true });\n\t\t\tconst tasks = entries\n\t\t\t\t.map((entry) =>\n\t\t\t\t\tprocessWalkEntry(entry, dir, liveByIdentity, walkAndClean)\n\t\t\t\t)\n\t\t\t\t.filter((task): task is Promise<void> => task !== null);\n\t\t\tawait Promise.all(tasks);\n\t\t};\n\t\tawait walkAndClean(buildDir);\n\t} catch {\n\t\t/* buildDir may not exist */\n\t}\n};\nexport const lookupAsset = (store: Map<string, Uint8Array>, path: string) =>\n\tstore.get(path);\n\nconst processScanEntry = (\n\tentry: import('node:fs').Dirent,\n\tdir: string,\n\tprefix: string,\n\tstore: Map<string, Uint8Array>,\n\tscanDir: (dir: string, prefix: string) => Promise<void>\n) => {\n\tif (entry.isDirectory()) {\n\t\treturn scanDir(resolve(dir, entry.name), `${prefix}${entry.name}/`);\n\t}\n\tif (!entry.name.startsWith('chunk-')) {\n\t\treturn null;\n\t}\n\tconst webPath = `/${prefix}${entry.name}`;\n\tif (store.has(webPath)) {\n\t\treturn null;\n\t}\n\n\treturn Bun.file(resolve(dir, entry.name))\n\t\t.bytes()\n\t\t.then((bytes) => {\n\t\t\tstore.set(webPath, bytes);\n\n\t\t\treturn undefined;\n\t\t})\n\t\t.catch(() => {\n\t\t\t/* noop */\n\t\t});\n};\n\nexport const populateAssetStore = async (\n\tstore: Map<string, Uint8Array>,\n\tmanifest: Record<string, string>,\n\tbuildDir: string\n) => {\n\tconst loadPromises: Promise<void>[] = [];\n\n\t// Build a set of logical identities from the new manifest so we can\n\t// evict old entries with different hashes for the same asset.\n\tconst newIdentities = new Map<string, string>();\n\tfor (const webPath of Object.values(manifest)) {\n\t\tif (!webPath.startsWith('/')) continue;\n\t\tnewIdentities.set(stripHash(webPath), webPath);\n\t}\n\n\t// Evict old store entries that are being replaced by a new hash (same\n\t// identity, different path). The manifest passed in may be PARTIAL — it\n\t// only covers the entries from the current build (e.g. one page during\n\t// HMR fast-paths). Entries whose identity isn't mentioned must be left\n\t// alone: they may belong to other pages that weren't rebuilt, and their\n\t// SSR-rendered HTML still references those hashes.\n\t// Disk-level cleanup of pages that were truly deleted is handled by\n\t// `cleanStaleAssets`. Chunk files (chunk-XXXX.js) are tracked separately\n\t// and are not part of the manifest.\n\tconst staleKeys = [...store.keys()].filter((existingPath) => {\n\t\tif (existingPath.includes('/chunk-')) return false;\n\t\tconst replacement = newIdentities.get(stripHash(existingPath));\n\n\t\treturn replacement !== undefined && replacement !== existingPath;\n\t});\n\tstaleKeys.forEach((key) => store.delete(key));\n\n\tfor (const webPath of newIdentities.values()) {\n\t\t// Skip entries already in the store — their content hasn't changed\n\t\t// (same hash in the filename). Only load new or replaced assets.\n\t\tif (store.has(webPath)) continue;\n\n\t\tloadPromises.push(\n\t\t\tBun.file(resolve(buildDir, webPath.slice(1)))\n\t\t\t\t.bytes()\n\t\t\t\t.then((bytes) => {\n\t\t\t\t\tstore.set(webPath, bytes);\n\n\t\t\t\t\treturn undefined;\n\t\t\t\t})\n\t\t\t\t.catch(() => {\n\t\t\t\t\t/* file may not exist yet (SSR-only entry) — ignore */\n\t\t\t\t})\n\t\t);\n\t}\n\n\t// Also pick up chunk files produced by Bun code-splitting that aren't\n\t// listed in the manifest (e.g. chunk-XXXX.js).\n\ttry {\n\t\tconst scanDir = async (dir: string, prefix: string) => {\n\t\t\tconst entries = await readdir(dir, { withFileTypes: true });\n\t\t\tconst subTasks = entries\n\t\t\t\t.map((entry) =>\n\t\t\t\t\tprocessScanEntry(entry, dir, prefix, store, scanDir)\n\t\t\t\t)\n\t\t\t\t.filter((task): task is Promise<void> => task !== null);\n\t\t\tawait Promise.all(subTasks);\n\t\t};\n\t\tawait scanDir(buildDir, '');\n\t} catch {\n\t\t/* buildDir may not exist yet */\n\t}\n\n\tawait Promise.all(loadPromises);\n};\n",
|