@markuplint/file-resolver 5.0.0-rc.0 → 5.0.0-rc.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,13 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [5.0.0-rc.1](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.0...v5.0.0-rc.1) (2026-03-27)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **file-resolver:** handle ERR_PACKAGE_PATH_NOT_EXPORTED in generalImport ([d563d60](https://github.com/markuplint/markuplint/commit/d563d6000c4c38be54c17ea015b0f26c80c5229c)), closes [#3516](https://github.com/markuplint/markuplint/issues/3516)
11
+ - **file-resolver:** resolve package subpaths before import to avoid runtime-specific errors ([cb2b641](https://github.com/markuplint/markuplint/commit/cb2b641e25cd9de487d395b6f219dd8b60ecd306))
12
+
6
13
  # [5.0.0-rc.0](https://github.com/markuplint/markuplint/compare/v5.0.0-alpha.3...v5.0.0-rc.0) (2026-03-12)
7
14
 
8
15
  ### Bug Fixes
@@ -2,8 +2,7 @@ import path from 'node:path';
2
2
  import { mergeConfig } from '@markuplint/ml-config';
3
3
  import { ConfigParserError } from '@markuplint/parser-utils';
4
4
  import { InvalidSelectorError, createSelector } from '@markuplint/selector';
5
- import { nonNullableFilter, toNoEmptyStringArrayFromStringOrArray } from '@markuplint/shared';
6
- import { ConfigLoadError } from './config-load-error.js';
5
+ import { nonNullableFilter, toNoEmptyStringArrayFromStringOrArray, ConfigLoadError } from '@markuplint/shared';
7
6
  import { load as loadConfig, search } from './cosmiconfig.js';
8
7
  import { log } from './debug.js';
9
8
  import { generalImport } from './general-import.js';
@@ -1,5 +1,5 @@
1
1
  import type { LoaderSync } from 'cosmiconfig';
2
- import { ConfigLoadError } from './config-load-error.js';
2
+ import { ConfigLoadError } from '@markuplint/shared';
3
3
  type CosmiConfig = ReturnType<LoaderSync>;
4
4
  export declare function search<T = CosmiConfig>(filePath: string, cacheClear: boolean): Promise<{
5
5
  filePath: string;
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import { ConfigParserError } from '@markuplint/parser-utils';
3
3
  import { cosmiconfig, defaultLoaders } from 'cosmiconfig';
4
4
  import { jsonc } from 'jsonc';
5
- import { ConfigLoadError } from './config-load-error.js';
5
+ import { ConfigLoadError } from '@markuplint/shared';
6
6
  import { log } from './debug.js';
7
7
  const searchLog = log.extend('search');
8
8
  const jsoncLoader = (path, content) => {
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import { createRequire } from 'node:module';
3
3
  import path from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
+ import { isFatalError } from '@markuplint/shared';
5
6
  import { resolve } from 'import-meta-resolve';
6
7
  import { log } from './debug.js';
7
8
  import { fromFileURL } from './path-utils.js';
@@ -14,6 +15,17 @@ export async function generalImport(name) {
14
15
  if (cache.has(name)) {
15
16
  return cache.get(name);
16
17
  }
18
+ // When the specifier is a package subpath (e.g., "@scope/pkg/file.json"),
19
+ // the package's exports map may not include it. Instead of relying on
20
+ // error codes (which differ between Node.js and vitest/vite), resolve
21
+ // the package directory upfront and convert to an absolute path.
22
+ const resolved = resolvePackageSubpath(name);
23
+ if (resolved) {
24
+ gLog('Resolved package subpath "%s" to "%s"', name, resolved);
25
+ const result = await generalImport(resolved);
26
+ cache.set(name, result);
27
+ return result;
28
+ }
17
29
  try {
18
30
  // Convert absolute paths to file:// URL format
19
31
  let importPath = name;
@@ -64,14 +76,110 @@ export async function generalImport(name) {
64
76
  }
65
77
  }
66
78
  if (path.isAbsolute(name) && name.endsWith('.json')) {
67
- const file = await fs.readFile(name, 'utf8');
68
- const json = JSON.parse(file);
69
- gLogSuccess('Success by readFile("%s") and JSON.parse: %O', name, json);
70
- cache.set(name, json);
71
- return json;
79
+ try {
80
+ const file = await fs.readFile(name, 'utf8');
81
+ const json = JSON.parse(file);
82
+ gLogSuccess('Success by readFile("%s") and JSON.parse: %O', name, json);
83
+ cache.set(name, json);
84
+ return json;
85
+ }
86
+ catch (readError) {
87
+ gLogError('Error in readFile("%s"): %O', name, readError);
88
+ }
72
89
  }
73
90
  gLogError('Error in `import()`: %O', error);
74
91
  cache.set(name, null);
75
92
  return null;
76
93
  }
77
94
  }
95
+ /**
96
+ * Detects a bare package subpath specifier (e.g., `@scope/pkg/file.json`)
97
+ * and resolves it to an absolute file path when the package's exports map
98
+ * does not include the subpath.
99
+ *
100
+ * Returns the absolute path if resolution succeeds, or `null` if:
101
+ * - The specifier is not a package subpath (absolute path, relative path, no subpath)
102
+ * - The package's exports map already includes the subpath (no bypass needed)
103
+ * - The package cannot be found at all
104
+ */
105
+ function resolvePackageSubpath(name) {
106
+ if (path.isAbsolute(name) || name.startsWith('.')) {
107
+ return null;
108
+ }
109
+ const packageName = name.startsWith('@') ? name.split('/').slice(0, 2).join('/') : name.split('/')[0];
110
+ if (!packageName) {
111
+ return null;
112
+ }
113
+ const subpath = name.slice(packageName.length + 1);
114
+ if (!subpath) {
115
+ return null;
116
+ }
117
+ const pkgDir = resolvePackageDir(packageName);
118
+ if (!pkgDir) {
119
+ return null;
120
+ }
121
+ try {
122
+ const pkgJson = require(path.join(pkgDir, 'package.json'));
123
+ if (!pkgJson.exports) {
124
+ return null; // No exports map — import() should work fine
125
+ }
126
+ // If the exports map explicitly includes this subpath, let import() handle it
127
+ const exportKey = `./${subpath}`;
128
+ if (typeof pkgJson.exports === 'object' && !Array.isArray(pkgJson.exports) && exportKey in pkgJson.exports) {
129
+ return null;
130
+ }
131
+ // Subpath not in exports — resolve to absolute path to bypass the restriction
132
+ return path.join(pkgDir, subpath);
133
+ }
134
+ catch (error) {
135
+ if (isFatalError(error)) {
136
+ throw error;
137
+ }
138
+ return null;
139
+ }
140
+ }
141
+ /**
142
+ * Resolves the root directory of a package by name.
143
+ */
144
+ function resolvePackageDir(packageName) {
145
+ // Try CJS require.resolve first — it can often resolve package.json
146
+ // even when the ESM exports map doesn't include it
147
+ try {
148
+ return path.dirname(require.resolve(`${packageName}/package.json`));
149
+ }
150
+ catch (error) {
151
+ if (isFatalError(error)) {
152
+ throw error;
153
+ }
154
+ // Fall through
155
+ }
156
+ // Fall back to import-meta-resolve to find the main entry, then walk up
157
+ try {
158
+ const mainUrl = resolve(packageName, import.meta.url);
159
+ const mainPath = mainUrl.startsWith('file:') ? fromFileURL(mainUrl) : mainUrl;
160
+ let dir = path.dirname(mainPath);
161
+ for (let i = 0; i < 10; i++) {
162
+ try {
163
+ require(path.join(dir, 'package.json'));
164
+ return dir;
165
+ }
166
+ catch (innerError) {
167
+ if (isFatalError(innerError)) {
168
+ throw innerError;
169
+ }
170
+ const parent = path.dirname(dir);
171
+ if (parent === dir) {
172
+ break; // Reached filesystem root
173
+ }
174
+ dir = parent;
175
+ }
176
+ }
177
+ }
178
+ catch (error) {
179
+ if (isFatalError(error)) {
180
+ throw error;
181
+ }
182
+ // Package not found at all
183
+ }
184
+ return null;
185
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/file-resolver",
3
- "version": "5.0.0-rc.0",
3
+ "version": "5.0.0-rc.1",
4
4
  "description": "The file resolver of markuplint",
5
5
  "repository": "git@github.com:markuplint/markuplint.git",
6
6
  "author": "Yusuke Hirao <yusukehirao@me.com>",
@@ -30,15 +30,15 @@
30
30
  "@types/node": "24.5.1"
31
31
  },
32
32
  "dependencies": {
33
- "@markuplint/html-parser": "5.0.0-rc.0",
34
- "@markuplint/ml-ast": "5.0.0-rc.0",
35
- "@markuplint/ml-config": "5.0.0-rc.0",
36
- "@markuplint/ml-core": "5.0.0-rc.0",
37
- "@markuplint/ml-spec": "5.0.0-rc.0",
38
- "@markuplint/parser-utils": "5.0.0-rc.0",
39
- "@markuplint/pretenders": "5.0.0-rc.0",
40
- "@markuplint/selector": "5.0.0-rc.0",
41
- "@markuplint/shared": "5.0.0-rc.0",
33
+ "@markuplint/html-parser": "5.0.0-rc.1",
34
+ "@markuplint/ml-ast": "5.0.0-rc.1",
35
+ "@markuplint/ml-config": "5.0.0-rc.1",
36
+ "@markuplint/ml-core": "5.0.0-rc.1",
37
+ "@markuplint/ml-spec": "5.0.0-rc.1",
38
+ "@markuplint/parser-utils": "5.0.0-rc.1",
39
+ "@markuplint/pretenders": "5.0.0-rc.1",
40
+ "@markuplint/selector": "5.0.0-rc.1",
41
+ "@markuplint/shared": "5.0.0-rc.1",
42
42
  "cosmiconfig": "9.0.1",
43
43
  "debug": "4.4.3",
44
44
  "glob": "13.0.6",
@@ -47,5 +47,5 @@
47
47
  "jsonc": "2.0.0",
48
48
  "minimatch": "10.2.4"
49
49
  },
50
- "gitHead": "ebf4d7cfca0c259aead3b292c6b8a202db4cd802"
50
+ "gitHead": "0d6b4324d9a7d6b9e1ba57d4a57e45d36975cba9"
51
51
  }
@@ -1,6 +0,0 @@
1
- export declare class ConfigLoadError extends Error {
2
- filePath: string;
3
- name: string;
4
- referrer: string;
5
- constructor(message: string, filePath: string, referrer: string);
6
- }
@@ -1,10 +0,0 @@
1
- export class ConfigLoadError extends Error {
2
- filePath;
3
- name = 'ConfigLoadError';
4
- referrer;
5
- constructor(message, filePath, referrer) {
6
- super(message + ` in ${referrer}`);
7
- this.filePath = filePath;
8
- this.referrer = referrer;
9
- }
10
- }