@weft-js/warp-icons 0.1.1 → 0.1.2
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 -4
- package/compile.mjs +267 -399
- package/package.json +12 -3
- package/plugin.mjs +33 -7
- package/src/action.tsx +7 -1
- package/src/all.tsx +4 -1
- package/src/compile-catalog.mjs +96 -0
- package/src/compile-closest.mjs +19 -0
- package/src/compile-names.mjs +33 -0
- package/src/compile-pascal.mjs +4 -0
- package/src/compile-read.mjs +227 -0
- package/src/compile-resolver.mjs +169 -0
- package/src/compile-rewrite-pack.mjs +39 -0
- package/src/compile-unwrap.mjs +19 -0
- package/src/gen/actions/bell.tsx +1 -1
- package/src/gen/actions/check.tsx +1 -1
- package/src/gen/actions/copy.tsx +1 -1
- package/src/gen/actions/refresh.tsx +1 -1
- package/src/gen/actions/trash.tsx +1 -1
- package/src/gen/all-accessibility-bus.tsx +274 -0
- package/src/gen/all-cake-cursor.tsx +284 -0
- package/src/gen/all-database-funnel.tsx +276 -0
- package/src/gen/all-gallery-logout.tsx +252 -0
- package/src/gen/all-magnet-puzzle.tsx +285 -0
- package/src/gen/all-qr-syringe.tsx +328 -0
- package/src/gen/all-tab-zoom.tsx +280 -0
- package/src/gen/all.tsx +8 -1959
- package/src/icon.tsx +16 -3
- package/src/icons.css +16 -3
- package/src/late.tsx +46 -12
- package/src/lookup.tsx +2 -1
- package/src/set.tsx +4 -1
- package/src/svg.tsx +23 -4
- package/src/tiers.css +1 -0
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weft-js/warp-icons",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Icons for warp and weft",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Ravonus/weft.git",
|
|
9
|
+
"directory": "packages/warp-icons"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Ravonus/weft/tree/main/packages/warp-icons#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Ravonus/weft/issues"
|
|
14
|
+
},
|
|
6
15
|
"type": "module",
|
|
7
16
|
"sideEffects": [
|
|
8
17
|
"*.css"
|
|
@@ -23,7 +32,7 @@
|
|
|
23
32
|
},
|
|
24
33
|
"peerDependencies": {
|
|
25
34
|
"vite": "*",
|
|
26
|
-
"@weft-js/core": "^0.1.
|
|
35
|
+
"@weft-js/core": "^0.1.2"
|
|
27
36
|
},
|
|
28
37
|
"publishConfig": {
|
|
29
38
|
"access": "public"
|
|
@@ -41,4 +50,4 @@
|
|
|
41
50
|
"README.md",
|
|
42
51
|
"LICENSE"
|
|
43
52
|
]
|
|
44
|
-
}
|
|
53
|
+
}
|
package/plugin.mjs
CHANGED
|
@@ -5,13 +5,18 @@ import { definePlugin } from '@weft-js/core/plugin'
|
|
|
5
5
|
const TIERS = ['quiet', 'balanced', 'vivid']
|
|
6
6
|
|
|
7
7
|
function options(input) {
|
|
8
|
-
for (const key of Object.keys(input))
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
for (const key of Object.keys(input))
|
|
9
|
+
if (key !== 'tier' && key !== 'source')
|
|
10
|
+
throw Object.assign(new Error(`unknown option \`${key}\``), { path: key })
|
|
11
|
+
if (input.tier !== undefined && !TIERS.includes(input.tier))
|
|
12
|
+
throw Object.assign(new Error(`must be one of ${TIERS.join(', ')}`), { path: 'tier' })
|
|
13
|
+
if (input.source !== undefined && !/^https?:\/\/.+\/$/.test(input.source))
|
|
14
|
+
throw Object.assign(new Error('must be an http(s) URL ending in /'), { path: 'source' })
|
|
11
15
|
return { ...(input.tier && { tier: input.tier }), ...(input.source && { source: input.source }) }
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
const agents =
|
|
18
|
+
const agents =
|
|
19
|
+
'Import icons from `@weft-js/warp-icons`: `CheckIcon` is hollow, the default, `CheckIconSolid` the other form (`<Icon name="check" solid />`); `<Icons.Check />` and warp\'s `<Icon name="check" />` build to the same bytes. Tiers are a prop, `tier`, or the page\'s `data-expression`. Decorative by default; `label` gives one a name. An unknown literal name is I001 at the build and in `weft check`. A name only known at runtime is legal and loads the pack lazily (I002 reports it), or single files with `icons({ source })` and `weft icons publish <dir>`. `trash`, `bell`, `check`, `refresh` and `copy` animate with `action`. `weft docs icons` has the rest.'
|
|
15
20
|
|
|
16
21
|
/** `icons()` for `weft.config.ts`: pins a tier if asked, and tells agents how the names work. */
|
|
17
22
|
export const icons = definePlugin({
|
|
@@ -21,10 +26,31 @@ export const icons = definePlugin({
|
|
|
21
26
|
options,
|
|
22
27
|
contribute(o) {
|
|
23
28
|
return {
|
|
24
|
-
tokens: {
|
|
29
|
+
tokens: {
|
|
30
|
+
namespace: 'icons',
|
|
31
|
+
values: { tier: o.tier ?? 'runtime' },
|
|
32
|
+
...((o.tier || o.source) && {
|
|
33
|
+
alias: {
|
|
34
|
+
...(o.tier && { '@weft-js/warp-icons': `@weft-js/warp-icons/${o.tier}` }),
|
|
35
|
+
...(o.source && { '@weft-js/warp-icons/source': o.source }),
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
25
39
|
agents: { title: 'Icons', text: agents },
|
|
26
|
-
catalog: [
|
|
27
|
-
|
|
40
|
+
catalog: [
|
|
41
|
+
{
|
|
42
|
+
name: 'Icon',
|
|
43
|
+
import: '@weft-js/warp-icons/all',
|
|
44
|
+
summary: 'Any icon by name, for names that come from data; costs the whole set',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
map: [
|
|
48
|
+
{
|
|
49
|
+
want: 'An icon',
|
|
50
|
+
call: 'Import `NameIcon` from `@weft-js/warp-icons`; `npm run sheet` in the package draws them all',
|
|
51
|
+
have: 'one export per icon',
|
|
52
|
+
},
|
|
53
|
+
],
|
|
28
54
|
}
|
|
29
55
|
},
|
|
30
56
|
})
|
package/src/action.tsx
CHANGED
|
@@ -8,5 +8,11 @@ export type ActionIconProps = IconProps & { action?: IconTrigger | boolean }
|
|
|
8
8
|
|
|
9
9
|
/*#__NO_SIDE_EFFECTS__*/
|
|
10
10
|
export function acting(line: string, pixels: string) {
|
|
11
|
-
return ({ action, ...rest }: ActionIconProps) =>
|
|
11
|
+
return ({ action, ...rest }: ActionIconProps) => (
|
|
12
|
+
<Svg
|
|
13
|
+
{...rest}
|
|
14
|
+
data-act={typeof action === 'string' ? action : 'hover focus press'}
|
|
15
|
+
html={line + pixels}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
12
18
|
}
|
package/src/all.tsx
CHANGED
|
@@ -6,4 +6,7 @@ import { lookup } from './lookup'
|
|
|
6
6
|
export { actions, lookup, names, type IconActionName, type IconName, type IconVariant }
|
|
7
7
|
export { Icon, type IconByNameProps } from './icon'
|
|
8
8
|
/** Every icon, by its name. */
|
|
9
|
-
export const icons = Object.fromEntries(names.map(n => [n, lookup(n)])) as Record<
|
|
9
|
+
export const icons = Object.fromEntries(names.map((n) => [n, lookup(n)])) as Record<
|
|
10
|
+
IconName,
|
|
11
|
+
IconComponent
|
|
12
|
+
>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { brotliCompressSync, constants } from 'node:zlib'
|
|
4
|
+
|
|
5
|
+
/** The icons that have an action, read from the generated names. */
|
|
6
|
+
export const actionList = () =>
|
|
7
|
+
[
|
|
8
|
+
...readFileSync(new URL('./gen/names.ts', import.meta.url), 'utf8')
|
|
9
|
+
.match(/const actions = (\[.*?\])/)[1]
|
|
10
|
+
.matchAll(/"([a-z0-9-]+)"/g),
|
|
11
|
+
].map((m) => m[1])
|
|
12
|
+
/** Every icon name, read from the generated names. */
|
|
13
|
+
export const list = () => [
|
|
14
|
+
...new Set(
|
|
15
|
+
[
|
|
16
|
+
...readFileSync(new URL('./gen/names.ts', import.meta.url), 'utf8')
|
|
17
|
+
.split('\n')[1]
|
|
18
|
+
.matchAll(/"([a-z0-9-]+)"/g),
|
|
19
|
+
].map((m) => m[1]),
|
|
20
|
+
),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
/** What the pack costs if a page takes it: every icon in one tier, brotli. */
|
|
24
|
+
export function packCost() {
|
|
25
|
+
const entry = readFileSync(new URL('./gen/all.tsx', import.meta.url), 'utf8')
|
|
26
|
+
const modules = [...entry.matchAll(/^export \* from '(\.\/all-[^']+)'$/gm)]
|
|
27
|
+
const header = entry
|
|
28
|
+
.slice(0, entry.indexOf('export * from'))
|
|
29
|
+
.replace("import '../svg'", "import { icon } from '../svg'")
|
|
30
|
+
const code =
|
|
31
|
+
header +
|
|
32
|
+
modules
|
|
33
|
+
.map(([, path]) =>
|
|
34
|
+
readFileSync(new URL(`./gen/${path.slice(2)}.tsx`, import.meta.url), 'utf8').replace(
|
|
35
|
+
/^.*\nimport \{ icon \} from '\.\.\/svg'\n\n/,
|
|
36
|
+
'',
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
.join('')
|
|
40
|
+
return brotliCompressSync(Buffer.from(code), { params: { [constants.BROTLI_PARAM_QUALITY]: 5 } })
|
|
41
|
+
.length
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `weft check`'s view: the same walk a build does, as findings. */
|
|
45
|
+
export function findings(root, analyze) {
|
|
46
|
+
const catalog = list(),
|
|
47
|
+
r = analyze(root, { catalog })
|
|
48
|
+
const kb = `${(packCost() / 1024).toFixed(0)} KB`
|
|
49
|
+
return [
|
|
50
|
+
...r.unknown.map((u) => ({
|
|
51
|
+
code: 'I001',
|
|
52
|
+
file: u.file,
|
|
53
|
+
line: u.line,
|
|
54
|
+
column: u.column,
|
|
55
|
+
why: u.why ?? `no icon '${u.name}', did you mean '${u.suggest}'?`,
|
|
56
|
+
})),
|
|
57
|
+
...r.dynamic.map((d) => ({
|
|
58
|
+
code: 'I002',
|
|
59
|
+
file: d.file,
|
|
60
|
+
line: d.line,
|
|
61
|
+
column: d.column,
|
|
62
|
+
why: `\`${d.why}\` isn't known until runtime, so this use loads the icon pack lazily (about ${kb} br, only on pages that reach it).`,
|
|
63
|
+
})),
|
|
64
|
+
...r.escaped.map((d) => ({
|
|
65
|
+
code: 'I003',
|
|
66
|
+
file: d.file,
|
|
67
|
+
line: d.line,
|
|
68
|
+
column: d.column,
|
|
69
|
+
why: `${d.why}, so every icon ships with the page (about ${kb} br).`,
|
|
70
|
+
})),
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Every icon and variant as a static .svg, for a CDN or bucket. Writes files; nothing is uploaded. */
|
|
75
|
+
export async function publish(dir) {
|
|
76
|
+
const { load } = await import('../scripts/build.mjs')
|
|
77
|
+
const { forms } = await import('../scripts/forms.mjs')
|
|
78
|
+
const icons = await load()
|
|
79
|
+
mkdirSync(dir, { recursive: true })
|
|
80
|
+
const svg = (html) =>
|
|
81
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">${html}</svg>\n`
|
|
82
|
+
let files = 0
|
|
83
|
+
for (const [name, entry] of Object.entries(icons)) {
|
|
84
|
+
const f = forms(name, entry)
|
|
85
|
+
const all = { '': f.hollow.line + f.hollow.pixels, '.solid': f.solid.line + f.solid.pixels }
|
|
86
|
+
for (const [suffix, html] of Object.entries(all)) {
|
|
87
|
+
writeFileSync(join(dir, `${name}${suffix}.svg`), svg(html))
|
|
88
|
+
files++
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
writeFileSync(
|
|
92
|
+
join(dir, 'manifest.json'),
|
|
93
|
+
JSON.stringify({ icons: Object.keys(icons), variants: ['', 'solid'] }) + '\n',
|
|
94
|
+
)
|
|
95
|
+
return { dir, files, icons: Object.keys(icons).length }
|
|
96
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function distance(a, b) {
|
|
2
|
+
let row = Array.from({ length: b.length + 1 }, (_, i) => i)
|
|
3
|
+
for (let i = 1; i <= a.length; i++) {
|
|
4
|
+
const next = [i]
|
|
5
|
+
for (let j = 1; j <= b.length; j++)
|
|
6
|
+
next[j] = Math.min(next[j - 1] + 1, row[j] + 1, row[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1))
|
|
7
|
+
row = next
|
|
8
|
+
}
|
|
9
|
+
return row[b.length]
|
|
10
|
+
}
|
|
11
|
+
/** The name nearest to a typo, for "did you mean": a prefix counts as one edit closer. */
|
|
12
|
+
export const closest = (name, names) =>
|
|
13
|
+
names.reduce(
|
|
14
|
+
(best, n) => {
|
|
15
|
+
const d = distance(name, n) - (n.startsWith(name) ? 1 : 0)
|
|
16
|
+
return d < best.d ? { n, d } : best
|
|
17
|
+
},
|
|
18
|
+
{ n: null, d: Infinity },
|
|
19
|
+
).n
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { pascal } from './compile-pascal.mjs'
|
|
2
|
+
|
|
3
|
+
/** The module warp's name lookup becomes in a build: these names and no others, and the pack only if a name escaped. */
|
|
4
|
+
/** @param {Iterable<string>} names */
|
|
5
|
+
export function namesModule(names, { lazy = false, source } = {}) {
|
|
6
|
+
const list = [...names].sort()
|
|
7
|
+
const id = (key) => pascal(...key.split(':'))
|
|
8
|
+
const imports = list.length
|
|
9
|
+
? `import { ${list.map(id).join(', ')} } from '@weft-js/warp-icons'\n`
|
|
10
|
+
: ''
|
|
11
|
+
const table = `const names = { ${list.map((n) => `${JSON.stringify(n)}: ${id(n)}`).join(', ')} }\nconst key = (name, variant) => (variant ? name + ':' + variant : name)\n`
|
|
12
|
+
const packExport = `export const pack = id => { const [, b, v = ''] = /^(.+?)(Hollow|Solid|Action)?$/.exec(id.replace(/Icon(?=$|Hollow$|Solid$|Action$)/, '')); return resolve(b.replace(/[A-Z]/g, c => '-' + c.toLowerCase()).replace(/^-/, ''), v.toLowerCase()) }\n`
|
|
13
|
+
if (source)
|
|
14
|
+
return `${imports}import { remote } from '@weft-js/warp-icons/late'\n${table}const get = remote(${JSON.stringify(source)})\nexport const resolve = (name, variant) => names[key(name, variant)] ?? get(name, variant)\n${packExport}`
|
|
15
|
+
if (lazy)
|
|
16
|
+
return `${imports}import { late } from '@weft-js/warp-icons/late'\n${table}const load = () => import('@weft-js/warp-icons/all').then(m => m.lookup)\nexport const resolve = (name, variant) => names[key(name, variant)] ?? late(name, variant, load)\n${packExport}`
|
|
17
|
+
return `${imports}${table}export const resolve = (name, variant) => names[key(name, variant)]\n${packExport}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const VISIBLE = { quiet: 'oq', balanced: 'ob', vivid: 'v' }
|
|
21
|
+
/** Drop the drawings no shipped stop can show. With one stop left there is nothing to switch, so the switch goes too. */
|
|
22
|
+
export function trimTiers(source, stops) {
|
|
23
|
+
// a build imports the pack as a namespace (rewrite), and this default would keep every icon alive
|
|
24
|
+
const code = source
|
|
25
|
+
if (stops.size === 3) return code
|
|
26
|
+
const keep = [...stops].map((s) => VISIBLE[s]).join('')
|
|
27
|
+
const out = code.replace(/<path data-t="([oqbv])"[^>]*?\/>/g, (m, t) =>
|
|
28
|
+
keep.includes(t) ? m : '',
|
|
29
|
+
)
|
|
30
|
+
return stops.size === 1
|
|
31
|
+
? out.replaceAll(/ data-t="[oqbv]"/g, '').replace("import '../tiers.css'\n", '')
|
|
32
|
+
: out
|
|
33
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const title = (name) => name.replace(/(^|-)(.)/g, (_, __, c) => c.toUpperCase())
|
|
2
|
+
/** The export a name and variant compile to: `check`, `solid` is `CheckIconSolid`. */
|
|
3
|
+
export const pascal = (name, variant = '') =>
|
|
4
|
+
`${title(name)}Icon${variant ? variant[0].toUpperCase() + variant.slice(1) : ''}`
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs'
|
|
2
|
+
import { dirname, extname, join, resolve as path } from 'node:path'
|
|
3
|
+
import { parseAst } from 'vite'
|
|
4
|
+
import { unwrap } from './compile-unwrap.mjs'
|
|
5
|
+
|
|
6
|
+
const ITERATORS = new Set([
|
|
7
|
+
'map',
|
|
8
|
+
'flatMap',
|
|
9
|
+
'filter',
|
|
10
|
+
'forEach',
|
|
11
|
+
'find',
|
|
12
|
+
'some',
|
|
13
|
+
'every',
|
|
14
|
+
'sort',
|
|
15
|
+
'toSorted',
|
|
16
|
+
])
|
|
17
|
+
function moduleFile(from, source) {
|
|
18
|
+
const base = path(dirname(from), source)
|
|
19
|
+
for (const c of [
|
|
20
|
+
base,
|
|
21
|
+
...['.tsx', '.ts', '.jsx', '.js', '.mjs'].map((e) => base + e),
|
|
22
|
+
...['tsx', 'ts', 'jsx', 'js'].map((e) => join(base, `index.${e}`)),
|
|
23
|
+
base.replace(/\.js$/, '.ts'),
|
|
24
|
+
base.replace(/\.js$/, '.tsx'),
|
|
25
|
+
])
|
|
26
|
+
if (existsSync(c) && statSync(c).isFile()) return c
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parse(file, code) {
|
|
31
|
+
try {
|
|
32
|
+
return parseAst(code, {
|
|
33
|
+
lang: file.endsWith('.tsx') ? 'tsx' : file.endsWith('.ts') ? 'ts' : 'jsx',
|
|
34
|
+
})
|
|
35
|
+
} catch {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One source: its imports, what its names are bound to, and every place an icon name, a stop or a pack is used. */
|
|
41
|
+
export function read(file, code) {
|
|
42
|
+
const unit = {
|
|
43
|
+
file,
|
|
44
|
+
code,
|
|
45
|
+
imports: new Map(),
|
|
46
|
+
bindings: new Map(),
|
|
47
|
+
exports: new Map(),
|
|
48
|
+
uses: [],
|
|
49
|
+
sites: [],
|
|
50
|
+
stops: [],
|
|
51
|
+
related: [],
|
|
52
|
+
members: [],
|
|
53
|
+
idents: [],
|
|
54
|
+
warp: false,
|
|
55
|
+
}
|
|
56
|
+
if (extname(file) === '.html') {
|
|
57
|
+
for (const m of code.matchAll(/data-expression=["']?([a-z]+)/g))
|
|
58
|
+
unit.stops.push({ value: m[1] })
|
|
59
|
+
return unit
|
|
60
|
+
}
|
|
61
|
+
for (const m of code.matchAll(
|
|
62
|
+
/setAttribute\(\s*['"]data-expression['"]\s*,\s*(?:['"]([a-z]+)['"])?/g,
|
|
63
|
+
))
|
|
64
|
+
unit.stops.push(m[1] ? { value: m[1] } : { dynamic: true, at: m.index })
|
|
65
|
+
if (/dataset\.expression\s*=/.test(code))
|
|
66
|
+
unit.stops.push({ dynamic: true, at: code.search(/dataset\.expression\s*=/) })
|
|
67
|
+
const ast = parse(file, code)
|
|
68
|
+
if (!ast) return unit
|
|
69
|
+
unit.ast = ast
|
|
70
|
+
visitUnit(unit, ast, file)
|
|
71
|
+
return unit
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function visitUnit(unit, ast, file) {
|
|
75
|
+
const bind = (name, binding) => {
|
|
76
|
+
if (!unit.bindings.has(name)) unit.bindings.set(name, [])
|
|
77
|
+
unit.bindings.get(name).push(binding)
|
|
78
|
+
}
|
|
79
|
+
const pattern = (id, make) => {
|
|
80
|
+
if (id?.type === 'Identifier') bind(id.name, make(null))
|
|
81
|
+
else if (id?.type === 'AssignmentPattern') pattern(id.left, make)
|
|
82
|
+
else if (id?.type === 'ObjectPattern')
|
|
83
|
+
for (const p of id.properties) {
|
|
84
|
+
const key = p.key?.name ?? p.key?.value
|
|
85
|
+
if (key !== undefined) pattern(p.value, (inner) => make(inner ? [key, ...inner] : [key]))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const component = (name, params) => {
|
|
89
|
+
if (!name || !/^[A-Z]/.test(name)) return
|
|
90
|
+
const first = params?.[0] ?? params?.items?.[0]
|
|
91
|
+
if (first?.type === 'Identifier') bind(first.name, { kind: 'props', comp: name })
|
|
92
|
+
else if (first) pattern(first, (keys) => ({ kind: 'prop', comp: name, key: keys?.[0] }))
|
|
93
|
+
}
|
|
94
|
+
const visit = (node) => {
|
|
95
|
+
if (!node || typeof node !== 'object') return
|
|
96
|
+
if (Array.isArray(node)) {
|
|
97
|
+
for (const n of node) visit(n)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
if (typeof node.type !== 'string') return
|
|
101
|
+
if (declarations(node, unit, file, pattern, component)) return
|
|
102
|
+
uses(node, unit)
|
|
103
|
+
for (const k of Object.keys(node)) {
|
|
104
|
+
if (k !== 'type' && k !== 'start' && k !== 'end') visit(node[k])
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
visit(ast.body)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function declarations(node, unit, file, pattern, component) {
|
|
111
|
+
switch (node.type) {
|
|
112
|
+
case 'ImportDeclaration': {
|
|
113
|
+
const source = node.source.value
|
|
114
|
+
if (source === '@weft-js/warp' || source.startsWith('@weft-js/warp/')) unit.warp = true
|
|
115
|
+
const target = source.startsWith('.') ? moduleFile(file, source) : null
|
|
116
|
+
if (target) unit.related.push(target)
|
|
117
|
+
for (const s of node.specifiers ?? []) {
|
|
118
|
+
const imported =
|
|
119
|
+
s.type === 'ImportNamespaceSpecifier'
|
|
120
|
+
? '*'
|
|
121
|
+
: s.type === 'ImportDefaultSpecifier'
|
|
122
|
+
? 'default'
|
|
123
|
+
: (s.imported.name ?? s.imported.value)
|
|
124
|
+
unit.imports.set(s.local.name, { source, imported, target })
|
|
125
|
+
}
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
case 'ExportNamedDeclaration':
|
|
129
|
+
if (node.source && node.source.value.startsWith('.')) {
|
|
130
|
+
const t = moduleFile(file, node.source.value)
|
|
131
|
+
if (t) {
|
|
132
|
+
unit.related.push(t)
|
|
133
|
+
for (const s of node.specifiers ?? [])
|
|
134
|
+
unit.exports.set(s.exported.name, {
|
|
135
|
+
reexport: t,
|
|
136
|
+
name: s.local.name ?? s.local.value,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
break
|
|
141
|
+
case 'ExportAllDeclaration':
|
|
142
|
+
if (node.source.value.startsWith('.')) {
|
|
143
|
+
const t = moduleFile(file, node.source.value)
|
|
144
|
+
if (t) {
|
|
145
|
+
unit.related.push(t)
|
|
146
|
+
;(unit.stars ??= []).push(t)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
break
|
|
150
|
+
case 'VariableDeclarator':
|
|
151
|
+
if (node.init) {
|
|
152
|
+
pattern(node.id, (keys) => ({ kind: 'value', expr: node.init, keys }))
|
|
153
|
+
const fn = unwrap(node.init)
|
|
154
|
+
if (node.id.type === 'Identifier' && /Function/.test(fn.type))
|
|
155
|
+
component(node.id.name, fn.params)
|
|
156
|
+
}
|
|
157
|
+
break
|
|
158
|
+
case 'FunctionDeclaration':
|
|
159
|
+
component(node.id?.name, node.params)
|
|
160
|
+
break
|
|
161
|
+
case 'CallExpression': {
|
|
162
|
+
const callee = node.callee
|
|
163
|
+
if (
|
|
164
|
+
callee.type === 'MemberExpression' &&
|
|
165
|
+
!callee.computed &&
|
|
166
|
+
ITERATORS.has(callee.property.name)
|
|
167
|
+
) {
|
|
168
|
+
const fn = node.arguments[0] && unwrap(node.arguments[0])
|
|
169
|
+
if (fn && /Function/.test(fn.type))
|
|
170
|
+
pattern(fn.params?.[0] ?? fn.params?.items?.[0], (keys) => ({
|
|
171
|
+
kind: 'each',
|
|
172
|
+
of: callee.object,
|
|
173
|
+
keys,
|
|
174
|
+
}))
|
|
175
|
+
}
|
|
176
|
+
break
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function uses(node, unit) {
|
|
183
|
+
switch (node.type) {
|
|
184
|
+
case 'MemberExpression':
|
|
185
|
+
if (node.object.type === 'Identifier') unit.members.push({ local: node.object.name, node })
|
|
186
|
+
break
|
|
187
|
+
case 'JSXMemberExpression':
|
|
188
|
+
if (node.object.type === 'JSXIdentifier')
|
|
189
|
+
unit.members.push({ local: node.object.name, node, jsx: true })
|
|
190
|
+
break
|
|
191
|
+
case 'Identifier':
|
|
192
|
+
case 'JSXIdentifier':
|
|
193
|
+
unit.idents.push(node)
|
|
194
|
+
break
|
|
195
|
+
case 'JSXOpeningElement': {
|
|
196
|
+
const comp =
|
|
197
|
+
node.name.type === 'JSXIdentifier'
|
|
198
|
+
? node.name.name
|
|
199
|
+
: node.name.type === 'JSXMemberExpression'
|
|
200
|
+
? `${node.name.object.name}.${node.name.property.name}`
|
|
201
|
+
: null
|
|
202
|
+
if (!comp) break
|
|
203
|
+
for (const attr of node.attributes) {
|
|
204
|
+
if (attr.type !== 'JSXAttribute' || attr.name.type !== 'JSXIdentifier') continue
|
|
205
|
+
const value =
|
|
206
|
+
attr.value?.type === 'JSXExpressionContainer' ? attr.value.expression : attr.value
|
|
207
|
+
if (attr.name.name === 'data-expression' && value)
|
|
208
|
+
unit.stops.push({ expr: value, at: attr.start })
|
|
209
|
+
if (value && /^[A-Z]/.test(comp))
|
|
210
|
+
unit.uses.push({
|
|
211
|
+
comp,
|
|
212
|
+
prop: attr.name.name,
|
|
213
|
+
expr: value,
|
|
214
|
+
at: attr.start,
|
|
215
|
+
end: attr.end,
|
|
216
|
+
element: node,
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
break
|
|
220
|
+
}
|
|
221
|
+
case 'Property':
|
|
222
|
+
case 'ObjectProperty':
|
|
223
|
+
if (!node.computed && (node.key.name ?? node.key.value) === 'icon' && !node.method)
|
|
224
|
+
unit.sites.push({ soft: true, expr: node.value, at: node.start })
|
|
225
|
+
break
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { unwrap } from './compile-unwrap.mjs'
|
|
2
|
+
|
|
3
|
+
const empty = () => ({ values: new Set(), shapes: [], dynamic: null })
|
|
4
|
+
const merge = (into, r) => {
|
|
5
|
+
for (const v of r.values) into.values.add(v)
|
|
6
|
+
into.shapes.push(...r.shapes)
|
|
7
|
+
into.dynamic ??= r.dynamic
|
|
8
|
+
return into
|
|
9
|
+
}
|
|
10
|
+
const text = (unit, n) => unit.code.slice(n.start, n.end).slice(0, 60)
|
|
11
|
+
|
|
12
|
+
function exported(ctx, file, name, seen) {
|
|
13
|
+
const u = ctx.units.get(file)
|
|
14
|
+
if (!u) return { ...empty(), dynamic: `export ${name}` }
|
|
15
|
+
const re = u.exports.get(name)
|
|
16
|
+
if (re?.reexport) return exported(ctx, re.reexport, re.name, seen)
|
|
17
|
+
if (u.bindings.has(name)) return identifier(ctx, u, name, seen)
|
|
18
|
+
for (const star of u.stars ?? []) {
|
|
19
|
+
const r = exported(ctx, star, name, seen)
|
|
20
|
+
if (!r.dynamic) return r
|
|
21
|
+
}
|
|
22
|
+
return { ...empty(), dynamic: `export ${name}` }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function identifier(ctx, unit, name, seen) {
|
|
26
|
+
if (name === 'undefined') return empty()
|
|
27
|
+
const id = `${unit.file}#${name}`
|
|
28
|
+
if (seen.has(id)) return empty()
|
|
29
|
+
seen.add(id)
|
|
30
|
+
const imp = unit.imports.get(name)
|
|
31
|
+
if (imp) {
|
|
32
|
+
if (imp.source === '@weft-js/warp-icons' || imp.source.startsWith('@weft-js/warp-icons/'))
|
|
33
|
+
return empty()
|
|
34
|
+
if (imp.target && imp.imported !== '*') return exported(ctx, imp.target, imp.imported, seen)
|
|
35
|
+
return { ...empty(), dynamic: name }
|
|
36
|
+
}
|
|
37
|
+
const list = unit.bindings.get(name)
|
|
38
|
+
if (!list) return { ...empty(), dynamic: name }
|
|
39
|
+
const out = empty()
|
|
40
|
+
for (const b of list) merge(out, binding(ctx, unit, b, seen))
|
|
41
|
+
seen.delete(id)
|
|
42
|
+
return out
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function pick(ctx, unit, from, keys, seen) {
|
|
46
|
+
let cur = from
|
|
47
|
+
for (const key of keys ?? []) {
|
|
48
|
+
const next = empty()
|
|
49
|
+
next.dynamic = cur.dynamic
|
|
50
|
+
for (const { shape, unit: u } of cur.shapes) merge(next, property(ctx, u, shape, key, seen))
|
|
51
|
+
cur = next
|
|
52
|
+
}
|
|
53
|
+
return cur
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function binding(ctx, unit, b, seen) {
|
|
57
|
+
if (b.kind === 'value') return pick(ctx, unit, expression(ctx, unit, b.expr, seen), b.keys, seen)
|
|
58
|
+
if (b.kind === 'each') {
|
|
59
|
+
const list = expression(ctx, unit, b.of, seen),
|
|
60
|
+
items = empty()
|
|
61
|
+
items.dynamic = list.dynamic
|
|
62
|
+
for (const { shape, unit: u } of list.shapes)
|
|
63
|
+
if (shape.type === 'ArrayExpression')
|
|
64
|
+
for (const el of shape.elements) if (el) merge(items, expression(ctx, u, el, seen))
|
|
65
|
+
return pick(ctx, unit, items, b.keys, seen)
|
|
66
|
+
}
|
|
67
|
+
if (b.kind === 'prop')
|
|
68
|
+
return b.key ? forwarded(ctx, b.comp, b.key, seen) : { ...empty(), dynamic: `${b.comp} props` }
|
|
69
|
+
return { ...empty(), dynamic: `${b.comp} props` }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function forwarded(ctx, comp, prop, seen) {
|
|
73
|
+
const out = empty()
|
|
74
|
+
for (const { unit, use } of ctx.usesOf.get(`${comp}.${prop}`) ?? [])
|
|
75
|
+
merge(out, expression(ctx, unit, use.expr, seen))
|
|
76
|
+
return out
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function property(ctx, unit, shape, key, seen) {
|
|
80
|
+
const out = empty()
|
|
81
|
+
if (shape.type === 'ObjectExpression') {
|
|
82
|
+
for (const p of shape.properties) {
|
|
83
|
+
if (p.type === 'SpreadElement') {
|
|
84
|
+
const s = expression(ctx, unit, p.argument, seen)
|
|
85
|
+
for (const x of s.shapes) merge(out, property(ctx, x.unit, x.shape, key, seen))
|
|
86
|
+
out.dynamic ??= s.dynamic
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
const k = p.key?.name ?? p.key?.value
|
|
90
|
+
if (key === null || k === key) merge(out, expression(ctx, unit, p.value, seen))
|
|
91
|
+
}
|
|
92
|
+
} else if (shape.type === 'ArrayExpression')
|
|
93
|
+
for (const el of shape.elements) if (el) merge(out, expression(ctx, unit, el, seen))
|
|
94
|
+
return out
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function expression(ctx, unit, raw, seen = new Set()) {
|
|
98
|
+
const node = unwrap(raw)
|
|
99
|
+
if (!node) return empty()
|
|
100
|
+
switch (node.type) {
|
|
101
|
+
case 'Literal':
|
|
102
|
+
case 'StringLiteral':
|
|
103
|
+
return typeof node.value === 'string'
|
|
104
|
+
? { ...empty(), values: new Set([node.value]) }
|
|
105
|
+
: empty()
|
|
106
|
+
case 'TemplateLiteral':
|
|
107
|
+
return node.expressions.length
|
|
108
|
+
? { ...empty(), dynamic: text(unit, node) }
|
|
109
|
+
: { ...empty(), values: new Set([node.quasis[0].value.cooked]) }
|
|
110
|
+
case 'ConditionalExpression':
|
|
111
|
+
return merge(
|
|
112
|
+
expression(ctx, unit, node.consequent, seen),
|
|
113
|
+
expression(ctx, unit, node.alternate, seen),
|
|
114
|
+
)
|
|
115
|
+
case 'LogicalExpression':
|
|
116
|
+
return merge(expression(ctx, unit, node.left, seen), expression(ctx, unit, node.right, seen))
|
|
117
|
+
case 'ArrayExpression':
|
|
118
|
+
case 'ObjectExpression':
|
|
119
|
+
return { ...empty(), shapes: [{ shape: node, unit }] }
|
|
120
|
+
case 'Identifier':
|
|
121
|
+
return identifier(ctx, unit, node.name, seen)
|
|
122
|
+
case 'ArrowFunctionExpression':
|
|
123
|
+
case 'FunctionExpression':
|
|
124
|
+
case 'JSXElement':
|
|
125
|
+
case 'JSXFragment':
|
|
126
|
+
return empty()
|
|
127
|
+
case 'MemberExpression': {
|
|
128
|
+
const object = unwrap(node.object)
|
|
129
|
+
const key = node.computed ? (unwrap(node.property)?.value ?? null) : node.property.name
|
|
130
|
+
if (object.type === 'Identifier') {
|
|
131
|
+
const props = unit.bindings.get(object.name)?.find((b) => b.kind === 'props')
|
|
132
|
+
if (props && key) return forwarded(ctx, props.comp, key, seen)
|
|
133
|
+
const imp = unit.imports.get(object.name)
|
|
134
|
+
if (imp?.imported === '*' && imp.target && key) return exported(ctx, imp.target, key, seen)
|
|
135
|
+
}
|
|
136
|
+
const base = expression(ctx, unit, object, seen),
|
|
137
|
+
out = empty()
|
|
138
|
+
out.dynamic = base.dynamic
|
|
139
|
+
for (const { shape, unit: u } of base.shapes) merge(out, property(ctx, u, shape, key, seen))
|
|
140
|
+
return out
|
|
141
|
+
}
|
|
142
|
+
case 'CallExpression': {
|
|
143
|
+
// a list method hands back the list, or an item of it
|
|
144
|
+
const c = node.callee
|
|
145
|
+
if (
|
|
146
|
+
c.type === 'MemberExpression' &&
|
|
147
|
+
!c.computed &&
|
|
148
|
+
['filter', 'sort', 'toSorted', 'slice', 'reverse', 'concat'].includes(c.property.name)
|
|
149
|
+
)
|
|
150
|
+
return expression(ctx, unit, c.object, seen)
|
|
151
|
+
if (c.type === 'MemberExpression' && !c.computed && c.property.name === 'find') {
|
|
152
|
+
const base = expression(ctx, unit, c.object, seen),
|
|
153
|
+
out = empty()
|
|
154
|
+
out.dynamic = base.dynamic
|
|
155
|
+
for (const { shape, unit: u } of base.shapes)
|
|
156
|
+
merge(out, property(ctx, u, shape, null, seen))
|
|
157
|
+
return out
|
|
158
|
+
}
|
|
159
|
+
return { ...empty(), dynamic: text(unit, node) }
|
|
160
|
+
}
|
|
161
|
+
default:
|
|
162
|
+
return { ...empty(), dynamic: text(unit, node) }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function resolver(units, usesOf) {
|
|
167
|
+
const ctx = { units, usesOf }
|
|
168
|
+
return { expression: (unit, raw, seen) => expression(ctx, unit, raw, seen) }
|
|
169
|
+
}
|