@technomoron/mail-magic 1.0.5 → 1.0.8

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.
@@ -15,7 +15,7 @@ export class MailerAPI extends ApiModule {
15
15
  if (parsed) {
16
16
  return parsed.address;
17
17
  }
18
- return null;
18
+ return undefined;
19
19
  }
20
20
  //
21
21
  // Validate a set of email addresses. Return arrays of invalid
@@ -51,6 +51,9 @@ export class MailerAPI extends ApiModule {
51
51
  if (!dbdomain) {
52
52
  throw new ApiError({ code: 401, message: `Unable to look up the domain ${domain}` });
53
53
  }
54
+ if (dbdomain.user_id !== user.user_id) {
55
+ throw new ApiError({ code: 403, message: `Domain ${domain} is not owned by this user` });
56
+ }
54
57
  apireq.domain = dbdomain;
55
58
  apireq.locale = locale || 'en';
56
59
  apireq.user = user;
@@ -85,7 +88,7 @@ export class MailerAPI extends ApiModule {
85
88
  const [templateRecord, created] = await api_txmail.upsert(data, {
86
89
  returning: true
87
90
  });
88
- console.log('Template upserted:', templateRecord.name, 'Created:', created);
91
+ this.server.storage.print_debug(`Template upserted: ${templateRecord.name} (created=${created})`);
89
92
  }
90
93
  catch (error) {
91
94
  throw new ApiError({
@@ -98,7 +101,7 @@ export class MailerAPI extends ApiModule {
98
101
  // Send a template using posted arguments.
99
102
  async post_send(apireq) {
100
103
  await this.assert_domain_and_user(apireq);
101
- const { name, rcpt, domain = '', locale = '', vars = {} } = apireq.req.body;
104
+ const { name, rcpt, domain = '', locale = '', vars = {}, replyTo, reply_to, headers } = apireq.req.body;
102
105
  if (!name || !rcpt || !domain) {
103
106
  throw new ApiError({ code: 400, message: 'name/rcpt/domain required' });
104
107
  }
@@ -107,7 +110,7 @@ export class MailerAPI extends ApiModule {
107
110
  try {
108
111
  parsedVars = JSON.parse(vars);
109
112
  }
110
- catch (error) {
113
+ catch {
111
114
  throw new ApiError({ code: 400, message: 'Invalid JSON provided in "vars"' });
112
115
  }
113
116
  }
@@ -118,7 +121,7 @@ export class MailerAPI extends ApiModule {
118
121
  throw new ApiError({ code: 400, message: 'Invalid email address(es): ' + invalid.join(',') });
119
122
  }
120
123
  let template = null;
121
- const deflocale = apireq.server.store.deflocale || '';
124
+ const deflocale = this.server.storage.deflocale || '';
122
125
  const domain_id = apireq.domain.domain_id;
123
126
  try {
124
127
  template =
@@ -159,8 +162,29 @@ export class MailerAPI extends ApiModule {
159
162
  for (const file of rawFiles) {
160
163
  attachmentMap[file.fieldname] = file.originalname;
161
164
  }
162
- console.log(JSON.stringify({ vars, thevars }, undefined, 2));
165
+ this.server.storage.print_debug(`Template vars: ${JSON.stringify({ vars, thevars }, undefined, 2)}`);
163
166
  const meta = buildRequestMeta(apireq.req);
167
+ const replyToValue = (replyTo || reply_to);
168
+ let normalizedReplyTo;
169
+ if (replyToValue) {
170
+ normalizedReplyTo = this.validateEmail(replyToValue);
171
+ if (!normalizedReplyTo) {
172
+ throw new ApiError({ code: 400, message: 'Invalid reply-to email address' });
173
+ }
174
+ }
175
+ let normalizedHeaders;
176
+ if (headers !== undefined) {
177
+ if (!headers || typeof headers !== 'object' || Array.isArray(headers)) {
178
+ throw new ApiError({ code: 400, message: 'headers must be a key/value object' });
179
+ }
180
+ normalizedHeaders = {};
181
+ for (const [key, value] of Object.entries(headers)) {
182
+ if (typeof value !== 'string') {
183
+ throw new ApiError({ code: 400, message: `headers.${key} must be a string` });
184
+ }
185
+ normalizedHeaders[key] = value;
186
+ }
187
+ }
164
188
  try {
165
189
  const env = new nunjucks.Environment(null, { autoescape: false });
166
190
  const compiled = nunjucks.compile(template.template, env);
@@ -180,9 +204,11 @@ export class MailerAPI extends ApiModule {
180
204
  subject: template.subject || apireq.req.body.subject || '',
181
205
  html,
182
206
  text,
183
- attachments
207
+ attachments,
208
+ ...(normalizedReplyTo ? { replyTo: normalizedReplyTo } : {}),
209
+ ...(normalizedHeaders ? { headers: normalizedHeaders } : {})
184
210
  };
185
- await apireq.server.storage.transport.sendMail(sendargs);
211
+ await this.server.storage.transport.sendMail(sendargs);
186
212
  }
187
213
  return [200, { Status: 'OK', Message: 'Emails sent successfully' }];
188
214
  }
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { pathToFileURL } from 'node:url';
2
+ import { AssetAPI } from './api/assets.js';
2
3
  import { FormAPI } from './api/forms.js';
3
4
  import { MailerAPI } from './api/mailer.js';
4
5
  import { mailApiServer } from './server.js';
@@ -10,13 +11,16 @@ function buildServerConfig(store, overrides) {
10
11
  apiPort: env.API_PORT,
11
12
  uploadPath: env.UPLOAD_PATH,
12
13
  debug: env.DEBUG,
14
+ apiBasePath: '',
15
+ swaggerEnabled: env.SWAGGER_ENABLED,
16
+ swaggerPath: env.SWAGGER_PATH,
13
17
  ...overrides
14
18
  };
15
19
  }
16
20
  export async function createMailMagicServer(overrides = {}) {
17
21
  const store = await new mailStore().init();
18
22
  const config = buildServerConfig(store, overrides);
19
- const server = new mailApiServer(config, store).api(new MailerAPI()).api(new FormAPI());
23
+ const server = new mailApiServer(config, store).api(new MailerAPI()).api(new FormAPI()).api(new AssetAPI());
20
24
  return { server, store, env: store.env };
21
25
  }
22
26
  export async function startMailMagicServer(overrides = {}) {
@@ -128,6 +128,16 @@ export async function init_api_form(api_db) {
128
128
  });
129
129
  return api_form;
130
130
  }
131
+ function assertSafeRelativePath(filename, label) {
132
+ const normalized = path.normalize(filename);
133
+ if (path.isAbsolute(normalized)) {
134
+ throw new Error(`${label} path must be relative`);
135
+ }
136
+ if (normalized.split(path.sep).includes('..')) {
137
+ throw new Error(`${label} path cannot include '..' segments`);
138
+ }
139
+ return normalized;
140
+ }
131
141
  export async function upsert_form(record) {
132
142
  const { user, domain } = await user_and_domain(record.domain_id);
133
143
  const idname = normalizeSlug(user.idname);
@@ -150,7 +160,7 @@ export async function upsert_form(record) {
150
160
  if (!record.filename.endsWith('.njk')) {
151
161
  record.filename += '.njk';
152
162
  }
153
- record.filename = path.normalize(record.filename);
163
+ record.filename = assertSafeRelativePath(record.filename, 'Form filename');
154
164
  let instance = null;
155
165
  instance = await api_form.findByPk(record.form_id);
156
166
  if (instance) {
@@ -14,34 +14,42 @@ const init_data_schema = z.object({
14
14
  form: z.array(api_form_schema).default([])
15
15
  });
16
16
  /**
17
- * Resolve an asset file within ./config/<userid>/<domain>/<type>/assets
17
+ * Resolve an asset file within ./config/<domain>/assets
18
18
  */
19
- function resolveAsset(basePath, type, domainName, assetName, locale) {
20
- const searchPaths = [];
21
- // always domain-scoped
22
- if (locale) {
23
- searchPaths.push(path.join(domainName, type, locale));
19
+ function resolveAsset(basePath, domainName, assetName) {
20
+ const assetsRoot = path.join(basePath, domainName, 'assets');
21
+ if (!fs.existsSync(assetsRoot)) {
22
+ return null;
24
23
  }
25
- searchPaths.push(path.join(domainName, type));
26
- // no domain fallback do not leak assets between domains
27
- // but allow locale fallbacks inside type
28
- if (locale) {
29
- searchPaths.push(path.join(type, locale));
24
+ const resolvedRoot = fs.realpathSync(assetsRoot);
25
+ const normalizedRoot = resolvedRoot.endsWith(path.sep) ? resolvedRoot : resolvedRoot + path.sep;
26
+ const candidate = path.resolve(assetsRoot, assetName);
27
+ if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) {
28
+ return null;
30
29
  }
31
- searchPaths.push(type);
32
- for (const p of searchPaths) {
33
- const candidate = path.join(basePath, p, 'assets', assetName);
34
- if (fs.existsSync(candidate)) {
35
- return candidate;
36
- }
30
+ const realCandidate = fs.realpathSync(candidate);
31
+ if (!realCandidate.startsWith(normalizedRoot)) {
32
+ return null;
37
33
  }
38
- return null;
34
+ return realCandidate;
35
+ }
36
+ function buildAssetUrl(baseUrl, route, domainName, assetPath) {
37
+ const trimmedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
38
+ const normalizedRoute = route.startsWith('/') ? route : `/${route}`;
39
+ const encodedDomain = encodeURIComponent(domainName);
40
+ const encodedPath = assetPath
41
+ .split('/')
42
+ .filter((segment) => segment.length > 0)
43
+ .map((segment) => encodeURIComponent(segment))
44
+ .join('/');
45
+ const trailing = encodedPath ? `/${encodedPath}` : '';
46
+ return `${trimmedBase}${normalizedRoute}/${encodedDomain}${trailing}`;
39
47
  }
40
48
  function extractAndReplaceAssets(html, opts) {
41
49
  const regex = /src=["']asset\(['"]([^'"]+)['"](?:,\s*(true|false|[01]))?\)["']/g;
42
50
  const assets = [];
43
51
  const replacedHtml = html.replace(regex, (_m, relPath, inlineFlag) => {
44
- const fullPath = resolveAsset(opts.basePath, opts.type, opts.domainName, relPath, opts.locale ?? undefined);
52
+ const fullPath = resolveAsset(opts.basePath, opts.domainName, relPath);
45
53
  if (!fullPath) {
46
54
  throw new Error(`Missing asset "${relPath}"`);
47
55
  }
@@ -52,13 +60,17 @@ function extractAndReplaceAssets(html, opts) {
52
60
  cid: isInline ? relPath : undefined
53
61
  };
54
62
  assets.push(storedFile);
55
- return isInline
56
- ? `src="cid:${relPath}"`
57
- : `src="${opts.apiUrl}/image/${opts.idname}/${opts.type}/` +
58
- `${opts.domainName ? opts.domainName + '/' : ''}` +
59
- `${opts.locale ? opts.locale + '/' : ''}` +
60
- relPath +
61
- '"';
63
+ if (isInline) {
64
+ return `src="cid:${relPath}"`;
65
+ }
66
+ const domainAssetsRoot = path.join(opts.basePath, opts.domainName, 'assets');
67
+ const relativeToAssets = path.relative(domainAssetsRoot, fullPath);
68
+ if (!relativeToAssets || relativeToAssets.startsWith('..')) {
69
+ throw new Error(`Asset path escapes domain assets directory: ${fullPath}`);
70
+ }
71
+ const normalizedAssetPath = relativeToAssets.split(path.sep).join('/');
72
+ const assetUrl = buildAssetUrl(opts.apiUrl, opts.assetRoute, opts.domainName, normalizedAssetPath);
73
+ return `src="${assetUrl}"`;
62
74
  });
63
75
  return { html: replacedHtml, assets };
64
76
  }
@@ -69,8 +81,10 @@ async function _load_template(store, filename, pathname, user, domain, locale, t
69
81
  if (filename.startsWith(prefix)) {
70
82
  relFile = filename.slice(prefix.length);
71
83
  }
72
- const absPath = path.resolve(rootDir, pathname || '', relFile);
73
- if (!absPath.startsWith(rootDir)) {
84
+ const resolvedRoot = path.resolve(rootDir);
85
+ const normalizedRoot = resolvedRoot.endsWith(path.sep) ? resolvedRoot : resolvedRoot + path.sep;
86
+ const absPath = path.resolve(resolvedRoot, pathname || '', relFile);
87
+ if (!absPath.startsWith(normalizedRoot)) {
74
88
  throw new Error(`Invalid template path "${filename}"`);
75
89
  }
76
90
  if (!fs.existsSync(absPath)) {
@@ -90,11 +104,9 @@ async function _load_template(store, filename, pathname, user, domain, locale, t
90
104
  const merged = processor.flattenNoAssets(templateKey);
91
105
  const { html, assets } = extractAndReplaceAssets(merged, {
92
106
  basePath: baseConfigPath,
93
- type,
94
107
  domainName: domain.name,
95
- locale,
96
108
  apiUrl: store.env.API_URL,
97
- idname: user.idname
109
+ assetRoute: store.env.ASSET_ROUTE
98
110
  });
99
111
  return { html, assets };
100
112
  }
@@ -23,6 +23,16 @@ export const api_txmail_schema = z.object({
23
23
  });
24
24
  export class api_txmail extends Model {
25
25
  }
26
+ function assertSafeRelativePath(filename, label) {
27
+ const normalized = path.normalize(filename);
28
+ if (path.isAbsolute(normalized)) {
29
+ throw new Error(`${label} path must be relative`);
30
+ }
31
+ if (normalized.split(path.sep).includes('..')) {
32
+ throw new Error(`${label} path cannot include '..' segments`);
33
+ }
34
+ return normalized;
35
+ }
26
36
  export async function upsert_txmail(record) {
27
37
  const { user, domain } = await user_and_domain(record.domain_id);
28
38
  const idname = normalizeSlug(user.idname);
@@ -45,7 +55,7 @@ export async function upsert_txmail(record) {
45
55
  if (!record.filename.endsWith('.njk')) {
46
56
  record.filename += '.njk';
47
57
  }
48
- record.filename = path.normalize(record.filename);
58
+ record.filename = assertSafeRelativePath(record.filename, 'Template filename');
49
59
  const [instance] = await api_txmail.upsert(record);
50
60
  return instance;
51
61
  }
@@ -91,7 +101,7 @@ export async function init_api_txmail(api_db) {
91
101
  unique: false
92
102
  },
93
103
  template: {
94
- type: DataTypes.STRING,
104
+ type: DataTypes.TEXT,
95
105
  allowNull: false,
96
106
  defaultValue: ''
97
107
  },
@@ -145,7 +155,6 @@ export async function init_api_txmail(api_db) {
145
155
  });
146
156
  api_txmail.addHook('beforeValidate', async (template) => {
147
157
  const { user, domain } = await user_and_domain(template.domain_id);
148
- console.log('HERE');
149
158
  const dname = normalizeSlug(domain.name);
150
159
  const name = normalizeSlug(template.name);
151
160
  const locale = normalizeSlug(template.locale || domain.locale || user.locale || '');
@@ -160,7 +169,7 @@ export async function init_api_txmail(api_db) {
160
169
  if (!template.filename.endsWith('.njk')) {
161
170
  template.filename += '.njk';
162
171
  }
163
- console.log(`FILENAME IS: ${template.filename}`);
172
+ template.filename = assertSafeRelativePath(template.filename, 'Template filename');
164
173
  });
165
174
  return api_txmail;
166
175
  }
@@ -28,31 +28,23 @@ export const envOptions = defineEnvOptions({
28
28
  description: 'Sets the public URL for the API (i.e. https://ml.example.com:3790)',
29
29
  default: 'http://localhost:3776'
30
30
  },
31
- CONFIG_PATH: {
32
- description: 'Path to directory where config files are located',
33
- default: './config/'
34
- },
35
- /*
36
- SWAGGER_ENABLE: {
37
- description: 'Enable Swagger API docs',
38
- default: 'false',
39
- type: 'boolean'
31
+ SWAGGER_ENABLED: {
32
+ description: 'Enable the Swagger/OpenAPI endpoint',
33
+ type: 'boolean',
34
+ default: false
40
35
  },
41
36
  SWAGGER_PATH: {
42
- description: 'Path for swagger api docs',
43
- default: '/api-docs'
37
+ description: 'Path to expose the Swagger/OpenAPI spec (default: /api/swagger when enabled)',
38
+ default: ''
44
39
  },
45
- */
46
- /*
47
- JWT_SECRET: {
48
- description: 'Secret key for generating JWT access tokens',
49
- required: true
40
+ ASSET_ROUTE: {
41
+ description: 'Route prefix exposed for config assets',
42
+ default: '/asset'
50
43
  },
51
- JWT_REFRESH: {
52
- description: 'Secret key for generating JWT refresh tokens',
53
- required: true
44
+ CONFIG_PATH: {
45
+ description: 'Path to directory where config files are located',
46
+ default: './config/'
54
47
  },
55
- */
56
48
  DB_USER: {
57
49
  description: 'Database username for API database'
58
50
  },
@@ -37,6 +37,7 @@ export class mailStore {
37
37
  api_db = null;
38
38
  keys = {};
39
39
  configpath = '';
40
+ deflocale;
40
41
  print_debug(msg) {
41
42
  if (this.env.DEBUG) {
42
43
  console.log(msg);
package/dist/util.js CHANGED
@@ -92,3 +92,26 @@ export function buildRequestMeta(rawReq) {
92
92
  ip_chain: uniqueIps
93
93
  };
94
94
  }
95
+ export function decodeComponent(value) {
96
+ if (!value) {
97
+ return '';
98
+ }
99
+ try {
100
+ return decodeURIComponent(value);
101
+ }
102
+ catch {
103
+ return value;
104
+ }
105
+ }
106
+ export function sendFileAsync(res, file) {
107
+ return new Promise((resolve, reject) => {
108
+ res.sendFile(file, (err) => {
109
+ if (err) {
110
+ reject(err);
111
+ }
112
+ else {
113
+ resolve();
114
+ }
115
+ });
116
+ });
117
+ }
package/eslint.config.mjs CHANGED
@@ -1,9 +1,45 @@
1
+ import tsPlugin from '@typescript-eslint/eslint-plugin';
1
2
  import tsParser from '@typescript-eslint/parser';
2
- import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript';
3
+ import eslintConfigPrettier from 'eslint-config-prettier/flat';
3
4
  import pluginImport from 'eslint-plugin-import';
4
- import pluginPrettier from 'eslint-plugin-prettier';
5
- import pluginVue from 'eslint-plugin-vue';
6
5
  import jsoncParser from 'jsonc-eslint-parser';
6
+ const TS_FILE_GLOBS = ['**/*.{ts,tsx,mts,cts,vue}'];
7
+ const TS_PLUGIN_FILE_GLOBS = ['**/*.{ts,tsx,mts,cts,js,mjs,cjs,vue}'];
8
+ const VUE_FILE_GLOBS = ['**/*.vue'];
9
+
10
+ const { hasVueSupport, pluginVue, vueTypeScriptConfigs } = await loadVueSupport();
11
+ const scopedVueTypeScriptConfigs = hasVueSupport
12
+ ? scopeVueConfigs(vueTypeScriptConfigs).map(stripTypeScriptPlugin)
13
+ : [];
14
+ const vueSpecificBlocks = hasVueSupport
15
+ ? [
16
+ ...scopedVueTypeScriptConfigs,
17
+ {
18
+ files: VUE_FILE_GLOBS,
19
+ plugins: {
20
+ vue: pluginVue
21
+ },
22
+ rules: {
23
+ 'vue/html-indent': 'off', // Let Prettier handle indentation
24
+ 'vue/max-attributes-per-line': 'off', // Let Prettier handle line breaks
25
+ 'vue/first-attribute-linebreak': 'off', // Let Prettier handle attribute positioning
26
+ 'vue/singleline-html-element-content-newline': 'off',
27
+ 'vue/html-self-closing': [
28
+ 'error',
29
+ {
30
+ html: {
31
+ void: 'always',
32
+ normal: 'always',
33
+ component: 'always'
34
+ }
35
+ }
36
+ ],
37
+ 'vue/multi-word-component-names': 'off', // Disable multi-word name restriction
38
+ 'vue/attribute-hyphenation': ['error', 'always']
39
+ }
40
+ }
41
+ ]
42
+ : [];
7
43
 
8
44
  export default [
9
45
  {
@@ -12,54 +48,31 @@ export default [
12
48
  'dist',
13
49
  '.output',
14
50
  '.nuxt',
51
+ '.netlify',
52
+ 'node_modules/.netlify',
53
+ '4000/.nuxt',
15
54
  'coverage',
16
55
  '**/*.d.ts',
56
+ 'configure-eslint.cjs',
17
57
  'configure-eslint.js',
18
58
  '*.config.js',
19
- '*.config.ts',
20
59
  'public'
21
60
  ]
22
61
  },
23
- ...defineConfigWithVueTs(vueTsConfigs.recommended),
24
62
  {
25
- files: ['**/*.vue'],
63
+ files: TS_PLUGIN_FILE_GLOBS,
26
64
  plugins: {
27
- vue: pluginVue,
28
- prettier: pluginPrettier
29
- },
30
- rules: {
31
- 'prettier/prettier': 'error', // Enforce Prettier rules
32
- 'vue/html-indent': 'off', // Let Prettier handle indentation
33
- 'vue/max-attributes-per-line': 'off', // Let Prettier handle line breaks
34
- 'vue/first-attribute-linebreak': 'off', // Let Prettier handle attribute positioning
35
- 'vue/singleline-html-element-content-newline': 'off',
36
- 'vue/html-self-closing': [
37
- 'error',
38
- {
39
- html: {
40
- void: 'always',
41
- normal: 'always',
42
- component: 'always'
43
- }
44
- }
45
- ],
46
- 'vue/multi-word-component-names': 'off', // Disable multi-word name restriction
47
- 'vue/attribute-hyphenation': ['error', 'always']
65
+ '@typescript-eslint': tsPlugin
48
66
  }
49
67
  },
68
+ ...vueSpecificBlocks,
50
69
  {
51
- files: ['*.json'],
70
+ files: ['**/*.json'],
52
71
  languageOptions: {
53
72
  parser: jsoncParser
54
73
  },
55
- plugins: {
56
- prettier: pluginPrettier
57
- },
58
74
  rules: {
59
- quotes: ['error', 'double'], // Enforce double quotes in JSON
60
- 'prettier/prettier': 'error',
61
- '@typescript-eslint/no-unused-expressions': 'off',
62
- '@typescript-eslint/no-unused-vars': 'off'
75
+ quotes: ['error', 'double'] // Enforce double quotes in JSON
63
76
  }
64
77
  },
65
78
  {
@@ -79,15 +92,9 @@ export default [
79
92
  }
80
93
  },
81
94
  plugins: {
82
- prettier: pluginPrettier,
83
95
  import: pluginImport
84
96
  },
85
97
  rules: {
86
- indent: ['error', 'tab', { SwitchCase: 1 }], // Use tabs for JS/TS
87
- quotes: ['warn', 'single', { avoidEscape: true }], // Prefer single quotes
88
- semi: ['error', 'always'], // Enforce semicolons
89
- 'comma-dangle': 'off', // Disable trailing commas
90
- 'prettier/prettier': 'error', // Enforce Prettier rules
91
98
  'import/order': [
92
99
  'error',
93
100
  {
@@ -100,5 +107,90 @@ export default [
100
107
  '@typescript-eslint/no-unused-vars': ['warn'],
101
108
  '@typescript-eslint/no-require-imports': 'off'
102
109
  }
110
+ },
111
+ {
112
+ ...eslintConfigPrettier
103
113
  }
104
114
  ];
115
+
116
+ async function loadVueSupport() {
117
+ try {
118
+ const [vuePluginModule, vueConfigModule] = await Promise.all([
119
+ import('eslint-plugin-vue'),
120
+ import('@vue/eslint-config-typescript')
121
+ ]);
122
+
123
+ const pluginVue = unwrapDefault(vuePluginModule);
124
+ const { defineConfigWithVueTs, vueTsConfigs } = vueConfigModule;
125
+ const configs = defineConfigWithVueTs(vueTsConfigs.recommended);
126
+
127
+ return {
128
+ hasVueSupport: Boolean(pluginVue && configs.length),
129
+ pluginVue,
130
+ vueTypeScriptConfigs: configs
131
+ };
132
+ } catch (error) {
133
+ if (isModuleNotFoundError(error)) {
134
+ return {
135
+ hasVueSupport: false,
136
+ pluginVue: null,
137
+ vueTypeScriptConfigs: []
138
+ };
139
+ }
140
+
141
+ throw error;
142
+ }
143
+ }
144
+
145
+ function scopeVueConfigs(configs) {
146
+ return configs.map((config) => {
147
+ const files = config.files ?? [];
148
+ const referencesOnlyVueFiles = files.length > 0 && files.every((pattern) => pattern.includes('.vue'));
149
+ const hasVuePlugin = Boolean(config.plugins?.vue);
150
+
151
+ if (hasVuePlugin || referencesOnlyVueFiles) {
152
+ return {
153
+ ...config,
154
+ files: VUE_FILE_GLOBS
155
+ };
156
+ }
157
+
158
+ return {
159
+ ...config,
160
+ files: TS_FILE_GLOBS
161
+ };
162
+ });
163
+ }
164
+
165
+ function stripTypeScriptPlugin(config) {
166
+ const { plugins = {}, ...rest } = config;
167
+
168
+ if (!plugins['@typescript-eslint']) {
169
+ return config;
170
+ }
171
+
172
+ const otherPlugins = { ...plugins };
173
+ delete otherPlugins['@typescript-eslint'];
174
+ const hasOtherPlugins = Object.keys(otherPlugins).length > 0;
175
+
176
+ return {
177
+ ...rest,
178
+ ...(hasOtherPlugins ? { plugins: otherPlugins } : {})
179
+ };
180
+ }
181
+
182
+ function unwrapDefault(module) {
183
+ return module?.default ?? module;
184
+ }
185
+
186
+ function isModuleNotFoundError(error) {
187
+ if (!error) {
188
+ return false;
189
+ }
190
+
191
+ if (error.code === 'ERR_MODULE_NOT_FOUND' || error.code === 'MODULE_NOT_FOUND') {
192
+ return true;
193
+ }
194
+
195
+ return typeof error.message === 'string' && error.message.includes('Cannot find module');
196
+ }