Gathering detailed insights and metrics for @typed-macro/core
Gathering detailed insights and metrics for @typed-macro/core
Gathering detailed insights and metrics for @typed-macro/core
Gathering detailed insights and metrics for @typed-macro/core
npm install @typed-macro/core
Typescript
Module System
TypeScript (91.67%)
JavaScript (8.28%)
Shell (0.05%)
Total Downloads
0
Last Day
0
Last Week
0
Last Month
0
Last Year
0
MIT License
143 Stars
11 Commits
6 Forks
4 Watchers
2 Branches
1 Contributors
Updated on Jun 08, 2024
Latest Version
1.0.0-alpha
Package Id
@typed-macro/core@1.0.0-alpha
Unpacked Size
33.61 kB
Size
8.96 kB
File Count
7
Published on
Dec 17, 2021
Cumulative downloads
Total Downloads
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year
For basic concepts, see documentation for typed-macro.
It's pretty easy to define a macro, as long as you know the basic principles of AST and basic operation APIs of Babel.
1import { defineMacro } from '@typed-macro/core' 2 3const echoMacro = defineMacro('echo') // macro builder 4 // give macro a function signature 5 .withSignature('(msg: Message, repeat?: number): void') 6 // custom type will be rendered to the final d.ts file with signatures 7 .withCustomType(`export type Message = string`) 8 // set the handler for macro 9 .withHandler(({ path, args }, { template, types }) => { 10 // you can throw errors directly; the error message with the row number and col number 11 // of the macro call currently being expanded will be in the terminal. 12 // so you don't have to worry about telling users where the wrong code is. 13 if (args.length === 0) throw new Error('empty arguments is invalid') 14 const firstArg = args[0] 15 if (!firstArg.isStringLiteral()) 16 throw new Error('please use literal string as message') 17 const msg = firstArg.node.value 18 19 let repeat = 5 20 if (args.length > 1) { 21 const secondArg = args[1] 22 if (!secondArg.isNumericLiteral()) 23 throw new Error('please use literal number as repeat') 24 repeat = secondArg.node.value 25 } 26 27 path.replaceWith( 28 template.statement.ast(`console.log("${msg.repeat(repeat)}")`) 29 ) 30 })
The above example is JUST to show how to define multiple signatures. The above scenario is more suitable for using optional parameters rather than overloading. See Do's and Don'ts.
As the above example shows, you should give a name, at least one signature, maybe with a comment, and a handler function to a macro. The custom type is optional.
Ideally, macros should be transparent to users; that is, users can use macros like normal functions. So it's essential to write types/comments correctly to provide a friendly development experience.
A macro handler receives three arguments: ctx
, babel
, and helper
.
ctx
- the transformation context, including the node path of
the call expression currently being processed, traversal states, argument nodes, and so on.babel
- a collection of Babel tools, containing
@babel/types,
@babel/traverse,
@babel/parser,
@babel/template.helper
- some functions that wrap Babel tools to make writing macro handlers easier.In order to handle nested macros correctly, and reduce the conflict on modifying the AST, you can't use asynchronous macro handlers.
If the handler is a normal function like the above example shows, the nested macros inside the current call expression will be expanded automatically before calling the handler.
If the handler is a generator function, you can:
e.g.
1const helloMacro = defineMacro(`hello`) 2 .withSignature(`(msg?: string): void`, `output hello message`) 3 .withHandler(function* ( 4 { path, args }, 5 { template }, 6 { prependImports, appendToBody } 7 ) { 8 // do some thing... 9 10 // expand macros inside the current call expression 11 yield args 12 13 // do some thing... 14 15 // actively collect the imported macro so it can be used immediately, 16 // or you should wait for the next traversal 17 yield prependImports({ 18 moduleName: '@other-macros', 19 exportName: 'macro', 20 localName: '__macro', 21 }) 22 23 // insert a macro call and expand it 24 yield appendToBody(template.statement.ast(`__macro()`)) 25 26 // do some thing.. 27 })
Though sometimes writing these lexical macros themselves is cumbersome enough, please always keep the following in mind:
It is not enough to have defined macros only. Macros should be organized,
at least, into some modules
so that users can import them.
The most basic organizational unit is Exportable
.
An Exportable
contains either Javascript code and corresponding types,
or macros and additional types.
1type Exportable = 2 | { macros: Macro[]; types?: string } 3 | { code: string; types?: string }
Macro authors often prefer to use external helpers in the expanded code
in order to reduce the final size.
So Exportable
is designed to be able to contain Javascript code.
ModularizedExportable
is a collection of Exportable
s,
establishing the mapping relationship between module name and exportable.
1type ModularizedExportable = { [moduleName: string]: Exportable }
Finally ModularizedExportable
s should be packaged into providers.
1import { defineMacroProvider } from '@typed-macro/core' 2 3defineMacroProvider({ 4 id: 'echo', 5 exports: { 6 '@macros': { 7 macros: [helloMacro], 8 types: `export type SomeThing<T> = T`, 9 }, 10 '@helper': { 11 code: `export const n = 1`, 12 }, 13 }, 14})
defineMacroProvider
accepts a builder function if needed.
1import { defineMacroProvider } from '@typed-macro/core' 2 3defineMacroProvider((env) => { 4 return { 5 id: 'test', 6 exports: { 7 '@macros': { 8 macros: [], 9 types: env.dev ? '...' : '...', 10 }, 11 }, 12 hooks: { 13 onStart() { 14 env.watcher?.add(someFile) 15 }, 16 }, 17 options: { 18 parserPlugins: ['decimal'], 19 }, 20 } 21})
You can get properties of env
via parameter ctx
within macro handler,
so you don't need to define macros inside provider builder function.
1defineMacro(`test`) 2 .withSignature(`(): void`) 3 .withHandler((ctx) => { 4 ctx.dev // env.dev 5 ctx.ssr // env.ssr 6 ctx.host // env.host 7 // ... 8 })
There are two special object in env
: env.watcher
and env.modules
.
Suppose your macro needs to be re-expanded when an external file changes,
you can use them like below.
1// in macros 2withHandler((ctx) => { 3 // ... 4 ctx.modules?.setTag(ctx.filepath, 'some_xyz') 5 // ... 6}) 7 8// in hooks 9{ 10 onStart: () => { 11 env.watcher?.add(someFile) 12 env.watcher?.on('change', (path) => { 13 if (path === someFile) { 14 env.modules?.invalidateByTag(/^some/) 15 } 16 }) 17 } 18}
Note that env.watcher
and env.modules
may be undefined,
of which different runtime wrappers may have different strategies.
No vulnerabilities found.
No security vulnerabilities found.