feat(cem-plugin): new distPaths plugin
This commit is contained in:
parent
da6ba502cd
commit
f2c05fb7fb
4 changed files with 185 additions and 3 deletions
|
|
@ -9,8 +9,8 @@ A plugin for
|
|||
that teaches it to read wcb's `static props` object.
|
||||
|
||||
```js
|
||||
import { wcbStaticProps } from 'web-component-base/cem-plugin'
|
||||
// also available as the module's default export
|
||||
import { wcbStaticProps, distPaths } from 'web-component-base/cem-plugin'
|
||||
// wcbStaticProps is also available as the module's default export
|
||||
import wcbStaticProps from 'web-component-base/cem-plugin'
|
||||
```
|
||||
|
||||
|
|
@ -53,3 +53,36 @@ attributes, so editor completion and Storybook controls have nothing to read.
|
|||
|
||||
The `static props` initializer must resolve to an object literal in the same
|
||||
source file.
|
||||
|
||||
## `distPaths(options?)`
|
||||
|
||||
A companion plugin that rewrites each module's `path` in the manifest from the
|
||||
scanned source to the built output a package publishes, so a shipped
|
||||
`custom-elements.json` points at files consumers can actually import.
|
||||
|
||||
```js
|
||||
// custom-elements-manifest.config.mjs
|
||||
import { wcbStaticProps, distPaths } from 'web-component-base/cem-plugin'
|
||||
|
||||
export default {
|
||||
globs: ['src/**/*.ts'],
|
||||
outdir: '.',
|
||||
plugins: [wcbStaticProps(), distPaths()],
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Type | Default | |
|
||||
| --------- | ------------------------ | -------- | ---------------------------------------------- |
|
||||
| `rootDir` | `string` | `'src'` | source directory prefix to replace |
|
||||
| `outDir` | `string` | `'dist'` | built-output directory to point at |
|
||||
| `ext` | `Record<string, string>` | see below | extension remap, merged over the defaults |
|
||||
|
||||
Zero-config it maps the `rootDir` prefix to `outDir` and rewrites TypeScript
|
||||
extensions to their emitted JS form — `.ts` → `.js`, `.mts` → `.mjs`,
|
||||
`.cts` → `.cjs`. Other extensions pass through, so a plain `.js` source only
|
||||
has its directory swapped. `options.ext` merges over that default map.
|
||||
|
||||
It runs in the analyzer's `packageLinkPhase` (after `wcbStaticProps`'s
|
||||
`analyzePhase`), so ordering the two in the `plugins` array does not matter.
|
||||
Run `cem analyze` after your build so the files the rewritten paths point at
|
||||
exist. See the [publishing note in the guide](/cem-plugin/#configure).
|
||||
|
|
|
|||
|
|
@ -49,6 +49,35 @@ Then run the analyzer:
|
|||
npx cem analyze
|
||||
```
|
||||
|
||||
<Aside type="caution" title="Publishing a package? Point the manifest at your built files">
|
||||
The analyzer stamps each module's `path` with the exact file it scanned —
|
||||
so with `globs: ['src/**/*.ts']` every `path` in `custom-elements.json` is a
|
||||
`.ts` source file. Most packages publish only their built `dist/` output
|
||||
(and can't `import` a `.ts` at runtime anyway), so a consumer reading that
|
||||
manifest resolves paths that aren't in the tarball.
|
||||
|
||||
Add `distPaths()` after `wcbStaticProps()` to rewrite those paths to the
|
||||
built output. Zero-config it maps `src/` → `dist/` and TypeScript extensions
|
||||
to their emitted JS form (`.ts` → `.js`, and `.mts` / `.cts` likewise):
|
||||
|
||||
```js
|
||||
import { wcbStaticProps, distPaths } from 'web-component-base/cem-plugin'
|
||||
|
||||
export default {
|
||||
globs: ['src/**/*.ts'],
|
||||
outdir: '.',
|
||||
plugins: [wcbStaticProps(), distPaths()],
|
||||
}
|
||||
```
|
||||
|
||||
For a different layout, override `rootDir` / `outDir` (named to mirror
|
||||
`tsconfig`): `distPaths({ rootDir: 'lib', outDir: 'dist/esm' })`. Run
|
||||
`cem analyze` **after** your build so the `dist/` files it points at exist.
|
||||
Not publishing the manifest — e.g. a local Storybook or editor setup over
|
||||
your own source — needs none of this.
|
||||
|
||||
</Aside>
|
||||
|
||||
<Aside type="caution" title="LOOK OUT! `cem analyze` fails by hanging, not by erroring">
|
||||
Run it **from the directory holding your config and a `package.json`**. Two
|
||||
things make it appear to do nothing, neither of which prints an error:
|
||||
|
|
|
|||
|
|
@ -204,4 +204,53 @@ export function wcbStaticProps() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A companion analyzer plugin that rewrites each module's `path` from the
|
||||
* scanned source to its built, published counterpart — so a shipped
|
||||
* `custom-elements.json` points at the files consumers actually import
|
||||
* (`dist/*.js`) rather than the source the analyzer read (`src/*.ts`), which a
|
||||
* package typically does not publish.
|
||||
*
|
||||
* Zero-config it maps the `src/` prefix to `dist/` and rewrites TypeScript
|
||||
* extensions to their emitted JS form (`.ts`→`.js`, `.mts`→`.mjs`,
|
||||
* `.cts`→`.cjs`); other extensions pass through untouched. Override `rootDir` /
|
||||
* `outDir` (named to mirror `tsconfig`) or extend `ext` for other layouts.
|
||||
* @param {object} [options]
|
||||
* @param {string} [options.rootDir] source directory prefix to replace (default `'src'`)
|
||||
* @param {string} [options.outDir] built-output directory to point at (default `'dist'`)
|
||||
* @param {Record<string, string>} [options.ext] extension remap, merged over the defaults
|
||||
* @returns {{name: string, packageLinkPhase: (ctx: any) => void}} a CEM analyzer plugin
|
||||
* @example
|
||||
* // custom-elements-manifest.config.mjs
|
||||
* import { wcbStaticProps, distPaths } from 'web-component-base/cem-plugin'
|
||||
* export default {
|
||||
* globs: ['src/**\/*.ts'],
|
||||
* plugins: [wcbStaticProps(), distPaths()],
|
||||
* }
|
||||
*/
|
||||
export function distPaths(options = {}) {
|
||||
const withSlash = (dir) => dir.replace(/\/*$/, '/')
|
||||
const from = withSlash(options.rootDir ?? 'src')
|
||||
const to = withSlash(options.outDir ?? 'dist')
|
||||
const ext = { '.ts': '.js', '.mts': '.mjs', '.cts': '.cjs', ...options.ext }
|
||||
return {
|
||||
name: 'wcb-dist-paths',
|
||||
packageLinkPhase({ customElementsManifest }) {
|
||||
for (const mod of customElementsManifest.modules ?? []) {
|
||||
if (!mod.path) continue
|
||||
let path = mod.path.startsWith(from)
|
||||
? to + mod.path.slice(from.length)
|
||||
: mod.path
|
||||
for (const [srcExt, outExt] of Object.entries(ext)) {
|
||||
if (path.endsWith(srcExt)) {
|
||||
path = path.slice(0, -srcExt.length) + outExt
|
||||
break
|
||||
}
|
||||
}
|
||||
mod.path = path
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default wcbStaticProps
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { createRequire } from 'node:module'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { create } from '@custom-elements-manifest/analyzer/src/create.js'
|
||||
import { wcbStaticProps } from '../src/cem-plugin.js'
|
||||
import { distPaths, wcbStaticProps } from '../src/cem-plugin.js'
|
||||
import { getKebabCase } from '../src/utils/index.js'
|
||||
|
||||
// The analyzer resolves its own `typescript`, which may be a different version
|
||||
|
|
@ -185,3 +185,74 @@ describe('cem-plugin: wcbStaticProps', () => {
|
|||
expect(doc.members.map((m) => m.name)).toContain('props')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Runs the real analyzer over one or more named source files with the given
|
||||
* plugins installed and returns the module paths from the generated manifest —
|
||||
* `packageLinkPhase` (where `distPaths` runs) fires as part of this.
|
||||
* @param {string[]} fileNames source paths to stamp on the scanned modules
|
||||
* @param {object[]} plugins analyzer plugins to install
|
||||
* @returns {string[]} the resulting `module.path` for each module
|
||||
*/
|
||||
function modulePaths(fileNames, plugins) {
|
||||
const manifest = create({
|
||||
modules: fileNames.map((fileName) =>
|
||||
ts.createSourceFile(fileName, '', ts.ScriptTarget.ES2015, true)
|
||||
),
|
||||
plugins,
|
||||
context: { dev: false, thirdPartyCEMs: [] },
|
||||
})
|
||||
return manifest.modules.map((m) => m.path)
|
||||
}
|
||||
|
||||
describe('cem-plugin: distPaths', () => {
|
||||
it('rewrites src/*.ts paths to dist/*.js by default', () => {
|
||||
expect(modulePaths(['src/wcb-button.ts'], [distPaths()])).toEqual([
|
||||
'dist/wcb-button.js',
|
||||
])
|
||||
})
|
||||
|
||||
it('maps .mts/.cts to their emitted JS extensions', () => {
|
||||
expect(
|
||||
modulePaths(['src/a.mts', 'src/b.cts', 'src/c.ts'], [distPaths()])
|
||||
).toEqual(['dist/a.mjs', 'dist/b.cjs', 'dist/c.js'])
|
||||
})
|
||||
|
||||
it('swaps the directory but keeps plain .js sources as-is', () => {
|
||||
expect(modulePaths(['src/nested/widget.js'], [distPaths()])).toEqual([
|
||||
'dist/nested/widget.js',
|
||||
])
|
||||
})
|
||||
|
||||
it('honors rootDir / outDir overrides for non-standard layouts', () => {
|
||||
expect(
|
||||
modulePaths(
|
||||
['lib/x.ts'],
|
||||
[distPaths({ rootDir: 'lib', outDir: 'dist/esm' })]
|
||||
)
|
||||
).toEqual(['dist/esm/x.js'])
|
||||
})
|
||||
|
||||
it('leaves paths outside rootDir untouched (dir), still remaps extension', () => {
|
||||
expect(modulePaths(['other/x.ts'], [distPaths()])).toEqual(['other/x.js'])
|
||||
})
|
||||
|
||||
it('runs alongside wcbStaticProps without interfering', () => {
|
||||
const manifest = create({
|
||||
modules: [
|
||||
ts.createSourceFile(
|
||||
'src/cozy-button.ts',
|
||||
COZY_BUTTON,
|
||||
ts.ScriptTarget.ES2015,
|
||||
true
|
||||
),
|
||||
],
|
||||
plugins: [wcbStaticProps(), distPaths()],
|
||||
context: { dev: false, thirdPartyCEMs: [] },
|
||||
})
|
||||
const mod = manifest.modules[0]
|
||||
expect(mod.path).toBe('dist/cozy-button.js')
|
||||
const doc = mod.declarations.find((d) => d.name === 'CozyButton')
|
||||
expect(doc.attributes.map((a) => a.name)).toContain('max-count')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue