tempest-react-sdk 0.25.0 → 0.26.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/README.md +14 -1
- package/bin/lib/alias/collect.mjs +94 -0
- package/bin/lib/alias/collect.test.mjs +95 -0
- package/bin/lib/alias/discover.mjs +77 -0
- package/bin/lib/alias/discover.test.mjs +189 -0
- package/bin/lib/alias/index.mjs +88 -0
- package/bin/lib/alias/index.test.mjs +138 -0
- package/bin/lib/alias/rewrite-css.mjs +76 -0
- package/bin/lib/alias/rewrite-css.test.mjs +85 -0
- package/bin/lib/alias/rewrite.mjs +141 -0
- package/bin/lib/alias/rewrite.test.mjs +148 -0
- package/bin/lib/alias/specifier.mjs +49 -0
- package/bin/lib/alias/specifier.test.mjs +85 -0
- package/bin/lib/alias/tsconfig.mjs +116 -0
- package/bin/lib/alias/typescript.mjs +23 -0
- package/bin/lib/doctor/lucide.mjs +74 -0
- package/bin/lib/doctor/lucide.test.mjs +107 -0
- package/bin/lib/icons/generate.mjs +156 -0
- package/bin/lib/icons/generate.test.mjs +125 -0
- package/bin/tempest.mjs +174 -13
- package/dist/audio/use-audio.cjs +1 -1
- package/dist/audio/use-audio.cjs.map +1 -1
- package/dist/audio/use-audio.js +12 -12
- package/dist/audio/use-audio.js.map +1 -1
- package/dist/charts/palette.cjs +1 -1
- package/dist/charts/palette.cjs.map +1 -1
- package/dist/charts/palette.js +2 -2
- package/dist/charts/palette.js.map +1 -1
- package/dist/charts.d.ts +5 -3
- package/dist/components/ClickOutside/ClickOutside.cjs +1 -1
- package/dist/components/ClickOutside/ClickOutside.cjs.map +1 -1
- package/dist/components/ClickOutside/ClickOutside.js +3 -1
- package/dist/components/ClickOutside/ClickOutside.js.map +1 -1
- package/dist/components/DataTable/DataTable.cjs +1 -1
- package/dist/components/DataTable/DataTable.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.js +21 -23
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/Kanban/Kanban.cjs +2 -0
- package/dist/components/Kanban/Kanban.cjs.map +1 -0
- package/dist/components/Kanban/Kanban.js +102 -0
- package/dist/components/Kanban/Kanban.js.map +1 -0
- package/dist/components/Kanban/Kanban.module.cjs +2 -0
- package/dist/components/Kanban/Kanban.module.cjs.map +1 -0
- package/dist/components/Kanban/Kanban.module.js +19 -0
- package/dist/components/Kanban/Kanban.module.js.map +1 -0
- package/dist/components/NotificationCenter/NotificationCenter.cjs +2 -0
- package/dist/components/NotificationCenter/NotificationCenter.cjs.map +1 -0
- package/dist/components/NotificationCenter/NotificationCenter.js +83 -0
- package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -0
- package/dist/components/NotificationCenter/NotificationCenter.module.cjs +2 -0
- package/dist/components/NotificationCenter/NotificationCenter.module.cjs.map +1 -0
- package/dist/components/NotificationCenter/NotificationCenter.module.js +21 -0
- package/dist/components/NotificationCenter/NotificationCenter.module.js.map +1 -0
- package/dist/components/NotificationCenter/use-notification-inbox.cjs +2 -0
- package/dist/components/NotificationCenter/use-notification-inbox.cjs.map +1 -0
- package/dist/components/NotificationCenter/use-notification-inbox.js +63 -0
- package/dist/components/NotificationCenter/use-notification-inbox.js.map +1 -0
- package/dist/components/RefreshIndicator/RefreshIndicator.cjs +1 -1
- package/dist/components/RefreshIndicator/RefreshIndicator.cjs.map +1 -1
- package/dist/components/RefreshIndicator/RefreshIndicator.js +17 -17
- package/dist/components/RefreshIndicator/RefreshIndicator.js.map +1 -1
- package/dist/components/Tooltip/Tooltip.cjs +1 -1
- package/dist/components/Tooltip/Tooltip.cjs.map +1 -1
- package/dist/components/Tooltip/Tooltip.js +26 -20
- package/dist/components/Tooltip/Tooltip.js.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.cjs +2 -0
- package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -0
- package/dist/components/VirtualTable/VirtualTable.js +116 -0
- package/dist/components/VirtualTable/VirtualTable.js.map +1 -0
- package/dist/components/VirtualTable/VirtualTable.module.cjs +2 -0
- package/dist/components/VirtualTable/VirtualTable.module.cjs.map +1 -0
- package/dist/components/VirtualTable/VirtualTable.module.js +20 -0
- package/dist/components/VirtualTable/VirtualTable.module.js.map +1 -0
- package/dist/components/Wizard/Wizard.cjs +1 -1
- package/dist/components/Wizard/Wizard.cjs.map +1 -1
- package/dist/components/Wizard/Wizard.js +53 -53
- package/dist/components/Wizard/Wizard.js.map +1 -1
- package/dist/geo/use-position-tracker.cjs +1 -1
- package/dist/geo/use-position-tracker.cjs.map +1 -1
- package/dist/geo/use-position-tracker.js +3 -1
- package/dist/geo/use-position-tracker.js.map +1 -1
- package/dist/hooks/use-async.cjs +1 -1
- package/dist/hooks/use-async.cjs.map +1 -1
- package/dist/hooks/use-async.js +3 -1
- package/dist/hooks/use-async.js.map +1 -1
- package/dist/hooks/use-click-outside.cjs +1 -1
- package/dist/hooks/use-click-outside.cjs.map +1 -1
- package/dist/hooks/use-click-outside.js +3 -1
- package/dist/hooks/use-click-outside.js.map +1 -1
- package/dist/hooks/use-deep-memo.cjs.map +1 -1
- package/dist/hooks/use-deep-memo.js.map +1 -1
- package/dist/hooks/use-event-listener.cjs +1 -1
- package/dist/hooks/use-event-listener.cjs.map +1 -1
- package/dist/hooks/use-event-listener.js +3 -1
- package/dist/hooks/use-event-listener.js.map +1 -1
- package/dist/hooks/use-interval.cjs +1 -1
- package/dist/hooks/use-interval.cjs.map +1 -1
- package/dist/hooks/use-interval.js +3 -1
- package/dist/hooks/use-interval.js.map +1 -1
- package/dist/hooks/use-is-first-render.cjs.map +1 -1
- package/dist/hooks/use-is-first-render.js.map +1 -1
- package/dist/hooks/use-long-press-handlers.cjs +1 -1
- package/dist/hooks/use-long-press-handlers.cjs.map +1 -1
- package/dist/hooks/use-long-press-handlers.js +25 -23
- package/dist/hooks/use-long-press-handlers.js.map +1 -1
- package/dist/hooks/use-long-press.cjs +1 -1
- package/dist/hooks/use-long-press.cjs.map +1 -1
- package/dist/hooks/use-long-press.js +3 -1
- package/dist/hooks/use-long-press.js.map +1 -1
- package/dist/hooks/use-online.cjs +1 -1
- package/dist/hooks/use-online.cjs.map +1 -1
- package/dist/hooks/use-online.js +6 -4
- package/dist/hooks/use-online.js.map +1 -1
- package/dist/hooks/use-previous.cjs.map +1 -1
- package/dist/hooks/use-previous.js.map +1 -1
- package/dist/hooks/use-service-worker-update.cjs +1 -1
- package/dist/hooks/use-service-worker-update.cjs.map +1 -1
- package/dist/hooks/use-service-worker-update.js +3 -1
- package/dist/hooks/use-service-worker-update.js.map +1 -1
- package/dist/hooks/use-sortable.cjs +2 -0
- package/dist/hooks/use-sortable.cjs.map +1 -0
- package/dist/hooks/use-sortable.js +169 -0
- package/dist/hooks/use-sortable.js.map +1 -0
- package/dist/hooks/use-stable-callback.cjs.map +1 -1
- package/dist/hooks/use-stable-callback.js.map +1 -1
- package/dist/hooks/use-timeout.cjs +1 -1
- package/dist/hooks/use-timeout.cjs.map +1 -1
- package/dist/hooks/use-timeout.js +3 -1
- package/dist/hooks/use-timeout.js.map +1 -1
- package/dist/icons/Icon.cjs +2 -0
- package/dist/icons/Icon.cjs.map +1 -0
- package/dist/icons/Icon.js +24 -0
- package/dist/icons/Icon.js.map +1 -0
- package/dist/icons/IconProvider.cjs +2 -0
- package/dist/icons/IconProvider.cjs.map +1 -0
- package/dist/icons/IconProvider.js +24 -0
- package/dist/icons/IconProvider.js.map +1 -0
- package/dist/icons/generated/aliases.cjs +2 -0
- package/dist/icons/generated/aliases.cjs.map +1 -0
- package/dist/icons/generated/aliases.js +255 -0
- package/dist/icons/generated/aliases.js.map +1 -0
- package/dist/icons/generated/icon-names.cjs +2 -0
- package/dist/icons/generated/icon-names.cjs.map +1 -0
- package/dist/icons/generated/icon-names.js +6 -0
- package/dist/icons/generated/icon-names.js.map +1 -0
- package/dist/icons/generated/loaders.cjs +2 -0
- package/dist/icons/generated/loaders.cjs.map +1 -0
- package/dist/icons/generated/loaders.js +32 -0
- package/dist/icons/generated/loaders.js.map +1 -0
- package/dist/icons/generated/shard-a.cjs +2 -0
- package/dist/icons/generated/shard-a.cjs.map +1 -0
- package/dist/icons/generated/shard-a.js +114 -0
- package/dist/icons/generated/shard-a.js.map +1 -0
- package/dist/icons/generated/shard-b.cjs +2 -0
- package/dist/icons/generated/shard-b.cjs.map +1 -0
- package/dist/icons/generated/shard-b.js +156 -0
- package/dist/icons/generated/shard-b.js.map +1 -0
- package/dist/icons/generated/shard-c.cjs +2 -0
- package/dist/icons/generated/shard-c.cjs.map +1 -0
- package/dist/icons/generated/shard-c.js +269 -0
- package/dist/icons/generated/shard-c.js.map +1 -0
- package/dist/icons/generated/shard-d.cjs +2 -0
- package/dist/icons/generated/shard-d.cjs.map +1 -0
- package/dist/icons/generated/shard-d.js +62 -0
- package/dist/icons/generated/shard-d.js.map +1 -0
- package/dist/icons/generated/shard-e.cjs +2 -0
- package/dist/icons/generated/shard-e.cjs.map +1 -0
- package/dist/icons/generated/shard-e.js +32 -0
- package/dist/icons/generated/shard-e.js.map +1 -0
- package/dist/icons/generated/shard-f.cjs +2 -0
- package/dist/icons/generated/shard-f.cjs.map +1 -0
- package/dist/icons/generated/shard-f.js +139 -0
- package/dist/icons/generated/shard-f.js.map +1 -0
- package/dist/icons/generated/shard-g.cjs +2 -0
- package/dist/icons/generated/shard-g.cjs.map +1 -0
- package/dist/icons/generated/shard-g.js +61 -0
- package/dist/icons/generated/shard-g.js.map +1 -0
- package/dist/icons/generated/shard-h.cjs +2 -0
- package/dist/icons/generated/shard-h.cjs.map +1 -0
- package/dist/icons/generated/shard-h.js +63 -0
- package/dist/icons/generated/shard-h.js.map +1 -0
- package/dist/icons/generated/shard-i.cjs +2 -0
- package/dist/icons/generated/shard-i.cjs.map +1 -0
- package/dist/icons/generated/shard-i.js +30 -0
- package/dist/icons/generated/shard-i.js.map +1 -0
- package/dist/icons/generated/shard-j.cjs +2 -0
- package/dist/icons/generated/shard-j.cjs.map +1 -0
- package/dist/icons/generated/shard-j.js +10 -0
- package/dist/icons/generated/shard-j.js.map +1 -0
- package/dist/icons/generated/shard-k.cjs +2 -0
- package/dist/icons/generated/shard-k.cjs.map +1 -0
- package/dist/icons/generated/shard-k.js +16 -0
- package/dist/icons/generated/shard-k.js.map +1 -0
- package/dist/icons/generated/shard-l.cjs +2 -0
- package/dist/icons/generated/shard-l.cjs.map +1 -0
- package/dist/icons/generated/shard-l.js +88 -0
- package/dist/icons/generated/shard-l.js.map +1 -0
- package/dist/icons/generated/shard-m.cjs +2 -0
- package/dist/icons/generated/shard-m.cjs.map +1 -0
- package/dist/icons/generated/shard-m.js +140 -0
- package/dist/icons/generated/shard-m.js.map +1 -0
- package/dist/icons/generated/shard-n.cjs +2 -0
- package/dist/icons/generated/shard-n.cjs.map +1 -0
- package/dist/icons/generated/shard-n.js +24 -0
- package/dist/icons/generated/shard-n.js.map +1 -0
- package/dist/icons/generated/shard-o.cjs +2 -0
- package/dist/icons/generated/shard-o.cjs.map +1 -0
- package/dist/icons/generated/shard-o.js +17 -0
- package/dist/icons/generated/shard-o.js.map +1 -0
- package/dist/icons/generated/shard-p.cjs +2 -0
- package/dist/icons/generated/shard-p.cjs.map +1 -0
- package/dist/icons/generated/shard-p.js +115 -0
- package/dist/icons/generated/shard-p.js.map +1 -0
- package/dist/icons/generated/shard-q.cjs +2 -0
- package/dist/icons/generated/shard-q.cjs.map +1 -0
- package/dist/icons/generated/shard-q.js +10 -0
- package/dist/icons/generated/shard-q.js.map +1 -0
- package/dist/icons/generated/shard-r.cjs +2 -0
- package/dist/icons/generated/shard-r.cjs.map +1 -0
- package/dist/icons/generated/shard-r.js +77 -0
- package/dist/icons/generated/shard-r.js.map +1 -0
- package/dist/icons/generated/shard-s.cjs +2 -0
- package/dist/icons/generated/shard-s.cjs.map +1 -0
- package/dist/icons/generated/shard-s.js +255 -0
- package/dist/icons/generated/shard-s.js.map +1 -0
- package/dist/icons/generated/shard-t.cjs +2 -0
- package/dist/icons/generated/shard-t.cjs.map +1 -0
- package/dist/icons/generated/shard-t.js +109 -0
- package/dist/icons/generated/shard-t.js.map +1 -0
- package/dist/icons/generated/shard-u.cjs +2 -0
- package/dist/icons/generated/shard-u.cjs.map +1 -0
- package/dist/icons/generated/shard-u.js +49 -0
- package/dist/icons/generated/shard-u.js.map +1 -0
- package/dist/icons/generated/shard-v.cjs +2 -0
- package/dist/icons/generated/shard-v.cjs.map +1 -0
- package/dist/icons/generated/shard-v.js +30 -0
- package/dist/icons/generated/shard-v.js.map +1 -0
- package/dist/icons/generated/shard-w.cjs +2 -0
- package/dist/icons/generated/shard-w.cjs.map +1 -0
- package/dist/icons/generated/shard-w.js +48 -0
- package/dist/icons/generated/shard-w.js.map +1 -0
- package/dist/icons/generated/shard-x.cjs +2 -0
- package/dist/icons/generated/shard-x.cjs.map +1 -0
- package/dist/icons/generated/shard-x.js +10 -0
- package/dist/icons/generated/shard-x.js.map +1 -0
- package/dist/icons/generated/shard-z.cjs +2 -0
- package/dist/icons/generated/shard-z.cjs.map +1 -0
- package/dist/icons/generated/shard-z.js +25 -0
- package/dist/icons/generated/shard-z.js.map +1 -0
- package/dist/icons/icon-context.cjs +2 -0
- package/dist/icons/icon-context.cjs.map +1 -0
- package/dist/icons/icon-context.js +13 -0
- package/dist/icons/icon-context.js.map +1 -0
- package/dist/icons/is-icon-name.cjs +2 -0
- package/dist/icons/is-icon-name.cjs.map +1 -0
- package/dist/icons/is-icon-name.js +10 -0
- package/dist/icons/is-icon-name.js.map +1 -0
- package/dist/icons/shard-cache.cjs +2 -0
- package/dist/icons/shard-cache.cjs.map +1 -0
- package/dist/icons/shard-cache.js +41 -0
- package/dist/icons/shard-cache.js.map +1 -0
- package/dist/icons/use-icon.cjs +2 -0
- package/dist/icons/use-icon.cjs.map +1 -0
- package/dist/icons/use-icon.js +20 -0
- package/dist/icons/use-icon.js.map +1 -0
- package/dist/icons-virtual.d.ts +22 -0
- package/dist/icons.cjs +1 -0
- package/dist/icons.d.ts +220 -0
- package/dist/icons.js +9 -0
- package/dist/offline/use-offline-sync.cjs +1 -1
- package/dist/offline/use-offline-sync.cjs.map +1 -1
- package/dist/offline/use-offline-sync.js +3 -1
- package/dist/offline/use-offline-sync.js.map +1 -1
- package/dist/push/use-push-subscription.cjs +1 -1
- package/dist/push/use-push-subscription.cjs.map +1 -1
- package/dist/push/use-push-subscription.js +3 -1
- package/dist/push/use-push-subscription.js.map +1 -1
- package/dist/sse/use-event-stream.cjs +1 -1
- package/dist/sse/use-event-stream.cjs.map +1 -1
- package/dist/sse/use-event-stream.js +3 -1
- package/dist/sse/use-event-stream.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +585 -3
- package/dist/tempest-react-sdk.js +189 -183
- package/dist/theme/ThemeProvider.cjs +1 -1
- package/dist/theme/ThemeProvider.cjs.map +1 -1
- package/dist/theme/ThemeProvider.js +6 -2
- package/dist/theme/ThemeProvider.js.map +1 -1
- package/dist/theme/color.cjs +1 -1
- package/dist/theme/color.cjs.map +1 -1
- package/dist/theme/color.js +77 -46
- package/dist/theme/color.js.map +1 -1
- package/dist/theme/create-theme.cjs +1 -1
- package/dist/theme/create-theme.cjs.map +1 -1
- package/dist/theme/create-theme.js +9 -3
- package/dist/theme/create-theme.js.map +1 -1
- package/dist/utils/compare-values.cjs +2 -0
- package/dist/utils/compare-values.cjs.map +1 -0
- package/dist/utils/compare-values.js +8 -0
- package/dist/utils/compare-values.js.map +1 -0
- package/dist/vision/use-camera-stream.cjs +1 -1
- package/dist/vision/use-camera-stream.cjs.map +1 -1
- package/dist/vision/use-camera-stream.js +3 -1
- package/dist/vision/use-camera-stream.js.map +1 -1
- package/dist/vite/create-vite-config.cjs +1 -1
- package/dist/vite/create-vite-config.cjs.map +1 -1
- package/dist/vite/create-vite-config.js +30 -22
- package/dist/vite/create-vite-config.js.map +1 -1
- package/dist/vite/tempest-icons.cjs +9 -0
- package/dist/vite/tempest-icons.cjs.map +1 -0
- package/dist/vite/tempest-icons.js +99 -0
- package/dist/vite/tempest-icons.js.map +1 -0
- package/dist/vite.cjs +1 -1
- package/dist/vite.d.ts +79 -0
- package/dist/vite.js +6 -5
- package/dist/ws/use-web-socket.cjs +1 -1
- package/dist/ws/use-web-socket.cjs.map +1 -1
- package/dist/ws/use-web-socket.js +3 -1
- package/dist/ws/use-web-socket.js.map +1 -1
- package/package.json +12 -3
- package/template/package.json +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { aliasImports } from "./index.mjs";
|
|
7
|
+
|
|
8
|
+
let root;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build a project fixture whose `typescript` resolves to this repo's copy.
|
|
12
|
+
*
|
|
13
|
+
* The symlinked `node_modules` is what makes `loadTypeScript` succeed from a
|
|
14
|
+
* directory outside the repo — the codemod deliberately refuses to fall back to
|
|
15
|
+
* a compiler the project does not have, so a fixture without it can only ever
|
|
16
|
+
* exercise the `no-typescript` branch.
|
|
17
|
+
*/
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
root = mkdtempSync(join(tmpdir(), "tempest-alias-run-"));
|
|
20
|
+
symlinkSync(join(process.cwd(), "node_modules"), join(root, "node_modules"), "dir");
|
|
21
|
+
mkdirSync(join(root, "src"), { recursive: true });
|
|
22
|
+
writeFileSync(
|
|
23
|
+
join(root, "tsconfig.json"),
|
|
24
|
+
JSON.stringify({ compilerOptions: { paths: { "@/*": ["./src/*"] } } }),
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
rmSync(root, { recursive: true, force: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** Write a fixture file, creating parent directories. */
|
|
33
|
+
function write(rel, contents) {
|
|
34
|
+
const path = join(root, rel);
|
|
35
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
36
|
+
writeFileSync(path, contents);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Read a fixture file back. */
|
|
40
|
+
function read(rel) {
|
|
41
|
+
return readFileSync(join(root, rel), "utf8");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const run = (targets = ["."], dryRun = false) => aliasImports({ root, targets, dryRun });
|
|
45
|
+
|
|
46
|
+
describe("aliasImports — writing", () => {
|
|
47
|
+
it("rewrites source and style files and reports the totals", () => {
|
|
48
|
+
write(
|
|
49
|
+
"src/pages/admin/Login.tsx",
|
|
50
|
+
`import { api } from "../../lib/api";\nimport "../x.css";\n`,
|
|
51
|
+
);
|
|
52
|
+
write("src/pages/admin/Login.module.css", `@import "../../styles/tokens.css";\n`);
|
|
53
|
+
|
|
54
|
+
const result = run();
|
|
55
|
+
|
|
56
|
+
expect(result.status).toBe("ok");
|
|
57
|
+
expect(result.prefix).toBe("@");
|
|
58
|
+
expect(result.total).toBe(3);
|
|
59
|
+
expect(result.files.map((f) => f.path.split("\\").join("/")).sort()).toEqual([
|
|
60
|
+
"src/pages/admin/Login.module.css",
|
|
61
|
+
"src/pages/admin/Login.tsx",
|
|
62
|
+
]);
|
|
63
|
+
expect(read("src/pages/admin/Login.tsx")).toBe(
|
|
64
|
+
`import { api } from "@/lib/api";\nimport "@/pages/x.css";\n`,
|
|
65
|
+
);
|
|
66
|
+
expect(read("src/pages/admin/Login.module.css")).toBe(`@import "@/styles/tokens.css";\n`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("leaves files it must not touch alone", () => {
|
|
70
|
+
write("src/pages/admin/Login.tsx", `import { Row } from "./Row";\n`);
|
|
71
|
+
write("src/pages/admin/Deep.tsx", `import cfg from "../../../vite.config";\n`);
|
|
72
|
+
write("vite.config.ts", `import x from "./src/lib/api";\n`);
|
|
73
|
+
write("e2e/spec.ts", `import { api } from "../src/lib/api";\n`);
|
|
74
|
+
|
|
75
|
+
const result = run();
|
|
76
|
+
|
|
77
|
+
expect(result.total).toBe(0);
|
|
78
|
+
expect(result.files).toEqual([]);
|
|
79
|
+
expect(read("src/pages/admin/Deep.tsx")).toBe(`import cfg from "../../../vite.config";\n`);
|
|
80
|
+
expect(read("vite.config.ts")).toBe(`import x from "./src/lib/api";\n`);
|
|
81
|
+
expect(read("e2e/spec.ts")).toBe(`import { api } from "../src/lib/api";\n`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("is idempotent across two runs", () => {
|
|
85
|
+
write("src/pages/admin/Login.tsx", `import { api } from "../../lib/api";\n`);
|
|
86
|
+
expect(run().total).toBe(1);
|
|
87
|
+
const after = read("src/pages/admin/Login.tsx");
|
|
88
|
+
expect(run().total).toBe(0);
|
|
89
|
+
expect(read("src/pages/admin/Login.tsx")).toBe(after);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("restricts the rewrite to a positional path", () => {
|
|
93
|
+
write("src/pages/admin/Login.tsx", `import { api } from "../../lib/api";\n`);
|
|
94
|
+
write("src/widgets/nested/Card.tsx", `import { api } from "../../lib/api";\n`);
|
|
95
|
+
|
|
96
|
+
expect(run(["src/pages"]).total).toBe(1);
|
|
97
|
+
expect(read("src/widgets/nested/Card.tsx")).toBe(`import { api } from "../../lib/api";\n`);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("aliasImports — dry run", () => {
|
|
102
|
+
it("reports the changes without writing", () => {
|
|
103
|
+
const before = `import { api } from "../../lib/api";\n`;
|
|
104
|
+
write("src/pages/admin/Login.tsx", before);
|
|
105
|
+
|
|
106
|
+
const result = run(["."], true);
|
|
107
|
+
|
|
108
|
+
expect(result.total).toBe(1);
|
|
109
|
+
expect(result.files[0].changes).toEqual([
|
|
110
|
+
{ from: "../../lib/api", to: "@/lib/api", line: 1 },
|
|
111
|
+
]);
|
|
112
|
+
expect(read("src/pages/admin/Login.tsx")).toBe(before);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("aliasImports — degraded projects", () => {
|
|
117
|
+
it("reports no-typescript when the project has no compiler", () => {
|
|
118
|
+
const bare = mkdtempSync(join(tmpdir(), "tempest-alias-bare-"));
|
|
119
|
+
try {
|
|
120
|
+
expect(aliasImports({ root: bare, targets: ["."] })).toMatchObject({
|
|
121
|
+
status: "no-typescript",
|
|
122
|
+
total: 0,
|
|
123
|
+
files: [],
|
|
124
|
+
});
|
|
125
|
+
} finally {
|
|
126
|
+
rmSync(bare, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("reports no-alias and writes nothing when no tsconfig declares one", () => {
|
|
131
|
+
rmSync(join(root, "tsconfig.json"));
|
|
132
|
+
const before = `import { api } from "../../lib/api";\n`;
|
|
133
|
+
write("src/pages/admin/Login.tsx", before);
|
|
134
|
+
|
|
135
|
+
expect(run()).toMatchObject({ status: "no-alias", total: 0, files: [] });
|
|
136
|
+
expect(read("src/pages/admin/Login.tsx")).toBe(before);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Rewrite relative paths inside CSS to the project alias. Vite resolves aliases
|
|
2
|
+
// in stylesheets too, but the TypeScript AST pass cannot see `.css` files.
|
|
3
|
+
import { isInsideBase, toAlias } from "./specifier.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `url(...)` in any position — `@import url("x")`, `background: url(x)`.
|
|
7
|
+
* Group 1 is everything before the value, group 2 the optional quote, group 3 the
|
|
8
|
+
* value, so the value's offset is derivable without searching inside the match.
|
|
9
|
+
*/
|
|
10
|
+
const URL_PATTERN = /(url\(\s*)(["']?)([^"')]+)\2(\s*\))/g;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Quoted `@import` without `url()`. The quote must follow the whitespace
|
|
14
|
+
* directly, which is what keeps this from also matching the `url()` form above
|
|
15
|
+
* and rewriting the same span twice.
|
|
16
|
+
*/
|
|
17
|
+
const IMPORT_PATTERN = /(@import\s+)(["'])([^"']+)\2/g;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Collect the rewritable value spans of a stylesheet.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} text - Stylesheet contents.
|
|
23
|
+
* @returns {Array<{ start: number, end: number, value: string }>}
|
|
24
|
+
*/
|
|
25
|
+
function collectSpans(text) {
|
|
26
|
+
const spans = [];
|
|
27
|
+
for (const pattern of [URL_PATTERN, IMPORT_PATTERN]) {
|
|
28
|
+
pattern.lastIndex = 0;
|
|
29
|
+
let match;
|
|
30
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
31
|
+
const start = match.index + match[1].length + match[2].length;
|
|
32
|
+
spans.push({ start, end: start + match[3].length, value: match[3] });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return spans.sort((a, b) => a.start - b.start);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Rewrite the relative paths of one stylesheet to the alias form.
|
|
40
|
+
*
|
|
41
|
+
* Only values that climb out of the file's directory *and* land inside the alias
|
|
42
|
+
* base are converted, so `data:`, `http:` and fragment URLs need no special
|
|
43
|
+
* casing — none of them starts with `../`.
|
|
44
|
+
*
|
|
45
|
+
* Pure, and back-to-front like the TypeScript pass, for the same reason: earlier
|
|
46
|
+
* offsets must stay valid while later edits are applied.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} params
|
|
49
|
+
* @param {string} params.text - Stylesheet contents.
|
|
50
|
+
* @param {string} params.filePath - Absolute path of the file.
|
|
51
|
+
* @param {string} params.prefix - Alias prefix, e.g. `"@"`.
|
|
52
|
+
* @param {string} params.baseDir - Absolute alias base directory.
|
|
53
|
+
* @returns {{ text: string, changes: Array<{ from: string, to: string, line: number }> }}
|
|
54
|
+
*/
|
|
55
|
+
export function rewriteCss({ text, filePath, prefix, baseDir }) {
|
|
56
|
+
if (!isInsideBase(filePath, baseDir)) return { text, changes: [] };
|
|
57
|
+
|
|
58
|
+
const edits = [];
|
|
59
|
+
const changes = [];
|
|
60
|
+
for (const { start, end, value } of collectSpans(text)) {
|
|
61
|
+
const to = toAlias({ spec: value, filePath, prefix, baseDir });
|
|
62
|
+
if (!to) continue;
|
|
63
|
+
edits.push({ start, end, to });
|
|
64
|
+
changes.push({
|
|
65
|
+
from: value,
|
|
66
|
+
to,
|
|
67
|
+
line: text.slice(0, start).split("\n").length,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let out = text;
|
|
72
|
+
for (const { start, end, to } of edits.reverse()) {
|
|
73
|
+
out = `${out.slice(0, start)}${to}${out.slice(end)}`;
|
|
74
|
+
}
|
|
75
|
+
return { text: out, changes };
|
|
76
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { rewriteCss } from "./rewrite-css.mjs";
|
|
4
|
+
|
|
5
|
+
const BASE = "/proj/src";
|
|
6
|
+
const FILE = "/proj/src/components/Card/Card.module.css";
|
|
7
|
+
|
|
8
|
+
const rewrite = (text, filePath = FILE) =>
|
|
9
|
+
rewriteCss({ text, filePath, prefix: "@", baseDir: BASE });
|
|
10
|
+
|
|
11
|
+
describe("rewriteCss — @import", () => {
|
|
12
|
+
it("converts a quoted @import", () => {
|
|
13
|
+
const { text, changes } = rewrite(`@import "../../styles/tokens.css";\n`);
|
|
14
|
+
expect(text).toBe(`@import "@/styles/tokens.css";\n`);
|
|
15
|
+
expect(changes).toEqual([
|
|
16
|
+
{ from: "../../styles/tokens.css", to: "@/styles/tokens.css", line: 1 },
|
|
17
|
+
]);
|
|
18
|
+
});
|
|
19
|
+
it("converts @import url(...) exactly once", () => {
|
|
20
|
+
const { text, changes } = rewrite(`@import url("../../styles/tokens.css");\n`);
|
|
21
|
+
expect(text).toBe(`@import url("@/styles/tokens.css");\n`);
|
|
22
|
+
expect(changes).toHaveLength(1);
|
|
23
|
+
});
|
|
24
|
+
it("preserves single quotes", () => {
|
|
25
|
+
const { text } = rewrite(`@import '../../styles/tokens.css';\n`);
|
|
26
|
+
expect(text).toBe(`@import '@/styles/tokens.css';\n`);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("rewriteCss — url()", () => {
|
|
31
|
+
it("converts an unquoted url()", () => {
|
|
32
|
+
const { text } = rewrite(`.a { background: url(../../assets/bg.png); }\n`);
|
|
33
|
+
expect(text).toBe(`.a { background: url(@/assets/bg.png); }\n`);
|
|
34
|
+
});
|
|
35
|
+
it("converts a quoted url()", () => {
|
|
36
|
+
const { text } = rewrite(`.a { background: url("../../assets/bg.png"); }\n`);
|
|
37
|
+
expect(text).toBe(`.a { background: url("@/assets/bg.png"); }\n`);
|
|
38
|
+
});
|
|
39
|
+
it("converts several url() on one line without sliding offsets", () => {
|
|
40
|
+
const { text } = rewrite(
|
|
41
|
+
`.a { background: url(../../a.png); border-image: url(../../b.png); }\n`,
|
|
42
|
+
);
|
|
43
|
+
expect(text).toBe(`.a { background: url(@/a.png); border-image: url(@/b.png); }\n`);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("rewriteCss — what stays", () => {
|
|
48
|
+
it("leaves data:, http: and fragment URLs alone", () => {
|
|
49
|
+
const src = `.a { background: url(data:image/png;base64,AAA); }\n.b { background: url("https://x/y.png"); }\n.c { fill: url(#grad); }\n`;
|
|
50
|
+
expect(rewrite(src).text).toBe(src);
|
|
51
|
+
});
|
|
52
|
+
it("leaves a sibling path alone", () => {
|
|
53
|
+
const src = `@import "./tokens.css";\n.a { background: url(./bg.png); }\n`;
|
|
54
|
+
expect(rewrite(src).text).toBe(src);
|
|
55
|
+
});
|
|
56
|
+
it("leaves a root-absolute path alone", () => {
|
|
57
|
+
const src = `.a { background: url(/icon.svg); }\n`;
|
|
58
|
+
expect(rewrite(src).text).toBe(src);
|
|
59
|
+
});
|
|
60
|
+
it("leaves a target outside the alias base alone", () => {
|
|
61
|
+
const src = `.a { background: url(../../../public/bg.png); }\n`;
|
|
62
|
+
expect(rewrite(src).text).toBe(src);
|
|
63
|
+
});
|
|
64
|
+
it("leaves a file outside the alias base untouched", () => {
|
|
65
|
+
const src = `@import "../src/styles/tokens.css";\n`;
|
|
66
|
+
const { text, changes } = rewrite(src, "/proj/e2e/fixture.css");
|
|
67
|
+
expect(text).toBe(src);
|
|
68
|
+
expect(changes).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("rewriteCss — mechanics", () => {
|
|
73
|
+
it("reports the line of each change", () => {
|
|
74
|
+
const { changes } = rewrite(
|
|
75
|
+
`@import "../../styles/a.css";\n\n.x { background: url(../../assets/b.png); }\n`,
|
|
76
|
+
);
|
|
77
|
+
expect(changes.map((ch) => ch.line)).toEqual([1, 3]);
|
|
78
|
+
});
|
|
79
|
+
it("is idempotent", () => {
|
|
80
|
+
const first = rewrite(`@import "../../styles/tokens.css";\n`);
|
|
81
|
+
const second = rewrite(first.text);
|
|
82
|
+
expect(second.text).toBe(first.text);
|
|
83
|
+
expect(second.changes).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Rewrite relative module specifiers to the project alias, driven by the
|
|
2
|
+
// TypeScript AST so only real specifier positions are touched.
|
|
3
|
+
import { extname } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { isInsideBase, toAlias } from "./specifier.mjs";
|
|
6
|
+
|
|
7
|
+
const SCRIPT_KIND_BY_EXT = {
|
|
8
|
+
".ts": "TS",
|
|
9
|
+
".mts": "TS",
|
|
10
|
+
".cts": "TS",
|
|
11
|
+
".tsx": "TSX",
|
|
12
|
+
".js": "JS",
|
|
13
|
+
".mjs": "JS",
|
|
14
|
+
".cjs": "JS",
|
|
15
|
+
".jsx": "JSX",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const MOCK_METHODS = new Set(["mock", "doMock", "unmock", "doUnmock"]);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Pick the `ts.ScriptKind` for a file so JSX parses as JSX.
|
|
22
|
+
*
|
|
23
|
+
* Getting this wrong is silent: parsed with `ScriptKind.TS`, a `.tsx` file's
|
|
24
|
+
* `<Foo />` becomes a type assertion and the specifiers after it are never
|
|
25
|
+
* visited.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} filePath
|
|
28
|
+
* @param {object} ts - The `typescript` module.
|
|
29
|
+
* @returns {number} A `ts.ScriptKind` value.
|
|
30
|
+
*/
|
|
31
|
+
function scriptKindFor(filePath, ts) {
|
|
32
|
+
return ts.ScriptKind[SCRIPT_KIND_BY_EXT[extname(filePath)] ?? "TSX"];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* True for a `vi.mock(...)`-family call expression.
|
|
37
|
+
*
|
|
38
|
+
* The mock path has to travel with the import it shadows: Vitest matches mocks
|
|
39
|
+
* by resolved id, so converting the import while leaving the mock relative keeps
|
|
40
|
+
* working, but a half-converted *pair* in a file someone later moves is exactly
|
|
41
|
+
* the silent-mock failure this avoids.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} node - A `ts.CallExpression`.
|
|
44
|
+
* @param {object} ts - The `typescript` module.
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
function isViMockCall(node, ts) {
|
|
48
|
+
const target = node.expression;
|
|
49
|
+
return (
|
|
50
|
+
ts.isPropertyAccessExpression(target) &&
|
|
51
|
+
ts.isIdentifier(target.expression) &&
|
|
52
|
+
target.expression.text === "vi" &&
|
|
53
|
+
ts.isIdentifier(target.name) &&
|
|
54
|
+
MOCK_METHODS.has(target.name.text)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Collect every string literal that sits in a module-specifier position.
|
|
60
|
+
*
|
|
61
|
+
* This is the whole reason the codemod parses instead of running a regex: a path
|
|
62
|
+
* inside a comment, a template literal, a plain variable or a `vi.mock` call with
|
|
63
|
+
* a computed argument never reaches this list.
|
|
64
|
+
*
|
|
65
|
+
* @param {object} sourceFile - A `ts.SourceFile`.
|
|
66
|
+
* @param {object} ts - The `typescript` module.
|
|
67
|
+
* @returns {object[]} The `ts.StringLiteral` nodes, in source order.
|
|
68
|
+
*/
|
|
69
|
+
function collectSpecifiers(sourceFile, ts) {
|
|
70
|
+
const found = [];
|
|
71
|
+
const push = (node) => {
|
|
72
|
+
if (node && ts.isStringLiteral(node)) found.push(node);
|
|
73
|
+
};
|
|
74
|
+
const visit = (node) => {
|
|
75
|
+
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
|
|
76
|
+
push(node.moduleSpecifier);
|
|
77
|
+
} else if (ts.isImportTypeNode(node)) {
|
|
78
|
+
if (ts.isLiteralTypeNode(node.argument)) push(node.argument.literal);
|
|
79
|
+
} else if (ts.isCallExpression(node)) {
|
|
80
|
+
const isDynamicImport = node.expression.kind === ts.SyntaxKind.ImportKeyword;
|
|
81
|
+
if (isDynamicImport || isViMockCall(node, ts)) push(node.arguments[0]);
|
|
82
|
+
}
|
|
83
|
+
ts.forEachChild(node, visit);
|
|
84
|
+
};
|
|
85
|
+
visit(sourceFile);
|
|
86
|
+
return found;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Rewrite the relative specifiers of one source file to the alias form.
|
|
91
|
+
*
|
|
92
|
+
* Pure: takes text, returns text. Nothing is read from or written to disk, which
|
|
93
|
+
* is what makes the rule itself testable without a fixture tree.
|
|
94
|
+
*
|
|
95
|
+
* Edits are applied back-to-front so that earlier offsets stay valid, and the
|
|
96
|
+
* original quote character is reused so the pass never fights Prettier over
|
|
97
|
+
* quote style. Formatting and import order are deliberately left alone — the
|
|
98
|
+
* `eslint --fix` and Prettier passes that follow own those.
|
|
99
|
+
*
|
|
100
|
+
* Files outside `baseDir` are returned untouched: see `isInsideBase`.
|
|
101
|
+
*
|
|
102
|
+
* @param {object} params
|
|
103
|
+
* @param {string} params.text - File contents.
|
|
104
|
+
* @param {string} params.filePath - Absolute path of the file.
|
|
105
|
+
* @param {string} params.prefix - Alias prefix, e.g. `"@"`.
|
|
106
|
+
* @param {string} params.baseDir - Absolute alias base directory.
|
|
107
|
+
* @param {object} params.ts - The `typescript` module.
|
|
108
|
+
* @returns {{ text: string, changes: Array<{ from: string, to: string, line: number }> }}
|
|
109
|
+
*/
|
|
110
|
+
export function rewriteSource({ text, filePath, prefix, baseDir, ts }) {
|
|
111
|
+
if (!isInsideBase(filePath, baseDir)) return { text, changes: [] };
|
|
112
|
+
|
|
113
|
+
const sourceFile = ts.createSourceFile(
|
|
114
|
+
filePath,
|
|
115
|
+
text,
|
|
116
|
+
ts.ScriptTarget.Latest,
|
|
117
|
+
true,
|
|
118
|
+
scriptKindFor(filePath, ts),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const edits = [];
|
|
122
|
+
const changes = [];
|
|
123
|
+
for (const literal of collectSpecifiers(sourceFile, ts)) {
|
|
124
|
+
const to = toAlias({ spec: literal.text, filePath, prefix, baseDir });
|
|
125
|
+
if (!to) continue;
|
|
126
|
+
const start = literal.getStart(sourceFile);
|
|
127
|
+
const end = literal.getEnd();
|
|
128
|
+
edits.push({ start, end, quote: text[start], to });
|
|
129
|
+
changes.push({
|
|
130
|
+
from: literal.text,
|
|
131
|
+
to,
|
|
132
|
+
line: sourceFile.getLineAndCharacterOfPosition(start).line + 1,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let out = text;
|
|
137
|
+
for (const { start, end, quote, to } of edits.reverse()) {
|
|
138
|
+
out = `${out.slice(0, start)}${quote}${to}${quote}${out.slice(end)}`;
|
|
139
|
+
}
|
|
140
|
+
return { text: out, changes };
|
|
141
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { rewriteSource } from "./rewrite.mjs";
|
|
4
|
+
import { loadTypeScript } from "./typescript.mjs";
|
|
5
|
+
|
|
6
|
+
const ts = loadTypeScript(process.cwd());
|
|
7
|
+
const BASE = "/proj/src";
|
|
8
|
+
const FILE = "/proj/src/pages/admin/Users.tsx";
|
|
9
|
+
|
|
10
|
+
const rewrite = (text, filePath = FILE) =>
|
|
11
|
+
rewriteSource({ text, filePath, prefix: "@", baseDir: BASE, ts });
|
|
12
|
+
|
|
13
|
+
describe("rewriteSource — import / export positions", () => {
|
|
14
|
+
it("converts a value import", () => {
|
|
15
|
+
const { text, changes } = rewrite(`import { api } from "../../lib/api";\n`);
|
|
16
|
+
expect(text).toBe(`import { api } from "@/lib/api";\n`);
|
|
17
|
+
expect(changes).toEqual([{ from: "../../lib/api", to: "@/lib/api", line: 1 }]);
|
|
18
|
+
});
|
|
19
|
+
it("converts a type-only import", () => {
|
|
20
|
+
const { text } = rewrite(`import type { User } from "../../types/user";\n`);
|
|
21
|
+
expect(text).toBe(`import type { User } from "@/types/user";\n`);
|
|
22
|
+
});
|
|
23
|
+
it("converts a side-effect import", () => {
|
|
24
|
+
const { text } = rewrite(`import "../../styles/global.css";\n`);
|
|
25
|
+
expect(text).toBe(`import "@/styles/global.css";\n`);
|
|
26
|
+
});
|
|
27
|
+
it("converts a re-export", () => {
|
|
28
|
+
const { text } = rewrite(`export { api } from "../../lib/api";\nexport * from "../x";\n`);
|
|
29
|
+
expect(text).toBe(`export { api } from "@/lib/api";\nexport * from "@/pages/x";\n`);
|
|
30
|
+
});
|
|
31
|
+
it("converts an inline import type node", () => {
|
|
32
|
+
const { text } = rewrite(`type A = import("../../types/user").User;\n`);
|
|
33
|
+
expect(text).toBe(`type A = import("@/types/user").User;\n`);
|
|
34
|
+
});
|
|
35
|
+
it("converts a dynamic import", () => {
|
|
36
|
+
const { text } = rewrite(`const m = await import("../../pages/Dashboard");\n`);
|
|
37
|
+
expect(text).toBe(`const m = await import("@/pages/Dashboard");\n`);
|
|
38
|
+
});
|
|
39
|
+
it("converts vi.mock and its paired import together", () => {
|
|
40
|
+
const { text, changes } = rewrite(
|
|
41
|
+
`import { api } from "../../lib/api";\nvi.mock("../../lib/api");\n`,
|
|
42
|
+
);
|
|
43
|
+
expect(text).toBe(`import { api } from "@/lib/api";\nvi.mock("@/lib/api");\n`);
|
|
44
|
+
expect(changes.map((ch) => ch.line)).toEqual([1, 2]);
|
|
45
|
+
});
|
|
46
|
+
it("converts vi.doMock", () => {
|
|
47
|
+
const { text } = rewrite(`vi.doMock("../../lib/api", () => ({}));\n`);
|
|
48
|
+
expect(text).toBe(`vi.doMock("@/lib/api", () => ({}));\n`);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("rewriteSource — positions it must not touch", () => {
|
|
53
|
+
it("ignores a path in a plain variable", () => {
|
|
54
|
+
const src = `const p = "../../lib/api";\n`;
|
|
55
|
+
expect(rewrite(src).text).toBe(src);
|
|
56
|
+
});
|
|
57
|
+
it("ignores a path in a line comment", () => {
|
|
58
|
+
const src = `// see "../../lib/api"\nconst a = 1;\n`;
|
|
59
|
+
expect(rewrite(src).text).toBe(src);
|
|
60
|
+
});
|
|
61
|
+
it("ignores a path in a block comment above an import", () => {
|
|
62
|
+
const src = `/* was "../../lib/old" */\nimport { api } from "../../lib/api";\n`;
|
|
63
|
+
expect(rewrite(src).text).toBe(
|
|
64
|
+
`/* was "../../lib/old" */\nimport { api } from "@/lib/api";\n`,
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
it("ignores a template literal", () => {
|
|
68
|
+
const src = "const p = `../../lib/api`;\n";
|
|
69
|
+
expect(rewrite(src).text).toBe(src);
|
|
70
|
+
});
|
|
71
|
+
it("ignores a dynamic import with an interpolated specifier", () => {
|
|
72
|
+
const src = "const m = await import(`../../pages/${name}`);\n";
|
|
73
|
+
expect(rewrite(src).text).toBe(src);
|
|
74
|
+
});
|
|
75
|
+
it("ignores a dynamic import with a computed specifier", () => {
|
|
76
|
+
const src = `const m = await import(path);\n`;
|
|
77
|
+
expect(rewrite(src).text).toBe(src);
|
|
78
|
+
});
|
|
79
|
+
it("ignores require()", () => {
|
|
80
|
+
const src = `const api = require("../../lib/api");\n`;
|
|
81
|
+
expect(rewrite(src).text).toBe(src);
|
|
82
|
+
});
|
|
83
|
+
it("ignores a mock-looking call on another object", () => {
|
|
84
|
+
const src = `jest.mock("../../lib/api");\n`;
|
|
85
|
+
expect(rewrite(src).text).toBe(src);
|
|
86
|
+
});
|
|
87
|
+
it("ignores a sibling import", () => {
|
|
88
|
+
const src = `import { Row } from "./Row";\nimport css from "./Users.module.css";\n`;
|
|
89
|
+
expect(rewrite(src).text).toBe(src);
|
|
90
|
+
});
|
|
91
|
+
it("ignores a target outside the alias base", () => {
|
|
92
|
+
const src = `import cfg from "../../../vite.config";\n`;
|
|
93
|
+
expect(rewrite(src).text).toBe(src);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("rewriteSource — file scope", () => {
|
|
98
|
+
it("leaves a file outside the alias base untouched", () => {
|
|
99
|
+
const src = `import { x } from "../src/lib/api";\n`;
|
|
100
|
+
const { text, changes } = rewrite(src, "/proj/e2e/spec.ts");
|
|
101
|
+
expect(text).toBe(src);
|
|
102
|
+
expect(changes).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("rewriteSource — mechanics", () => {
|
|
107
|
+
it("preserves single quotes", () => {
|
|
108
|
+
const { text } = rewrite(`import { api } from '../../lib/api';\n`);
|
|
109
|
+
expect(text).toBe(`import { api } from '@/lib/api';\n`);
|
|
110
|
+
});
|
|
111
|
+
it("rewrites several specifiers on one line without sliding offsets", () => {
|
|
112
|
+
const { text } = rewrite(
|
|
113
|
+
`import { a } from "../../lib/a";import { b } from "../../lib/b";\n`,
|
|
114
|
+
);
|
|
115
|
+
expect(text).toBe(`import { a } from "@/lib/a";import { b } from "@/lib/b";\n`);
|
|
116
|
+
});
|
|
117
|
+
it("reports the line of each change", () => {
|
|
118
|
+
const { changes } = rewrite(
|
|
119
|
+
`import { a } from "../../lib/a";\n\n\nimport { b } from "../../lib/b";\n`,
|
|
120
|
+
);
|
|
121
|
+
expect(changes.map((ch) => ch.line)).toEqual([1, 4]);
|
|
122
|
+
});
|
|
123
|
+
it("does not reorder or reformat anything", () => {
|
|
124
|
+
const { text } = rewrite(
|
|
125
|
+
`import {z} from "../../lib/z";\nimport {a} from "../../lib/a";\n`,
|
|
126
|
+
);
|
|
127
|
+
expect(text).toBe(`import {z} from "@/lib/z";\nimport {a} from "@/lib/a";\n`);
|
|
128
|
+
});
|
|
129
|
+
it("is idempotent", () => {
|
|
130
|
+
const first = rewrite(`import { api } from "../../lib/api";\n`);
|
|
131
|
+
const second = rewrite(first.text);
|
|
132
|
+
expect(second.text).toBe(first.text);
|
|
133
|
+
expect(second.changes).toEqual([]);
|
|
134
|
+
});
|
|
135
|
+
it("parses JSX without losing the specifiers after it", () => {
|
|
136
|
+
const { text } = rewrite(
|
|
137
|
+
`const el = <div className="x" />;\nimport { api } from "../../lib/api";\n`,
|
|
138
|
+
);
|
|
139
|
+
expect(text).toContain(`"@/lib/api"`);
|
|
140
|
+
});
|
|
141
|
+
it("parses a .ts file where < is a comparison", () => {
|
|
142
|
+
const { text } = rewrite(
|
|
143
|
+
`const ok = a < b;\nimport { api } from "../../lib/api";\n`,
|
|
144
|
+
"/proj/src/pages/admin/users.ts",
|
|
145
|
+
);
|
|
146
|
+
expect(text).toContain(`"@/lib/api"`);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The single rule both rewriters share: when does a relative specifier become an
|
|
2
|
+
// alias specifier, and what does it become.
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* True when `filePath` sits inside `baseDir`.
|
|
7
|
+
*
|
|
8
|
+
* The gate exists because `@/` only resolves for files the bundler and the
|
|
9
|
+
* type-checker process through the alias. A file outside the base — a
|
|
10
|
+
* `vite.config.ts`, a script under `scripts/` — would get a specifier that
|
|
11
|
+
* resolves nowhere, and `vite.config.ts` in particular is loaded *without* the
|
|
12
|
+
* aliases it declares.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} filePath - Absolute file path.
|
|
15
|
+
* @param {string} baseDir - Absolute alias base directory.
|
|
16
|
+
* @returns {boolean}
|
|
17
|
+
*/
|
|
18
|
+
export function isInsideBase(filePath, baseDir) {
|
|
19
|
+
const rel = relative(baseDir, filePath);
|
|
20
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert one relative specifier to its alias form, or report that it stays.
|
|
25
|
+
*
|
|
26
|
+
* Only specifiers that climb out of their own directory are converted: a sibling
|
|
27
|
+
* `./x` already reads as "next to this file", which is information the alias
|
|
28
|
+
* form throws away. A specifier that resolves *outside* the alias base is left
|
|
29
|
+
* alone — that is what keeps `../../vite.config` and `../../../scripts/x`
|
|
30
|
+
* intact.
|
|
31
|
+
*
|
|
32
|
+
* The extension is never touched: whatever the specifier carried (`.module.css`,
|
|
33
|
+
* `.svg`, nothing at all) survives, because resolution of extensionless imports
|
|
34
|
+
* is the bundler's business, not this codemod's.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} params
|
|
37
|
+
* @param {string} params.spec - The original specifier text.
|
|
38
|
+
* @param {string} params.filePath - Absolute path of the importing file.
|
|
39
|
+
* @param {string} params.prefix - Alias prefix, e.g. `"@"`.
|
|
40
|
+
* @param {string} params.baseDir - Absolute alias base directory.
|
|
41
|
+
* @returns {string | null} The alias specifier, or `null` to leave it as is.
|
|
42
|
+
*/
|
|
43
|
+
export function toAlias({ spec, filePath, prefix, baseDir }) {
|
|
44
|
+
if (!spec.startsWith("../")) return null;
|
|
45
|
+
const target = resolve(dirname(filePath), spec);
|
|
46
|
+
const rel = relative(baseDir, target);
|
|
47
|
+
if (!rel || rel.startsWith("..") || isAbsolute(rel)) return null;
|
|
48
|
+
return `${prefix}/${rel.split(sep).join("/")}`;
|
|
49
|
+
}
|