Skip to content

Commit 1156aaf

Browse files
authored
Merge pull request nightscout#8582 from nightscout/dependabot/npm_and_yarn/postcss-8.5.26
build(deps): Update PostCSS to 8.5.28 with regression coverage
2 parents d6e90d0 + bf4a7c0 commit 1156aaf

5 files changed

Lines changed: 194 additions & 7 deletions

File tree

.github/workflows/main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ jobs:
3939

4040
- name: Install dependencies
4141
run: npm install
42-
- name: Validate brace-expansion dependency resolution
43-
run: npm ls brace-expansion --all
42+
- name: Validate dependency resolution
43+
run: npm ls brace-expansion postcss --all
4444
- name: Run Tests
4545
run: npm run-script test-ci
4646
- name: Run client-core tests

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ All notable changes to cgm-remote-monitor are documented in this file.
66

77
### Fixed
88

9+
- **PostCSS:** Update to 8.5.28 and its required Nano ID 3.3.18 dependency.
10+
This includes CSS parsing fixes and tighter restrictions on loading source
11+
maps referenced by CSS. CI now checks stylesheet output, source-map
12+
boundaries and sanitizer compatibility. No Nightscout configuration changes
13+
are required.
914
- **Dependency security:** Update all `brace-expansion` copies to the latest
1015
compatible releases (1.1.18, 2.1.4 and 5.0.9), including nested tooling
1116
dependencies. Keep overrides scoped by major version because older

package-lock.json

Lines changed: 7 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"test": "env-cmd -f ./my.test.env mocha --timeout 5000 --require ./tests/hooks.js --exit ./tests/*.test.js",
3131
"test-single": "env-cmd -f ./my.test.env mocha --timeout 5000 --require ./tests/hooks.js --exit ./tests/$TEST.test.js",
3232
"test:core": "mocha --timeout 5000 --exit ./tests/client-core/**/*.test.js",
33-
"test:dependencies": "npm ls brace-expansion --all && mocha --timeout 5000 --exit ./tests/dependency-brace-expansion.test.js",
33+
"test:dependencies": "npm ls brace-expansion postcss --all && mocha --timeout 5000 --exit ./tests/dependency-*.test.js",
3434
"test-ci": "env-cmd -f ./tests/ci.test.env nyc --reporter=lcov --reporter=text-summary mocha --timeout 5000 --require ./tests/hooks.js --exit ./tests/*.test.js",
3535
"test:fast": "env-cmd -f ./my.test.env mocha --timeout 5000 --require ./tests/hooks.js --exit --reporter min ./tests/*.test.js",
3636
"test:unit": "env-cmd -f ./my.test.env mocha --timeout 5000 --require ./tests/hooks.js --exit --parallel --jobs 2 ./tests/{admintools,ar2,basalprofileplugin,bgnow,boluswizardpreview,bridge,bridge-connect-compat,cannulaage,careportal,cob,data.*,ddata,direction,env,errorcodes,expressextensions,hashauth,insulinage,iob,language,levels,loop,maker,mmconnect,mongo-pool-config,pluginbase,plugins,profile,profileeditor,pushover,query,sandbox,security,sensorage,settings,simplealarms,timeago,times,treatmentnotify,units,upbat,utils,verifyauth}.test.js",
@@ -185,7 +185,7 @@
185185
"node-forge": "1.4.0",
186186
"lodash": "4.18.1",
187187
"ip-address": "10.2.0",
188-
"postcss": "8.5.14",
188+
"postcss": "8.5.28",
189189
"qs": "6.15.1",
190190
"flatted": "3.4.2",
191191
"picomatch": "2.3.2",

tests/dependency-postcss.test.js

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
'use strict';
2+
3+
// Filesystem paths are repository fixtures or files in a new temporary directory.
4+
/* eslint-disable security/detect-non-literal-fs-filename */
5+
6+
const assert = require('assert');
7+
const fs = require('fs');
8+
const os = require('os');
9+
const path = require('path');
10+
const { createRequire } = require('module');
11+
const semver = require('semver');
12+
const postcss = require('postcss');
13+
const sanitizeHtml = require('sanitize-html');
14+
const lock = require('../package-lock.json');
15+
16+
describe('PostCSS dependency regressions', function () {
17+
it('installs the reviewed release for CSS loading and HTML sanitization', function () {
18+
const copies = Object.entries(lock.packages).filter(([name]) => name.endsWith('/postcss'));
19+
assert.ok(copies.length > 0);
20+
copies.forEach(([name, entry]) => {
21+
// These module paths come only from the committed lockfile.
22+
// eslint-disable-next-line security/detect-non-literal-require
23+
const installed = require(path.resolve(__dirname, '..', name, 'package.json'));
24+
assert.strictEqual(installed.version, entry.version);
25+
assert.ok(semver.satisfies(installed.version, '>=8.5.28 <9'));
26+
});
27+
for (const consumer of ['css-loader', 'sanitize-html']) {
28+
const consumerRequire = createRequire(require.resolve(consumer));
29+
const installed = consumerRequire('postcss/package.json');
30+
// eslint-disable-next-line security/detect-non-literal-require
31+
const parent = require(consumer + '/package.json');
32+
assert.ok(semver.satisfies(installed.version, parent.dependencies.postcss));
33+
assert.strictEqual(consumerRequire('postcss'), postcss);
34+
}
35+
});
36+
37+
const stylesheets = [
38+
'static/css/drawer.css',
39+
'static/css/dropdown.css',
40+
'static/css/sgv.css',
41+
'views/clockviews/clock-shared.css',
42+
'views/clockviews/clock-config.css'
43+
];
44+
stylesheets.forEach(file => {
45+
it('preserves CSS and source-map content for ' + file, function () {
46+
const from = path.resolve(__dirname, '..', file);
47+
const css = fs.readFileSync(from, 'utf8');
48+
const result = postcss([{ postcssPlugin: 'round-trip', Once() {} }]).process(css, {
49+
from,
50+
map: { inline: false, annotation: false }
51+
});
52+
assert.strictEqual(result.css, css);
53+
assert.deepStrictEqual(result.map.toJSON().sourcesContent, [css]);
54+
});
55+
});
56+
57+
it('preserves harmless hash-prefixed comments and a leading BOM', function () {
58+
const css = '\uFEFF/*# Nightscout theme */\n.widget { color: red }';
59+
const result = postcss([{ postcssPlugin: 'round-trip', Once() {} }])
60+
.process(css, { from: 'theme.css', map: false });
61+
assert.strictEqual(result.css, css);
62+
assert.strictEqual(postcss().process(css, { from: 'theme.css', map: false }).css, css);
63+
});
64+
65+
it('keeps a custom property separate from an appended comment', function () {
66+
const root = postcss.parse('.widget{--accent:red}');
67+
root.first.append(postcss.comment({ text: 'theme' }));
68+
const parsed = postcss.parse(root.toString());
69+
assert.strictEqual(parsed.first.first.value, 'red');
70+
assert.deepStrictEqual(parsed.first.nodes.map(node => node.type), ['decl', 'comment']);
71+
});
72+
73+
it('preserves empty comma-list items without inventing whitespace items', function () {
74+
assert.deepStrictEqual(postcss.list.comma('red,,blue,'), ['red', '', 'blue', '']);
75+
assert.deepStrictEqual(postcss.list.space(' \t\r\n '), []);
76+
assert.deepStrictEqual(postcss.list.comma('rgb(1, 2, 3), "a,b"'), ['rgb(1, 2, 3)', '"a,b"']);
77+
});
78+
79+
it('visits inserted CSS rules consistently in sync and async processing', async function () {
80+
function plugin(order) {
81+
return {
82+
postcssPlugin: 'unwrap-nested',
83+
Rule(rule) {
84+
order.push(rule.selector);
85+
rule.each(child => {
86+
if (child.type === 'rule') {
87+
child.selector = rule.selector + ' ' + child.selector;
88+
rule.after(child);
89+
}
90+
});
91+
},
92+
RootExit() { order.push('exit'); }
93+
};
94+
}
95+
const syncOrder = [];
96+
const asyncOrder = [];
97+
const css = '.widget { .value { span {} } }';
98+
const options = { from: 'theme.css', map: false };
99+
const sync = postcss([plugin(syncOrder)]).process(css, options).css;
100+
const asyncResult = await postcss([plugin(asyncOrder)]).process(css, options);
101+
assert.strictEqual(asyncResult.css, sync);
102+
assert.deepStrictEqual(asyncOrder, syncOrder);
103+
const nestedVisit = syncOrder.indexOf('.widget .value span');
104+
assert.ok(nestedVisit >= 0 && nestedVisit < syncOrder.indexOf('exit'));
105+
assert.ok(sync.includes('.widget .value span'));
106+
});
107+
108+
it('preserves formatting when a CSS AST is serialized and restored', function () {
109+
const css = '.one {}\n.two {}\n\n.three {}\n';
110+
const json = JSON.parse(JSON.stringify(postcss.parse(css).toJSON()));
111+
assert.strictEqual(postcss.fromJSON(json).toString(), css);
112+
});
113+
114+
it('prevents serialized AST properties from replacing the node prototype', function () {
115+
const json = JSON.parse('{"type":"decl","prop":"color","value":"red","__proto__":{"hijacked":true}}');
116+
const node = postcss.fromJSON(json);
117+
assert.strictEqual(node.hijacked, undefined);
118+
assert.strictEqual(node.toString(), 'color: red');
119+
});
120+
121+
it('preserves permitted inline styles through sanitize-html', function () {
122+
const result = sanitizeHtml('<p style="color: red; /* note */ font-weight: 700; position: fixed">note</p>', {
123+
allowedTags: ['p'],
124+
allowedAttributes: { p: ['style'] },
125+
allowedStyles: { '*': { color: [/^red$/], 'font-weight': [/^700$/] } }
126+
});
127+
assert.strictEqual(result, '<p style="color:red;font-weight:700">note</p>');
128+
});
129+
130+
it('preserves Nightscout note text while stripping styles and active markup', function () {
131+
const purify = require('../lib/server/purifier')();
132+
const note = { notes: '<p style="color:red" onclick="alert(1)">BG &lt; 70</p><script>alert(1)</script>' };
133+
purify.purifyObject(note);
134+
assert.deepStrictEqual(note, { notes: '<p>BG &lt; 70</p>' });
135+
assert.strictEqual(purify.sanitizeString('Fish & Chips; BG < 70'), 'Fish & Chips; BG < 70');
136+
});
137+
138+
describe('previous source-map boundaries', function () {
139+
let directory;
140+
let from;
141+
const sourceMap = JSON.stringify({
142+
version: 3, sources: ['theme.scss'], names: [], mappings: 'AAAA',
143+
sourcesContent: ['.widget { color: red }']
144+
});
145+
function input(annotation, options = { from }) {
146+
return postcss.parse('.widget { color: red }\n/*# sourceMappingURL=' + annotation + ' */', options).source.input;
147+
}
148+
before(function () {
149+
directory = fs.mkdtempSync(path.join(os.tmpdir(), 'nightscout-postcss-'));
150+
fs.mkdirSync(path.join(directory, 'css'));
151+
from = path.join(directory, 'css', 'theme.css');
152+
fs.writeFileSync(path.join(directory, 'css', 'theme.css.map'), sourceMap);
153+
fs.writeFileSync(path.join(directory, 'outside.map'), sourceMap);
154+
});
155+
after(function () {
156+
fs.rmSync(directory, { recursive: true, force: true });
157+
});
158+
159+
it('loads a source map beside the stylesheet', function () {
160+
assert.strictEqual(input('theme.css.map').map.text, sourceMap);
161+
});
162+
it('continues to accept inline and explicitly supplied maps', function () {
163+
const inline = 'data:application/json;base64,' + Buffer.from(sourceMap).toString('base64');
164+
assert.strictEqual(input(inline).map.text, sourceMap);
165+
assert.strictEqual(input('ignored.map', { from, map: { prev: sourceMap } }).map.text, sourceMap);
166+
});
167+
it('ignores annotations outside the stylesheet directory', function () {
168+
assert.strictEqual(input('../outside.map').map, undefined);
169+
});
170+
it('ignores file annotations when no source filename is provided', function () {
171+
assert.strictEqual(input(path.join(directory, 'outside.map'), {}).map, undefined);
172+
});
173+
it('does not follow a source-map symlink outside the stylesheet directory', function () {
174+
fs.symlinkSync(path.join(directory, 'outside.map'), path.join(directory, 'css', 'linked.map'), 'file');
175+
assert.strictEqual(input('linked.map').map, undefined);
176+
});
177+
});
178+
});

0 commit comments

Comments
 (0)