Skip to content

Commit f58b580

Browse files
Merge branch 'main' into ENG-1306
2 parents 0c84f1e + 7380e1d commit f58b580

321 files changed

Lines changed: 30846 additions & 2106 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Tiltfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ def include_if_exists(path):
88
print("skipping %s because it does not exist", path)
99

1010
include_if_exists('../daily-api/Tiltfile')
11+
include_if_exists('../flyting/Tiltfile')
1112
include_if_exists('../adhoc-infra/Tiltfile')
1213
include_if_exists('../post-scraper-one-ai/Tiltfile')
1314
include_if_exists('../njord/Tiltfile')

packages/eslint-config/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"eslint-plugin-prettier": "^4.2.1",
1818
"eslint-plugin-react": "^7.37.5",
1919
"eslint-plugin-react-hooks": "^4.6.2",
20-
"eslint-plugin-tailwindcss": "^3.18.2",
20+
"eslint-plugin-tailwindcss": "^3.18.3",
2121
"eslint-plugin-unused-imports": "^3.2.0",
2222
"typescript": "5.6.3"
2323
},
@@ -30,7 +30,7 @@
3030
"eslint-plugin-prettier": "^4.2.1",
3131
"eslint-plugin-react": "^7.37.5",
3232
"eslint-plugin-react-hooks": "^4.6.2",
33-
"eslint-plugin-testing-library": "^7.16.1",
33+
"eslint-plugin-testing-library": "^7.16.2",
3434
"typescript": "^5.6.3"
3535
}
3636
}

packages/eslint-rules/lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
module.exports.rules = {
22
'no-custom-color': require('./rules/no-custom-color'),
3+
'no-raw-button-class': require('./rules/no-raw-button-class'),
34
};
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/**
2+
* Disallow raw `btn-*` and `btn-v2-*` class strings in JSX className /
3+
* `classed(...)` / `classNames(...)` / `cn(...)` calls.
4+
*
5+
* The token classes are an implementation detail of <Button> / <ButtonV2>
6+
* and the buttons.ts / buttons-v2.ts Tailwind plugins. Hand-stamping
7+
* them in className strings bypasses every variant guarantee
8+
* (typography, focus ring, disabled state, theme contract).
9+
*
10+
* Use the component instead:
11+
*
12+
* // BAD
13+
* <a className="btn btn-primary">Click</a>
14+
*
15+
* // GOOD
16+
* <Button tag="a" variant={ButtonVariant.Primary}>Click</Button>
17+
*/
18+
19+
const RAW_BUTTON_CLASS_PATTERN = /(?:^|\s)(btn(?:-v2)?(?:-[a-zA-Z]+)*)\b/;
20+
21+
const collectStringPieces = (node, out = []) => {
22+
if (!node) {
23+
return out;
24+
}
25+
switch (node.type) {
26+
case 'Literal':
27+
if (typeof node.value === 'string') {
28+
out.push({ value: node.value, node });
29+
}
30+
return out;
31+
case 'TemplateLiteral':
32+
node.quasis.forEach((quasi) => {
33+
out.push({ value: quasi.value.cooked || quasi.value.raw, node: quasi });
34+
});
35+
node.expressions.forEach((expr) => collectStringPieces(expr, out));
36+
return out;
37+
case 'TemplateElement':
38+
out.push({ value: node.value.cooked || node.value.raw, node });
39+
return out;
40+
case 'JSXExpressionContainer':
41+
return collectStringPieces(node.expression, out);
42+
case 'CallExpression':
43+
node.arguments.forEach((arg) => collectStringPieces(arg, out));
44+
return out;
45+
case 'ConditionalExpression':
46+
collectStringPieces(node.consequent, out);
47+
collectStringPieces(node.alternate, out);
48+
return out;
49+
case 'LogicalExpression':
50+
collectStringPieces(node.left, out);
51+
collectStringPieces(node.right, out);
52+
return out;
53+
case 'ArrayExpression':
54+
node.elements.forEach((el) => collectStringPieces(el, out));
55+
return out;
56+
case 'ObjectExpression':
57+
node.properties.forEach((prop) => {
58+
if (prop.type === 'Property' && prop.key) {
59+
collectStringPieces(prop.key, out);
60+
}
61+
});
62+
return out;
63+
default:
64+
return out;
65+
}
66+
};
67+
68+
const reportMatches = (context, pieces) => {
69+
pieces.forEach(({ value, node }) => {
70+
if (!value || typeof value !== 'string') {
71+
return;
72+
}
73+
const match = value.match(RAW_BUTTON_CLASS_PATTERN);
74+
if (!match) {
75+
return;
76+
}
77+
context.report({
78+
node,
79+
message:
80+
`Raw button class "${match[1]}" is not allowed. ` +
81+
'Use the <Button> or <ButtonV2> component with a `variant` prop.',
82+
});
83+
});
84+
};
85+
86+
const isClassNameAttribute = (node) =>
87+
node.name && /^class(Name)?$/.test(node.name.name);
88+
89+
const CLASSNAME_HELPERS = new Set([
90+
'classNames',
91+
'classnames',
92+
'cn',
93+
'clsx',
94+
'classed',
95+
'twMerge',
96+
]);
97+
98+
module.exports = {
99+
meta: {
100+
type: 'problem',
101+
docs: {
102+
description:
103+
'Disallow raw `btn-*` / `btn-v2-*` class strings; use <Button> instead.',
104+
recommended: false,
105+
},
106+
schema: [],
107+
},
108+
create(context) {
109+
return {
110+
JSXAttribute(node) {
111+
if (!isClassNameAttribute(node) || !node.value) {
112+
return;
113+
}
114+
const pieces = collectStringPieces(node.value);
115+
reportMatches(context, pieces);
116+
},
117+
CallExpression(node) {
118+
const callee = node.callee;
119+
const calleeName =
120+
(callee.type === 'Identifier' && callee.name) ||
121+
(callee.type === 'MemberExpression' &&
122+
callee.property &&
123+
callee.property.name);
124+
if (!calleeName || !CLASSNAME_HELPERS.has(calleeName)) {
125+
return;
126+
}
127+
const pieces = [];
128+
node.arguments.forEach((arg) => collectStringPieces(arg, pieces));
129+
reportMatches(context, pieces);
130+
},
131+
};
132+
},
133+
};

packages/extension/package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "extension",
3-
"version": "3.43.5",
3+
"version": "3.44.0",
44
"scripts": {
55
"dev": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=chrome rspack build -c rspack.config.js --watch",
66
"build": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=chrome rspack build -c rspack.config.js",
@@ -29,7 +29,7 @@
2929
"graphql": "^16.9.0",
3030
"graphql-request": "^3.6.1",
3131
"idb-keyval": "^5.1.5",
32-
"next": "16.2.1",
32+
"next": "16.2.4",
3333
"react": "18.3.1",
3434
"react-dom": "18.3.1",
3535
"react-intersection-observer": "^8.34.0",
@@ -38,7 +38,7 @@
3838
"webextension-polyfill": "^0.12.0"
3939
},
4040
"devDependencies": {
41-
"@babel/preset-env": "^7.29.2",
41+
"@babel/preset-env": "^7.29.3",
4242
"@babel/preset-react": "^7.28.5",
4343
"@babel/preset-typescript": "^7.28.5",
4444
"@dailydotdev/eslint-config": "workspace:*",
@@ -65,7 +65,7 @@
6565
"babel-jest": "^29.7.0",
6666
"babel-plugin-dynamic-import-node": "^2.3.3",
6767
"babel-plugin-react-remove-properties": "^0.3.1",
68-
"caniuse-lite": "^1.0.30001780",
68+
"caniuse-lite": "^1.0.30001791",
6969
"cross-env": "^7.0.3",
7070
"css-loader": "^5.2.7",
7171
"dotenv": "^17.2.3",
@@ -78,8 +78,8 @@
7878
"eslint-plugin-prettier": "^4.2.1",
7979
"eslint-plugin-react": "^7.37.5",
8080
"eslint-plugin-react-hooks": "^4.6.2",
81-
"eslint-plugin-tailwindcss": "^3.18.2",
82-
"eslint-plugin-testing-library": "^7.16.1",
81+
"eslint-plugin-tailwindcss": "^3.18.3",
82+
"eslint-plugin-testing-library": "^7.16.2",
8383
"fake-indexeddb": "^3.1.7",
8484
"filemanager-webpack-plugin": "^3.0.0-alpha.7",
8585
"identity-obj-proxy": "^3.0.0",
@@ -88,7 +88,7 @@
8888
"jest-junit": "^12.3.0",
8989
"nock": "^13.2.4",
9090
"node-fetch": "^2.6.6",
91-
"postcss": "^8.5.8",
91+
"postcss": "^8.5.13",
9292
"postcss-100vh-fix": "^1.0.2",
9393
"postcss-custom-media": "^11.0.3",
9494
"postcss-focus-visible": "^10.0.0",

packages/extension/rspack.config.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,10 @@ const mainConfig = {
220220
import: path.join(sourcePath, 'frame', 'index.ts'),
221221
runtime: false,
222222
},
223+
ping: {
224+
import: path.join(sourcePath, 'ping', 'index.ts'),
225+
runtime: false,
226+
},
223227
newtab: {
224228
import: path.join(sourcePath, 'newtab', 'index.tsx'),
225229
runtime: 'runtime',
@@ -253,7 +257,9 @@ const mainConfig = {
253257
...baseConfig.optimization,
254258
splitChunks: {
255259
chunks(chunk) {
256-
return !['content', 'companion', 'manifest'].includes(chunk.name);
260+
return !['content', 'companion', 'manifest', 'ping'].includes(
261+
chunk.name,
262+
);
257263
},
258264
maxSize: 244000,
259265
cacheGroups: {

packages/extension/src/content/index.tsx

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,34 @@ import browser from 'webextension-polyfill';
22
import { removeLinkTargetElement } from '@dailydotdev/shared/src/lib/strings';
33
import { ExtensionMessageType } from '@dailydotdev/shared/src/lib/extension';
44

5-
const isRendered = !!document.querySelector('daily-companion-app');
5+
// Keep the companion out of embedded article/reader iframes so our injected
6+
// host element and CSS can never alter the page being previewed.
7+
if (window.top === window.self) {
8+
const isRendered = !!document.querySelector('daily-companion-app');
69

7-
if (!isRendered) {
8-
// Inject app div
9-
const appContainer = document.createElement('daily-companion-app');
10-
document.body.appendChild(appContainer);
10+
if (!isRendered) {
11+
// Inject app div
12+
const appContainer = document.createElement('daily-companion-app');
13+
document.body.appendChild(appContainer);
1114

12-
// Create shadow dom
13-
const shadow = document
14-
.querySelector('daily-companion-app')
15-
.attachShadow({ mode: 'open' });
15+
// Create shadow dom
16+
const shadow = appContainer.attachShadow({ mode: 'open' });
1617

17-
const wrapper = document.createElement('div');
18-
wrapper.id = 'daily-companion-wrapper';
19-
shadow.appendChild(wrapper);
18+
const wrapper = document.createElement('div');
19+
wrapper.id = 'daily-companion-wrapper';
20+
shadow.appendChild(wrapper);
2021

21-
browser.runtime.sendMessage({ type: ExtensionMessageType.ContentLoaded });
22+
browser.runtime.sendMessage({ type: ExtensionMessageType.ContentLoaded });
2223

23-
let lastUrl = removeLinkTargetElement(window.location.href);
24-
new MutationObserver(() => {
25-
const current = removeLinkTargetElement(window.location.href);
26-
if (current !== lastUrl) {
27-
lastUrl = current;
28-
browser.runtime.sendMessage({ type: ExtensionMessageType.ContentLoaded });
29-
}
30-
}).observe(document, { subtree: true, childList: true });
24+
let lastUrl = removeLinkTargetElement(window.location.href);
25+
new MutationObserver(() => {
26+
const current = removeLinkTargetElement(window.location.href);
27+
if (current !== lastUrl) {
28+
lastUrl = current;
29+
browser.runtime.sendMessage({
30+
type: ExtensionMessageType.ContentLoaded,
31+
});
32+
}
33+
}).observe(document, { subtree: true, childList: true });
34+
}
3135
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { extensionSiteEmbedFrameEvent } from '@dailydotdev/shared/src/features/extensionEmbed/common';
2+
import browser from 'webextension-polyfill';
3+
import { initializeFrame } from './controller';
4+
import {
5+
enableFrameEmbeddingViaBackground,
6+
hasFrameEmbeddingPermissions,
7+
requestFrameEmbeddingPermissions,
8+
} from '../lib/frameEmbedding';
9+
import { renderMessage, renderPermissionPrompt } from './render';
10+
11+
jest.mock('../lib/frameEmbedding', () => ({
12+
enableFrameEmbeddingViaBackground: jest.fn(),
13+
hasFrameEmbeddingPermissions: jest.fn(),
14+
requestFrameEmbeddingPermissions: jest.fn(),
15+
}));
16+
17+
jest.mock('webextension-polyfill', () => ({
18+
runtime: {
19+
reload: jest.fn(),
20+
},
21+
}));
22+
23+
jest.mock('./render', () => ({
24+
renderMessage: jest.fn(),
25+
renderPermissionPrompt: jest.fn(),
26+
}));
27+
28+
describe('initializeFrame', () => {
29+
const root = document.createElement('div');
30+
const target = new URL('https://example.com/article');
31+
const sendParentMessage = jest.fn();
32+
const onEmbeddingEnabled = jest.fn();
33+
34+
beforeEach(() => {
35+
jest.useFakeTimers();
36+
jest.clearAllMocks();
37+
});
38+
39+
afterEach(() => {
40+
jest.useRealTimers();
41+
});
42+
43+
it('requests an extension reload after permission is granted', async () => {
44+
(hasFrameEmbeddingPermissions as jest.Mock).mockResolvedValue(false);
45+
(requestFrameEmbeddingPermissions as jest.Mock).mockResolvedValue(true);
46+
47+
let requestPermission:
48+
| (() => Promise<'granted' | 'dismissed' | 'failed'>)
49+
| undefined;
50+
(renderPermissionPrompt as jest.Mock).mockImplementation(
51+
({
52+
onRequestPermission,
53+
}: {
54+
onRequestPermission: () => Promise<'granted' | 'dismissed' | 'failed'>;
55+
}) => {
56+
requestPermission = onRequestPermission;
57+
},
58+
);
59+
60+
await initializeFrame({
61+
root,
62+
target,
63+
sendParentMessage,
64+
onEmbeddingEnabled,
65+
});
66+
67+
expect(requestPermission).toBeDefined();
68+
await expect(requestPermission?.()).resolves.toBe('granted');
69+
70+
expect(sendParentMessage).toHaveBeenCalledWith(
71+
extensionSiteEmbedFrameEvent.Error,
72+
{
73+
reason: 'missing-permission',
74+
target: target.href,
75+
},
76+
);
77+
expect(sendParentMessage).toHaveBeenCalledWith(
78+
extensionSiteEmbedFrameEvent.ReloadRequested,
79+
{
80+
target: target.href,
81+
},
82+
);
83+
expect(enableFrameEmbeddingViaBackground).not.toHaveBeenCalled();
84+
expect(onEmbeddingEnabled).not.toHaveBeenCalled();
85+
expect(renderMessage).not.toHaveBeenCalled();
86+
expect(browser.runtime.reload).toHaveBeenCalledTimes(0);
87+
jest.runOnlyPendingTimers();
88+
expect(browser.runtime.reload).toHaveBeenCalledTimes(1);
89+
});
90+
});

0 commit comments

Comments
 (0)