portapack 0.2.1

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.
Files changed (76) hide show
  1. package/.eslintrc.json +9 -0
  2. package/.github/workflows/ci.yml +73 -0
  3. package/.github/workflows/deploy-pages.yml +56 -0
  4. package/.prettierrc +9 -0
  5. package/.releaserc.js +29 -0
  6. package/CHANGELOG.md +21 -0
  7. package/README.md +288 -0
  8. package/commitlint.config.js +36 -0
  9. package/dist/cli/cli-entry.js +1694 -0
  10. package/dist/cli/cli-entry.js.map +1 -0
  11. package/dist/index.d.ts +275 -0
  12. package/dist/index.js +1405 -0
  13. package/dist/index.js.map +1 -0
  14. package/docs/.vitepress/config.ts +89 -0
  15. package/docs/.vitepress/sidebar-generator.ts +73 -0
  16. package/docs/cli.md +117 -0
  17. package/docs/code-of-conduct.md +65 -0
  18. package/docs/configuration.md +151 -0
  19. package/docs/contributing.md +107 -0
  20. package/docs/demo.md +46 -0
  21. package/docs/deployment.md +132 -0
  22. package/docs/development.md +168 -0
  23. package/docs/getting-started.md +106 -0
  24. package/docs/index.md +40 -0
  25. package/docs/portapack-transparent.png +0 -0
  26. package/docs/portapack.jpg +0 -0
  27. package/docs/troubleshooting.md +107 -0
  28. package/examples/main.ts +118 -0
  29. package/examples/sample-project/index.html +12 -0
  30. package/examples/sample-project/logo.png +1 -0
  31. package/examples/sample-project/script.js +1 -0
  32. package/examples/sample-project/styles.css +1 -0
  33. package/jest.config.ts +124 -0
  34. package/jest.setup.cjs +211 -0
  35. package/nodemon.json +11 -0
  36. package/output.html +1 -0
  37. package/package.json +161 -0
  38. package/site-packed.html +1 -0
  39. package/src/cli/cli-entry.ts +28 -0
  40. package/src/cli/cli.ts +139 -0
  41. package/src/cli/options.ts +151 -0
  42. package/src/core/bundler.ts +201 -0
  43. package/src/core/extractor.ts +618 -0
  44. package/src/core/minifier.ts +233 -0
  45. package/src/core/packer.ts +191 -0
  46. package/src/core/parser.ts +115 -0
  47. package/src/core/web-fetcher.ts +292 -0
  48. package/src/index.ts +262 -0
  49. package/src/types.ts +163 -0
  50. package/src/utils/font.ts +41 -0
  51. package/src/utils/logger.ts +139 -0
  52. package/src/utils/meta.ts +100 -0
  53. package/src/utils/mime.ts +90 -0
  54. package/src/utils/slugify.ts +70 -0
  55. package/test-output.html +0 -0
  56. package/tests/__fixtures__/sample-project/index.html +5 -0
  57. package/tests/unit/cli/cli-entry.test.ts +104 -0
  58. package/tests/unit/cli/cli.test.ts +230 -0
  59. package/tests/unit/cli/options.test.ts +316 -0
  60. package/tests/unit/core/bundler.test.ts +287 -0
  61. package/tests/unit/core/extractor.test.ts +1129 -0
  62. package/tests/unit/core/minifier.test.ts +414 -0
  63. package/tests/unit/core/packer.test.ts +193 -0
  64. package/tests/unit/core/parser.test.ts +540 -0
  65. package/tests/unit/core/web-fetcher.test.ts +374 -0
  66. package/tests/unit/index.test.ts +339 -0
  67. package/tests/unit/utils/font.test.ts +81 -0
  68. package/tests/unit/utils/logger.test.ts +275 -0
  69. package/tests/unit/utils/meta.test.ts +70 -0
  70. package/tests/unit/utils/mime.test.ts +96 -0
  71. package/tests/unit/utils/slugify.test.ts +71 -0
  72. package/tsconfig.build.json +11 -0
  73. package/tsconfig.jest.json +17 -0
  74. package/tsconfig.json +20 -0
  75. package/tsup.config.ts +71 -0
  76. package/typedoc.json +28 -0
package/src/types.ts ADDED
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @file types.ts
3
+ *
4
+ * @description
5
+ * Centralized types used across the PortaPack CLI, API, core modules, and bundling pipeline.
6
+ *
7
+ * This file defines:
8
+ * - Asset structure
9
+ * - HTML parsing result
10
+ * - Bundling options and metadata
11
+ * - Page structures for recursive bundling
12
+ * - CLI execution output format
13
+ */
14
+
15
+ /**
16
+ * Represents a single discovered, downloaded, or embedded asset.
17
+ * This includes JS, CSS, images, fonts, etc.
18
+ */
19
+ export interface Asset {
20
+ type: 'css' | 'js' | 'image' | 'font' | 'video' | 'audio' | 'other'; // Add video and audio
21
+
22
+ /** The resolved or original URL of the asset */
23
+ url: string;
24
+
25
+ /** Inlined or fetched content */
26
+ content?: string; // Content is optional as it might not be embedded
27
+
28
+ /** Font-specific metadata for font-face usage */
29
+ fontMeta?: {
30
+ familyName: string;
31
+ weight?: number;
32
+ style?: 'normal' | 'italic' | 'oblique';
33
+ format?: string;
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Represents raw HTML and any linked/discovered assets.
39
+ * Result of the parsing stage.
40
+ */
41
+ export interface ParsedHTML {
42
+ htmlContent: string;
43
+ assets: Asset[]; // List of assets found in the HTML
44
+ }
45
+
46
+ /**
47
+ * Represents a single page crawled during recursive bundling.
48
+ * Used as input for the multi-page bundler.
49
+ */
50
+ export interface PageEntry {
51
+ /** Full resolved URL of the crawled page */
52
+ url: string;
53
+
54
+ /** Raw HTML content of the crawled page */
55
+ html: string;
56
+ }
57
+
58
+ /**
59
+ * Configuration options provided by the user via CLI or API call.
60
+ * Controls various aspects of the bundling process.
61
+ */
62
+ export interface BundleOptions {
63
+ /** Embed all discovered assets as data URIs (default: true) */
64
+ embedAssets?: boolean;
65
+
66
+ /** Enable HTML minification using html-minifier-terser (default: true) */
67
+ minifyHtml?: boolean;
68
+
69
+ /** Enable CSS minification using clean-css (default: true) */
70
+ minifyCss?: boolean;
71
+
72
+ /** Enable JavaScript minification using terser (default: true) */
73
+ minifyJs?: boolean;
74
+
75
+ /** Base URL for resolving relative links, especially for remote fetches or complex local structures */
76
+ baseUrl?: string;
77
+
78
+ /** Enable verbose logging during CLI execution */
79
+ verbose?: boolean;
80
+
81
+ /** Skip writing output file to disk (CLI dry-run mode) */
82
+ dryRun?: boolean;
83
+
84
+ /** Enable recursive crawling. If a number, specifies max depth. If true, uses default depth. */
85
+ recursive?: number | boolean;
86
+
87
+ /** Optional output file path override (CLI uses this) */
88
+ output?: string;
89
+
90
+ /** Log level for the internal logger */
91
+ logLevel?: LogLevel;
92
+ }
93
+
94
+ // --- LogLevel Enum ---
95
+ // Defines available log levels as a numeric enum for comparisons.
96
+ export enum LogLevel {
97
+ NONE = 0, // No logging (equivalent to 'silent')
98
+ ERROR = 1, // Only errors
99
+ WARN = 2, // Errors and warnings
100
+ INFO = 3, // Errors, warnings, and info (Default)
101
+ DEBUG = 4 // All messages (Verbose)
102
+ }
103
+
104
+ // --- String Literal Type for LogLevel Names (Optional, useful for CLI parsing) ---
105
+ export type LogLevelName = 'debug' | 'info' | 'warn' | 'error' | 'silent' | 'none';
106
+
107
+
108
+ /**
109
+ * Summary statistics and metadata returned after the packing/bundling process completes.
110
+ */
111
+ export interface BundleMetadata {
112
+ /** Source HTML file path or URL */
113
+ input: string;
114
+
115
+ /** Total number of unique assets discovered (CSS, JS, images, fonts etc.) */
116
+ assetCount: number; // Kept as required - should always be calculated or defaulted (e.g., to 0)
117
+
118
+ /** Final output HTML size in bytes */
119
+ outputSize: number;
120
+
121
+ /** Elapsed build time in milliseconds */
122
+ buildTimeMs: number;
123
+
124
+ /** If recursive bundling was performed, the number of pages successfully crawled and included */
125
+ pagesBundled?: number; // Optional, only relevant for recursive mode
126
+
127
+ /** Any non-critical errors or warnings encountered during bundling (e.g., asset fetch failure) */
128
+ errors?: string[]; // Optional array of error/warning messages
129
+ }
130
+
131
+ /**
132
+ * Standard result object returned from the main public API functions.
133
+ */
134
+ export interface BuildResult {
135
+ /** The final generated HTML string */
136
+ html: string;
137
+ /** Metadata summarizing the build process */
138
+ metadata: BundleMetadata;
139
+ }
140
+
141
+
142
+ /** CLI-specific options extending BundleOptions. */
143
+ export interface CLIOptions extends BundleOptions {
144
+ /** Input file or URL (positional). */
145
+ input?: string;
146
+ /** Max depth for recursive crawling (numeric alias for recursive). */
147
+ maxDepth?: number; // Used by commander, then merged into 'recursive'
148
+ minify?: boolean; // Minify assets (defaults to true)
149
+ }
150
+
151
+ /**
152
+ * Result object specifically for the CLI runner, capturing output streams and exit code.
153
+ */
154
+ export interface CLIResult {
155
+ /** Captured content written to stdout */
156
+ stdout?: string;
157
+
158
+ /** Captured content written to stderr */
159
+ stderr?: string;
160
+
161
+ /** Final exit code intended for the process (0 for success, non-zero for errors) */
162
+ exitCode: number;
163
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * utils/font.ts
3
+ *
4
+ * Utilities for detecting and encoding font files for embedding.
5
+ */
6
+
7
+ import path from 'path';
8
+ import fs from 'fs/promises';
9
+
10
+ /**
11
+ * Returns the correct MIME type for a given font file.
12
+ *
13
+ * @param fontUrl - The path or URL of the font file
14
+ */
15
+ export function getFontMimeType(fontUrl: string): string {
16
+ const ext = path.extname(fontUrl).toLowerCase().replace('.', '');
17
+
18
+ switch (ext) {
19
+ case 'woff': return 'font/woff';
20
+ case 'woff2': return 'font/woff2';
21
+ case 'ttf': return 'font/ttf';
22
+ case 'otf': return 'font/otf';
23
+ case 'eot': return 'application/vnd.ms-fontobject';
24
+ default: return 'application/octet-stream';
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Reads a font file and encodes it as a base64 data URI.
30
+ *
31
+ * NOTE: Not currently used in the pipeline, but useful for testing and future features.
32
+ *
33
+ * @param fontPath - Absolute or relative path to font
34
+ * @returns Full `data:` URI as a string
35
+ */
36
+ export async function encodeFontToDataURI(fontPath: string): Promise<string> {
37
+ const mime = getFontMimeType(fontPath);
38
+ const buffer = await fs.readFile(fontPath);
39
+ const base64 = buffer.toString('base64');
40
+ return `data:${mime};base64,${base64}`;
41
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @file src/utils/logger.ts
3
+ * @description Provides a standardized logging utility with configurable levels (based on an enum)
4
+ * to control output verbosity throughout the application (core, API, CLI).
5
+ */
6
+
7
+ // FIX: Use a regular import for the enum, not 'import type'
8
+ import { LogLevel } from '../types';
9
+ // Assuming LogLevel enum is defined and exported in '../types' like:
10
+ // export enum LogLevel { NONE = 0, ERROR = 1, WARN = 2, INFO = 3, DEBUG = 4 }
11
+
12
+ /**
13
+ * Optional configuration for creating a Logger instance.
14
+ * (Note: Currently constructor only accepts LogLevel directly)
15
+ */
16
+ export interface LoggerOptions {
17
+ level?: LogLevel;
18
+ }
19
+
20
+ /**
21
+ * A simple logger class that allows filtering messages based on severity levels.
22
+ * Uses standard console methods (debug, info, warn, error) for output.
23
+ */
24
+ export class Logger {
25
+ /** The current minimum log level required for a message to be output. */
26
+ public level: LogLevel;
27
+
28
+ /**
29
+ * Creates a new Logger instance.
30
+ * Defaults to LogLevel.INFO if no level is provided.
31
+ *
32
+ * @param {LogLevel} [level=LogLevel.INFO] - The initial log level for this logger instance.
33
+ * Must be one of the values from the LogLevel enum.
34
+ */
35
+ constructor(level: LogLevel = LogLevel.INFO) { // Defaulting to INFO level using the enum value
36
+ // Ensure a valid LogLevel enum member is provided or default correctly
37
+ this.level = (level !== undefined && LogLevel[level] !== undefined)
38
+ ? level
39
+ : LogLevel.INFO; // Use the enum value for default
40
+ }
41
+
42
+ /**
43
+ * Updates the logger's current level. Messages below this level will be suppressed.
44
+ *
45
+ * @param {LogLevel} level - The new log level to set. Must be a LogLevel enum member.
46
+ */
47
+ setLevel(level: LogLevel): void {
48
+ this.level = level;
49
+ }
50
+
51
+ /**
52
+ * Logs a debug message if the current log level is DEBUG or higher.
53
+ *
54
+ * @param {string} message - The debug message string.
55
+ */
56
+ debug(message: string): void {
57
+ // Use enum member for comparison
58
+ if (this.level >= LogLevel.DEBUG) {
59
+ console.debug(`[DEBUG] ${message}`);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Logs an informational message if the current log level is INFO or higher.
65
+ *
66
+ * @param {string} message - The informational message string.
67
+ */
68
+ info(message: string): void {
69
+ // Use enum member for comparison
70
+ if (this.level >= LogLevel.INFO) {
71
+ console.info(`[INFO] ${message}`);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Logs a warning message if the current log level is WARN or higher.
77
+ *
78
+ * @param {string} message - The warning message string.
79
+ */
80
+ warn(message: string): void {
81
+ // Use enum member for comparison
82
+ if (this.level >= LogLevel.WARN) {
83
+ console.warn(`[WARN] ${message}`);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Logs an error message if the current log level is ERROR or higher.
89
+ *
90
+ * @param {string} message - The error message string.
91
+ */
92
+ error(message: string): void {
93
+ // Use enum member for comparison
94
+ if (this.level >= LogLevel.ERROR) {
95
+ console.error(`[ERROR] ${message}`);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Static factory method to create a Logger instance based on a simple boolean `verbose` flag.
101
+ *
102
+ * @static
103
+ * @param {{ verbose?: boolean }} [options={}] - An object potentially containing a `verbose` flag.
104
+ * @returns {Logger} A new Logger instance set to LogLevel.DEBUG if options.verbose is true,
105
+ * otherwise set to LogLevel.INFO.
106
+ */
107
+ static fromVerboseFlag(options: { verbose?: boolean } = {}): Logger {
108
+ // Use enum members for assignment
109
+ return new Logger(options.verbose ? LogLevel.DEBUG : LogLevel.INFO);
110
+ }
111
+
112
+ /**
113
+ * Static factory method to create a Logger instance based on a LogLevel string name.
114
+ * Useful for creating a logger from config files or environments variables.
115
+ *
116
+ * @static
117
+ * @param {string | undefined} levelName - The name of the log level (e.g., 'debug', 'info', 'warn', 'error', 'silent'/'none'). Case-insensitive.
118
+ * @param {LogLevel} [defaultLevel=LogLevel.INFO] - The level to use if levelName is invalid or undefined.
119
+ * @returns {Logger} A new Logger instance set to the corresponding LogLevel.
120
+ */
121
+ static fromLevelName(levelName?: string, defaultLevel: LogLevel = LogLevel.INFO): Logger {
122
+ if (!levelName) {
123
+ return new Logger(defaultLevel);
124
+ }
125
+ switch (levelName.toLowerCase()) {
126
+ // Return enum members
127
+ case 'debug': return new Logger(LogLevel.DEBUG);
128
+ case 'info': return new Logger(LogLevel.INFO);
129
+ case 'warn': return new Logger(LogLevel.WARN);
130
+ case 'error': return new Logger(LogLevel.ERROR);
131
+ case 'silent':
132
+ case 'none': return new Logger(LogLevel.NONE);
133
+ default:
134
+ // Use console.warn directly here as logger might not be ready
135
+ console.warn(`[Logger] Invalid log level name "${levelName}". Defaulting to ${LogLevel[defaultLevel]}.`);
136
+ return new Logger(defaultLevel);
137
+ }
138
+ }
139
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * @file src/utils/meta.ts
3
+ * @description Utility class for tracking bundle statistics like size, time,
4
+ * asset counts, page counts, and errors during the build process.
5
+ * Used by both CLI and API to return metadata consistently.
6
+ */
7
+
8
+ import type { BundleMetadata } from '../types'; // Assuming types are in ../types
9
+
10
+ /**
11
+ * Tracks build performance (timing, output size) and collects metadata
12
+ * (asset counts, page counts, errors) during the HTML bundling process.
13
+ */
14
+ export class BuildTimer {
15
+ private startTime: number;
16
+ private input: string;
17
+ private pagesBundled?: number; // Tracks pages for recursive bundles
18
+ private assetCount: number = 0; // Tracks discovered/processed assets
19
+ private errors: string[] = []; // Collects warnings/errors
20
+
21
+ /**
22
+ * Creates and starts a build timer session for a given input.
23
+ *
24
+ * @param {string} input - The source file path or URL being processed.
25
+ */
26
+ constructor(input: string) {
27
+ this.startTime = Date.now();
28
+ this.input = input;
29
+ }
30
+
31
+ /**
32
+ * Explicitly sets the number of assets discovered or processed.
33
+ * This might be called after asset extraction/minification.
34
+ *
35
+ * @param {number} count - The total number of assets.
36
+ */
37
+ setAssetCount(count: number): void {
38
+ this.assetCount = count;
39
+ }
40
+
41
+ /**
42
+ * Records a warning or error message encountered during the build.
43
+ * These are added to the final metadata.
44
+ *
45
+ * @param {string} message - The warning or error description.
46
+ */
47
+ addError(message: string): void {
48
+ this.errors.push(message);
49
+ }
50
+
51
+ /**
52
+ * Sets the number of pages bundled, typically used in multi-page
53
+ * or recursive bundling scenarios.
54
+ *
55
+ * @param {number} count - The number of HTML pages included in the bundle.
56
+ */
57
+ setPageCount(count: number): void {
58
+ this.pagesBundled = count;
59
+ }
60
+
61
+ /**
62
+ * Stops the timer, calculates final metrics, and returns the complete
63
+ * BundleMetadata object. Merges any explicitly provided metadata
64
+ * (like assetCount calculated elsewhere) with the timer's tracked data.
65
+ *
66
+ * @param {string} finalHtml - The final generated HTML string, used to calculate output size.
67
+ * @param {Partial<BundleMetadata>} [extra] - Optional object containing metadata fields
68
+ * (like assetCount or pre-calculated errors) that should override the timer's internal values.
69
+ * @returns {BundleMetadata} The finalized metadata object for the build process.
70
+ */
71
+ finish(html: string, extra?: Partial<BundleMetadata>): BundleMetadata {
72
+ const buildTimeMs = Date.now() - this.startTime;
73
+ const outputSize = Buffer.byteLength(html || '', 'utf-8');
74
+
75
+ // Combine internal errors with any errors passed in 'extra', avoiding duplicates
76
+ // FIX: Ensure extra.errors is treated as an empty array if undefined/null
77
+ const combinedErrors = Array.from(new Set([...this.errors, ...(extra?.errors ?? [])]));
78
+
79
+ const finalMetadata: BundleMetadata = {
80
+ input: this.input,
81
+ outputSize,
82
+ buildTimeMs,
83
+ assetCount: extra?.assetCount ?? this.assetCount,
84
+ pagesBundled: extra?.pagesBundled ?? this.pagesBundled,
85
+ // Assign the combined errors array
86
+ errors: combinedErrors,
87
+ };
88
+
89
+ // Clean up optional fields if they weren't set/provided or are empty
90
+ if (finalMetadata.pagesBundled === undefined) {
91
+ delete finalMetadata.pagesBundled;
92
+ }
93
+ // Delete errors only if the *combined* array is empty
94
+ if (finalMetadata.errors?.length === 0) {
95
+ delete finalMetadata.errors;
96
+ }
97
+
98
+ return finalMetadata;
99
+ }
100
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @file src/utils/mime.ts
3
+ * @description Utilities for guessing MIME types and asset types from URLs/paths.
4
+ */
5
+
6
+ import path from 'path';
7
+ import type { Asset } from '../types'; // Assuming types are in ../types
8
+
9
+ /**
10
+ * Maps common file extensions to their corresponding MIME types and general Asset types.
11
+ */
12
+ const MIME_MAP: Record<string, { mime: string; assetType: Asset['type'] }> = {
13
+ // CSS
14
+ '.css': { mime: 'text/css', assetType: 'css' },
15
+ // JavaScript
16
+ '.js': { mime: 'application/javascript', assetType: 'js' },
17
+ '.mjs': { mime: 'application/javascript', assetType: 'js' },
18
+ // Images
19
+ '.png': { mime: 'image/png', assetType: 'image' },
20
+ '.jpg': { mime: 'image/jpeg', assetType: 'image' },
21
+ '.jpeg': { mime: 'image/jpeg', assetType: 'image' },
22
+ '.gif': { mime: 'image/gif', assetType: 'image' },
23
+ '.svg': { mime: 'image/svg+xml', assetType: 'image' },
24
+ '.webp': { mime: 'image/webp', assetType: 'image' },
25
+ '.ico': { mime: 'image/x-icon', assetType: 'image' },
26
+ '.avif': { mime: 'image/avif', assetType: 'image' },
27
+ // Fonts
28
+ '.woff': { mime: 'font/woff', assetType: 'font' },
29
+ '.woff2': { mime: 'font/woff2', assetType: 'font' },
30
+ '.ttf': { mime: 'font/ttf', assetType: 'font' },
31
+ '.otf': { mime: 'font/otf', assetType: 'font' },
32
+ '.eot': { mime: 'application/vnd.ms-fontobject', assetType: 'font' },
33
+ // Audio/Video (add more as needed)
34
+ '.mp3': { mime: 'audio/mpeg', assetType: 'other' },
35
+ '.ogg': { mime: 'audio/ogg', assetType: 'other' },
36
+ '.wav': { mime: 'audio/wav', assetType: 'other' },
37
+ '.mp4': { mime: 'video/mp4', assetType: 'other' },
38
+ '.webm': { mime: 'video/webm', assetType: 'other' },
39
+ // Other common web types
40
+ '.json': { mime: 'application/json', assetType: 'other' },
41
+ '.webmanifest': { mime: 'application/manifest+json', assetType: 'other' },
42
+ '.xml': { mime: 'application/xml', assetType: 'other' },
43
+ '.html': { mime: 'text/html', assetType: 'other' }, // Usually not needed as asset, but for completeness
44
+ '.txt': { mime: 'text/plain', assetType: 'other' },
45
+ };
46
+
47
+ /**
48
+ * Default MIME type and Asset type for unknown file extensions.
49
+ */
50
+ const DEFAULT_MIME_TYPE = {
51
+ mime: 'application/octet-stream',
52
+ assetType: 'other' as Asset['type'] // Explicit cast needed
53
+ };
54
+
55
+ /**
56
+ * Guesses the MIME type and general Asset type based on a URL or file path's extension.
57
+ *
58
+ * @param {string} urlOrPath - The URL or file path string.
59
+ * @returns {{ mime: string; assetType: Asset['type'] }} An object containing the guessed MIME type
60
+ * and the corresponding Asset type (e.g., 'image', 'font', 'css', 'js', 'other'). Returns a default
61
+ * if the extension is unknown.
62
+ */
63
+ export function guessMimeType(urlOrPath: string): { mime: string; assetType: Asset['type'] } {
64
+ if (!urlOrPath) {
65
+ return DEFAULT_MIME_TYPE;
66
+ }
67
+ // Extract the extension, handling potential query parameters or fragments
68
+ let ext = '';
69
+ try {
70
+ // Use URL parsing first to handle URLs correctly
71
+ const parsedUrl = new URL(urlOrPath);
72
+ ext = path.extname(parsedUrl.pathname).toLowerCase();
73
+ } catch {
74
+ // If it's not a valid URL, treat it as a path
75
+ ext = path.extname(urlOrPath).toLowerCase();
76
+ }
77
+
78
+ return MIME_MAP[ext] || DEFAULT_MIME_TYPE;
79
+ }
80
+
81
+ /**
82
+ * Gets the appropriate font MIME type based on the file extension.
83
+ * Deprecated: Prefer `guessMimeType`.
84
+ * @deprecated Use guessMimeType instead.
85
+ * @param {string} fontUrl - The URL or path of the font file.
86
+ * @returns {string} The corresponding font MIME type or a default.
87
+ */
88
+ export function getFontMimeType(fontUrl: string): string {
89
+ return guessMimeType(fontUrl).mime; // Delegate to the main function
90
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @file src/utils/slugify.ts
3
+ * @description Converts any URL or string to a safe HTML slug usable in IDs, hashes, filenames, etc.
4
+ */
5
+
6
+ /**
7
+ * Converts a URL or path string into a clean slug suitable for use as an HTML ID or filename segment.
8
+ * - Handles relative and absolute URLs.
9
+ * - Removes common file extensions (.html, .htm, .php, etc.).
10
+ * - Removes URL fragments (#...).
11
+ * - Attempts to parse pathname and search parameters.
12
+ * - Replaces spaces, slashes, and other unsafe characters with hyphens.
13
+ * - Converts to lowercase.
14
+ * - Collapses and trims hyphens.
15
+ * - Returns 'index' for empty or invalid input.
16
+ *
17
+ * @param url - The raw URL or string to slugify.
18
+ * @returns A safe, lowercase slug string.
19
+ */
20
+ export function slugify(url: string): string {
21
+ if (!url || typeof url !== 'string') return 'index';
22
+
23
+ let cleaned = url.trim();
24
+ let pathAndSearch = '';
25
+
26
+ try {
27
+ const urlObj = new URL(url, 'https://placeholder.base');
28
+ pathAndSearch = (urlObj.pathname ?? '') + (urlObj.search ?? '');
29
+ } catch {
30
+ pathAndSearch = cleaned.split('#')[0]; // Remove fragment
31
+ }
32
+
33
+ // Decode URI components AFTER parsing from URL to handle %20 etc.
34
+ try {
35
+ cleaned = decodeURIComponent(pathAndSearch);
36
+ } catch (e) {
37
+ cleaned = pathAndSearch; // Proceed if decoding fails
38
+ }
39
+
40
+ cleaned = cleaned
41
+ // Remove common web extensions FIRST
42
+ .replace(/\.(html?|php|aspx?|jsp)$/i, '')
43
+ // Replace path separators and common separators/spaces with a hyphen
44
+ .replace(/[\s/?=&\\]+/g, '-') // Target spaces, /, ?, =, &, \
45
+ // Remove any remaining characters that are not alphanumeric, hyphen, underscore, or period
46
+ .replace(/[^\w._-]+/g, '') // Allow word chars, '.', '_', '-'
47
+ // Collapse consecutive hyphens
48
+ .replace(/-+/g, '-')
49
+ // Trim leading/trailing hyphens
50
+ .replace(/^-+|-+$/g, '')
51
+ // Convert to lowercase
52
+ .toLowerCase();
53
+
54
+ // Return 'index' if the process results in an empty string
55
+ return cleaned || 'index';
56
+ }
57
+
58
+
59
+ /**
60
+ * Converts a URL or path string into a clean slug suitable for use as an HTML ID.
61
+ * Note: This implementation might be very similar or identical to slugify depending on exact needs.
62
+ * This example uses the refined slugify logic. Consider consolidating if appropriate.
63
+ *
64
+ * @param rawUrl - The raw page URL or path.
65
+ * @returns A safe, lowercase slug string (e.g. "products-item-1", "search-q-test-page-2")
66
+ */
67
+ export function sanitizeSlug(rawUrl: string): string {
68
+ // Re-use the improved slugify logic for consistency
69
+ return slugify(rawUrl);
70
+ }
File without changes
@@ -0,0 +1,5 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head><title>Test</title></head>
4
+ <body><h1>Test Page</h1></body>
5
+ </html>