@inlang/paraglide-js 2.26.0 → 2.28.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/README.md +11 -1
- package/dist/bundler-plugins/per-locale-build/index.js +14 -7
- package/dist/bundler-plugins/per-locale-build/index.js.map +1 -1
- package/dist/bundler-plugins/per-locale-build/vite-environments.test.js +237 -4
- package/dist/bundler-plugins/per-locale-build/vite-environments.test.js.map +1 -1
- package/dist/bundler-plugins/unplugin.d.ts +1 -1
- package/dist/bundler-plugins/unplugin.js +79 -10
- package/dist/bundler-plugins/unplugin.js.map +1 -1
- package/dist/bundler-plugins/unplugin.test.js +334 -1
- package/dist/bundler-plugins/unplugin.test.js.map +1 -1
- package/dist/bundler-plugins/vite.js +2 -1
- package/dist/bundler-plugins/vite.js.map +1 -1
- package/dist/bundler-plugins/vite.test.js +1 -1
- package/dist/bundler-plugins/vite.test.js.map +1 -1
- package/dist/cli/commands/compile/command.js +13 -0
- package/dist/cli/commands/compile/command.js.map +1 -1
- package/dist/cli/commands/compile/command.test.js +87 -3
- package/dist/cli/commands/compile/command.test.js.map +1 -1
- package/dist/cli/commands/init/command.d.ts +5 -0
- package/dist/cli/commands/init/command.js +216 -44
- package/dist/cli/commands/init/command.js.map +1 -1
- package/dist/cli/commands/init/command.test.d.ts +1 -0
- package/dist/cli/commands/init/command.test.js +123 -0
- package/dist/cli/commands/init/command.test.js.map +1 -0
- package/dist/cli/defaults.js +2 -2
- package/dist/cli/defaults.js.map +1 -1
- package/dist/cli/init-options.d.ts +11 -0
- package/dist/cli/init-options.js +2 -0
- package/dist/cli/init-options.js.map +1 -0
- package/dist/cli/steps/add-inlang-check.d.ts +19 -0
- package/dist/cli/steps/add-inlang-check.js +60 -0
- package/dist/cli/steps/add-inlang-check.js.map +1 -0
- package/dist/cli/steps/add-inlang-check.test.d.ts +1 -0
- package/dist/cli/steps/add-inlang-check.test.js +88 -0
- package/dist/cli/steps/add-inlang-check.test.js.map +1 -0
- package/dist/cli/steps/add-message-lint.d.ts +34 -0
- package/dist/cli/steps/add-message-lint.js +105 -0
- package/dist/cli/steps/add-message-lint.js.map +1 -0
- package/dist/cli/steps/add-message-lint.test.d.ts +1 -0
- package/dist/cli/steps/add-message-lint.test.js +74 -0
- package/dist/cli/steps/add-message-lint.test.js.map +1 -0
- package/dist/cli/steps/add-vite-plugin.d.ts +18 -0
- package/dist/cli/steps/add-vite-plugin.js +34 -6
- package/dist/cli/steps/add-vite-plugin.js.map +1 -1
- package/dist/cli/steps/add-vite-plugin.test.js +25 -9
- package/dist/cli/steps/add-vite-plugin.test.js.map +1 -1
- package/dist/cli/steps/agents-md-suggestion.d.ts +11 -0
- package/dist/cli/steps/agents-md-suggestion.js +19 -0
- package/dist/cli/steps/agents-md-suggestion.js.map +1 -0
- package/dist/cli/steps/agents-md-suggestion.test.d.ts +1 -0
- package/dist/cli/steps/agents-md-suggestion.test.js +36 -0
- package/dist/cli/steps/agents-md-suggestion.test.js.map +1 -0
- package/dist/cli/steps/check-for-uncomitted-changes.d.ts +3 -0
- package/dist/cli/steps/check-for-uncomitted-changes.js +15 -3
- package/dist/cli/steps/check-for-uncomitted-changes.js.map +1 -1
- package/dist/cli/steps/detect-bundler.d.ts +1 -0
- package/dist/cli/steps/detect-bundler.js +7 -1
- package/dist/cli/steps/detect-bundler.js.map +1 -1
- package/dist/cli/steps/initialize-inlang-project.d.ts +4 -3
- package/dist/cli/steps/initialize-inlang-project.js +60 -157
- package/dist/cli/steps/initialize-inlang-project.js.map +1 -1
- package/dist/cli/steps/maybe-add-machine-translation.d.ts +4 -0
- package/dist/cli/steps/maybe-add-machine-translation.js +14 -8
- package/dist/cli/steps/maybe-add-machine-translation.js.map +1 -1
- package/dist/cli/steps/maybe-add-sherlock.d.ts +2 -0
- package/dist/cli/steps/maybe-add-sherlock.js +13 -7
- package/dist/cli/steps/maybe-add-sherlock.js.map +1 -1
- package/dist/cli/steps/maybe-add-sherlock.test.d.ts +1 -0
- package/dist/cli/steps/maybe-add-sherlock.test.js +44 -0
- package/dist/cli/steps/maybe-add-sherlock.test.js.map +1 -0
- package/dist/cli/steps/prompt-for-outdir.d.ts +2 -0
- package/dist/cli/steps/prompt-for-outdir.js +4 -0
- package/dist/cli/steps/prompt-for-outdir.js.map +1 -1
- package/dist/cli/steps/update-package-json.js +8 -13
- package/dist/cli/steps/update-package-json.js.map +1 -1
- package/dist/cli/steps/update-ts-config.d.ts +6 -0
- package/dist/cli/steps/update-ts-config.js +27 -4
- package/dist/cli/steps/update-ts-config.js.map +1 -1
- package/dist/cli/utils.d.ts +11 -0
- package/dist/cli/utils.js +21 -0
- package/dist/cli/utils.js.map +1 -1
- package/dist/cli/utils.test.d.ts +1 -0
- package/dist/cli/utils.test.js +16 -0
- package/dist/cli/utils.test.js.map +1 -0
- package/dist/compiler/compile-annotation.d.ts +48 -1
- package/dist/compiler/compile-annotation.js +243 -10
- package/dist/compiler/compile-annotation.js.map +1 -1
- package/dist/compiler/compile-bundle.d.ts +7 -2
- package/dist/compiler/compile-bundle.js +97 -3
- package/dist/compiler/compile-bundle.js.map +1 -1
- package/dist/compiler/compile-bundle.test.js +232 -19
- package/dist/compiler/compile-bundle.test.js.map +1 -1
- package/dist/compiler/compile-lifecycle.test.d.ts +1 -0
- package/dist/compiler/compile-lifecycle.test.js +69 -0
- package/dist/compiler/compile-lifecycle.test.js.map +1 -0
- package/dist/compiler/compile-local-variable.d.ts +3 -0
- package/dist/compiler/compile-local-variable.js +5 -2
- package/dist/compiler/compile-local-variable.js.map +1 -1
- package/dist/compiler/compile-local-variable.test.js +55 -15
- package/dist/compiler/compile-local-variable.test.js.map +1 -1
- package/dist/compiler/compile-message.d.ts +29 -1
- package/dist/compiler/compile-message.exact-number.test.d.ts +1 -0
- package/dist/compiler/compile-message.exact-number.test.js +786 -0
- package/dist/compiler/compile-message.exact-number.test.js.map +1 -0
- package/dist/compiler/compile-message.js +162 -66
- package/dist/compiler/compile-message.js.map +1 -1
- package/dist/compiler/compile-message.locals.test.d.ts +1 -0
- package/dist/compiler/compile-message.locals.test.js +498 -0
- package/dist/compiler/compile-message.locals.test.js.map +1 -0
- package/dist/compiler/compile-message.test.js +113 -22
- package/dist/compiler/compile-message.test.js.map +1 -1
- package/dist/compiler/compile-pattern.d.ts +5 -0
- package/dist/compiler/compile-pattern.js +6 -18
- package/dist/compiler/compile-pattern.js.map +1 -1
- package/dist/compiler/compile-pattern.test.js +169 -5
- package/dist/compiler/compile-pattern.test.js.map +1 -1
- package/dist/compiler/compile-project.js +5 -1
- package/dist/compiler/compile-project.js.map +1 -1
- package/dist/compiler/compile-project.test.js +1260 -185
- package/dist/compiler/compile-project.test.js.map +1 -1
- package/dist/compiler/compile.js +36 -17
- package/dist/compiler/compile.js.map +1 -1
- package/dist/compiler/compile.test.js +71 -1
- package/dist/compiler/compile.test.js.map +1 -1
- package/dist/compiler/compiler-options.d.ts +41 -0
- package/dist/compiler/compiler-options.js.map +1 -1
- package/dist/compiler/create-readme.d.ts +4 -1
- package/dist/compiler/create-readme.js +33 -125
- package/dist/compiler/create-readme.js.map +1 -1
- package/dist/compiler/emit-ts-declarations.interop.test.js +2 -2
- package/dist/compiler/emit-ts-declarations.interop.test.js.map +1 -1
- package/dist/compiler/emit-ts-declarations.test.js +4 -4
- package/dist/compiler/emit-ts-declarations.test.js.map +1 -1
- package/dist/compiler/index.d.ts +1 -1
- package/dist/compiler/index.js +1 -1
- package/dist/compiler/index.js.map +1 -1
- package/dist/compiler/input-alias.d.ts +21 -0
- package/dist/compiler/input-alias.js +42 -0
- package/dist/compiler/input-alias.js.map +1 -0
- package/dist/compiler/input-alias.test.d.ts +1 -0
- package/dist/compiler/input-alias.test.js +58 -0
- package/dist/compiler/input-alias.test.js.map +1 -0
- package/dist/compiler/message-locals.d.ts +46 -0
- package/dist/compiler/message-locals.js +129 -0
- package/dist/compiler/message-locals.js.map +1 -0
- package/dist/compiler/output-structure/locale-modules.js +7 -1
- package/dist/compiler/output-structure/locale-modules.js.map +1 -1
- package/dist/compiler/output-structure/locale-modules.test.js +35 -0
- package/dist/compiler/output-structure/locale-modules.test.js.map +1 -1
- package/dist/compiler/output-structure/message-modules.js +6 -4
- package/dist/compiler/output-structure/message-modules.js.map +1 -1
- package/dist/compiler/output-structure/message-modules.test.js +35 -0
- package/dist/compiler/output-structure/message-modules.test.js.map +1 -1
- package/dist/compiler/registry.d.ts +12 -0
- package/dist/compiler/registry.js +158 -31
- package/dist/compiler/registry.js.map +1 -1
- package/dist/compiler/registry.test.d.ts +1 -0
- package/dist/compiler/registry.test.js +291 -0
- package/dist/compiler/registry.test.js.map +1 -0
- package/dist/compiler/runtime/create-runtime.d.ts +2 -0
- package/dist/compiler/runtime/create-runtime.js +9 -0
- package/dist/compiler/runtime/create-runtime.js.map +1 -1
- package/dist/compiler/runtime/extract-locale-from-cookie.js +8 -1
- package/dist/compiler/runtime/extract-locale-from-cookie.js.map +1 -1
- package/dist/compiler/runtime/extract-locale-from-cookie.test.js +26 -0
- package/dist/compiler/runtime/extract-locale-from-cookie.test.js.map +1 -1
- package/dist/compiler/runtime/extract-locale-from-header.js +47 -5
- package/dist/compiler/runtime/extract-locale-from-header.js.map +1 -1
- package/dist/compiler/runtime/extract-locale-from-header.test.js +58 -0
- package/dist/compiler/runtime/extract-locale-from-header.test.js.map +1 -1
- package/dist/compiler/runtime/get-locale.js +7 -1
- package/dist/compiler/runtime/get-locale.js.map +1 -1
- package/dist/compiler/runtime/get-locale.test.js +95 -0
- package/dist/compiler/runtime/get-locale.test.js.map +1 -1
- package/dist/compiler/runtime/localize-href.d.ts +8 -0
- package/dist/compiler/runtime/localize-href.js +60 -0
- package/dist/compiler/runtime/localize-href.js.map +1 -1
- package/dist/compiler/runtime/localize-href.test.js +227 -1
- package/dist/compiler/runtime/localize-href.test.js.map +1 -1
- package/dist/compiler/runtime/set-locale.js +24 -6
- package/dist/compiler/runtime/set-locale.js.map +1 -1
- package/dist/compiler/runtime/set-locale.test.js +182 -0
- package/dist/compiler/runtime/set-locale.test.js.map +1 -1
- package/dist/compiler/runtime/type.d.ts +2 -0
- package/dist/compiler/runtime/type.js.map +1 -1
- package/dist/compiler/runtime/type.test.js +34 -0
- package/dist/compiler/runtime/type.test.js.map +1 -1
- package/dist/compiler/runtime/variables.d.ts +2 -0
- package/dist/compiler/runtime/variables.js +7 -0
- package/dist/compiler/runtime/variables.js.map +1 -1
- package/dist/compiler/server/create-server-file.js +24 -0
- package/dist/compiler/server/create-server-file.js.map +1 -1
- package/dist/compiler/server/create-server-file.test.js +8 -2
- package/dist/compiler/server/create-server-file.test.js.map +1 -1
- package/dist/compiler/server/middleware.d.ts +8 -2
- package/dist/compiler/server/middleware.js +204 -11
- package/dist/compiler/server/middleware.js.map +1 -1
- package/dist/compiler/server/middleware.test.js +750 -3
- package/dist/compiler/server/middleware.test.js.map +1 -1
- package/dist/eslint/index.d.ts +4 -0
- package/dist/eslint/index.js +8 -0
- package/dist/eslint/index.js.map +1 -0
- package/dist/eslint/no-dynamic-message-access.d.ts +3 -0
- package/dist/eslint/no-dynamic-message-access.js +150 -0
- package/dist/eslint/no-dynamic-message-access.js.map +1 -0
- package/dist/eslint/no-dynamic-message-access.test.d.ts +1 -0
- package/dist/eslint/no-dynamic-message-access.test.js +126 -0
- package/dist/eslint/no-dynamic-message-access.test.js.map +1 -0
- package/dist/eslint/oxlint.test.d.ts +1 -0
- package/dist/eslint/oxlint.test.js +64 -0
- package/dist/eslint/oxlint.test.js.map +1 -0
- package/dist/services/config/config-schema.d.ts +2 -0
- package/dist/services/config/config-schema.js +2 -0
- package/dist/services/config/config-schema.js.map +1 -1
- package/dist/services/config/load-paraglide-config.test.js +12 -0
- package/dist/services/config/load-paraglide-config.test.js.map +1 -1
- package/dist/services/env-variables/index.js +1 -1
- package/dist/services/env-variables/index.js.map +1 -1
- package/dist/services/file-handling/write-output.bench.js +51 -11
- package/dist/services/file-handling/write-output.bench.js.map +1 -1
- package/dist/services/file-handling/write-output.js +60 -20
- package/dist/services/file-handling/write-output.js.map +1 -1
- package/dist/services/file-handling/write-output.test.js +175 -0
- package/dist/services/file-handling/write-output.test.js.map +1 -1
- package/package.json +15 -5
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { noDynamicMessageAccess } from "./no-dynamic-message-access.js";
|
|
2
|
+
/** ESLint plugin for applications using generated Paraglide messages. */
|
|
3
|
+
const plugin = {
|
|
4
|
+
meta: { name: "@inlang/paraglide-js/eslint" },
|
|
5
|
+
rules: { "no-dynamic-message-access": noDynamicMessageAccess },
|
|
6
|
+
};
|
|
7
|
+
export default plugin;
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/eslint/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAExE,yEAAyE;AACzE,MAAM,MAAM,GAAkB;IAC7B,IAAI,EAAE,EAAE,IAAI,EAAE,6BAA6B,EAAE;IAC7C,KAAK,EAAE,EAAE,2BAA2B,EAAE,sBAAsB,EAAE;CAC9D,CAAC;AAEF,eAAe,MAAM,CAAC","sourcesContent":["import type { ESLint } from \"eslint\";\nimport { noDynamicMessageAccess } from \"./no-dynamic-message-access.js\";\n\n/** ESLint plugin for applications using generated Paraglide messages. */\nconst plugin: ESLint.Plugin = {\n\tmeta: { name: \"@inlang/paraglide-js/eslint\" },\n\trules: { \"no-dynamic-message-access\": noDynamicMessageAccess },\n};\n\nexport default plugin;\n"]}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/** Only syntax-level static keys are allowed; this rule does not evaluate expressions. */
|
|
2
|
+
function staticKey(node) {
|
|
3
|
+
if (node.type === "Identifier")
|
|
4
|
+
return node.name;
|
|
5
|
+
if (node.type === "Literal" &&
|
|
6
|
+
(typeof node.value === "string" || typeof node.value === "number"))
|
|
7
|
+
return String(node.value);
|
|
8
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0)
|
|
9
|
+
return node.quasis[0]?.value.cooked ?? undefined;
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
function staticMemberKey(node) {
|
|
13
|
+
return node.computed && node.property.type === "Identifier"
|
|
14
|
+
? undefined
|
|
15
|
+
: staticKey(node.property);
|
|
16
|
+
}
|
|
17
|
+
// TypeScript wrappers do not change the identity of a namespace reference.
|
|
18
|
+
function unwrap(node) {
|
|
19
|
+
while ([
|
|
20
|
+
"ChainExpression",
|
|
21
|
+
"TSAsExpression",
|
|
22
|
+
"TSTypeAssertion",
|
|
23
|
+
"TSNonNullExpression",
|
|
24
|
+
"TSSatisfiesExpression",
|
|
25
|
+
].includes(node.type)) {
|
|
26
|
+
node = node.expression;
|
|
27
|
+
}
|
|
28
|
+
return node;
|
|
29
|
+
}
|
|
30
|
+
/** Prevent runtime catalog indexing from retaining all generated messages. */
|
|
31
|
+
export const noDynamicMessageAccess = {
|
|
32
|
+
meta: {
|
|
33
|
+
type: "problem",
|
|
34
|
+
docs: {
|
|
35
|
+
description: "Require static references or an explicit map for runtime-selected messages",
|
|
36
|
+
url: "https://paraglidejs.com/basics#dynamic-messages",
|
|
37
|
+
},
|
|
38
|
+
schema: [
|
|
39
|
+
{
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
messageModuleDirectories: {
|
|
43
|
+
type: "array",
|
|
44
|
+
items: { type: "string", minLength: 1 },
|
|
45
|
+
uniqueItems: true,
|
|
46
|
+
},
|
|
47
|
+
messageModules: {
|
|
48
|
+
type: "array",
|
|
49
|
+
items: { type: "string" },
|
|
50
|
+
uniqueItems: true,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
messages: {
|
|
57
|
+
dynamicAccess: "Dynamic access to the Paraglide message catalog can bundle every message. Create a local map of explicit message references (for example, { greeting: m.greeting }) and index that map instead.",
|
|
58
|
+
catalogCopy: "Copying the Paraglide message catalog can bundle every message. Create a local map containing only the messages this UI renders.",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
create(context) {
|
|
62
|
+
const sourceCode = context.sourceCode;
|
|
63
|
+
const options = context.options[0];
|
|
64
|
+
const configuredModules = new Set(options?.messageModules ?? []);
|
|
65
|
+
const directories = new Set([
|
|
66
|
+
"paraglide",
|
|
67
|
+
...(options?.messageModuleDirectories ?? []),
|
|
68
|
+
]);
|
|
69
|
+
const matchesModule = (value) => typeof value === "string" &&
|
|
70
|
+
(configuredModules.has(value) ||
|
|
71
|
+
[...directories].some((directory) => ["messages", "messages.js"].some((file) => value === `${directory}/${file}` ||
|
|
72
|
+
value.endsWith(`/${directory}/${file}`))));
|
|
73
|
+
function variableFor(node) {
|
|
74
|
+
for (let scope = sourceCode.getScope(node); scope; scope = scope.upper) {
|
|
75
|
+
const variable = scope.set.get(node.name);
|
|
76
|
+
if (variable)
|
|
77
|
+
return variable;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
function isCatalog(node, namespaceOnly = false, seen = new Set()) {
|
|
82
|
+
if (!node)
|
|
83
|
+
return false;
|
|
84
|
+
const expression = unwrap(node);
|
|
85
|
+
if (expression.type === "MemberExpression") {
|
|
86
|
+
return (!namespaceOnly &&
|
|
87
|
+
staticMemberKey(expression) === "m" &&
|
|
88
|
+
isCatalog(expression.object, true, seen));
|
|
89
|
+
}
|
|
90
|
+
if (expression.type !== "Identifier")
|
|
91
|
+
return false;
|
|
92
|
+
const variable = variableFor(expression);
|
|
93
|
+
if (!variable || seen.has(variable))
|
|
94
|
+
return false;
|
|
95
|
+
seen.add(variable);
|
|
96
|
+
for (const definition of variable.defs) {
|
|
97
|
+
if (definition.type === "ImportBinding" &&
|
|
98
|
+
matchesModule(definition.parent.source.value)) {
|
|
99
|
+
const specifier = definition.node;
|
|
100
|
+
if (specifier.type === "ImportNamespaceSpecifier")
|
|
101
|
+
return true;
|
|
102
|
+
if (!namespaceOnly &&
|
|
103
|
+
specifier.type === "ImportSpecifier" &&
|
|
104
|
+
staticKey(specifier.imported) === "m")
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
if (definition.type === "Variable" &&
|
|
108
|
+
definition.parent.kind === "const") {
|
|
109
|
+
const declaration = definition.node;
|
|
110
|
+
if (declaration.id.type === "Identifier")
|
|
111
|
+
return isCatalog(declaration.init, namespaceOnly, seen);
|
|
112
|
+
if (!namespaceOnly && declaration.id.type === "ObjectPattern") {
|
|
113
|
+
return declaration.id.properties.some((property) => property.type === "Property" &&
|
|
114
|
+
!property.computed &&
|
|
115
|
+
staticKey(property.key) === "m" &&
|
|
116
|
+
property.value.type === "Identifier" &&
|
|
117
|
+
property.value.name === expression.name &&
|
|
118
|
+
isCatalog(declaration.init, true, seen));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
MemberExpression(node) {
|
|
126
|
+
if (node.computed &&
|
|
127
|
+
staticMemberKey(node) === undefined &&
|
|
128
|
+
isCatalog(node.object))
|
|
129
|
+
context.report({ node, messageId: "dynamicAccess" });
|
|
130
|
+
},
|
|
131
|
+
VariableDeclarator(node) {
|
|
132
|
+
if (node.id.type !== "ObjectPattern" || !isCatalog(node.init))
|
|
133
|
+
return;
|
|
134
|
+
for (const property of node.id.properties) {
|
|
135
|
+
if (property.type === "RestElement")
|
|
136
|
+
context.report({ node: property, messageId: "catalogCopy" });
|
|
137
|
+
else if (property.computed &&
|
|
138
|
+
(property.key.type === "Identifier" ||
|
|
139
|
+
staticKey(property.key) === undefined))
|
|
140
|
+
context.report({ node: property, messageId: "dynamicAccess" });
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
SpreadElement(node) {
|
|
144
|
+
if (isCatalog(node.argument))
|
|
145
|
+
context.report({ node, messageId: "catalogCopy" });
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
//# sourceMappingURL=no-dynamic-message-access.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-dynamic-message-access.js","sourceRoot":"","sources":["../../src/eslint/no-dynamic-message-access.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Rule, Scope } from \"eslint\";\nimport type { Identifier, Node, MemberExpression } from \"estree\";\n\n/** Only syntax-level static keys are allowed; this rule does not evaluate expressions. */\nfunction staticKey(node: Node): string | undefined {\n\tif (node.type === \"Identifier\") return node.name;\n\tif (\n\t\tnode.type === \"Literal\" &&\n\t\t(typeof node.value === \"string\" || typeof node.value === \"number\")\n\t)\n\t\treturn String(node.value);\n\tif (node.type === \"TemplateLiteral\" && node.expressions.length === 0)\n\t\treturn node.quasis[0]?.value.cooked ?? undefined;\n\treturn undefined;\n}\n\nfunction staticMemberKey(node: MemberExpression): string | undefined {\n\treturn node.computed && node.property.type === \"Identifier\"\n\t\t? undefined\n\t\t: staticKey(node.property);\n}\n\n// TypeScript wrappers do not change the identity of a namespace reference.\nfunction unwrap(node: Node): Node {\n\twhile (\n\t\t[\n\t\t\t\"ChainExpression\",\n\t\t\t\"TSAsExpression\",\n\t\t\t\"TSTypeAssertion\",\n\t\t\t\"TSNonNullExpression\",\n\t\t\t\"TSSatisfiesExpression\",\n\t\t].includes(node.type)\n\t) {\n\t\tnode = (node as Node & { expression: Node }).expression;\n\t}\n\treturn node;\n}\n\n/** Prevent runtime catalog indexing from retaining all generated messages. */\nexport const noDynamicMessageAccess: Rule.RuleModule = {\n\tmeta: {\n\t\ttype: \"problem\",\n\t\tdocs: {\n\t\t\tdescription:\n\t\t\t\t\"Require static references or an explicit map for runtime-selected messages\",\n\t\t\turl: \"https://paraglidejs.com/basics#dynamic-messages\",\n\t\t},\n\t\tschema: [\n\t\t\t{\n\t\t\t\ttype: \"object\",\n\t\t\t\tproperties: {\n\t\t\t\t\tmessageModuleDirectories: {\n\t\t\t\t\t\ttype: \"array\",\n\t\t\t\t\t\titems: { type: \"string\", minLength: 1 },\n\t\t\t\t\t\tuniqueItems: true,\n\t\t\t\t\t},\n\t\t\t\t\tmessageModules: {\n\t\t\t\t\t\ttype: \"array\",\n\t\t\t\t\t\titems: { type: \"string\" },\n\t\t\t\t\t\tuniqueItems: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadditionalProperties: false,\n\t\t\t},\n\t\t],\n\t\tmessages: {\n\t\t\tdynamicAccess:\n\t\t\t\t\"Dynamic access to the Paraglide message catalog can bundle every message. Create a local map of explicit message references (for example, { greeting: m.greeting }) and index that map instead.\",\n\t\t\tcatalogCopy:\n\t\t\t\t\"Copying the Paraglide message catalog can bundle every message. Create a local map containing only the messages this UI renders.\",\n\t\t},\n\t},\n\tcreate(context) {\n\t\tconst sourceCode = context.sourceCode;\n\t\tconst options = context.options[0] as\n\t\t\t| { messageModules?: string[]; messageModuleDirectories?: string[] }\n\t\t\t| undefined;\n\t\tconst configuredModules = new Set(options?.messageModules ?? []);\n\t\tconst directories = new Set([\n\t\t\t\"paraglide\",\n\t\t\t...(options?.messageModuleDirectories ?? []),\n\t\t]);\n\t\tconst matchesModule = (value: unknown) =>\n\t\t\ttypeof value === \"string\" &&\n\t\t\t(configuredModules.has(value) ||\n\t\t\t\t[...directories].some((directory) =>\n\t\t\t\t\t[\"messages\", \"messages.js\"].some(\n\t\t\t\t\t\t(file) =>\n\t\t\t\t\t\t\tvalue === `${directory}/${file}` ||\n\t\t\t\t\t\t\tvalue.endsWith(`/${directory}/${file}`)\n\t\t\t\t\t)\n\t\t\t\t));\n\n\t\tfunction variableFor(node: Identifier): Scope.Variable | undefined {\n\t\t\tfor (\n\t\t\t\tlet scope: Scope.Scope | null = sourceCode.getScope(node);\n\t\t\t\tscope;\n\t\t\t\tscope = scope.upper\n\t\t\t) {\n\t\t\t\tconst variable = scope.set.get(node.name);\n\t\t\t\tif (variable) return variable;\n\t\t\t}\n\t\t\treturn undefined;\n\t\t}\n\n\t\tfunction isCatalog(\n\t\t\tnode: Node | null | undefined,\n\t\t\tnamespaceOnly = false,\n\t\t\tseen = new Set<Scope.Variable>()\n\t\t): boolean {\n\t\t\tif (!node) return false;\n\t\t\tconst expression = unwrap(node);\n\t\t\tif (expression.type === \"MemberExpression\") {\n\t\t\t\treturn (\n\t\t\t\t\t!namespaceOnly &&\n\t\t\t\t\tstaticMemberKey(expression) === \"m\" &&\n\t\t\t\t\tisCatalog(expression.object, true, seen)\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (expression.type !== \"Identifier\") return false;\n\t\t\tconst variable = variableFor(expression);\n\t\t\tif (!variable || seen.has(variable)) return false;\n\t\t\tseen.add(variable);\n\t\t\tfor (const definition of variable.defs) {\n\t\t\t\tif (\n\t\t\t\t\tdefinition.type === \"ImportBinding\" &&\n\t\t\t\t\tmatchesModule(definition.parent.source.value)\n\t\t\t\t) {\n\t\t\t\t\tconst specifier = definition.node;\n\t\t\t\t\tif (specifier.type === \"ImportNamespaceSpecifier\") return true;\n\t\t\t\t\tif (\n\t\t\t\t\t\t!namespaceOnly &&\n\t\t\t\t\t\tspecifier.type === \"ImportSpecifier\" &&\n\t\t\t\t\t\tstaticKey(specifier.imported) === \"m\"\n\t\t\t\t\t)\n\t\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tif (\n\t\t\t\t\tdefinition.type === \"Variable\" &&\n\t\t\t\t\tdefinition.parent.kind === \"const\"\n\t\t\t\t) {\n\t\t\t\t\tconst declaration = definition.node;\n\t\t\t\t\tif (declaration.id.type === \"Identifier\")\n\t\t\t\t\t\treturn isCatalog(declaration.init, namespaceOnly, seen);\n\t\t\t\t\tif (!namespaceOnly && declaration.id.type === \"ObjectPattern\") {\n\t\t\t\t\t\treturn declaration.id.properties.some(\n\t\t\t\t\t\t\t(property) =>\n\t\t\t\t\t\t\t\tproperty.type === \"Property\" &&\n\t\t\t\t\t\t\t\t!property.computed &&\n\t\t\t\t\t\t\t\tstaticKey(property.key) === \"m\" &&\n\t\t\t\t\t\t\t\tproperty.value.type === \"Identifier\" &&\n\t\t\t\t\t\t\t\tproperty.value.name === expression.name &&\n\t\t\t\t\t\t\t\tisCatalog(declaration.init, true, seen)\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t\treturn {\n\t\t\tMemberExpression(node) {\n\t\t\t\tif (\n\t\t\t\t\tnode.computed &&\n\t\t\t\t\tstaticMemberKey(node) === undefined &&\n\t\t\t\t\tisCatalog(node.object)\n\t\t\t\t)\n\t\t\t\t\tcontext.report({ node, messageId: \"dynamicAccess\" });\n\t\t\t},\n\t\t\tVariableDeclarator(node) {\n\t\t\t\tif (node.id.type !== \"ObjectPattern\" || !isCatalog(node.init)) return;\n\t\t\t\tfor (const property of node.id.properties) {\n\t\t\t\t\tif (property.type === \"RestElement\")\n\t\t\t\t\t\tcontext.report({ node: property, messageId: \"catalogCopy\" });\n\t\t\t\t\telse if (\n\t\t\t\t\t\tproperty.computed &&\n\t\t\t\t\t\t(property.key.type === \"Identifier\" ||\n\t\t\t\t\t\t\tstaticKey(property.key) === undefined)\n\t\t\t\t\t)\n\t\t\t\t\t\tcontext.report({ node: property, messageId: \"dynamicAccess\" });\n\t\t\t\t}\n\t\t\t},\n\t\t\tSpreadElement(node) {\n\t\t\t\tif (isCatalog(node.argument))\n\t\t\t\t\tcontext.report({ node, messageId: \"catalogCopy\" });\n\t\t\t},\n\t\t};\n\t},\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { RuleTester, Linter } from "eslint";
|
|
3
|
+
import { RuleTester as RuleTester9 } from "eslint9";
|
|
4
|
+
import tsParser from "@typescript-eslint/parser";
|
|
5
|
+
import plugin from "./index.js";
|
|
6
|
+
import { noDynamicMessageAccess } from "./no-dynamic-message-access.js";
|
|
7
|
+
const named = 'import { m } from "./paraglide/messages.js";';
|
|
8
|
+
const namespace = 'import * as labels from "$lib/paraglide/messages";';
|
|
9
|
+
const dynamic = [{ messageId: "dynamicAccess" }];
|
|
10
|
+
const copy = [{ messageId: "catalogCopy" }];
|
|
11
|
+
// The two majors expose different generic tester types; exercise the same runtime API.
|
|
12
|
+
for (const Tester of [
|
|
13
|
+
RuleTester9,
|
|
14
|
+
RuleTester,
|
|
15
|
+
]) {
|
|
16
|
+
Tester.describe = (name, fn) => describe(name, fn);
|
|
17
|
+
Tester.it = (name, fn) => test(name, fn);
|
|
18
|
+
new Tester({
|
|
19
|
+
languageOptions: { ecmaVersion: "latest", sourceType: "module" },
|
|
20
|
+
}).run("no-dynamic-message-access", noDynamicMessageAccess, {
|
|
21
|
+
valid: [
|
|
22
|
+
`${named} m.greeting({ name: "Ada" });`,
|
|
23
|
+
`${named} m["user.profile.title"]();`,
|
|
24
|
+
`${named} m[\`greeting\`]();`,
|
|
25
|
+
`${named} m[42]();`,
|
|
26
|
+
`${named} const allowed = new Map([["hello", m.greeting]]); allowed.get(key)?.();`,
|
|
27
|
+
`${named} const messages = { greeting: m.greeting }; messages[key]?.();`,
|
|
28
|
+
`${namespace} labels.greeting();`,
|
|
29
|
+
`${named} function render(m) { return m[key](); }`,
|
|
30
|
+
`${namespace} function render(labels) { return labels[key](); }`,
|
|
31
|
+
`${named} { const m = {}; m[key]; }`,
|
|
32
|
+
'import { m } from "./unrelated/messages.js"; m[key]();',
|
|
33
|
+
'import * as m from "./other.js"; m[key]();',
|
|
34
|
+
`${named} m.greeting[method]();`,
|
|
35
|
+
`${named} const { greeting } = m; greeting();`,
|
|
36
|
+
`${named} const { ["greeting"]: greeting } = m; greeting();`,
|
|
37
|
+
`${named} const a = b; const b = a; a[key]();`,
|
|
38
|
+
'import { greeting as m } from "./paraglide/messages.js"; m[method]();',
|
|
39
|
+
],
|
|
40
|
+
invalid: [
|
|
41
|
+
{
|
|
42
|
+
code: 'import { m } from "$lib/i18n/messages.js"; m[key]();',
|
|
43
|
+
options: [{ messageModuleDirectories: ["i18n"] }],
|
|
44
|
+
errors: dynamic,
|
|
45
|
+
},
|
|
46
|
+
{ code: `${named} m[key]();`, errors: dynamic },
|
|
47
|
+
{ code: `${named} const fn = m[key];`, errors: dynamic },
|
|
48
|
+
{ code: `${named} m[\`hello_\${name}\`]();`, errors: dynamic },
|
|
49
|
+
{ code: `${named} m["hello_" + name]();`, errors: dynamic },
|
|
50
|
+
{ code: `${named} m?.[key]?.();`, errors: dynamic },
|
|
51
|
+
{
|
|
52
|
+
code: 'import { m as messages } from "@/paraglide/messages.js"; messages[key]();',
|
|
53
|
+
errors: dynamic,
|
|
54
|
+
},
|
|
55
|
+
{ code: `${namespace} labels[key]();`, errors: dynamic },
|
|
56
|
+
{ code: `${namespace} labels.m[key]();`, errors: dynamic },
|
|
57
|
+
{ code: `${namespace} labels["m"][key]();`, errors: dynamic },
|
|
58
|
+
{
|
|
59
|
+
code: `${named} const alias = m; const second = alias; second[key]();`,
|
|
60
|
+
errors: dynamic,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
code: `${namespace} const { m: alias } = labels; alias[key]();`,
|
|
64
|
+
errors: dynamic,
|
|
65
|
+
},
|
|
66
|
+
{ code: `${named} const { [key]: message } = m;`, errors: dynamic },
|
|
67
|
+
{ code: `${named} const { greeting, ...rest } = m;`, errors: copy },
|
|
68
|
+
{ code: `${named} const catalog = { ...m };`, errors: copy },
|
|
69
|
+
{
|
|
70
|
+
code: 'import * as labels from "./generated/messages.js"; labels[key]();',
|
|
71
|
+
options: [{ messageModules: ["./generated/messages.js"] }],
|
|
72
|
+
errors: dynamic,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
code: 'import { m } from "@app/translations"; m[key]();',
|
|
76
|
+
options: [{ messageModules: ["@app/translations"] }],
|
|
77
|
+
errors: dynamic,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
new Tester({ languageOptions: { parser: tsParser } }).run("no-dynamic-message-access (TypeScript)", noDynamicMessageAccess, {
|
|
82
|
+
valid: [
|
|
83
|
+
{
|
|
84
|
+
code: `${named} const labels: Record<string, () => string> = { hello: m.greeting }; labels[key]();`,
|
|
85
|
+
filename: "app.ts",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
code: `${named} function render(m: Record<string, () => string>, key: string) { return m[key](); }`,
|
|
89
|
+
filename: "app.ts",
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
invalid: [
|
|
93
|
+
{
|
|
94
|
+
code: `${named} (m as typeof m)[key]();`,
|
|
95
|
+
filename: "app.ts",
|
|
96
|
+
errors: dynamic,
|
|
97
|
+
},
|
|
98
|
+
{ code: `${named} m![key]();`, filename: "app.ts", errors: dynamic },
|
|
99
|
+
{
|
|
100
|
+
code: `${named} (m satisfies typeof m)[key]();`,
|
|
101
|
+
filename: "app.ts",
|
|
102
|
+
errors: dynamic,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
code: `${named} const alias = m as typeof m; alias[key]();`,
|
|
106
|
+
filename: "app.ts",
|
|
107
|
+
errors: dynamic,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
test("public plugin config reports dynamic access without an unsafe autofix", () => {
|
|
113
|
+
const linter = new Linter();
|
|
114
|
+
const config = {
|
|
115
|
+
plugins: { paraglide: plugin },
|
|
116
|
+
rules: { "paraglide/no-dynamic-message-access": "error" },
|
|
117
|
+
};
|
|
118
|
+
const code = `${named} m[key]();`;
|
|
119
|
+
const result = linter.verifyAndFix(code, config);
|
|
120
|
+
expect(result.messages).toHaveLength(1);
|
|
121
|
+
expect(result.messages[0]?.ruleId).toBe("paraglide/no-dynamic-message-access");
|
|
122
|
+
expect(result.messages[0]?.message).toContain("local map");
|
|
123
|
+
expect(result.fixed).toBe(false);
|
|
124
|
+
expect(result.output).toBe(code);
|
|
125
|
+
});
|
|
126
|
+
//# sourceMappingURL=no-dynamic-message-access.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-dynamic-message-access.test.js","sourceRoot":"","sources":["../../src/eslint/no-dynamic-message-access.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, test, expect } from \"vitest\";\nimport { RuleTester, Linter } from \"eslint\";\nimport { RuleTester as RuleTester9 } from \"eslint9\";\nimport tsParser from \"@typescript-eslint/parser\";\nimport plugin from \"./index.js\";\nimport { noDynamicMessageAccess } from \"./no-dynamic-message-access.js\";\n\nconst named = 'import { m } from \"./paraglide/messages.js\";';\nconst namespace = 'import * as labels from \"$lib/paraglide/messages\";';\nconst dynamic = [{ messageId: \"dynamicAccess\" }];\nconst copy = [{ messageId: \"catalogCopy\" }];\n\n// The two majors expose different generic tester types; exercise the same runtime API.\nfor (const Tester of [\n\tRuleTester9 as unknown as typeof RuleTester,\n\tRuleTester,\n]) {\n\tTester.describe = (name, fn) => describe(name, fn);\n\tTester.it = (name, fn) => test(name, fn);\n\tnew Tester({\n\t\tlanguageOptions: { ecmaVersion: \"latest\", sourceType: \"module\" },\n\t}).run(\"no-dynamic-message-access\", noDynamicMessageAccess, {\n\t\tvalid: [\n\t\t\t`${named} m.greeting({ name: \"Ada\" });`,\n\t\t\t`${named} m[\"user.profile.title\"]();`,\n\t\t\t`${named} m[\\`greeting\\`]();`,\n\t\t\t`${named} m[42]();`,\n\t\t\t`${named} const allowed = new Map([[\"hello\", m.greeting]]); allowed.get(key)?.();`,\n\t\t\t`${named} const messages = { greeting: m.greeting }; messages[key]?.();`,\n\t\t\t`${namespace} labels.greeting();`,\n\t\t\t`${named} function render(m) { return m[key](); }`,\n\t\t\t`${namespace} function render(labels) { return labels[key](); }`,\n\t\t\t`${named} { const m = {}; m[key]; }`,\n\t\t\t'import { m } from \"./unrelated/messages.js\"; m[key]();',\n\t\t\t'import * as m from \"./other.js\"; m[key]();',\n\t\t\t`${named} m.greeting[method]();`,\n\t\t\t`${named} const { greeting } = m; greeting();`,\n\t\t\t`${named} const { [\"greeting\"]: greeting } = m; greeting();`,\n\t\t\t`${named} const a = b; const b = a; a[key]();`,\n\t\t\t'import { greeting as m } from \"./paraglide/messages.js\"; m[method]();',\n\t\t],\n\t\tinvalid: [\n\t\t\t{\n\t\t\t\tcode: 'import { m } from \"$lib/i18n/messages.js\"; m[key]();',\n\t\t\t\toptions: [{ messageModuleDirectories: [\"i18n\"] }],\n\t\t\t\terrors: dynamic,\n\t\t\t},\n\t\t\t{ code: `${named} m[key]();`, errors: dynamic },\n\t\t\t{ code: `${named} const fn = m[key];`, errors: dynamic },\n\t\t\t{ code: `${named} m[\\`hello_\\${name}\\`]();`, errors: dynamic },\n\t\t\t{ code: `${named} m[\"hello_\" + name]();`, errors: dynamic },\n\t\t\t{ code: `${named} m?.[key]?.();`, errors: dynamic },\n\t\t\t{\n\t\t\t\tcode: 'import { m as messages } from \"@/paraglide/messages.js\"; messages[key]();',\n\t\t\t\terrors: dynamic,\n\t\t\t},\n\t\t\t{ code: `${namespace} labels[key]();`, errors: dynamic },\n\t\t\t{ code: `${namespace} labels.m[key]();`, errors: dynamic },\n\t\t\t{ code: `${namespace} labels[\"m\"][key]();`, errors: dynamic },\n\t\t\t{\n\t\t\t\tcode: `${named} const alias = m; const second = alias; second[key]();`,\n\t\t\t\terrors: dynamic,\n\t\t\t},\n\t\t\t{\n\t\t\t\tcode: `${namespace} const { m: alias } = labels; alias[key]();`,\n\t\t\t\terrors: dynamic,\n\t\t\t},\n\t\t\t{ code: `${named} const { [key]: message } = m;`, errors: dynamic },\n\t\t\t{ code: `${named} const { greeting, ...rest } = m;`, errors: copy },\n\t\t\t{ code: `${named} const catalog = { ...m };`, errors: copy },\n\t\t\t{\n\t\t\t\tcode: 'import * as labels from \"./generated/messages.js\"; labels[key]();',\n\t\t\t\toptions: [{ messageModules: [\"./generated/messages.js\"] }],\n\t\t\t\terrors: dynamic,\n\t\t\t},\n\t\t\t{\n\t\t\t\tcode: 'import { m } from \"@app/translations\"; m[key]();',\n\t\t\t\toptions: [{ messageModules: [\"@app/translations\"] }],\n\t\t\t\terrors: dynamic,\n\t\t\t},\n\t\t],\n\t});\n\n\tnew Tester({ languageOptions: { parser: tsParser } }).run(\n\t\t\"no-dynamic-message-access (TypeScript)\",\n\t\tnoDynamicMessageAccess,\n\t\t{\n\t\t\tvalid: [\n\t\t\t\t{\n\t\t\t\t\tcode: `${named} const labels: Record<string, () => string> = { hello: m.greeting }; labels[key]();`,\n\t\t\t\t\tfilename: \"app.ts\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tcode: `${named} function render(m: Record<string, () => string>, key: string) { return m[key](); }`,\n\t\t\t\t\tfilename: \"app.ts\",\n\t\t\t\t},\n\t\t\t],\n\t\t\tinvalid: [\n\t\t\t\t{\n\t\t\t\t\tcode: `${named} (m as typeof m)[key]();`,\n\t\t\t\t\tfilename: \"app.ts\",\n\t\t\t\t\terrors: dynamic,\n\t\t\t\t},\n\t\t\t\t{ code: `${named} m![key]();`, filename: \"app.ts\", errors: dynamic },\n\t\t\t\t{\n\t\t\t\t\tcode: `${named} (m satisfies typeof m)[key]();`,\n\t\t\t\t\tfilename: \"app.ts\",\n\t\t\t\t\terrors: dynamic,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tcode: `${named} const alias = m as typeof m; alias[key]();`,\n\t\t\t\t\tfilename: \"app.ts\",\n\t\t\t\t\terrors: dynamic,\n\t\t\t\t},\n\t\t\t],\n\t\t}\n\t);\n}\n\ntest(\"public plugin config reports dynamic access without an unsafe autofix\", () => {\n\tconst linter = new Linter();\n\tconst config = {\n\t\tplugins: { paraglide: plugin },\n\t\trules: { \"paraglide/no-dynamic-message-access\": \"error\" as const },\n\t};\n\tconst code = `${named} m[key]();`;\n\tconst result = linter.verifyAndFix(code, config);\n\texpect(result.messages).toHaveLength(1);\n\texpect(result.messages[0]?.ruleId).toBe(\n\t\t\"paraglide/no-dynamic-message-access\"\n\t);\n\texpect(result.messages[0]?.message).toContain(\"local map\");\n\texpect(result.fixed).toBe(false);\n\texpect(result.output).toBe(code);\n});\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { test, expect } from "vitest";
|
|
2
|
+
import { mkdtemp, readFile, writeFile, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import ts from "typescript";
|
|
9
|
+
test("Oxlint runs the same plugin on JS and TS with import and scope tracking", async () => {
|
|
10
|
+
const root = await mkdtemp(join(tmpdir(), "paraglide-oxlint-"));
|
|
11
|
+
try {
|
|
12
|
+
await writeFile(join(root, "package.json"), '{"type":"module"}');
|
|
13
|
+
// Compile the current rule rather than depending on a potentially stale dist/.
|
|
14
|
+
for (const file of ["index", "no-dynamic-message-access"]) {
|
|
15
|
+
const source = await readFile(new URL(`./${file}.ts`, import.meta.url), "utf8");
|
|
16
|
+
await writeFile(join(root, `${file}.js`), ts.transpileModule(source, {
|
|
17
|
+
compilerOptions: {
|
|
18
|
+
module: ts.ModuleKind.ESNext,
|
|
19
|
+
target: ts.ScriptTarget.ES2022,
|
|
20
|
+
},
|
|
21
|
+
}).outputText);
|
|
22
|
+
}
|
|
23
|
+
await writeFile(join(root, ".oxlintrc.json"), JSON.stringify({
|
|
24
|
+
plugins: [],
|
|
25
|
+
jsPlugins: [{ name: "paraglide", specifier: "./index.js" }],
|
|
26
|
+
rules: { "paraglide/no-dynamic-message-access": "error" },
|
|
27
|
+
}));
|
|
28
|
+
const fixtures = {
|
|
29
|
+
"dynamic.js": 'import { m as labels } from "./paraglide/messages.js"; labels[key](); labels?.[key]?.(); const alias = labels; alias[key]; const { [key]: fn } = labels;',
|
|
30
|
+
"dynamic.ts": 'import * as labels from "$lib/paraglide/messages"; (labels as typeof labels)[key](); labels![key](); (labels satisfies typeof labels)[key](); const { m: alias } = labels; alias[key]();',
|
|
31
|
+
"static.ts": 'import { m } from "./paraglide/messages.js"; m.greeting(); m["greeting"](); const allowed = new Map([["hello", m.greeting]]); allowed.get(key)?.(); function render(m: Record<string, () => string>, key: string) { return m[key](); }',
|
|
32
|
+
};
|
|
33
|
+
for (const [file, code] of Object.entries(fixtures))
|
|
34
|
+
await writeFile(join(root, file), code);
|
|
35
|
+
let stdout;
|
|
36
|
+
try {
|
|
37
|
+
({ stdout } = await promisify(execFile)(process.execPath, [
|
|
38
|
+
fileURLToPath(new URL("../../node_modules/oxlint/bin/oxlint", import.meta.url)),
|
|
39
|
+
"--config",
|
|
40
|
+
join(root, ".oxlintrc.json"),
|
|
41
|
+
"--format",
|
|
42
|
+
"json",
|
|
43
|
+
...Object.keys(fixtures).map((file) => join(root, file)),
|
|
44
|
+
]));
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const failure = error;
|
|
48
|
+
if (failure.code !== 1 || !failure.stdout)
|
|
49
|
+
throw error;
|
|
50
|
+
stdout = failure.stdout;
|
|
51
|
+
}
|
|
52
|
+
const output = JSON.parse(stdout);
|
|
53
|
+
const diagnostics = output.diagnostics.filter((item) => item.code === "paraglide(no-dynamic-message-access)");
|
|
54
|
+
expect(diagnostics).toHaveLength(8);
|
|
55
|
+
expect(diagnostics.filter((item) => item.filename.endsWith("dynamic.js"))).toHaveLength(4);
|
|
56
|
+
expect(diagnostics.filter((item) => item.filename.endsWith("dynamic.ts"))).toHaveLength(4);
|
|
57
|
+
expect(diagnostics.some((item) => item.filename.endsWith("static.ts"))).toBe(false);
|
|
58
|
+
expect(diagnostics.every((item) => item.message.includes("local map"))).toBe(true);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
await rm(root, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
//# sourceMappingURL=oxlint.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oxlint.test.js","sourceRoot":"","sources":["../../src/eslint/oxlint.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { test, expect } from \"vitest\";\nimport { mkdtemp, readFile, writeFile, rm } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport ts from \"typescript\";\n\ntest(\"Oxlint runs the same plugin on JS and TS with import and scope tracking\", async () => {\n\tconst root = await mkdtemp(join(tmpdir(), \"paraglide-oxlint-\"));\n\ttry {\n\t\tawait writeFile(join(root, \"package.json\"), '{\"type\":\"module\"}');\n\t\t// Compile the current rule rather than depending on a potentially stale dist/.\n\t\tfor (const file of [\"index\", \"no-dynamic-message-access\"]) {\n\t\t\tconst source = await readFile(\n\t\t\t\tnew URL(`./${file}.ts`, import.meta.url),\n\t\t\t\t\"utf8\"\n\t\t\t);\n\t\t\tawait writeFile(\n\t\t\t\tjoin(root, `${file}.js`),\n\t\t\t\tts.transpileModule(source, {\n\t\t\t\t\tcompilerOptions: {\n\t\t\t\t\t\tmodule: ts.ModuleKind.ESNext,\n\t\t\t\t\t\ttarget: ts.ScriptTarget.ES2022,\n\t\t\t\t\t},\n\t\t\t\t}).outputText\n\t\t\t);\n\t\t}\n\t\tawait writeFile(\n\t\t\tjoin(root, \".oxlintrc.json\"),\n\t\t\tJSON.stringify({\n\t\t\t\tplugins: [],\n\t\t\t\tjsPlugins: [{ name: \"paraglide\", specifier: \"./index.js\" }],\n\t\t\t\trules: { \"paraglide/no-dynamic-message-access\": \"error\" },\n\t\t\t})\n\t\t);\n\t\tconst fixtures = {\n\t\t\t\"dynamic.js\":\n\t\t\t\t'import { m as labels } from \"./paraglide/messages.js\"; labels[key](); labels?.[key]?.(); const alias = labels; alias[key]; const { [key]: fn } = labels;',\n\t\t\t\"dynamic.ts\":\n\t\t\t\t'import * as labels from \"$lib/paraglide/messages\"; (labels as typeof labels)[key](); labels![key](); (labels satisfies typeof labels)[key](); const { m: alias } = labels; alias[key]();',\n\t\t\t\"static.ts\":\n\t\t\t\t'import { m } from \"./paraglide/messages.js\"; m.greeting(); m[\"greeting\"](); const allowed = new Map([[\"hello\", m.greeting]]); allowed.get(key)?.(); function render(m: Record<string, () => string>, key: string) { return m[key](); }',\n\t\t};\n\t\tfor (const [file, code] of Object.entries(fixtures))\n\t\t\tawait writeFile(join(root, file), code);\n\t\tlet stdout: string;\n\t\ttry {\n\t\t\t({ stdout } = await promisify(execFile)(process.execPath, [\n\t\t\t\tfileURLToPath(\n\t\t\t\t\tnew URL(\"../../node_modules/oxlint/bin/oxlint\", import.meta.url)\n\t\t\t\t),\n\t\t\t\t\"--config\",\n\t\t\t\tjoin(root, \".oxlintrc.json\"),\n\t\t\t\t\"--format\",\n\t\t\t\t\"json\",\n\t\t\t\t...Object.keys(fixtures).map((file) => join(root, file)),\n\t\t\t]));\n\t\t} catch (error) {\n\t\t\tconst failure = error as { code?: number; stdout?: string };\n\t\t\tif (failure.code !== 1 || !failure.stdout) throw error;\n\t\t\tstdout = failure.stdout;\n\t\t}\n\t\tconst output = JSON.parse(stdout) as {\n\t\t\tdiagnostics: Array<{ code: string; filename: string; message: string }>;\n\t\t};\n\t\tconst diagnostics = output.diagnostics.filter(\n\t\t\t(item) => item.code === \"paraglide(no-dynamic-message-access)\"\n\t\t);\n\t\texpect(diagnostics).toHaveLength(8);\n\t\texpect(\n\t\t\tdiagnostics.filter((item) => item.filename.endsWith(\"dynamic.js\"))\n\t\t).toHaveLength(4);\n\t\texpect(\n\t\t\tdiagnostics.filter((item) => item.filename.endsWith(\"dynamic.ts\"))\n\t\t).toHaveLength(4);\n\t\texpect(\n\t\t\tdiagnostics.some((item) => item.filename.endsWith(\"static.ts\"))\n\t\t).toBe(false);\n\t\texpect(\n\t\t\tdiagnostics.every((item) => item.message.includes(\"local map\"))\n\t\t).toBe(true);\n\t} finally {\n\t\tawait rm(root, { recursive: true, force: true });\n\t}\n});\n"]}
|
|
@@ -46,6 +46,8 @@ export declare const paraglideConfigSchema: v.ObjectSchema<{
|
|
|
46
46
|
readonly cookieName: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
47
47
|
readonly cookieMaxAge: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
48
48
|
readonly cookieDomain: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
49
|
+
readonly cookieSameSite: v.OptionalSchema<v.PicklistSchema<["lax", "strict", "none"], undefined>, undefined>;
|
|
50
|
+
readonly cookieSecure: v.OptionalSchema<v.UnionSchema<[v.BooleanSchema<undefined>, v.LiteralSchema<"auto", undefined>], undefined>, undefined>;
|
|
49
51
|
readonly additionalFiles: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.StringSchema<undefined>, undefined>, undefined>;
|
|
50
52
|
readonly emitPrettierIgnore: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
51
53
|
readonly emitReadme: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
@@ -59,6 +59,8 @@ export const paraglideConfigSchema = v.object({
|
|
|
59
59
|
cookieName: v.optional(v.string()),
|
|
60
60
|
cookieMaxAge: v.optional(v.number()),
|
|
61
61
|
cookieDomain: v.optional(v.string()),
|
|
62
|
+
cookieSameSite: v.optional(v.picklist(["lax", "strict", "none"])),
|
|
63
|
+
cookieSecure: v.optional(v.union([v.boolean(), v.literal("auto")])),
|
|
62
64
|
additionalFiles: v.optional(v.record(v.string(), v.string())),
|
|
63
65
|
emitPrettierIgnore: v.optional(v.boolean()),
|
|
64
66
|
emitReadme: v.optional(v.boolean()),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-schema.js","sourceRoot":"","sources":["../../../src/services/config/config-schema.ts"],"names":[],"mappings":"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","sourcesContent":["import * as v from \"valibot\";\nimport type { CompilerOptions } from \"../../compiler/compiler-options.js\";\nimport type { Runtime } from \"../../compiler/runtime/type.js\";\n\n/**\n * The schema of a `paraglide.config.js/ts` file inside the project\n * directory.\n *\n * The config cannot specify `project` — it lives inside the inlang project\n * directory and therefore cannot name it. Values that are not provided\n * fall back to the bundler plugin options / command line flags and\n * ultimately to the built-in defaults.\n */\nexport type ParaglideConfig = Partial<Omit<CompilerOptions, \"fs\" | \"project\">>;\n\n/**\n * Strategy names that are built into the runtime. Custom strategies are\n * prefixed with `custom-` and are matched separately.\n */\nconst BUILTIN_STRATEGY_NAMES = [\n\t\"cookie\",\n\t\"baseLocale\",\n\t\"globalVariable\",\n\t\"url\",\n\t\"preferredLanguage\",\n\t\"localStorage\",\n] as const satisfies readonly Exclude<\n\tRuntime[\"strategy\"][number],\n\t`custom-${string}`\n>[];\n\n// Fails to compile when the runtime gains a strategy name that is neither\n// built-in above nor a `custom-` prefixed name.\ntype UncoveredStrategies = Exclude<\n\tRuntime[\"strategy\"][number],\n\t(typeof BUILTIN_STRATEGY_NAMES)[number] | `custom-${string}`\n>;\nexport type AllStrategiesCovered = [UncoveredStrategies] extends [never]\n\t? true\n\t: never;\n\n// Value-position pins so the type-level asserts below are actually\n// evaluated by tsc — an unused alias resolving to `never` would compile.\nexport const allStrategiesCovered: AllStrategiesCovered = true;\n\n/**\n * Matches strategy names like `custom-myStrategy`.\n *\n * The pattern mirrors the runtime's `isCustomStrategy` check\n * (`custom-[A-Za-z0-9_-]+`) so the config file rejects names the runtime\n * would refuse.\n *\n * Valibot has no template-literal schema, so the regex-checked string\n * schema is narrowed to the compiler's type here.\n */\nconst customStrategySchema = v.pipe(\n\tv.string(),\n\tv.regex(\n\t\t/^custom-[A-Za-z0-9_-]+$/,\n\t\t'Custom strategies must match \"custom-\" followed by alphanumeric characters, hyphens, or underscores.'\n\t)\n) as v.BaseSchema<\n\tstring,\n\t`custom-${string}`,\n\tv.StringIssue | v.RegexIssue<string>\n>;\n\nexport const strategyNameSchema = v.union([\n\tv.picklist(BUILTIN_STRATEGY_NAMES),\n\tcustomStrategySchema,\n]);\n\nconst routeStrategySchema = v.union([\n\tv.object({\n\t\tmatch: v.string(),\n\t\tstrategy: v.array(strategyNameSchema),\n\t\texclude: v.optional(v.never()),\n\t}),\n\tv.object({\n\t\tmatch: v.string(),\n\t\texclude: v.literal(true),\n\t\tstrategy: v.optional(v.never()),\n\t}),\n]);\n\n/**\n * The valibot schema behind {@link ParaglideConfig}.\n *\n * Every option is optional — values that are not provided fall back to\n * the bundler plugin options / command line flags and ultimately to\n * `defaultCompilerOptions`.\n */\nexport const paraglideConfigSchema = v.object({\n\toutdir: v.optional(\n\t\tv.pipe(v.string(), v.nonEmpty('\"outdir\" must not be empty'))\n\t),\n\tstrategy: v.optional(v.array(strategyNameSchema)),\n\trouteStrategies: v.optional(v.array(routeStrategySchema)),\n\texperimentalMiddlewareLocaleSplitting: v.optional(v.boolean()),\n\tlocalStorageKey: v.optional(v.string()),\n\tisServer: v.optional(v.string()),\n\texperimentalStaticLocale: v.optional(v.string()),\n\tcookieName: v.optional(v.string()),\n\tcookieMaxAge: v.optional(v.number()),\n\tcookieDomain: v.optional(v.string()),\n\tadditionalFiles: v.optional(v.record(v.string(), v.string())),\n\temitPrettierIgnore: v.optional(v.boolean()),\n\temitReadme: v.optional(v.boolean()),\n\temitTsDeclarations: v.optional(v.boolean()),\n\turlPatterns: v.optional(\n\t\tv.array(\n\t\t\tv.object({\n\t\t\t\tpattern: v.string(),\n\t\t\t\tlocalized: v.array(v.tuple([v.string(), v.string()])),\n\t\t\t})\n\t\t)\n\t),\n\ttrailingSlash: v.optional(v.picklist([\"always\", \"never\"])),\n\tincludeEslintDisableComment: v.optional(v.boolean()),\n\tdisableAsyncLocalStorage: v.optional(v.boolean()),\n\temitGitIgnore: v.optional(v.boolean()),\n\toutputStructure: v.optional(\n\t\tv.picklist([\"message-modules\", \"locale-modules\"])\n\t),\n\tcleanOutdir: v.optional(v.boolean()),\n});\n\n// Fails to compile when the schema and the compiler options drift apart:\n// every compiler option (except the internal `fs` and the `project` path,\n// which the config cannot specify about itself) must be represented in the\n// schema, the schema must not contain extra options, and the parsed\n// values must match the compiler option types in both directions — a\n// schema narrower than a widened compiler option would wrongly reject\n// valid values.\ntype SchemaOutput = v.InferOutput<typeof paraglideConfigSchema>;\n// `keyof ParaglideConfig` already excludes `fs` and `project` (see its\n// definition) — this assertion is the only place that needs the\n// configurable-keys view.\nexport type SchemaMatchesCompilerOptions = [keyof ParaglideConfig] extends [\n\tkeyof SchemaOutput,\n]\n\t? [keyof SchemaOutput] extends [keyof ParaglideConfig]\n\t\t? SchemaOutput extends ParaglideConfig\n\t\t\t? ParaglideConfig extends SchemaOutput\n\t\t\t\t? true\n\t\t\t\t: never\n\t\t\t: never\n\t\t: never\n\t: never;\n\nexport const schemaMatchesCompilerOptions: SchemaMatchesCompilerOptions = true;\n"]}
|
|
1
|
+
{"version":3,"file":"config-schema.js","sourceRoot":"","sources":["../../../src/services/config/config-schema.ts"],"names":[],"mappings":"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","sourcesContent":["import * as v from \"valibot\";\nimport type { CompilerOptions } from \"../../compiler/compiler-options.js\";\nimport type { Runtime } from \"../../compiler/runtime/type.js\";\n\n/**\n * The schema of a `paraglide.config.js/ts` file inside the project\n * directory.\n *\n * The config cannot specify `project` — it lives inside the inlang project\n * directory and therefore cannot name it. Values that are not provided\n * fall back to the bundler plugin options / command line flags and\n * ultimately to the built-in defaults.\n */\nexport type ParaglideConfig = Partial<Omit<CompilerOptions, \"fs\" | \"project\">>;\n\n/**\n * Strategy names that are built into the runtime. Custom strategies are\n * prefixed with `custom-` and are matched separately.\n */\nconst BUILTIN_STRATEGY_NAMES = [\n\t\"cookie\",\n\t\"baseLocale\",\n\t\"globalVariable\",\n\t\"url\",\n\t\"preferredLanguage\",\n\t\"localStorage\",\n] as const satisfies readonly Exclude<\n\tRuntime[\"strategy\"][number],\n\t`custom-${string}`\n>[];\n\n// Fails to compile when the runtime gains a strategy name that is neither\n// built-in above nor a `custom-` prefixed name.\ntype UncoveredStrategies = Exclude<\n\tRuntime[\"strategy\"][number],\n\t(typeof BUILTIN_STRATEGY_NAMES)[number] | `custom-${string}`\n>;\nexport type AllStrategiesCovered = [UncoveredStrategies] extends [never]\n\t? true\n\t: never;\n\n// Value-position pins so the type-level asserts below are actually\n// evaluated by tsc — an unused alias resolving to `never` would compile.\nexport const allStrategiesCovered: AllStrategiesCovered = true;\n\n/**\n * Matches strategy names like `custom-myStrategy`.\n *\n * The pattern mirrors the runtime's `isCustomStrategy` check\n * (`custom-[A-Za-z0-9_-]+`) so the config file rejects names the runtime\n * would refuse.\n *\n * Valibot has no template-literal schema, so the regex-checked string\n * schema is narrowed to the compiler's type here.\n */\nconst customStrategySchema = v.pipe(\n\tv.string(),\n\tv.regex(\n\t\t/^custom-[A-Za-z0-9_-]+$/,\n\t\t'Custom strategies must match \"custom-\" followed by alphanumeric characters, hyphens, or underscores.'\n\t)\n) as v.BaseSchema<\n\tstring,\n\t`custom-${string}`,\n\tv.StringIssue | v.RegexIssue<string>\n>;\n\nexport const strategyNameSchema = v.union([\n\tv.picklist(BUILTIN_STRATEGY_NAMES),\n\tcustomStrategySchema,\n]);\n\nconst routeStrategySchema = v.union([\n\tv.object({\n\t\tmatch: v.string(),\n\t\tstrategy: v.array(strategyNameSchema),\n\t\texclude: v.optional(v.never()),\n\t}),\n\tv.object({\n\t\tmatch: v.string(),\n\t\texclude: v.literal(true),\n\t\tstrategy: v.optional(v.never()),\n\t}),\n]);\n\n/**\n * The valibot schema behind {@link ParaglideConfig}.\n *\n * Every option is optional — values that are not provided fall back to\n * the bundler plugin options / command line flags and ultimately to\n * `defaultCompilerOptions`.\n */\nexport const paraglideConfigSchema = v.object({\n\toutdir: v.optional(\n\t\tv.pipe(v.string(), v.nonEmpty('\"outdir\" must not be empty'))\n\t),\n\tstrategy: v.optional(v.array(strategyNameSchema)),\n\trouteStrategies: v.optional(v.array(routeStrategySchema)),\n\texperimentalMiddlewareLocaleSplitting: v.optional(v.boolean()),\n\tlocalStorageKey: v.optional(v.string()),\n\tisServer: v.optional(v.string()),\n\texperimentalStaticLocale: v.optional(v.string()),\n\tcookieName: v.optional(v.string()),\n\tcookieMaxAge: v.optional(v.number()),\n\tcookieDomain: v.optional(v.string()),\n\tcookieSameSite: v.optional(v.picklist([\"lax\", \"strict\", \"none\"])),\n\tcookieSecure: v.optional(v.union([v.boolean(), v.literal(\"auto\")])),\n\tadditionalFiles: v.optional(v.record(v.string(), v.string())),\n\temitPrettierIgnore: v.optional(v.boolean()),\n\temitReadme: v.optional(v.boolean()),\n\temitTsDeclarations: v.optional(v.boolean()),\n\turlPatterns: v.optional(\n\t\tv.array(\n\t\t\tv.object({\n\t\t\t\tpattern: v.string(),\n\t\t\t\tlocalized: v.array(v.tuple([v.string(), v.string()])),\n\t\t\t})\n\t\t)\n\t),\n\ttrailingSlash: v.optional(v.picklist([\"always\", \"never\"])),\n\tincludeEslintDisableComment: v.optional(v.boolean()),\n\tdisableAsyncLocalStorage: v.optional(v.boolean()),\n\temitGitIgnore: v.optional(v.boolean()),\n\toutputStructure: v.optional(\n\t\tv.picklist([\"message-modules\", \"locale-modules\"])\n\t),\n\tcleanOutdir: v.optional(v.boolean()),\n});\n\n// Fails to compile when the schema and the compiler options drift apart:\n// every compiler option (except the internal `fs` and the `project` path,\n// which the config cannot specify about itself) must be represented in the\n// schema, the schema must not contain extra options, and the parsed\n// values must match the compiler option types in both directions — a\n// schema narrower than a widened compiler option would wrongly reject\n// valid values.\ntype SchemaOutput = v.InferOutput<typeof paraglideConfigSchema>;\n// `keyof ParaglideConfig` already excludes `fs` and `project` (see its\n// definition) — this assertion is the only place that needs the\n// configurable-keys view.\nexport type SchemaMatchesCompilerOptions = [keyof ParaglideConfig] extends [\n\tkeyof SchemaOutput,\n]\n\t? [keyof SchemaOutput] extends [keyof ParaglideConfig]\n\t\t? SchemaOutput extends ParaglideConfig\n\t\t\t? ParaglideConfig extends SchemaOutput\n\t\t\t\t? true\n\t\t\t\t: never\n\t\t\t: never\n\t\t: never\n\t: never;\n\nexport const schemaMatchesCompilerOptions: SchemaMatchesCompilerOptions = true;\n"]}
|
|
@@ -93,6 +93,18 @@ describe("loadParaglideConfig", () => {
|
|
|
93
93
|
await writeFile(path.join(projectDir, "paraglide.config.ts"), "export default { cookieMaxAge: 'forever' }");
|
|
94
94
|
await expect(loadParaglideConfig({ projectDir })).rejects.toThrow(/"cookieMaxAge"/);
|
|
95
95
|
});
|
|
96
|
+
test("accepts the cookie samesite and secure options", async () => {
|
|
97
|
+
const projectDir = await createProjectDirectory();
|
|
98
|
+
await writeFile(path.join(projectDir, "paraglide.config.ts"), "export default { cookieSameSite: 'none', cookieSecure: 'auto' }");
|
|
99
|
+
const loaded = await loadParaglideConfig({ projectDir });
|
|
100
|
+
expect(loaded?.config.cookieSameSite).toBe("none");
|
|
101
|
+
expect(loaded?.config.cookieSecure).toBe("auto");
|
|
102
|
+
});
|
|
103
|
+
test("throws when cookieSameSite has an invalid value", async () => {
|
|
104
|
+
const projectDir = await createProjectDirectory();
|
|
105
|
+
await writeFile(path.join(projectDir, "paraglide.config.ts"), "export default { cookieSameSite: 'relaxed' }");
|
|
106
|
+
await expect(loadParaglideConfig({ projectDir })).rejects.toThrow(/"cookieSameSite"/);
|
|
107
|
+
});
|
|
96
108
|
test("throws when a strategy name is unknown", async () => {
|
|
97
109
|
const projectDir = await createProjectDirectory();
|
|
98
110
|
await writeFile(path.join(projectDir, "paraglide.config.ts"), "export default { strategy: ['cookie', 'cookies'] }");
|