@equinor/fusion-imports 1.1.7 → 1.2.0-next.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@equinor/fusion-imports",
3
- "version": "1.1.7",
3
+ "version": "1.2.0-next.0",
4
4
  "description": "Package import files and configurations",
5
5
  "keywords": [
6
6
  "esbuild",
@@ -38,7 +38,7 @@
38
38
  "url": "https://github.com/equinor/fusion-framework/issues"
39
39
  },
40
40
  "dependencies": {
41
- "esbuild": "^0.25.1",
41
+ "esbuild": "^0.27.0",
42
42
  "read-package-up": "^11.0.0"
43
43
  },
44
44
  "devDependencies": {
@@ -0,0 +1,171 @@
1
+ import type { BuildResult, Plugin } from 'esbuild';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+
5
+ /**
6
+ * Transforms import.meta.resolve() calls in code to resolved file:// URLs
7
+ * @param code - The code to transform
8
+ * @param baseDir - The base directory for resolving relative paths
9
+ * @returns The transformed code
10
+ */
11
+ function transformImportMetaResolve(code: string, baseDir: string): string {
12
+ return code.replace(
13
+ /import\.meta\.resolve\((['"`])([^'"`]+)\1\)/g,
14
+ (match: string, quote: string, specifier: string) => {
15
+ try {
16
+ // Only resolve relative paths (./ or ../), skip package imports
17
+ if (!specifier.startsWith('./') && !specifier.startsWith('../')) {
18
+ return match;
19
+ }
20
+
21
+ const resolvedPath = path.resolve(baseDir, specifier);
22
+ const fileUrl = pathToFileURL(resolvedPath).href;
23
+ return `${quote}${fileUrl}${quote}`;
24
+ } catch {
25
+ // If resolution fails, leave it as-is
26
+ return match;
27
+ }
28
+ },
29
+ );
30
+ }
31
+
32
+ /**
33
+ * Gets the appropriate esbuild loader for a file extension
34
+ */
35
+ function getLoader(filePath: string): 'ts' | 'tsx' | 'js' | 'jsx' | undefined {
36
+ const ext = path.extname(filePath);
37
+ if (ext === '.tsx' || ext === '.jsx') {
38
+ return ext === '.tsx' ? 'tsx' : 'jsx';
39
+ }
40
+ if (ext === '.ts' || ext === '.mts') {
41
+ return 'ts';
42
+ }
43
+ if (ext === '.js' || ext === '.mjs') {
44
+ return 'js';
45
+ }
46
+ return undefined;
47
+ }
48
+
49
+ /**
50
+ * Safely reads a file, returning undefined on error
51
+ */
52
+ async function readFileSafe(filePath: string): Promise<string | undefined> {
53
+ try {
54
+ const fs = await import('node:fs/promises');
55
+ return await fs.readFile(filePath, 'utf-8');
56
+ } catch {
57
+ return undefined;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Collects output files from esbuild result (handles both write: true and write: false)
63
+ */
64
+ async function collectOutputFiles(
65
+ result: BuildResult,
66
+ ): Promise<Array<{ path: string; text: string }>> {
67
+ const files: Array<{ path: string; text: string }> = [];
68
+ const processedPaths = new Set<string>();
69
+
70
+ // Collect from outputFiles (write: false case - files in memory)
71
+ if (result.outputFiles) {
72
+ for (const outputFile of result.outputFiles) {
73
+ if (outputFile.path.endsWith('.js') || outputFile.path.endsWith('.mjs')) {
74
+ files.push({ path: outputFile.path, text: outputFile.text });
75
+ processedPaths.add(outputFile.path);
76
+ }
77
+ }
78
+ }
79
+
80
+ // Collect from metafile (write: true case - files on disk)
81
+ if (result.metafile?.outputs) {
82
+ for (const outputPath of Object.keys(result.metafile.outputs)) {
83
+ if (processedPaths.has(outputPath)) {
84
+ continue;
85
+ }
86
+
87
+ if (outputPath.endsWith('.js') || outputPath.endsWith('.mjs')) {
88
+ const absolutePath = path.isAbsolute(outputPath)
89
+ ? outputPath
90
+ : path.resolve(process.cwd(), outputPath);
91
+
92
+ const text = (await readFileSafe(absolutePath)) ?? (await readFileSafe(outputPath));
93
+ if (text) {
94
+ files.push({ path: absolutePath, text });
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ return files;
101
+ }
102
+
103
+ /**
104
+ * Creates an esbuild plugin that transforms `import.meta.resolve()` calls
105
+ * to resolved file:// URLs at build time.
106
+ *
107
+ * This plugin is necessary because esbuild doesn't handle `import.meta.resolve()`
108
+ * calls during bundling - it leaves them as runtime calls. For local imports
109
+ * (relative paths), we need to resolve them at build time so they work correctly
110
+ * in the bundled output.
111
+ */
112
+ export const importMetaResolvePlugin = (): Plugin => {
113
+ return {
114
+ name: 'import-meta-resolve',
115
+ setup(build) {
116
+ let entryPointDir: string | undefined;
117
+
118
+ // Transform source files during load phase
119
+ build.onLoad({ filter: /.*/ }, async (args) => {
120
+ // Track entry point directory (first non-node_modules file)
121
+ if (args.namespace === 'file' && !entryPointDir && !args.path.includes('node_modules')) {
122
+ entryPointDir = path.dirname(args.path);
123
+ }
124
+
125
+ // Skip node_modules
126
+ if (args.path.includes('node_modules')) {
127
+ return undefined;
128
+ }
129
+
130
+ // Read and transform file if it contains import.meta.resolve
131
+ const contents = await readFileSafe(args.path);
132
+ if (!contents || !contents.includes('import.meta.resolve')) {
133
+ return undefined;
134
+ }
135
+
136
+ const transformedContents = transformImportMetaResolve(contents, path.dirname(args.path));
137
+ if (transformedContents === contents) {
138
+ return undefined;
139
+ }
140
+
141
+ const loader = getLoader(args.path);
142
+ return loader ? { contents: transformedContents, loader } : undefined;
143
+ });
144
+
145
+ // Transform bundled output files
146
+ build.onEnd(async (result) => {
147
+ if (!entryPointDir) {
148
+ return;
149
+ }
150
+
151
+ const files = await collectOutputFiles(result);
152
+ const fs = await import('node:fs/promises');
153
+
154
+ for (const file of files) {
155
+ if (!file.text.includes('import.meta.resolve')) {
156
+ continue;
157
+ }
158
+
159
+ const transformedText = transformImportMetaResolve(file.text, entryPointDir);
160
+ if (transformedText !== file.text) {
161
+ try {
162
+ await fs.writeFile(file.path, transformedText, 'utf-8');
163
+ } catch (error) {
164
+ console.warn(`Failed to write transformed output to ${file.path}:`, error);
165
+ }
166
+ }
167
+ }
168
+ });
169
+ },
170
+ };
171
+ };
@@ -5,6 +5,8 @@ import { pathToFileURL } from 'node:url';
5
5
  import { processAccessError } from './error.js';
6
6
 
7
7
  import { readPackageUp } from 'read-package-up';
8
+ import { importMetaResolvePlugin } from './import-meta-resolve-plugin.js';
9
+ import { rawMarkdownPlugin } from './markdown-plugin.js';
8
10
 
9
11
  /**
10
12
  * Represents a Node.js module with an optional default export.
@@ -79,6 +81,9 @@ export const importScript = async <M extends EsmModule>(
79
81
  outfile,
80
82
  platform: 'node',
81
83
  write: true,
84
+ plugins: [importMetaResolvePlugin(), rawMarkdownPlugin()],
85
+ // Enable metafile so the plugin can find output files when write: true
86
+ metafile: true,
82
87
  },
83
88
  options, // provided options
84
89
  {
@@ -88,6 +93,9 @@ export const importScript = async <M extends EsmModule>(
88
93
  bundle: true,
89
94
  packages: 'external',
90
95
  format: 'esm',
96
+ // Override plugins to ensure import-meta-resolve is included
97
+ // Ensure metafile is enabled for the plugin to work with write: true
98
+ metafile: options?.metafile ?? true,
91
99
  },
92
100
  ) as BuildOptions;
93
101
 
package/src/index.ts CHANGED
@@ -7,5 +7,7 @@ export {
7
7
  type ImportConfigResult,
8
8
  } from './import-config.js';
9
9
  export { resolveConfigFile } from './resolve-config-file.js';
10
+ export { importMetaResolvePlugin as createImportMetaResolvePlugin } from './import-meta-resolve-plugin.js';
11
+ export { rawMarkdownPlugin } from './markdown-plugin.js';
10
12
 
11
13
  export { FileNotFoundError, FileNotAccessibleError } from './error.js';
@@ -0,0 +1,79 @@
1
+ import type { Plugin } from 'esbuild';
2
+ import path from 'node:path';
3
+ import { readFile } from 'node:fs/promises';
4
+
5
+ /**
6
+ * Options for configuring the markdown raw plugin.
7
+ */
8
+ export interface RawMarkdownPluginOptions {
9
+ /**
10
+ * Regular expression filter to match file imports.
11
+ * @default /\.mdx?\?raw$/
12
+ */
13
+ filter?: RegExp;
14
+ }
15
+
16
+ /**
17
+ * Creates an esbuild plugin that handles `?raw` imports for markdown files.
18
+ *
19
+ * This plugin allows importing markdown files as raw strings using the `?raw` query parameter:
20
+ * ```typescript
21
+ * import readmeContent from '../../README.md?raw';
22
+ * import mdxContent from '../../docs/guide.mdx?raw';
23
+ * ```
24
+ *
25
+ * The plugin intercepts imports ending with `?raw` (or `.md?raw`/`.mdx?raw`), reads the file content,
26
+ * and returns it as a default export string.
27
+ *
28
+ * @param options - Configuration options for the plugin
29
+ * @returns An esbuild plugin
30
+ */
31
+ export const rawMarkdownPlugin = (options: RawMarkdownPluginOptions = {}): Plugin => {
32
+ const { filter = /\.mdx?\?raw$/ } = options;
33
+
34
+ return {
35
+ name: 'markdown-raw',
36
+ setup(build) {
37
+ // Handle imports ending with ?raw
38
+ build.onResolve({ filter }, (args) => {
39
+ // Remove the ?raw suffix to get the actual file path
40
+ const filePath = args.path.replace(/\?raw$/, '');
41
+
42
+ // Determine the resolve directory: use importer's directory if available, otherwise use resolveDir
43
+ const resolveDir = args.importer
44
+ ? path.dirname(args.importer)
45
+ : args.resolveDir || process.cwd();
46
+
47
+ const resolvedPath = path.isAbsolute(filePath)
48
+ ? filePath
49
+ : path.resolve(resolveDir, filePath);
50
+
51
+ return {
52
+ path: resolvedPath,
53
+ namespace: 'markdown-raw',
54
+ };
55
+ });
56
+
57
+ // Load the file content as a string
58
+ build.onLoad({ filter: /.*/, namespace: 'markdown-raw' }, async (args) => {
59
+ try {
60
+ const content = await readFile(args.path, 'utf-8');
61
+ // Export the content as a default export string
62
+ return {
63
+ contents: `export default ${JSON.stringify(content)};`,
64
+ loader: 'js',
65
+ };
66
+ } catch (error) {
67
+ return {
68
+ errors: [
69
+ {
70
+ text: `Failed to read file: ${args.path}`,
71
+ detail: error instanceof Error ? error.message : String(error),
72
+ },
73
+ ],
74
+ };
75
+ }
76
+ });
77
+ },
78
+ };
79
+ };
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '1.1.7';
2
+ export const version = '1.2.0-next.0';