Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace static blocks with private static field initializers #17

Merged
merged 2 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ Example Config:
}
```

### staticBlock

The `staticBlock` option controls how `decorator-transforms` outputs static class blocks:

- `"native"` (_default_) will output native `static { }` blocks ([caniuse](https://caniuse.com/mdn-javascript_classes_static_initialization_blocks))
- `"fields"` will shim the same functionality using private static class fields. These have slightly wider browser support. ([caniuse](https://caniuse.com/?search=static%20class%20fields))

## Trying this in an Ember App

1. Install the `decorator-transforms` package.
Expand Down
63 changes: 43 additions & 20 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface State extends Babel.PluginPass {
decorated: [
"field" | "method",
t.Expression, // for the property name
t.Expression[], // for the decorators applied to it
t.Expression[] // for the decorators applied to it
][];
}[];
opts: Options;
Expand All @@ -22,6 +22,7 @@ interface State extends Babel.PluginPass {

export interface Options {
runtime?: "globals" | { import: string };
staticBlock?: "native" | "field";
}

export default function legacyDecoratorCompat(
Expand All @@ -37,6 +38,7 @@ export default function legacyDecoratorCompat(
state.currentObjectExpressions = [];
state.optsWithDefaults = {
runtime: "globals",
staticBlock: "native",
...state.opts,
};
let importUtil = new ImportUtil(t, path);
Expand Down Expand Up @@ -172,11 +174,12 @@ export default function legacyDecoratorCompat(
);
}
path.insertBefore(
t.staticBlock([
t.expressionStatement(
t.callExpression(state.runtime(path, "g"), args)
),
])
compatStaticBlock(
state,
t,
path.node.key,
t.callExpression(state.runtime(path, "g"), args)
)
);
path.insertBefore(
t.classPrivateProperty(
Expand Down Expand Up @@ -212,20 +215,21 @@ export default function legacyDecoratorCompat(
);
}
path.insertAfter(
t.staticBlock([
t.expressionStatement(
t.callExpression(state.runtime(path, "n"), [
prototype,
valueForFieldKey(t, path.node.key),
t.arrayExpression(
decorators
.slice()
.reverse()
.map((d) => d.node.expression)
),
])
),
])
compatStaticBlock(
state,
t,
path.node.key,
t.callExpression(state.runtime(path, "n"), [
prototype,
valueForFieldKey(t, path.node.key),
t.arrayExpression(
decorators
.slice()
.reverse()
.map((d) => d.node.expression)
),
])
)
);
for (let decorator of decorators) {
decorator.remove();
Expand Down Expand Up @@ -364,3 +368,22 @@ function valueForFieldKey(
}
return expr;
}

// create a static block or a private class field depending on the staticBlock option
function compatStaticBlock(
state: State,
t: (typeof Babel)["types"],
key: t.Expression,
expression: t.Expression
) {
if (state.optsWithDefaults.staticBlock === "native") {
return t.staticBlock([t.expressionStatement(expression)]);
} else {
return t.classPrivateProperty(
t.privateName(t.identifier(unusedPrivateNameLike(state, propName(key)))),
expression,
null,
true
);
}
}
3 changes: 2 additions & 1 deletion tests/class-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { module, test } from "qunit";
import { oldBuild, newBuild, Builder } from "./helpers.ts";
import { oldBuild, newBuild, Builder, compatNewBuild } from "./helpers.ts";
import { type LegacyClassDecorator } from "../src/runtime.ts";
import * as runtimeImpl from "../src/runtime.ts";
import { globalId } from "../src/global-id.ts";
Expand Down Expand Up @@ -200,3 +200,4 @@ function classTests(title: string, build: Builder) {

classTests("old-build", oldBuild);
classTests("new-build", newBuild);
classTests("compat-new-build", compatNewBuild);
26 changes: 26 additions & 0 deletions tests/compat-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { module, test } from "qunit";
import { newBuild, compatNewBuild } from "./helpers.ts";

module(`Compat`, () => {
test("uses real static blocks when staticBlocks: native", (assert) => {
const transformedSrc = newBuild.transformSrc(
`
class Example {
@withColors myField;
}
`
);
assert.true(transformedSrc.includes("static {"));
});

test("uses private static class fields when staticBlocks: fields", (assert) => {
const transformedSrc = compatNewBuild.transformSrc(
`
class Example {
@withColors myField;
}
`
);
assert.false(transformedSrc.includes("static {"));
});
});
3 changes: 2 additions & 1 deletion tests/field-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { module, test } from "qunit";
import { oldBuild, newBuild, Builder } from "./helpers.ts";
import { oldBuild, newBuild, Builder, compatNewBuild } from "./helpers.ts";
import { type LegacyDecorator } from "../src/runtime.ts";
import * as runtimeImpl from "../src/runtime.ts";
import { globalId } from "../src/global-id.ts";
Expand Down Expand Up @@ -417,3 +417,4 @@ function fieldTests(title: string, build: Builder) {
}
fieldTests("old-build", oldBuild);
fieldTests("new-build", newBuild);
fieldTests("compat-new-build", compatNewBuild);
34 changes: 24 additions & 10 deletions tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@ function builder(
exprPlugins: TransformOptions["plugins"],
modulePlugins?: TransformOptions["plugins"]
): Builder {
function transformSrc(src: string) {
return transform(src, { plugins: exprPlugins })!.code!;
}

function expression(src: string, scope: Record<string, any>) {
let transformedSrc = transform(
`
(function(${Object.keys(scope).join(",")}) {
return (${src})
})
`,
{ plugins: exprPlugins }
)!.code!;
let transformedSrc = transformSrc(`
(function(${Object.keys(scope).join(",")}) {
return (${src})
})
`);
let fn = eval(transformedSrc);
return fn(...Object.values(scope));
}
Expand Down Expand Up @@ -64,10 +65,11 @@ function builder(
return m.namespace;
}

return { expression, module };
return { expression, module, transformSrc };
}

export interface Builder {
transformSrc: (src: string) => string;
expression: (src: string, scope: Record<string, any>) => any;
module: (src: string, deps: Record<string, any>) => Promise<any>;
}
Expand All @@ -78,12 +80,24 @@ export const oldBuild: Builder = builder([
classPrivateMethods,
]);

let globalOpts: Options = { runtime: "globals" };
let globalOpts: Options = { runtime: "globals", staticBlock: "native" };
let importOpts: Options = {
runtime: { import: "decorator-transforms/runtime" },
staticBlock: "native",
};

export const newBuild: Builder = builder(
[[ourDecorators, globalOpts]],
[[ourDecorators, importOpts]]
);

let compatGlobalOpts: Options = { runtime: "globals", staticBlock: "field" };
let compatImportOpts: Options = {
runtime: { import: "decorator-transforms/runtime" },
staticBlock: "field",
};

export const compatNewBuild: Builder = builder(
[[ourDecorators, compatGlobalOpts]],
[[ourDecorators, compatImportOpts]]
);
3 changes: 2 additions & 1 deletion tests/method-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { module, test } from "qunit";
import { oldBuild, newBuild, Builder } from "./helpers.ts";
import { oldBuild, newBuild, Builder, compatNewBuild } from "./helpers.ts";
import { type LegacyDecorator } from "../src/runtime.ts";
import * as runtimeImpl from "../src/runtime.ts";
import { globalId } from "../src/global-id.ts";
Expand Down Expand Up @@ -220,3 +220,4 @@ function methodTests(title: string, build: Builder) {

methodTests("old-build", oldBuild);
methodTests("new-build", newBuild);
methodTests("compat-new-build", compatNewBuild);