@dialekt/adapter-laravel 0.1.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/TESTING.md +29 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.mjs +173 -0
- package/package.json +27 -0
- package/src/adapter.test.ts +179 -0
- package/src/adapter.ts +140 -0
- package/src/index.ts +2 -0
- package/src/php-array-writer.test.ts +58 -0
- package/src/php-array-writer.ts +38 -0
- package/src/resources.test.ts +59 -0
- package/src/resources.ts +78 -0
- package/src/unused-keys.test.ts +78 -0
- package/src/unused-keys.ts +70 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.ts +7 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
import { FileSystem, Path } from '@effect/platform';
|
|
4
|
+
import { NodePlatformLayer } from 'dialekt';
|
|
5
|
+
import { listLaravelLocales, listLaravelResources } from './resources.js';
|
|
6
|
+
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
|
|
10
|
+
describe('listLaravelLocales', () => {
|
|
11
|
+
it('detects locales from directory structure', async () => {
|
|
12
|
+
const dir = join(tmpdir(), `laravel-locales-${Date.now()}`);
|
|
13
|
+
mkdirSync(join(dir, 'en'), { recursive: true });
|
|
14
|
+
mkdirSync(join(dir, 'de'), { recursive: true });
|
|
15
|
+
mkdirSync(join(dir, 'es'), { recursive: true });
|
|
16
|
+
|
|
17
|
+
const program = listLaravelLocales(dir).pipe(Effect.provide(NodePlatformLayer));
|
|
18
|
+
const result = await Effect.runPromise(program);
|
|
19
|
+
expect(result).toContain('en');
|
|
20
|
+
expect(result).toContain('de');
|
|
21
|
+
expect(result).toContain('es');
|
|
22
|
+
|
|
23
|
+
rmSync(dir, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('excludes vendor and lang directories', async () => {
|
|
27
|
+
const dir = join(tmpdir(), `laravel-locales-exclude-${Date.now()}`);
|
|
28
|
+
mkdirSync(join(dir, 'en'), { recursive: true });
|
|
29
|
+
mkdirSync(join(dir, 'vendor'), { recursive: true });
|
|
30
|
+
mkdirSync(join(dir, 'lang'), { recursive: true });
|
|
31
|
+
|
|
32
|
+
const program = listLaravelLocales(dir).pipe(Effect.provide(NodePlatformLayer));
|
|
33
|
+
const result = await Effect.runPromise(program);
|
|
34
|
+
expect(result).toContain('en');
|
|
35
|
+
expect(result).not.toContain('vendor');
|
|
36
|
+
expect(result).not.toContain('lang');
|
|
37
|
+
|
|
38
|
+
rmSync(dir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('listLaravelResources', () => {
|
|
43
|
+
it('lists PHP domain files and JSON file', async () => {
|
|
44
|
+
const dir = join(tmpdir(), `laravel-resources-${Date.now()}`);
|
|
45
|
+
mkdirSync(join(dir, 'en'), { recursive: true });
|
|
46
|
+
writeFileSync(join(dir, 'en', 'validation.php'), '<?php return [];');
|
|
47
|
+
writeFileSync(join(dir, 'en', 'auth.php'), '<?php return [];');
|
|
48
|
+
writeFileSync(join(dir, 'en.json'), '{}');
|
|
49
|
+
|
|
50
|
+
const program = listLaravelResources(dir, 'en').pipe(Effect.provide(NodePlatformLayer));
|
|
51
|
+
const result = await Effect.runPromise(program);
|
|
52
|
+
const keys = result.map((r) => r.key);
|
|
53
|
+
expect(keys).toContain('validation');
|
|
54
|
+
expect(keys).toContain('auth');
|
|
55
|
+
expect(keys).toContain('json');
|
|
56
|
+
|
|
57
|
+
rmSync(dir, { recursive: true, force: true });
|
|
58
|
+
});
|
|
59
|
+
});
|
package/src/resources.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { FileSystem, Path } from '@effect/platform';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
import type { ResourceRef, AdapterReadError } from 'dialekt';
|
|
4
|
+
import { AdapterReadError as AdapterReadErrorClass } from 'dialekt';
|
|
5
|
+
|
|
6
|
+
export function listLaravelLocales(langDir: string) {
|
|
7
|
+
return Effect.gen(function* () {
|
|
8
|
+
const fs = yield* FileSystem.FileSystem;
|
|
9
|
+
const path = yield* Path.Path;
|
|
10
|
+
const entries = yield* fs.readDirectory(langDir).pipe(
|
|
11
|
+
Effect.orElseSucceed(() => [] as string[]),
|
|
12
|
+
);
|
|
13
|
+
const locales: string[] = [];
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
const fullPath = path.join(langDir, entry);
|
|
16
|
+
const stat = yield* fs.stat(fullPath).pipe(Effect.option);
|
|
17
|
+
if (
|
|
18
|
+
stat._tag === 'Some' &&
|
|
19
|
+
stat.value.type === 'Directory' &&
|
|
20
|
+
entry !== 'vendor' &&
|
|
21
|
+
entry !== 'lang'
|
|
22
|
+
) {
|
|
23
|
+
locales.push(entry);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return locales;
|
|
27
|
+
}).pipe(
|
|
28
|
+
Effect.mapError(
|
|
29
|
+
(cause) =>
|
|
30
|
+
new AdapterReadErrorClass({
|
|
31
|
+
adapter: 'laravel',
|
|
32
|
+
locale: '',
|
|
33
|
+
resource: '',
|
|
34
|
+
cause,
|
|
35
|
+
}) as AdapterReadError,
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function listLaravelResources(langDir: string, locale: string) {
|
|
41
|
+
return Effect.gen(function* () {
|
|
42
|
+
const fs = yield* FileSystem.FileSystem;
|
|
43
|
+
const path = yield* Path.Path;
|
|
44
|
+
const localeDir = path.join(langDir, locale);
|
|
45
|
+
|
|
46
|
+
const refs: ResourceRef[] = [];
|
|
47
|
+
|
|
48
|
+
// PHP domain files
|
|
49
|
+
const entries = yield* fs.readDirectory(localeDir).pipe(
|
|
50
|
+
Effect.orElseSucceed(() => [] as string[]),
|
|
51
|
+
);
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (entry.endsWith('.php')) {
|
|
54
|
+
const domain = entry.replace(/\.php$/, '');
|
|
55
|
+
refs.push({ key: domain, label: domain });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// JSON locale file (e.g. en.json)
|
|
60
|
+
const jsonPath = path.join(langDir, `${locale}.json`);
|
|
61
|
+
const jsonExists = yield* fs.exists(jsonPath).pipe(Effect.orElseSucceed(() => false));
|
|
62
|
+
if (jsonExists) {
|
|
63
|
+
refs.push({ key: 'json', label: `${locale}.json` });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return refs;
|
|
67
|
+
}).pipe(
|
|
68
|
+
Effect.mapError(
|
|
69
|
+
(cause) =>
|
|
70
|
+
new AdapterReadErrorClass({
|
|
71
|
+
adapter: 'laravel',
|
|
72
|
+
locale,
|
|
73
|
+
resource: '',
|
|
74
|
+
cause,
|
|
75
|
+
}) as AdapterReadError,
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
import { FileSystem, Path } from '@effect/platform';
|
|
4
|
+
import { NodePlatformLayer } from 'dialekt';
|
|
5
|
+
import { findUnusedLaravelKeys } from './unused-keys.js';
|
|
6
|
+
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
|
|
10
|
+
describe('findUnusedLaravelKeys', () => {
|
|
11
|
+
it('finds keys not referenced in source files', async () => {
|
|
12
|
+
const dir = join(tmpdir(), `laravel-unused-${Date.now()}`);
|
|
13
|
+
mkdirSync(join(dir, 'views'), { recursive: true });
|
|
14
|
+
writeFileSync(
|
|
15
|
+
join(dir, 'views', 'email.blade.php'),
|
|
16
|
+
"<div>{{ __('validation.email') }}</div>",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const program = findUnusedLaravelKeys([dir], 'validation', ['email', 'password']).pipe(
|
|
20
|
+
Effect.provide(NodePlatformLayer),
|
|
21
|
+
);
|
|
22
|
+
const result = await Effect.runPromise(program);
|
|
23
|
+
expect(result).toEqual(['password']);
|
|
24
|
+
|
|
25
|
+
rmSync(dir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('finds @lang references too', async () => {
|
|
29
|
+
const dir = join(tmpdir(), `laravel-unused-lang-${Date.now()}`);
|
|
30
|
+
mkdirSync(join(dir, 'views'), { recursive: true });
|
|
31
|
+
writeFileSync(
|
|
32
|
+
join(dir, 'views', 'page.blade.php'),
|
|
33
|
+
"@lang('validation.password')",
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const program = findUnusedLaravelKeys([dir], 'validation', ['email', 'password']).pipe(
|
|
37
|
+
Effect.provide(NodePlatformLayer),
|
|
38
|
+
);
|
|
39
|
+
const result = await Effect.runPromise(program);
|
|
40
|
+
expect(result).toEqual(['email']);
|
|
41
|
+
|
|
42
|
+
rmSync(dir, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('finds trans() references too', async () => {
|
|
46
|
+
const dir = join(tmpdir(), `laravel-unused-trans-${Date.now()}`);
|
|
47
|
+
mkdirSync(join(dir, 'app'), { recursive: true });
|
|
48
|
+
writeFileSync(
|
|
49
|
+
join(dir, 'app', 'Controller.php'),
|
|
50
|
+
"trans('validation.email');",
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const program = findUnusedLaravelKeys([dir], 'validation', ['email', 'password']).pipe(
|
|
54
|
+
Effect.provide(NodePlatformLayer),
|
|
55
|
+
);
|
|
56
|
+
const result = await Effect.runPromise(program);
|
|
57
|
+
expect(result).toEqual(['password']);
|
|
58
|
+
|
|
59
|
+
rmSync(dir, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('does not treat substrings as matches', async () => {
|
|
63
|
+
const dir = join(tmpdir(), `laravel-unused-substr-${Date.now()}`);
|
|
64
|
+
mkdirSync(join(dir, 'views'), { recursive: true });
|
|
65
|
+
writeFileSync(
|
|
66
|
+
join(dir, 'views', 'page.blade.php'),
|
|
67
|
+
"__('validation.email_longer')",
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const program = findUnusedLaravelKeys([dir], 'validation', ['email', 'email_longer']).pipe(
|
|
71
|
+
Effect.provide(NodePlatformLayer),
|
|
72
|
+
);
|
|
73
|
+
const result = await Effect.runPromise(program);
|
|
74
|
+
expect(result).toEqual(['email']);
|
|
75
|
+
|
|
76
|
+
rmSync(dir, { recursive: true, force: true });
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { FileSystem, Path } from '@effect/platform';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
import type { AdapterReadError } from 'dialekt';
|
|
4
|
+
import { AdapterReadError as AdapterReadErrorClass } from 'dialekt';
|
|
5
|
+
|
|
6
|
+
export function findUnusedLaravelKeys(
|
|
7
|
+
scanPaths: readonly string[],
|
|
8
|
+
domain: string,
|
|
9
|
+
keys: readonly string[],
|
|
10
|
+
) {
|
|
11
|
+
return Effect.gen(function* () {
|
|
12
|
+
const fs = yield* FileSystem.FileSystem;
|
|
13
|
+
const path = yield* Path.Path;
|
|
14
|
+
|
|
15
|
+
// Collect all referenced domain.key strings from source files.
|
|
16
|
+
const referenced = new Set<string>();
|
|
17
|
+
|
|
18
|
+
for (const scanPath of scanPaths) {
|
|
19
|
+
const exists = yield* fs.exists(scanPath).pipe(Effect.orElseSucceed(() => false));
|
|
20
|
+
if (!exists) continue;
|
|
21
|
+
|
|
22
|
+
const entries = yield* fs.readDirectory(scanPath, { recursive: true }).pipe(
|
|
23
|
+
Effect.orElseSucceed(() => [] as string[]),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
for (const relativePath of entries) {
|
|
27
|
+
if (!relativePath.endsWith('.php') && !relativePath.endsWith('.blade.php')) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const filePath = path.join(scanPath, relativePath);
|
|
31
|
+
const content = yield* fs.readFileString(filePath).pipe(Effect.orElseSucceed(() => ''));
|
|
32
|
+
|
|
33
|
+
// Extract all quoted string literals from the file content.
|
|
34
|
+
const quotedStrings: string[] = [];
|
|
35
|
+
const patterns = [
|
|
36
|
+
/'((?:[^'\\]|\\.)*)'/g,
|
|
37
|
+
/"((?:[^"\\]|\\.)*)"/g,
|
|
38
|
+
];
|
|
39
|
+
for (const pattern of patterns) {
|
|
40
|
+
let m: RegExpExecArray | null;
|
|
41
|
+
while ((m = pattern.exec(content)) !== null) {
|
|
42
|
+
if (m[1] !== undefined) quotedStrings.push(m[1]!);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const str of quotedStrings) {
|
|
47
|
+
for (const key of keys) {
|
|
48
|
+
const fullKey = `${domain}.${key}`;
|
|
49
|
+
// Match literal fullKey as a whole string (not substring).
|
|
50
|
+
if (str === fullKey) {
|
|
51
|
+
referenced.add(key);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return keys.filter((key) => !referenced.has(key));
|
|
59
|
+
}).pipe(
|
|
60
|
+
Effect.mapError(
|
|
61
|
+
(cause) =>
|
|
62
|
+
new AdapterReadErrorClass({
|
|
63
|
+
adapter: 'laravel',
|
|
64
|
+
locale: '',
|
|
65
|
+
resource: domain,
|
|
66
|
+
cause,
|
|
67
|
+
}) as AdapterReadError,
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
}
|