Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sour-bananas-tease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/package': patch
---

fix: resolve more alias issues
39 changes: 21 additions & 18 deletions packages/package/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ export function resolve_aliases(input, file, content, aliases) {
*/
const replace_import_path = (match, quote, import_path) => {
for (const [alias, value] of Object.entries(aliases)) {
if (import_path !== alias && !import_path.startsWith(alias + '/')) continue;
if (
import_path !== alias &&
!import_path.startsWith(alias + (alias.endsWith('/') ? '' : '/'))
) {
continue;
}

const full_path = path.join(input, file);
const full_import_path = path.join(value, import_path.slice(alias.length));
Expand All @@ -33,24 +38,22 @@ export function resolve_aliases(input, file, content, aliases) {
return match;
};

// import/export ... from ...
// import/export (type) (xxx | xxx,) { ... } from ...
content = content.replace(
// Regex parts for import/export ... from ... statements:
// 1. \b(import|export) - Match 'import' or 'export' at a word boundary
// 2. (?:\s+type)? - Optionally match ' type'
// 3. \s+ - At least one whitespace
// 4. ( - Start of specifier group
// (?:(?:\*\s+as\s+)?\p{L}[\p{L}0-9]*\s+) - default import/export, e.g. 'name', or named star import/export, e.g. '* as name '
// | (?:\*\s+) - e.g. star import/export, e.g. '* '
// | (?:(?:\p{L}[\p{L}0-9]*\s*,\s*)?\{[^}]*\}\s*) - e.g. named imports/exports with optional default import/export, e.g. 'name, { ... }' or '{ ... }'
// )
// 5. from\s* - Match 'from' with optional whitespace
// 6. (['"]) - Capture quote
// 7. ([^'";]+) - Capture import path
// 8. \3 - Match the same quote as before
/\b(import|export)(?:\s+type)?\s+((?:(?:\*\s+as\s+)?\p{L}[\p{L}0-9]*\s+)|(?:\*\s+)|(?:(?:\p{L}[\p{L}0-9]*\s*,\s*)?\{[^}]*\}\s*))from\s*(['"])([^'";]+)\3/gmu,
(match, _keyword, _specifier, quote, import_path) =>
replace_import_path(match, quote, import_path)
/\b(?:import|export)(?:\s+type)?(?:(?:\s+\p{L}[\p{L}0-9]*\s+)|(?:(?:\s+\p{L}[\p{L}0-9]*\s*,\s*)?\s*\{[^}]*\}\s*))from\s*(['"])([^'";]+)\1/gmu,
(match, quote, import_path) => replace_import_path(match, quote, import_path)
);

// import/export (type) * as xxx from ...
content = content.replace(
/\b(?:import|export)(?:\s+type)?\s*\*\s*as\s+\p{L}[\p{L}0-9]*\s+from\s*(['"])([^'";]+)\1/gmu,
(match, quote, import_path) => replace_import_path(match, quote, import_path)
);

// export (type) * from ...
content = content.replace(
/\b(?:export)(?:\s+type)?\s*\*\s*from\s*(['"])([^'";]+)\1/gmu,
(match, quote, import_path) => replace_import_path(match, quote, import_path)
);

// import(...)
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

2 changes: 0 additions & 2 deletions packages/package/test/fixtures/resolve-alias/expected/baz.js

This file was deleted.

10 changes: 0 additions & 10 deletions packages/package/test/fixtures/resolve-alias/expected/index.d.ts

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

11 changes: 0 additions & 11 deletions packages/package/test/fixtures/resolve-alias/expected/sub/bar.js

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

16 changes: 0 additions & 16 deletions packages/package/test/fixtures/resolve-alias/package.json

This file was deleted.

This file was deleted.

7 changes: 0 additions & 7 deletions packages/package/test/fixtures/resolve-alias/src/lib/baz.ts

This file was deleted.

11 changes: 0 additions & 11 deletions packages/package/test/fixtures/resolve-alias/src/lib/index.ts

This file was deleted.

This file was deleted.

13 changes: 0 additions & 13 deletions packages/package/test/fixtures/resolve-alias/src/lib/sub/bar.ts

This file was deleted.

This file was deleted.

This file was deleted.

15 changes: 0 additions & 15 deletions packages/package/test/fixtures/resolve-alias/svelte.config.js

This file was deleted.

13 changes: 0 additions & 13 deletions packages/package/test/fixtures/resolve-alias/tsconfig.json

This file was deleted.

105 changes: 101 additions & 4 deletions packages/package/test/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { build, watch } from '../src/index.js';
import { load_config } from '../src/config.js';
import { rimraf, walk } from '../src/filesystem.js';
import { _create_validator } from '../src/validate.js';
import { resolve_aliases } from '../src/utils.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = join(__filename, '..');
Expand Down Expand Up @@ -157,10 +158,6 @@ test('create package with SvelteComponentTyped for backwards compatibility', asy
await test_make_package('svelte-3-types');
});

test('create package and resolves $lib alias', async () => {
await test_make_package('resolve-alias');
});

test('SvelteKit interop', async () => {
await test_make_package('svelte-kit');
});
Expand Down Expand Up @@ -351,3 +348,103 @@ test('create package with preserved output', async () => {
fs.writeFileSync(join(output, 'assets', 'theme.css'), ':root { color: red }');
await test_make_package('preserve-output', { preserve_output: true });
});

test('resolves aliases correctly', () => {
const input = '/project/src/lib';
const file = 'components/Button.svelte';
const alias = { $lib: '/project/src/lib/components', '@/': '/project/src/' };

// Test all static import variants
const source = `
// Static imports
import Button from '$lib/Button.svelte';
import { named } from '$lib/utils.js';
import { named1, named2 } from '$lib/utils.js';
import defaultExport, { named } from '$lib/utils.js';
import * as All from '$lib/utils.js';
import { foo } from '@/foo.js';

// Import types
import type { TypedInterface } from '$lib/types.js';
import type DefaultType from '$lib/types.js';
import type DefaultType, { TypedInterface } from '$lib/types.js';
import { type TypedInterface } from '$lib/types.js';
import defaultExport, { type TypedInterface } from '$lib/types.js';

// Export re-exports
export { reexported } from '$lib/utils.js';
export { reexported1, reexported2 } from '$lib/utils.js';
export * from '$lib/utils.js';
export * as NamedExport from '$lib/utils.js';
export type { TypeExport } from '$lib/types.js';

// Side-effect imports
import '$lib/styles.css';
import '$lib/polyfill.js';

// Dynamic imports
const dynamicImport = import('$lib/dynamic.js');
const dynamicWithAwait = await import('$lib/async.js');
const dynamicInFunction = () => import('$lib/function.js');

// False positives that should NOT be replaced
const notAnImport = "This string contains $lib/fake.js but is not an import";
const inString = 'The path $lib/fake.js should not be changed';
const invalidSyntax = 'import $lib/invalid without quotes';
const partialMatch = '$library/notmatching.js';
import * as AllWithDefault, { named } from '$lib/utils.js';

// Edge cases with whitespace and formatting
import Button2 from '$lib/Button2.svelte';
import{named3}from'$lib/utils2.js';
import( '$lib/dynamic2.js' );
`;

const expectedResolved = `
// Static imports
import Button from './Button.svelte';
import { named } from './utils.js';
import { named1, named2 } from './utils.js';
import defaultExport, { named } from './utils.js';
import * as All from './utils.js';
import { foo } from '../../foo.js';

// Import types
import type { TypedInterface } from './types.js';
import type DefaultType from './types.js';
import type DefaultType, { TypedInterface } from './types.js';
import { type TypedInterface } from './types.js';
import defaultExport, { type TypedInterface } from './types.js';

// Export re-exports
export { reexported } from './utils.js';
export { reexported1, reexported2 } from './utils.js';
export * from './utils.js';
export * as NamedExport from './utils.js';
export type { TypeExport } from './types.js';

// Side-effect imports
import './styles.css';
import './polyfill.js';

// Dynamic imports
const dynamicImport = import('./dynamic.js');
const dynamicWithAwait = await import('./async.js');
const dynamicInFunction = () => import('./function.js');

// False positives that should NOT be replaced
const notAnImport = "This string contains $lib/fake.js but is not an import";
const inString = 'The path $lib/fake.js should not be changed';
const invalidSyntax = 'import $lib/invalid without quotes';
const partialMatch = '$library/notmatching.js';
import * as AllWithDefault, { named } from '$lib/utils.js';

// Edge cases with whitespace and formatting
import Button2 from './Button2.svelte';
import{named3}from'./utils2.js';
import( './dynamic2.js' );
`;

const resolved = resolve_aliases(input, file, source, alias);
expect(resolved.trim()).toBe(expectedResolved.trim());
});
Loading