fg-devkit 1.0.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.
@@ -0,0 +1,38 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "fg-devkit/tsconfig.base.json",
4
+ "include": [
5
+ "env.d.ts",
6
+ "src/**/*",
7
+ "src/**/*.vue",
8
+ "src/**/*.ts",
9
+ "src/**/*.d.ts",
10
+ "src/vuex.d.ts"
11
+ ],
12
+ "exclude": ["src/**/__tests__/*"],
13
+ "compilerOptions": {
14
+ "strict": true,
15
+ "composite": true,
16
+ "noEmit": true,
17
+ "types": ["@intlify/unplugin-vue-i18n/messages", "vue-i18n", "vite/client"],
18
+ "moduleResolution": "bundler",
19
+ "resolvePackageJsonExports": true,
20
+ "paths": {
21
+ "@/*": ["./src/*"],
22
+ "@intlify/*": ["./node_modules/@intlify/*"]
23
+ },
24
+ "baseUrl": ".",
25
+ "allowJs": true,
26
+ "esModuleInterop": true,
27
+ "allowSyntheticDefaultImports": true,
28
+ "noUnusedLocals": true,
29
+ "noUnusedParameters": true,
30
+ "skipLibCheck": true,
31
+ "preserveSymlinks": false
32
+ },
33
+ "vueCompilerOptions": {
34
+ "strictTemplates": true,
35
+ "experimentalCompatMode": 2,
36
+ "types": ["fds-vue-core"]
37
+ }
38
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
4
+ "compilerOptions": {
5
+ "skipLibCheck": true,
6
+ "resolveJsonModule": true,
7
+ "allowJs": true,
8
+ "noUnusedLocals": true,
9
+ "noUnusedParameters": true,
10
+ "paths": {
11
+ "@/*": ["./src/*"]
12
+ }
13
+ },
14
+ "vueCompilerOptions": {
15
+ "strictTemplates": true,
16
+ "types": ["fds-vue-core"]
17
+ }
18
+ }
@@ -0,0 +1,74 @@
1
+ {
2
+ "i18n-ally.localesPaths": ["src/lang"],
3
+ "i18n-ally.sourceLanguage": "sv",
4
+ "i18n-ally.displayLanguage": "sv",
5
+ "i18n-ally.keystyle": "nested",
6
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
7
+ "editor.formatOnSave": true,
8
+ "[javascript]": {
9
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
10
+ },
11
+ "[typescript]": {
12
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
13
+ },
14
+ "editor.codeActionsOnSave": {
15
+ "source.fixAll.eslint": "explicit",
16
+ "source.organizeImports": "explicit"
17
+ },
18
+ "editor.formatOnSaveMode": "file",
19
+ "typescript.format.semicolons": "remove",
20
+ "javascript.format.semicolons": "remove",
21
+ "editor.quickSuggestions": {
22
+ "other": true,
23
+ "comments": false,
24
+ "strings": true
25
+ },
26
+ "editor.suggest.showKeywords": true,
27
+ "editor.suggest.showSnippets": false,
28
+ "editor.suggest.showFolders": false,
29
+ "editor.wordBasedSuggestions": "off",
30
+ "editor.suggestOnTriggerCharacters": true,
31
+ "editor.tabCompletion": "on",
32
+ "editor.acceptSuggestionOnCommitCharacter": true,
33
+ "editor.quickSuggestionsDelay": 50,
34
+ "editor.suggestSelection": "first",
35
+ "editor.suggest.localityBonus": true,
36
+ "editor.parameterHints.enabled": false,
37
+ "editor.inlayHints.enabled": "off",
38
+ "editor.suggest.filterGraceful": true,
39
+ "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "vue"],
40
+ "css.customData": [".vscode/tailwind.json"],
41
+ "typescript.tsdk": "node_modules/typescript/lib",
42
+ "typescript.enablePromptUseWorkspaceTsdk": true,
43
+ "typescript.preferences.includePackageJsonAutoImports": "on",
44
+ "typescript.suggest.autoImports": true,
45
+ "typescript.updateImportsOnFileMove.enabled": "always",
46
+ "typescript.inlayHints.parameterNames.enabled": "none",
47
+ "typescript.inlayHints.variableTypes.enabled": false,
48
+ "typescript.inlayHints.propertyDeclarationTypes.enabled": false,
49
+ "typescript.inlayHints.functionLikeReturnTypes.enabled": false,
50
+ "javascript.inlayHints.parameterNames.enabled": "none",
51
+ "javascript.inlayHints.variableTypes.enabled": false,
52
+ "typescript.suggest.includeCompletionsForImportStatements": true,
53
+ "typescript.preferences.importModuleSpecifier": "relative",
54
+ "typescript.preferences.importModuleSpecifierEnding": "auto",
55
+ "typescript.suggest.completeFunctionCalls": true,
56
+ "typescript.suggest.paths": false,
57
+ "[vue]": {
58
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
59
+ "editor.quickSuggestions": {
60
+ "other": true,
61
+ "comments": false,
62
+ "strings": true
63
+ },
64
+ "editor.suggest.showSnippets": false,
65
+ "editor.suggest.showFiles": false,
66
+ "editor.suggest.showFolders": false,
67
+ "editor.suggest.snippetsPreventQuickSuggestions": true,
68
+ "editor.wordBasedSuggestions": "off",
69
+ "editor.quickSuggestionsDelay": 50,
70
+ "editor.suggestSelection": "first",
71
+ "editor.parameterHints.enabled": false,
72
+ "editor.inlayHints.enabled": "off"
73
+ }
74
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "fg-devkit",
3
+ "version": "1.0.0",
4
+ "description": "Shared configs, scripts, and DX tooling for Vue / TypeScript frontend repos",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "bin": {
8
+ "fg": "./scripts/fg-devkit.mjs",
9
+ "fg-devkit": "./scripts/fg-devkit.mjs",
10
+ "fg-vuln-commit": "./scripts/vuln-commit.mjs",
11
+ "fds-vuln-commit": "./scripts/vuln-commit.mjs"
12
+ },
13
+ "exports": {
14
+ "./tsconfig.base.json": "./configs/tsconfig.base.json",
15
+ "./tsconfig.app.sync.json": "./configs/tsconfig.app.sync.json",
16
+ "./eslint.config.base.js": "./configs/eslint.config.base.js",
17
+ "./prettier.config.js": "./configs/prettier.config.js",
18
+ "./vscode-settings.json": "./configs/vscode-settings.json",
19
+ "./dependency-policy.vue-workspace.json": "./configs/dependency-policy.vue-workspace.json",
20
+ "./vuln-commit": "./scripts/vuln-commit.mjs",
21
+ "./cz-adapter": "./scripts/cz/cz-adapter.cjs",
22
+ "./cz-config": "./scripts/cz/.cz-config.cjs",
23
+ "./scripts/sync-peers-from-dev.mjs": "./scripts/sync-peers-from-dev.mjs",
24
+ "./scripts/sync-vscode-settings.mjs": "./scripts/sync-vscode-settings.mjs",
25
+ "./scripts/sync-tooling-configs.mjs": "./scripts/sync-tooling-configs.mjs",
26
+ "./scripts/sync-project-configs.mjs": "./scripts/sync-project-configs.mjs",
27
+ "./scripts/check-dependency-policy.mjs": "./scripts/check-dependency-policy.mjs",
28
+ "./scripts/sync-dependency-policy.mjs": "./scripts/sync-dependency-policy.mjs",
29
+ "./scripts/resolve-dependency-policy-path.mjs": "./scripts/resolve-dependency-policy-path.mjs",
30
+ "./scripts/refresh-dependency-policy-from-repos.mjs": "./scripts/refresh-dependency-policy-from-repos.mjs",
31
+ "./scripts/fg-devkit.mjs": "./scripts/fg-devkit.mjs",
32
+ "./scripts/release.sh": "./scripts/release.sh",
33
+ "./scripts/cz/cz-adapter.cjs": "./scripts/cz/cz-adapter.cjs",
34
+ "./scripts/cz/.cz-config.cjs": "./scripts/cz/.cz-config.cjs",
35
+ "./cursor-rules/core-coding-standards.mdc": "./.cursor/rules/core-coding-standards.mdc"
36
+ },
37
+ "files": [
38
+ "configs",
39
+ "scripts",
40
+ ".cursor/rules/core-coding-standards.mdc"
41
+ ],
42
+ "engines": {
43
+ "node": "^20.19.0 || >=22.12.0"
44
+ },
45
+ "keywords": [
46
+ "eslint",
47
+ "prettier",
48
+ "typescript",
49
+ "vue"
50
+ ],
51
+ "peerDependencies": {
52
+ "@typescript-eslint/eslint-plugin": "^8.57.0",
53
+ "@typescript-eslint/parser": "^8.57.0",
54
+ "@vitest/eslint-plugin": "^1.5.0",
55
+ "@vue/eslint-config-prettier": "^10.0.0",
56
+ "@vue/tsconfig": "^0.8.0",
57
+ "eslint": "^9.0.0 || ^10.0.0",
58
+ "eslint-plugin-vue": "^10.0.0",
59
+ "typescript": ">=5.0.0",
60
+ "vue-eslint-parser": "^10.0.0"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "@vitest/eslint-plugin": {
64
+ "optional": true
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Validates the current package.json against the shared dependency policy.
4
+ *
5
+ * - Ensures the policy file itself uses pinned versions.
6
+ * - For each dependency listed in the policy, checks the consumer’s declared version matches.
7
+ * - For managedAnywhere packages, allows either dependencies or devDependencies but requires one consistent version.
8
+ * - Optionally checks policy-defined pnpm overrides against the consumer’s package.json.
9
+ * - Enforces pinned semver for any other dependencies/devDependencies/optionalDependencies and consumer pnpm.overrides.
10
+ *
11
+ * Policy path: first CLI argument, or default policy under fg-devkit/configs/ (see resolve-dependency-policy-path.mjs).
12
+ * Exits non-zero and prints mismatches if anything fails.
13
+ */
14
+
15
+ import fs from 'node:fs'
16
+ import path from 'node:path'
17
+ import { fileURLToPath } from 'node:url'
18
+
19
+ import { collectPinViolations, isPinnedDependencyVersion } from './dependency-policy-pins.mjs'
20
+ import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mjs'
21
+
22
+ const cwd = process.cwd()
23
+ const pkgPath = path.resolve(cwd, 'package.json')
24
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
25
+ const policyPath = resolveDependencyPolicyPath(scriptDir, cwd, process.argv)
26
+
27
+ if (!fs.existsSync(pkgPath)) {
28
+ console.error('[check-deps] Could not find package.json in current working directory')
29
+ process.exit(1)
30
+ }
31
+
32
+ if (!fs.existsSync(policyPath)) {
33
+ console.error(`[check-deps] Could not find dependency policy: ${policyPath}`)
34
+ console.error(
35
+ '[check-deps] Usage: node …/check-dependency-policy.mjs [policy-file]\n' +
36
+ ' policy-file: absolute path, path relative to cwd, or filename under fg-devkit/configs/ (default: dependency-policy.vue-workspace.json)',
37
+ )
38
+ process.exit(1)
39
+ }
40
+
41
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
42
+ const policy = JSON.parse(fs.readFileSync(policyPath, 'utf8'))
43
+ const mismatches = []
44
+
45
+ /** True if this package+section is governed by the policy (so global pin checks skip it). */
46
+ function isPolicyManagedInSection(section, name) {
47
+ if (policy.managedAnywhere?.[name] !== undefined) {
48
+ return true
49
+ }
50
+ return policy.managedDependencies?.[section]?.[name] !== undefined
51
+ }
52
+
53
+ // Policy file must only contain pinned versions.
54
+ for (const [section, dependencies] of Object.entries(policy.managedDependencies ?? {})) {
55
+ mismatches.push(...collectPinViolations(`[policy] managedDependencies.${section}.`, dependencies))
56
+ }
57
+
58
+ mismatches.push(...collectPinViolations('[policy] managedAnywhere.', policy.managedAnywhere ?? {}))
59
+ mismatches.push(...collectPinViolations('[policy] overrides.', policy.overrides ?? {}))
60
+
61
+ // Intersection: only packages that exist in both policy and consumer are compared.
62
+ for (const [section, dependencies] of Object.entries(policy.managedDependencies ?? {})) {
63
+ const current = pkg[section] ?? {}
64
+
65
+ for (const [name, expected] of Object.entries(dependencies)) {
66
+ const actual = current[name]
67
+ if (actual === undefined) {
68
+ continue
69
+ }
70
+ if (actual !== expected) {
71
+ mismatches.push(`${section}.${name}: expected ${expected}, got ${actual ?? '(missing)'}`)
72
+ } else if (!isPinnedDependencyVersion(actual)) {
73
+ mismatches.push(`${section}.${name}: must be pinned, got ${JSON.stringify(actual)}`)
74
+ }
75
+ }
76
+ }
77
+
78
+ // Packages allowed in either dependencies or devDependencies with a single expected version.
79
+ for (const [name, expected] of Object.entries(policy.managedAnywhere ?? {})) {
80
+ const dep = pkg.dependencies?.[name]
81
+ const dev = pkg.devDependencies?.[name]
82
+ if (dep === undefined && dev === undefined) {
83
+ continue
84
+ }
85
+ if (dep !== undefined && dev !== undefined && dep !== dev) {
86
+ mismatches.push(
87
+ `managedAnywhere.${name}: inconsistent versions (dependencies: ${dep}, devDependencies: ${dev})`,
88
+ )
89
+ continue
90
+ }
91
+ const actual = dep !== undefined ? dep : dev
92
+ if (actual !== expected) {
93
+ mismatches.push(`managedAnywhere.${name}: expected ${expected}, got ${actual ?? '(missing)'}`)
94
+ } else if (!isPinnedDependencyVersion(actual)) {
95
+ mismatches.push(`managedAnywhere.${name}: must be pinned, got ${JSON.stringify(actual)}`)
96
+ }
97
+ }
98
+
99
+ // Optional: policy may define pnpm.overrides entries the consumer must mirror.
100
+ const overrides = pkg.pnpm?.overrides ?? {}
101
+ for (const [name, expected] of Object.entries(policy.overrides ?? {})) {
102
+ const actual = overrides[name]
103
+ if (actual !== expected) {
104
+ mismatches.push(`pnpm.overrides.${name}: expected ${expected}, got ${actual ?? '(missing)'}`)
105
+ } else if (actual !== undefined && !isPinnedDependencyVersion(actual)) {
106
+ mismatches.push(`pnpm.overrides.${name}: must be pinned, got ${JSON.stringify(actual)}`)
107
+ }
108
+ }
109
+
110
+ // Everything not covered by the policy must still use exact pinned semver.
111
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
112
+ for (const [name, value] of Object.entries(pkg[section] ?? {})) {
113
+ if (isPolicyManagedInSection(section, name)) {
114
+ continue
115
+ }
116
+ if (!isPinnedDependencyVersion(value)) {
117
+ mismatches.push(
118
+ `[pins] ${section}.${name}: must be exact pinned semver (no ^ ~ ranges); got ${JSON.stringify(value)}`,
119
+ )
120
+ }
121
+ }
122
+ }
123
+
124
+ // Consumer pnpm.overrides must be pinned (ranges reported even if not in policy).
125
+ mismatches.push(...collectPinViolations('[pins] pnpm.overrides.', pkg.pnpm?.overrides ?? {}))
126
+
127
+ if (mismatches.length > 0) {
128
+ console.error('[check-deps] Dependency policy mismatches found:')
129
+ for (const mismatch of mismatches) {
130
+ console.error(`- ${mismatch}`)
131
+ }
132
+ process.exit(1)
133
+ }
134
+
135
+ console.log('[check-deps] package.json matches dependency policy')
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Commitizen configuration (legacy path): commit types, prompts, and message limits.
3
+ * Used when tooling points commitizen at this file instead of the cz-adapter prompter.
4
+ */
5
+ module.exports = {
6
+ types: [
7
+ { value: 'feat', name: 'feat(scope): Added' },
8
+ { value: 'fix', name: 'fix(scope): Fixed' },
9
+ { value: 'perf', name: 'perf(scope): Performance' },
10
+ { value: 'refactor', name: 'refactor(scope): Changed' },
11
+ { value: 'chore', name: 'chore(scope): Changed' },
12
+ { value: 'dep', name: 'dep(scope): Dependencies (single package or batch)' },
13
+ { value: 'vuln', name: 'vuln(scope): Security dependency update (single package or batch)' },
14
+ { value: 'fix(security)', name: 'fix(security): Security' }
15
+ ],
16
+ allowCustomScopes: true,
17
+ allowBreakingChanges: [],
18
+ skipQuestions: ['footer', 'breaking'],
19
+ subjectLimit: 120,
20
+ messages: {
21
+ type: 'Select the type of change you are committing:',
22
+ customScope: 'Packages (for dep/vuln) or scope:',
23
+ subject: 'Commit subject (imperative, no trailing period):',
24
+ body: "Optional body (use '|' for newline):"
25
+ }
26
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Custom Commitizen adapter: interactive prompts for type, scope, dep/vuln flows, and message body.
3
+ * Builds the final conventional commit string and passes it to commit().
4
+ */
5
+ module.exports = {
6
+ prompter(cz, commit) {
7
+ const types = [
8
+ { value: "feat", name: "feat(scope): Added" },
9
+ { value: "fix", name: "fix(scope): Fixed" },
10
+ { value: "perf", name: "perf(scope): Performance" },
11
+ { value: "refactor", name: "refactor(scope): Changed" },
12
+ { value: "chore", name: "chore(scope): Changed" },
13
+ { value: "docs", name: "docs(scope): Documentation" },
14
+ { value: "dep", name: "dep(scope): Dependencies (single package or batch)" },
15
+ { value: "vuln", name: "vuln(scope): Security dependency update (single package or batch)" },
16
+ { value: "fix(security)", name: "fix(security): Security" },
17
+ ];
18
+
19
+ const questions = [
20
+ {
21
+ type: "list",
22
+ name: "change_type",
23
+ message: "Select the type of change you are committing",
24
+ choices: types,
25
+ },
26
+ {
27
+ type: "input",
28
+ name: "scope",
29
+ when: (answers) => !["dep", "vuln"].includes(answers.change_type),
30
+ message: "Scope:",
31
+ },
32
+ {
33
+ type: "input",
34
+ name: "packages",
35
+ when: (answers) => ["dep", "vuln"].includes(answers.change_type),
36
+ message: "Packages:",
37
+ },
38
+ {
39
+ type: "list",
40
+ name: "vuln_change_kind",
41
+ when: (answers) => answers.change_type === "vuln",
42
+ message: "Vulnerability change type:",
43
+ choices: [
44
+ { value: "override", name: "override" },
45
+ { value: "bump", name: "bump" },
46
+ ],
47
+ },
48
+ {
49
+ type: "input",
50
+ name: "vuln_target_version",
51
+ when: (answers) => answers.change_type === "vuln",
52
+ message: "Target version/range (e.g. 5.2.2 or >=5.2.2):",
53
+ validate: (value) => (value && value.trim() ? true : "Provide a target version/range"),
54
+ },
55
+ {
56
+ type: "list",
57
+ name: "dep_change_kind",
58
+ when: (answers) => answers.change_type === "dep",
59
+ message: "Dependency change type:",
60
+ choices: [
61
+ { value: "new", name: "new package" },
62
+ { value: "update", name: "updated package" },
63
+ { value: "remove", name: "removed package" },
64
+ ],
65
+ },
66
+ {
67
+ type: "input",
68
+ name: "dep_version",
69
+ when: (answers) => answers.change_type === "dep",
70
+ message: "Version (x.y.z):",
71
+ validate: (value) => (/^\d+\.\d+\.\d+$/.test(value) ? true : "Use format x.y.z"),
72
+ },
73
+ {
74
+ type: "input",
75
+ name: "shortcut_id",
76
+ when: (answers) => !["dep", "vuln"].includes(answers.change_type),
77
+ message: "Shortcut ID digits only (optional, e.g. 1234):",
78
+ },
79
+ {
80
+ type: "input",
81
+ name: "message",
82
+ when: (answers) => !["dep", "vuln"].includes(answers.change_type),
83
+ message: "Commit subject (imperative, no trailing period):",
84
+ },
85
+ {
86
+ type: "input",
87
+ name: "body",
88
+ message: "Optional body (use '|' for newline):",
89
+ },
90
+ ];
91
+
92
+ cz.prompt(questions).then((answers) => {
93
+ const type = answers.change_type;
94
+ const scopeValue = ["dep", "vuln"].includes(type) ? answers.packages : answers.scope;
95
+ const needsEmptyParens = ["dep", "vuln"].includes(type) && !scopeValue;
96
+ const scopePart = scopeValue ? `(${scopeValue})` : needsEmptyParens ? "()" : "";
97
+ const shortcutPart = answers.shortcut_id ? ` [sc-${answers.shortcut_id}]` : "";
98
+ const bodyPart = answers.body ? `\n\n${answers.body.split("|").join("\n")}` : "";
99
+ const depSubject =
100
+ answers.dep_change_kind === "new"
101
+ ? `add ${answers.dep_version}`
102
+ : answers.dep_change_kind === "remove"
103
+ ? `yeet ${answers.dep_version}`
104
+ : `bump to ${answers.dep_version}`;
105
+ const vulnSubject =
106
+ type === "vuln"
107
+ ? answers.vuln_change_kind === "override"
108
+ ? `override ${answers.vuln_target_version}`
109
+ : `bump to ${answers.vuln_target_version}`
110
+ : "";
111
+ const subject = type === "dep" ? depSubject : type === "vuln" ? vulnSubject : answers.message;
112
+ const commitMessage = `${type}${scopePart}: ${subject}${shortcutPart}${bodyPart}`;
113
+ commit(commitMessage);
114
+ });
115
+ },
116
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Helpers for dependency policy tooling: decide whether a version string is an exact pin
3
+ * and collect human-readable violations for a record of package → version.
4
+ *
5
+ * Strict pinning: exact x.y.z (optional -prerelease). No ^ ~ >= ranges or wildcards.
6
+ * Local / monorepo protocols (file:, workspace:, link:, relative paths) count as pinned.
7
+ */
8
+ export function isPinnedDependencyVersion(value) {
9
+ if (typeof value !== 'string' || value.length === 0) {
10
+ return false
11
+ }
12
+ if (/^(file:|workspace:|link:|\.\/|\.\.\/)/.test(value)) {
13
+ return true
14
+ }
15
+ if (/[\^~><=*|]/.test(value)) {
16
+ return false
17
+ }
18
+ if (/\s/.test(value)) {
19
+ return false
20
+ }
21
+ return /^\d+\.\d+\.\d+(-[\w.]+)?$/.test(value)
22
+ }
23
+
24
+ /** Returns one message per entry in `record` whose value is not pinned per isPinnedDependencyVersion. */
25
+ export function collectPinViolations(labelPrefix, record) {
26
+ const out = []
27
+ for (const [name, value] of Object.entries(record ?? {})) {
28
+ if (!isPinnedDependencyVersion(value)) {
29
+ out.push(
30
+ `${labelPrefix}${name}: must be exact pinned semver (no ^ ~ ranges); got ${JSON.stringify(value)}`,
31
+ )
32
+ }
33
+ }
34
+ return out
35
+ }