ember.nvp 1.0.0 → 1.2.0
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/.github/workflows/ci.yml +6 -0
- package/.release-plan.json +11 -15
- package/CHANGELOG.md +47 -0
- package/package.json +1 -1
- package/packages/rolldown/README.md +101 -1
- package/packages/rolldown/package.json +13 -4
- package/packages/rolldown/rolldown.config.js +5 -3
- package/packages/rolldown/src/app-reexports.test.ts +220 -0
- package/packages/rolldown/src/app-reexports.ts +152 -0
- package/packages/rolldown/src/babel.test.ts +17 -0
- package/packages/rolldown/src/babel.ts +21 -3
- package/packages/rolldown/src/config.ts +1 -1
- package/packages/rolldown/src/dts.test.ts +366 -0
- package/packages/rolldown/src/transform.test.ts +208 -0
- package/packages/rolldown/src/transform.ts +64 -31
- package/src/bases/minimal-extension/extension.test.ts +251 -0
- package/src/bases/minimal-extension/files/.env.development +8 -0
- package/src/bases/minimal-extension/files/README.md +36 -0
- package/src/bases/minimal-extension/files/app/app.ts +21 -0
- package/src/bases/minimal-extension/files/app/boot.ts +8 -0
- package/src/bases/minimal-extension/files/app/config.ts +22 -0
- package/src/bases/minimal-extension/files/app/router.ts +10 -0
- package/src/bases/minimal-extension/files/app/templates/application.gts +5 -0
- package/src/bases/minimal-extension/files/babel.config.js +49 -0
- package/src/bases/minimal-extension/files/index.html +17 -0
- package/src/bases/minimal-extension/files/package.json +58 -0
- package/src/bases/minimal-extension/files/public/manifest.json +9 -0
- package/src/bases/minimal-extension/files/tsconfig.json +13 -0
- package/src/bases/minimal-extension/files/vite.config.mjs +6 -0
- package/src/bases/minimal-extension/index.js +142 -0
- package/src/cli/generator.js +4 -0
- package/src/cli/questions/project-type.js +9 -2
- package/src/layers/renovate/index.js +4 -3
- package/src/layers/typescript/index.js +6 -0
- package/src/utils/types.ts +1 -1
- package/test/ember.nvp/base-library.test.ts +39 -32
- package/test/ember.nvp/layer-publish-checks.test.ts +4 -13
- package/test/ember.nvp/layer-qunit.test.ts +13 -30
- package/test/ember.nvp/layer-vitest.test.ts +4 -18
- package/test/ember.nvp/permutations/app-checks.test.ts +3 -0
- package/test/ember.nvp/permutations/extension-1.test.ts +3 -0
- package/test/ember.nvp/permutations/extension-2.test.ts +3 -0
- package/test/ember.nvp/permutations/extension-checks.test.ts +3 -0
- package/test/ember.nvp/permutations/library-checks.test.ts +3 -0
- package/test/ember.nvp/permutations/shared.ts +68 -19
- package/test/ember.nvp/scoped-css-library.test.ts +211 -0
- package/test/test-helpers.ts +25 -30
- package/vitest.config.js +1 -1
|
@@ -38,13 +38,32 @@ async function fixDtsExtensionsInDir(dir: string): Promise<void> {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* A sourcemap mapping every generated line to the same line of the input.
|
|
43
|
+
*
|
|
44
|
+
* `AAAA` is the [0, 0, 0, 0] VLQ segment (first line maps to source 0, line 0,
|
|
45
|
+
* column 0); `AACA` is [0, 0, +1, 0] (each following line advances the source
|
|
46
|
+
* line by one). Exact at line granularity, which is all the specifier rewrite
|
|
47
|
+
* can disturb.
|
|
48
|
+
*/
|
|
49
|
+
function lineIdentityMap(id: string, source: string) {
|
|
50
|
+
return {
|
|
51
|
+
version: 3,
|
|
52
|
+
sources: [id],
|
|
53
|
+
sourcesContent: [source],
|
|
54
|
+
names: [],
|
|
55
|
+
mappings: source
|
|
56
|
+
.split("\n")
|
|
57
|
+
.map((_, line) => (line === 0 ? "AAAA" : "AACA"))
|
|
58
|
+
.join(";"),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
41
62
|
/**
|
|
42
63
|
* Maps `.gts` -> `.ts` and `.gjs` -> `.js` (so rolldown can identify them as
|
|
43
64
|
* ts/js) and preprocesses `<template>` via content-tag.
|
|
44
65
|
*/
|
|
45
66
|
export function emberTransform(): Plugin {
|
|
46
|
-
const virtualFiles = new Set<string>();
|
|
47
|
-
|
|
48
67
|
return {
|
|
49
68
|
name: "ember:transform",
|
|
50
69
|
|
|
@@ -55,41 +74,39 @@ export function emberTransform(): Plugin {
|
|
|
55
74
|
|
|
56
75
|
const fileName = path.join(path.dirname(importer), id);
|
|
57
76
|
|
|
58
|
-
if (id.endsWith(".gts")
|
|
59
|
-
virtualFiles.add(fileName);
|
|
77
|
+
if (id.endsWith(".gts")) {
|
|
60
78
|
return {
|
|
61
79
|
id: fileName.replace(/\.gts$/, ".ts"),
|
|
62
80
|
meta: { fileName },
|
|
63
81
|
};
|
|
64
82
|
}
|
|
65
83
|
|
|
66
|
-
if (id.endsWith(".gjs")
|
|
67
|
-
virtualFiles.add(fileName);
|
|
84
|
+
if (id.endsWith(".gjs")) {
|
|
68
85
|
return {
|
|
69
86
|
id: fileName.replace(/\.gjs$/, ".js"),
|
|
70
87
|
meta: { fileName },
|
|
71
88
|
};
|
|
72
89
|
}
|
|
73
90
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
// A `.ts`/`.js` specifier whose only backing file is a `.gts`/`.gjs`
|
|
92
|
+
// resolves to the virtual module. This must also serve `.d.ts`
|
|
93
|
+
// importers: rolldown-plugin-dts resolves a declaration module's
|
|
94
|
+
// imports through the plugin pipeline (`this.resolve`), and — when the
|
|
95
|
+
// resolution is a source file — loads it (registering its declaration)
|
|
96
|
+
// before mapping the import to the declaration id. Virtual modules
|
|
97
|
+
// exist nowhere else, so if we don't answer here the import either
|
|
98
|
+
// fails to resolve (no file on disk) or, worse, resolves to a
|
|
99
|
+
// declaration id that was never registered.
|
|
100
|
+
if (id.endsWith(".ts")) {
|
|
82
101
|
const gtsFileName = fileName.replace(/\.ts$/, ".gts");
|
|
83
102
|
if (existsSync(gtsFileName) && !existsSync(fileName)) {
|
|
84
|
-
virtualFiles.add(gtsFileName);
|
|
85
103
|
return { id: fileName, meta: { fileName: gtsFileName } };
|
|
86
104
|
}
|
|
87
105
|
}
|
|
88
106
|
|
|
89
|
-
if (id.endsWith(".js")
|
|
107
|
+
if (id.endsWith(".js")) {
|
|
90
108
|
const gjsFileName = fileName.replace(/\.js$/, ".gjs");
|
|
91
109
|
if (existsSync(gjsFileName) && !existsSync(fileName)) {
|
|
92
|
-
virtualFiles.add(gjsFileName);
|
|
93
110
|
return { id: fileName, meta: { fileName: gjsFileName } };
|
|
94
111
|
}
|
|
95
112
|
}
|
|
@@ -105,7 +122,18 @@ export function emberTransform(): Plugin {
|
|
|
105
122
|
},
|
|
106
123
|
async handler(id) {
|
|
107
124
|
const meta = this.getModuleInfo(id)?.meta ?? {};
|
|
108
|
-
|
|
125
|
+
let fileName = meta?.fileName;
|
|
126
|
+
|
|
127
|
+
// A virtual id can be loaded without having passed through our
|
|
128
|
+
// resolveId (rolldown-plugin-dts calls `this.load({ id })` with just
|
|
129
|
+
// the id), so no meta is attached: recover the backing file from disk.
|
|
130
|
+
if (!fileName && !existsSync(id)) {
|
|
131
|
+
if (id.endsWith(".ts") && existsSync(id.replace(/\.ts$/, ".gts"))) {
|
|
132
|
+
fileName = id.replace(/\.ts$/, ".gts");
|
|
133
|
+
} else if (id.endsWith(".js") && existsSync(id.replace(/\.js$/, ".gjs"))) {
|
|
134
|
+
fileName = id.replace(/\.js$/, ".gjs");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
109
137
|
|
|
110
138
|
if (fileName) {
|
|
111
139
|
this.addWatchFile(fileName);
|
|
@@ -132,21 +160,26 @@ export function emberTransform(): Plugin {
|
|
|
132
160
|
id: /\.(js|ts)$/,
|
|
133
161
|
},
|
|
134
162
|
handler(input, id) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
163
|
+
// Rewrite `.gts` specifiers to `.ts` — the (virtual) source id — in
|
|
164
|
+
// runtime modules AND declaration modules alike. Declaration modules
|
|
165
|
+
// must NOT be rewritten to `.d.ts`: rolldown-plugin-dts's resolver
|
|
166
|
+
// treats a source-file resolution as "load it (registering its
|
|
167
|
+
// declaration), then map to the declaration id", while a `.d.ts` id is
|
|
168
|
+
// returned as-is — unloadable when the module isn't registered yet
|
|
169
|
+
// (e.g. the `.gts` is only ever type-imported).
|
|
170
|
+
const output = input.replace(
|
|
171
|
+
/(['"`])((?:\.\.?\/|\/|@|[A-Za-z0-9_\-])[^'"]*?\.gts)\1/g,
|
|
172
|
+
(_m, q, p) => `${q}${p.replace(/\.gts$/, ".ts")}${q}`,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (output === input) {
|
|
176
|
+
return null;
|
|
147
177
|
}
|
|
148
178
|
|
|
149
|
-
|
|
179
|
+
// The rewrite only shortens import specifiers in place, so a
|
|
180
|
+
// line-identity map is accurate to the line (and to the column for
|
|
181
|
+
// everything before the first rewritten specifier on a line).
|
|
182
|
+
return { code: output, map: lineIdentityMap(id, input) };
|
|
150
183
|
},
|
|
151
184
|
},
|
|
152
185
|
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { beforeAll, describe, it, expect as hardExpect, afterAll } from "vitest";
|
|
2
|
+
import { generate, build, reapply } from "#test-helpers";
|
|
3
|
+
import { packageJson } from "ember-apply";
|
|
4
|
+
|
|
5
|
+
import type { Project } from "ember.nvp";
|
|
6
|
+
import { readFile, rm } from "node:fs/promises";
|
|
7
|
+
import { globSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
const expect = hardExpect.soft;
|
|
11
|
+
|
|
12
|
+
describe("typescript", () => {
|
|
13
|
+
let project: Project;
|
|
14
|
+
beforeAll(async () => {
|
|
15
|
+
project = await generate({
|
|
16
|
+
name: "my-extension",
|
|
17
|
+
type: "extension",
|
|
18
|
+
layers: ["typescript"],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
let { exitCode } = await project.install();
|
|
22
|
+
|
|
23
|
+
hardExpect(exitCode, "Install succeeds").toBe(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterAll(async () => {
|
|
27
|
+
await rm(project.directory, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("project check", () => {
|
|
31
|
+
expect(project.desires.layers.map((layer) => layer.name)).toMatchInlineSnapshot(`
|
|
32
|
+
[
|
|
33
|
+
"typescript",
|
|
34
|
+
]
|
|
35
|
+
`);
|
|
36
|
+
expect(project.wantsTypeScript).toBe(true);
|
|
37
|
+
expect(project.type).toBe("extension");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("has the typescript babel plugin", async () => {
|
|
41
|
+
let content = await project.read("babel.config.js");
|
|
42
|
+
|
|
43
|
+
expect(content).toContain("@babel/plugin-transform-typescript");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("named the extension in the manifest", async () => {
|
|
47
|
+
let manifest = JSON.parse((await project.read("public/manifest.json"))!);
|
|
48
|
+
|
|
49
|
+
expect(manifest.name).toBe("my-extension");
|
|
50
|
+
expect(manifest.manifest_version).toBe(3);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("boots from an external script (extension CSP forbids inline scripts)", async () => {
|
|
54
|
+
let html = await project.read("index.html");
|
|
55
|
+
|
|
56
|
+
expect(html).toContain(`src="./app/boot.ts"`);
|
|
57
|
+
expect(html).not.toContain("Application.create");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("build for development (testing, etc)", async () => {
|
|
61
|
+
let { exitCode } = await build(project);
|
|
62
|
+
|
|
63
|
+
expect(exitCode).toBe(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("build for production", async () => {
|
|
67
|
+
let { exitCode } = await build(project, "production");
|
|
68
|
+
|
|
69
|
+
expect(exitCode).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("dist is loadable as an unpacked extension", async () => {
|
|
73
|
+
let manifest = JSON.parse(
|
|
74
|
+
await readFile(join(project.directory, "dist/manifest.json"), "utf-8"),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(manifest.name).toBe("my-extension");
|
|
78
|
+
expect(manifest.action.default_popup).toBe("index.html");
|
|
79
|
+
|
|
80
|
+
let html = await readFile(join(project.directory, "dist/index.html"), "utf-8");
|
|
81
|
+
|
|
82
|
+
expect(html).toContain(`<script type="module"`);
|
|
83
|
+
expect(html).not.toContain("Application.create");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("has no JS files", async () => {
|
|
87
|
+
let results = globSync("app/**/*", {
|
|
88
|
+
exclude: ["node_modules", "dist"],
|
|
89
|
+
cwd: project.directory,
|
|
90
|
+
});
|
|
91
|
+
expect(results).toMatchInlineSnapshot(`
|
|
92
|
+
[
|
|
93
|
+
"app/app.ts",
|
|
94
|
+
"app/boot.ts",
|
|
95
|
+
"app/config.ts",
|
|
96
|
+
"app/router.ts",
|
|
97
|
+
"app/templates",
|
|
98
|
+
"app/templates/application.gts",
|
|
99
|
+
]
|
|
100
|
+
`);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("when re-applying, it no-ops correctly", async () => {
|
|
104
|
+
let files = new Set(
|
|
105
|
+
globSync("**/*", { cwd: project.directory, exclude: ["node_modules", "dist"] }),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
await reapply(project, []);
|
|
109
|
+
|
|
110
|
+
let filesAfter = new Set(
|
|
111
|
+
globSync("**/*", { cwd: project.directory, exclude: ["node_modules", "dist"] }),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(filesAfter).toEqual(files);
|
|
115
|
+
expect(files).toMatchInlineSnapshot(`
|
|
116
|
+
Set {
|
|
117
|
+
"README.md",
|
|
118
|
+
"app",
|
|
119
|
+
"babel.config.js",
|
|
120
|
+
"index.html",
|
|
121
|
+
"package.json",
|
|
122
|
+
"pnpm-lock.yaml",
|
|
123
|
+
"public",
|
|
124
|
+
"tsconfig.json",
|
|
125
|
+
"vite.config.mjs",
|
|
126
|
+
"public/manifest.json",
|
|
127
|
+
"app/app.ts",
|
|
128
|
+
"app/boot.ts",
|
|
129
|
+
"app/config.ts",
|
|
130
|
+
"app/router.ts",
|
|
131
|
+
"app/templates",
|
|
132
|
+
"app/templates/application.gts",
|
|
133
|
+
}
|
|
134
|
+
`);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("javascript", () => {
|
|
139
|
+
let project: Project;
|
|
140
|
+
beforeAll(async () => {
|
|
141
|
+
project = await generate({
|
|
142
|
+
name: "my-extension",
|
|
143
|
+
type: "extension",
|
|
144
|
+
layers: [],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
let { exitCode } = await project.install();
|
|
148
|
+
|
|
149
|
+
hardExpect(exitCode, "Install succeeds").toBe(0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
afterAll(async () => {
|
|
153
|
+
await rm(project.directory, { recursive: true, force: true });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("project check", () => {
|
|
157
|
+
expect(project.desires.layers.map((layer) => layer.name)).toMatchInlineSnapshot(`[]`);
|
|
158
|
+
expect(project.wantsTypeScript).toBe(false);
|
|
159
|
+
expect(project.type).toBe("extension");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("does not have the typescript babel plugin", async () => {
|
|
163
|
+
let content = await project.read("babel.config.js");
|
|
164
|
+
|
|
165
|
+
expect(content).not.toContain("@babel/plugin-transform-typescript");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("updated the sub-path imports to not have ts extensions", async () => {
|
|
169
|
+
let manifest = await packageJson.read(project.directory);
|
|
170
|
+
|
|
171
|
+
expect(manifest.imports).toMatchInlineSnapshot(`
|
|
172
|
+
{
|
|
173
|
+
"#app/*": "./app/*",
|
|
174
|
+
"#components/*": "./app/components/*",
|
|
175
|
+
"#config": "./app/config.js",
|
|
176
|
+
"#services/*": "./app/services/*",
|
|
177
|
+
"#test-helpers/*": "./tests/helpers/*",
|
|
178
|
+
"#utils/*": "./app/utils/*",
|
|
179
|
+
}
|
|
180
|
+
`);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("the popup points at the renamed boot script", async () => {
|
|
184
|
+
let html = await project.read("index.html");
|
|
185
|
+
|
|
186
|
+
expect(html).toContain(`src="./app/boot.js"`);
|
|
187
|
+
expect(html).not.toContain("boot.ts");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("build for development (testing, etc)", async () => {
|
|
191
|
+
let { exitCode } = await build(project);
|
|
192
|
+
|
|
193
|
+
expect(exitCode).toBe(0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("build for production", async () => {
|
|
197
|
+
let { exitCode } = await build(project, "production");
|
|
198
|
+
|
|
199
|
+
expect(exitCode).toBe(0);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("has no TS files", async () => {
|
|
203
|
+
let results = globSync("app/**/*", {
|
|
204
|
+
exclude: ["node_modules", "dist"],
|
|
205
|
+
cwd: project.directory,
|
|
206
|
+
});
|
|
207
|
+
expect(results).toMatchInlineSnapshot(`
|
|
208
|
+
[
|
|
209
|
+
"app/app.js",
|
|
210
|
+
"app/boot.js",
|
|
211
|
+
"app/config.js",
|
|
212
|
+
"app/router.js",
|
|
213
|
+
"app/templates",
|
|
214
|
+
"app/templates/application.gjs",
|
|
215
|
+
]
|
|
216
|
+
`);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("when re-applying, it no-ops correctly", async () => {
|
|
220
|
+
let files = new Set(
|
|
221
|
+
globSync("**/*", { cwd: project.directory, exclude: ["node_modules", "dist"] }),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
await reapply(project, []);
|
|
225
|
+
|
|
226
|
+
let filesAfter = new Set(
|
|
227
|
+
globSync("**/*", { cwd: project.directory, exclude: ["node_modules", "dist"] }),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
expect(filesAfter).toEqual(files);
|
|
231
|
+
expect(files).toMatchInlineSnapshot(`
|
|
232
|
+
Set {
|
|
233
|
+
"README.md",
|
|
234
|
+
"app",
|
|
235
|
+
"babel.config.js",
|
|
236
|
+
"index.html",
|
|
237
|
+
"package.json",
|
|
238
|
+
"pnpm-lock.yaml",
|
|
239
|
+
"public",
|
|
240
|
+
"vite.config.mjs",
|
|
241
|
+
"public/manifest.json",
|
|
242
|
+
"app/app.js",
|
|
243
|
+
"app/boot.js",
|
|
244
|
+
"app/config.js",
|
|
245
|
+
"app/router.js",
|
|
246
|
+
"app/templates",
|
|
247
|
+
"app/templates/application.gjs",
|
|
248
|
+
}
|
|
249
|
+
`);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# This file is committed to git and should not contain any secrets.
|
|
2
|
+
#
|
|
3
|
+
# Vite recommends using .env.local or .env.[mode].local if you need to manage secrets
|
|
4
|
+
# SEE: https://vite.dev/guide/env-and-mode.html#env-files for more information.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Default NODE_ENV with vite build --mode=test is production
|
|
8
|
+
NODE_ENV=development
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# minimal-extension
|
|
2
|
+
|
|
3
|
+
A [Manifest V3](https://developer.chrome.com/docs/extensions/develop/migrate/what-is-mv3) browser extension whose popup is an Ember app.
|
|
4
|
+
|
|
5
|
+
## Developing
|
|
6
|
+
|
|
7
|
+
The popup is a regular web page, so the fastest feedback loop is the vite dev server in a normal browser tab:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
To run as an actual extension, build (in watch mode) and load the `dist/` directory as an unpacked extension:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm build:watch
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- **Chrome / Edge / Brave**: `chrome://extensions` → enable "Developer mode" → "Load unpacked" → select `dist/`
|
|
20
|
+
- **Firefox**: `about:debugging#/runtime/this-firefox` → "Load Temporary Add-on…" → select `dist/manifest.json`
|
|
21
|
+
|
|
22
|
+
After a rebuild, re-open the popup (or reload the extension) to see changes.
|
|
23
|
+
|
|
24
|
+
## Anatomy
|
|
25
|
+
|
|
26
|
+
- `public/manifest.json` — the extension manifest; copied verbatim into `dist/`. Add [`background`](https://developer.chrome.com/docs/extensions/reference/manifest/background), [`content_scripts`](https://developer.chrome.com/docs/extensions/reference/manifest/content-scripts), permissions, and icons here as your extension grows.
|
|
27
|
+
- `index.html` — the popup page (`action.default_popup`). Extension pages are served with a `script-src 'self'` Content-Security-Policy, so the app boots from `app/boot.ts` rather than an inline script.
|
|
28
|
+
- `app/` — a normal Ember app. Routing uses `locationType: "none"`, since extension pages have no URL bar to bind to.
|
|
29
|
+
|
|
30
|
+
## Building for release
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Zip the contents of `dist/` and upload it to the extension store(s) of your choosing.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Looking for services that come from addons?
|
|
3
|
+
*
|
|
4
|
+
* See: https://github.com/embroider-build/embroider/issues/2659
|
|
5
|
+
*
|
|
6
|
+
* We currently don't support app-tree merging from libraries.
|
|
7
|
+
*
|
|
8
|
+
* For services, I highly recommend looking in to either of
|
|
9
|
+
* - https://github.com/chancancode/ember-polaris-service-
|
|
10
|
+
* - https://ember-primitives.pages.dev/6-utils/createService.md
|
|
11
|
+
* - https://ember-primitives.pages.dev/6-utils/createAsyncService.md
|
|
12
|
+
*/
|
|
13
|
+
import Application from "ember-strict-application-resolver";
|
|
14
|
+
|
|
15
|
+
export default class App extends Application {
|
|
16
|
+
modules = {
|
|
17
|
+
...import.meta.glob("./router.*", { eager: true }),
|
|
18
|
+
...import.meta.glob("./templates/**/*", { eager: true }),
|
|
19
|
+
...import.meta.glob("./services/**/*", { eager: true }),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
interface Config {
|
|
2
|
+
environment: "development" | "production";
|
|
3
|
+
locationType: "history" | "hash" | "none" | "auto";
|
|
4
|
+
rootURL: string;
|
|
5
|
+
EmberENV?: Record<string, unknown>;
|
|
6
|
+
APP: Record<string, unknown> & { rootElement?: string; autoboot?: boolean };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const ENV: Config = {
|
|
10
|
+
environment: import.meta.env.DEV ? "development" : "production",
|
|
11
|
+
rootURL: "/",
|
|
12
|
+
/**
|
|
13
|
+
* Extension pages live at chrome-extension://<id>/index.html (or
|
|
14
|
+
* moz-extension://), where history-based routing has nothing to bind
|
|
15
|
+
* to. "none" keeps routing fully programmatic.
|
|
16
|
+
*/
|
|
17
|
+
locationType: "none",
|
|
18
|
+
EmberENV: {},
|
|
19
|
+
APP: {},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default ENV;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { buildMacros } from "@embroider/macros/babel";
|
|
2
|
+
|
|
3
|
+
const macros = buildMacros({
|
|
4
|
+
configure(config) {
|
|
5
|
+
if (process.env.EMBER_ENV === "test") {
|
|
6
|
+
config.enableRuntimeMode();
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
plugins: [
|
|
13
|
+
[
|
|
14
|
+
"@babel/plugin-transform-typescript",
|
|
15
|
+
{
|
|
16
|
+
allExtensions: true,
|
|
17
|
+
onlyRemoveTypeImports: true,
|
|
18
|
+
allowDeclareFields: true,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
[
|
|
22
|
+
"babel-plugin-ember-template-compilation",
|
|
23
|
+
{
|
|
24
|
+
transforms: [...macros.templateMacros],
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
"module:decorator-transforms",
|
|
29
|
+
{
|
|
30
|
+
runtime: {
|
|
31
|
+
import: import.meta.resolve("decorator-transforms/runtime-esm"),
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
"@babel/plugin-transform-runtime",
|
|
37
|
+
{
|
|
38
|
+
absoluteRuntime: import.meta.dirname,
|
|
39
|
+
useESModules: true,
|
|
40
|
+
regenerator: false,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
...macros.babelMacros,
|
|
44
|
+
],
|
|
45
|
+
|
|
46
|
+
generatorOpts: {
|
|
47
|
+
compact: false,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Minimal Extension</title>
|
|
6
|
+
<meta name="description" content="" />
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<!--
|
|
11
|
+
Unlike a web app, this boot script cannot be inlined:
|
|
12
|
+
extension pages are served with a Content-Security-Policy of
|
|
13
|
+
`script-src 'self'`, which forbids inline scripts.
|
|
14
|
+
-->
|
|
15
|
+
<script type="module" src="./app/boot.ts"></script>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "minimal-extension",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Your Browser Extension's description here",
|
|
6
|
+
"repository": "",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"imports": {
|
|
11
|
+
"#app/*": "./app/*",
|
|
12
|
+
"#config": "./app/config.ts",
|
|
13
|
+
"#components/*": "./app/components/*",
|
|
14
|
+
"#services/*": "./app/services/*",
|
|
15
|
+
"#test-helpers/*": "./tests/helpers/*",
|
|
16
|
+
"#utils/*": "./app/utils/*"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
"./tests/*": "./tests/*",
|
|
20
|
+
"./*": "./app/*"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "vite build",
|
|
24
|
+
"build:watch": "vite build --watch --mode development",
|
|
25
|
+
"dev": "vite",
|
|
26
|
+
"start": "vite"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@embroider/macros": "^1.20.3",
|
|
30
|
+
"@embroider/router": "^3.0.6",
|
|
31
|
+
"@glimmer/component": "^2.1.1",
|
|
32
|
+
"decorator-transforms": "^2.3.2",
|
|
33
|
+
"ember-source": "~7.0.0",
|
|
34
|
+
"ember-strict-application-resolver": "^0.1.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@babel/core": "^7.29.7",
|
|
38
|
+
"@babel/plugin-transform-runtime": "^7.29.7",
|
|
39
|
+
"@babel/plugin-transform-typescript": "^7.29.7",
|
|
40
|
+
"@babel/runtime": "^7.29.7",
|
|
41
|
+
"@ember/app-tsconfig": "^2.0.0",
|
|
42
|
+
"@embroider/core": "^4.4.2",
|
|
43
|
+
"@glint/ember-tsc": "^1.7.5",
|
|
44
|
+
"@glint/template": "^1.7.7",
|
|
45
|
+
"@glint/tsserver-plugin": "^2.5.5",
|
|
46
|
+
"@nullvoxpopuli/ember-vite": "workspace:*",
|
|
47
|
+
"@rollup/plugin-babel": "^7.1.0",
|
|
48
|
+
"babel-plugin-ember-template-compilation": "^4.0.0",
|
|
49
|
+
"typescript": "^6.0.3",
|
|
50
|
+
"vite": "^8.0.14"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">= 24"
|
|
54
|
+
},
|
|
55
|
+
"ember": {
|
|
56
|
+
"edition": "octane"
|
|
57
|
+
}
|
|
58
|
+
}
|