Skip to content

Commit ac8efca

Browse files
committed
refactor(many): convert ui-scripts to TypeScript and add tests
INSTUI-5038
1 parent 3dd1736 commit ac8efca

59 files changed

Lines changed: 1921 additions & 211 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@
118118
"git-raw-commits>dargs": "Force dargs@7.0.0 (CommonJS) for git-raw-commits@3.0.0 used by lerna. pnpm hoisting was causing ESM dargs@8.1.0 to be resolved instead, breaking 'pnpm run bump' with 'TypeError: dargs is not a function'"
119119
},
120120
"engines": {
121-
"node": ">=22",
121+
"node": ">=22.18.0",
122122
"npm": "Use pnpm instead.",
123123
"pnpm": ">=10"
124124
},

packages/command-utils/lib/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class Command {
7676
/**
7777
* @param bin {string} The binary name, e.g. `pwd`
7878
* @param args {string[]} command arguments
79-
* @param envVars {Object.<string, string>} Environment variables
79+
* @param [envVars] {Object.<string, string>} Environment variables
8080
*/
8181
function getCommand(bin, args, envVars) {
8282
return new Command(bin, args, envVars)

packages/pkg-utils/lib/get-changed-packages.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ const path = require('path')
2626
const getPackages = require('./get-packages')
2727
const childProcess = require('child_process')
2828

29+
/**
30+
* @param [commitIsh] {string}
31+
* @param [allPackages] {any[]}
32+
*/
2933
module.exports = function getChangedPackages(
3034
commitIsh = 'HEAD^1',
3135
allPackages

packages/pkg-utils/lib/get-package.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,17 @@ const path = require('path')
2727
const readPkgUp = require('read-pkg-up')
2828
const Package = require('@lerna/package').Package
2929

30+
/**
31+
* @param [options] {readPkgUp.NormalizeOptions}
32+
*/
3033
exports.getPackage = function getPackage(options) {
3134
const result = readPackage(options)
3235
return new Package(result.packageJson, path.dirname(result.path))
3336
}
3437

3538
/**
3639
* Reads a package.json
37-
* @param options {readPkgUp.NormalizeOptions}
40+
* @param [options] {readPkgUp.NormalizeOptions}
3841
* @returns {readPkgUp.NormalizedPackageJson}
3942
*/
4043
exports.getPackageJSON = function getPackageJSON(options) {

packages/ui-codemods/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"lint": "ui-scripts lint",
1717
"lint:fix": "ui-scripts lint --fix",
1818
"ts:check": "tsc -p tsconfig.build.json --noEmit --emitDeclarationOnly false",
19-
"generate:versioned-exports": "node --experimental-strip-types scripts/generateVersionedExports.ts"
19+
"generate:versioned-exports": "node scripts/generateVersionedExports.ts"
2020
},
2121
"license": "MIT",
2222
"dependencies": {

packages/ui-codemods/scripts/generateVersionedExports.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
/**
2626
* Generates a list of all versioned components based on the @instructure/ui meta package
27-
* Run with: node --experimental-strip-types scripts/generateVersionedExports.ts
27+
* Run with: node scripts/generateVersionedExports.ts
2828
* from the packages/ui-codemods directory.
2929
*/
3030

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2015 - present Instructure, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
import { describe, it, expect } from 'vitest'
26+
import { fuzzyMatch } from '../commands/create-component-version.ts'
27+
28+
const components = [
29+
{ label: '', dir: '', pkg: 'ui-buttons', name: 'Button', versions: ['v1'] },
30+
{ label: '', dir: '', pkg: 'ui-text', name: 'Text', versions: ['v1'] },
31+
{ label: '', dir: '', pkg: 'ui-tabs', name: 'Tabs', versions: ['v1', 'v2'] },
32+
{
33+
label: '',
34+
dir: '',
35+
pkg: 'ui-buttons',
36+
name: 'IconButton',
37+
versions: ['v1']
38+
}
39+
]
40+
41+
describe('fuzzyMatch', () => {
42+
it('matches when the query is a substring of pkg or name', () => {
43+
expect(
44+
fuzzyMatch(components, 'button')
45+
.map((c) => c.name)
46+
.sort()
47+
).toEqual(['Button', 'IconButton'])
48+
})
49+
50+
it('matches characters in order even when they are not contiguous', () => {
51+
const matches = fuzzyMatch(components, 'utt').map((c) => c.name)
52+
expect(matches).toContain('Button')
53+
})
54+
55+
it('is case-insensitive', () => {
56+
expect(
57+
fuzzyMatch(components, 'BUTTON')
58+
.map((c) => c.name)
59+
.sort()
60+
).toEqual(['Button', 'IconButton'])
61+
})
62+
63+
it('returns an empty array when no component matches', () => {
64+
expect(fuzzyMatch(components, 'xyz')).toEqual([])
65+
})
66+
67+
it('returns every component for an empty query string', () => {
68+
expect(fuzzyMatch(components, '')).toHaveLength(components.length)
69+
})
70+
71+
it('considers both pkg and name when scoring (ordered chars)', () => {
72+
const matches = fuzzyMatch(components, 'tabt').map((c) => c.name)
73+
expect(matches).toContain('Tabs')
74+
})
75+
})
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2015 - present Instructure, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
import { describe, it, expect, afterEach } from 'vitest'
26+
import {
27+
existsSync,
28+
mkdtempSync,
29+
readFileSync,
30+
rmSync,
31+
writeFileSync
32+
} from 'fs'
33+
import { tmpdir } from 'os'
34+
import { join } from 'path'
35+
import createFile from '../build/buildThemes/createFile.ts'
36+
37+
describe('createFile', () => {
38+
let dir: string
39+
40+
afterEach(() => {
41+
if (dir) rmSync(dir, { recursive: true, force: true })
42+
})
43+
44+
it('writes a file with the license header prepended to the content', async () => {
45+
dir = mkdtempSync(join(tmpdir(), 'create-file-')) + '/'
46+
const target = dir + 'output.ts'
47+
48+
await createFile(target, 'export const x = 1')
49+
50+
const written = readFileSync(target, 'utf-8')
51+
expect(written).toContain('The MIT License (MIT)')
52+
expect(written).toContain('export const x = 1')
53+
expect(written.indexOf('The MIT License')).toBeLessThan(
54+
written.indexOf('export const x = 1')
55+
)
56+
})
57+
58+
it('creates missing parent directories', async () => {
59+
dir = mkdtempSync(join(tmpdir(), 'create-file-')) + '/'
60+
const target = dir + 'deeply/nested/output.ts'
61+
62+
await createFile(target, 'hello')
63+
64+
expect(existsSync(target)).toBe(true)
65+
})
66+
67+
it('overwrites an existing file at the same path', async () => {
68+
dir = mkdtempSync(join(tmpdir(), 'create-file-')) + '/'
69+
const target = dir + 'output.ts'
70+
71+
writeFileSync(target, 'old content', 'utf-8')
72+
73+
await createFile(target, 'new content')
74+
75+
const written = readFileSync(target, 'utf-8')
76+
expect(written).toContain('new content')
77+
expect(written).not.toContain('old content')
78+
})
79+
80+
it('does not throw when the target does not already exist', async () => {
81+
dir = mkdtempSync(join(tmpdir(), 'create-file-')) + '/'
82+
const target = dir + 'output.ts'
83+
84+
// The function tries to unlink first — ENOENT must be swallowed
85+
await expect(createFile(target, 'hello')).resolves.not.toThrow()
86+
})
87+
})
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2015 - present Instructure, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
import { describe, it, expect, afterEach, vi } from 'vitest'
26+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs'
27+
import { tmpdir } from 'os'
28+
import { join } from 'path'
29+
import generateCustomIndex from '../icons/generate-custom-index.ts'
30+
31+
describe('generateCustomIndex', () => {
32+
let dir: string
33+
34+
afterEach(() => {
35+
if (dir) rmSync(dir, { recursive: true, force: true })
36+
vi.restoreAllMocks()
37+
})
38+
39+
function setupFixtures() {
40+
dir = mkdtempSync(join(tmpdir(), 'gen-custom-')) + '/'
41+
mkdirSync(dir + 'svg/Custom', { recursive: true })
42+
writeFileSync(
43+
dir + 'svg/Custom/ai-info.svg',
44+
'<svg viewBox="0 0 24 24"><path d="M0,0" fill="currentColor"/></svg>'
45+
)
46+
writeFileSync(
47+
dir + 'svg/Custom/canvas-logo.svg',
48+
'<svg viewBox="0 0 32 32"><circle cx="16" cy="16" r="8"/></svg>'
49+
)
50+
vi.spyOn(process, 'cwd').mockReturnValue(dir.slice(0, -1))
51+
vi.spyOn(console, 'log').mockImplementation(() => {})
52+
}
53+
54+
it('writes one InstUIIcon export per SVG file', () => {
55+
setupFixtures()
56+
generateCustomIndex()
57+
const content = readFileSync(
58+
dir + 'src/generated/custom/index.tsx',
59+
'utf-8'
60+
)
61+
const exportCount = (content.match(/^export const /gm) || []).length
62+
expect(exportCount).toBe(2)
63+
})
64+
65+
it('converts kebab-case filenames to PascalCase icon names', () => {
66+
setupFixtures()
67+
generateCustomIndex()
68+
const content = readFileSync(
69+
dir + 'src/generated/custom/index.tsx',
70+
'utf-8'
71+
)
72+
expect(content).toContain('export const AiInfoInstUIIcon')
73+
expect(content).toContain('export const CanvasLogoInstUIIcon')
74+
})
75+
76+
it('forwards the SVG viewBox to wrapCustomIcon', () => {
77+
setupFixtures()
78+
generateCustomIndex()
79+
const content = readFileSync(
80+
dir + 'src/generated/custom/index.tsx',
81+
'utf-8'
82+
)
83+
expect(content).toContain(
84+
"wrapCustomIcon(AiInfoPaths, 'AiInfo', '0 0 24 24')"
85+
)
86+
expect(content).toContain(
87+
"wrapCustomIcon(CanvasLogoPaths, 'CanvasLogo', '0 0 32 32')"
88+
)
89+
})
90+
91+
it('rewrites fill="currentColor" to ={color} so consumers can theme the icon', () => {
92+
setupFixtures()
93+
generateCustomIndex()
94+
const content = readFileSync(
95+
dir + 'src/generated/custom/index.tsx',
96+
'utf-8'
97+
)
98+
expect(content).toContain('fill={color}')
99+
expect(content).not.toContain('fill="currentColor"')
100+
})
101+
102+
it('throws if the svg/Custom directory does not exist', () => {
103+
dir = mkdtempSync(join(tmpdir(), 'gen-custom-')) + '/'
104+
vi.spyOn(process, 'cwd').mockReturnValue(dir.slice(0, -1))
105+
vi.spyOn(console, 'log').mockImplementation(() => {})
106+
expect(() => generateCustomIndex()).toThrow(/SVG directory not found/)
107+
})
108+
})

0 commit comments

Comments
 (0)