sku 13.3.0 β†’ 13.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # sku
2
2
 
3
+ ## 13.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add WebP and AVIF image format support to sku ([#1119](https://github.com/seek-oss/sku/pull/1119))
8
+
9
+ Support for `webp` and `avif` image formats has been added. Note that browser support for these formats may vary. To ensure compatibility across browsers, consumers are advised to use the `<picture>` element with fallback formats.
10
+
11
+ ### Patch Changes
12
+
13
+ - Fixes a bug that caused React 19 applications to fail during `sku build` ([#1143](https://github.com/seek-oss/sku/pull/1143))
14
+
15
+ Note: remaining on React 18 is recommended until `sku` officially supports React 19. Updating your application to React 19 may require overriding dependency versions by configuring your package manager. Additionally, there may be other incompatibilities that haven't been found yet.
16
+
3
17
  ## 13.3.0
4
18
 
5
19
  ### Minor Changes
@@ -45,7 +45,6 @@ module.exports = ({
45
45
 
46
46
  if (isProductionBuild) {
47
47
  plugins.push(
48
- require.resolve('@babel/plugin-transform-react-inline-elements'),
49
48
  require.resolve('babel-plugin-transform-react-remove-prop-types'),
50
49
  require.resolve('@babel/plugin-transform-react-constant-elements'),
51
50
  );
@@ -1,3 +1,6 @@
1
+ // @ts-check
2
+
3
+ /** @type {import("prettier").Options} */
1
4
  module.exports = {
2
5
  singleQuote: true,
3
6
  tabWidth: 2,
@@ -13,6 +13,6 @@ module.exports = {
13
13
  resolvePackage,
14
14
  TYPESCRIPT: new RegExp(`\.(${extensions.ts.join('|')})$`),
15
15
  JAVASCRIPT: new RegExp(`\.(${extensions.js.join('|')})$`),
16
- IMAGE: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
16
+ IMAGE: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/, /\.webp$/, /\.avif$/],
17
17
  SVG: /\.svg$/,
18
18
  };
@@ -0,0 +1,186 @@
1
+ // @ts-check
2
+
3
+ const assert = require('node:assert');
4
+ const { readFile, writeFile } = require('node:fs/promises');
5
+ const t = require('@babel/types');
6
+ const { parseModule, builders, generateCode } = require('magicast');
7
+ // eslint-plugin-import doesn't support subpath imports
8
+ // eslint-disable-next-line import/no-unresolved
9
+ const { getConfigFromVariableDeclaration } = require('magicast/helpers');
10
+
11
+ const debug = require('debug');
12
+ const log = debug('sku:update-sku-config');
13
+
14
+ const prettier = require('prettier');
15
+ const prettierConfig = require('../config/prettier/prettierConfig.js');
16
+
17
+ class SkuConfigUpdater {
18
+ /** @typedef {import("sku").SkuConfig} SkuConfig */
19
+ /** @typedef {import("magicast").ProxifiedObject<SkuConfig>} ProxifiedSkuConfig */
20
+ /** @typedef {import("@babel/types").ObjectExpression} ObjectExpression */
21
+ /** @typedef {import("@babel/types").VariableDeclarator} VariableDeclarator */
22
+
23
+ /** @typedef {{type: 'esm', configAst: ProxifiedSkuConfig}} EsmConfig */
24
+ /** @typedef {{type: 'esm-non-literal', configAst: ProxifiedSkuConfig, configDeclaration: VariableDeclarator}} EsmNonLiteralConfig */
25
+ /** @typedef {{type: 'cjs', configAst: ObjectExpression }} CjsConfig */
26
+
27
+ /** @type {EsmConfig | EsmNonLiteralConfig | CjsConfig} The AST or AST proxy of the sku config */
28
+ #config;
29
+ /** The path to the sku config being modified */
30
+ #path;
31
+ /** The parsed sku config file from magicast. Used for serializing the AST after updating it. */
32
+ #module;
33
+
34
+ /**
35
+ * @param {object} options
36
+ * @param {string} options.path - An absolute path to a sku config
37
+ * @param {string} options.contents - The contents of the sku config
38
+ */
39
+ constructor({ path, contents }) {
40
+ this.#path = path;
41
+
42
+ const skuConfigModule = parseModule(contents);
43
+ this.#module = skuConfigModule;
44
+
45
+ if (typeof skuConfigModule.exports.default === 'undefined') {
46
+ /** @type {ObjectExpression} */
47
+ let configAst;
48
+
49
+ log(
50
+ 'No default export found in sku config. Config is either CJS or invalid.',
51
+ );
52
+
53
+ t.assertProgram(skuConfigModule.$ast);
54
+ const lastStatement = skuConfigModule.$ast.body.at(-1);
55
+ t.assertExpressionStatement(lastStatement);
56
+
57
+ const { expression } = lastStatement;
58
+ t.assertAssignmentExpression(expression);
59
+ t.assertMemberExpression(expression.left);
60
+ t.assertIdentifier(expression.left.object, {
61
+ name: 'module',
62
+ });
63
+ t.assertIdentifier(expression.left.property, {
64
+ name: 'exports',
65
+ });
66
+
67
+ if (t.isObjectExpression(expression.right)) {
68
+ configAst = expression.right;
69
+ } else if (t.isIdentifier(expression.right)) {
70
+ const skuConfigIdentifierName = expression.right.name;
71
+ const skuConfigDeclaration = skuConfigModule.$ast.body.find(
72
+ (node) =>
73
+ t.isVariableDeclaration(node) &&
74
+ t.isIdentifier(node.declarations[0].id) &&
75
+ node.declarations[0].id.name === skuConfigIdentifierName,
76
+ );
77
+ assert(skuConfigDeclaration, 'Expected skuConfig to be defined');
78
+ t.assertVariableDeclaration(skuConfigDeclaration);
79
+ t.assertVariableDeclarator(skuConfigDeclaration.declarations[0]);
80
+ t.assertObjectExpression(skuConfigDeclaration.declarations[0].init);
81
+ configAst = skuConfigDeclaration.declarations[0].init;
82
+ } else {
83
+ throw new Error("Couldn't find config object in CJS sku config");
84
+ }
85
+
86
+ this.#config = {
87
+ type: 'cjs',
88
+ configAst,
89
+ };
90
+ } else {
91
+ log('Found sku config with ESM export');
92
+
93
+ if (skuConfigModule.exports.default.$type === 'object') {
94
+ const configAst = skuConfigModule.exports.default;
95
+ this.#config = {
96
+ type: 'esm',
97
+ configAst,
98
+ };
99
+ } else {
100
+ const { declaration: configDeclaration, config: configAst } =
101
+ getConfigFromVariableDeclaration(skuConfigModule);
102
+
103
+ assert(configAst, 'Expected skuConfig to be defined');
104
+ this.#config = {
105
+ type: 'esm-non-literal',
106
+ configAst,
107
+ configDeclaration,
108
+ };
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Creates a new instance of SkuConfigUpdater from a file path
115
+ *
116
+ * @param {string} path - An absoulte path to a sku config
117
+ */
118
+ static async fromFile(path) {
119
+ const contents = await readFile(path, 'utf8');
120
+
121
+ return new SkuConfigUpdater({ path, contents });
122
+ }
123
+
124
+ /**
125
+ * Updates `property` in sku config with the provided `value`. Inserts the `property` and `value` if it doesn't exist.
126
+ *
127
+ * This method does not write the changes to the file system. Use `commitConfig` to do that.
128
+ *
129
+ * @template {keyof SkuConfig} T
130
+ * @param {{property: T, value: SkuConfig[T]}} options
131
+ */
132
+ upsertConfig({ property, value }) {
133
+ if (this.#config.type === 'cjs') {
134
+ const propertyToUpdate = this.#config.configAst.properties.find(
135
+ (prop) =>
136
+ t.isObjectProperty(prop) &&
137
+ t.isIdentifier(prop.key) &&
138
+ prop.key.name === property,
139
+ );
140
+
141
+ if (propertyToUpdate) {
142
+ t.assertObjectProperty(propertyToUpdate);
143
+ propertyToUpdate.value = builders.literal(value);
144
+ } else {
145
+ const {
146
+ properties: [propertyLiteral],
147
+ } = builders.literal({
148
+ [property]: value,
149
+ });
150
+ this.#config.configAst.properties.push(propertyLiteral);
151
+ }
152
+
153
+ return;
154
+ }
155
+
156
+ // @ts-expect-error We have to mutate here because of magicast, but typescript complains
157
+ this.#config.configAst[property] = value;
158
+
159
+ if (this.#config.type === 'esm') {
160
+ return;
161
+ }
162
+
163
+ // At this point `this.#config.type` is `esm-non-literal`
164
+
165
+ // Copied from magicast/helpers https://github.com/unjs/magicast/blob/50e2207842672e2c1c75898f0b1b97909f3b6c92/src/helpers/vite.ts#L129
166
+ // @ts-expect-error This works because of recast magic
167
+ this.#config.configDeclaration.init = generateCode(
168
+ this.#config.configAst,
169
+ ).code;
170
+ }
171
+
172
+ /**
173
+ * Writes the current state of the sku config to the file system
174
+ */
175
+ async commitConfig() {
176
+ const newContents = this.#module.generate().code;
177
+ const formattedNewContents = prettier.format(newContents, {
178
+ parser: 'typescript',
179
+ ...prettierConfig,
180
+ });
181
+
182
+ await writeFile(this.#path, formattedNewContents);
183
+ }
184
+ }
185
+
186
+ module.exports = { SkuConfigUpdater };
@@ -0,0 +1,213 @@
1
+ /**
2
+ * @jest-environment node
3
+ */
4
+
5
+ // @ts-check
6
+ import { createFixture } from 'fs-fixture';
7
+ import dedent from 'dedent';
8
+ import { SkuConfigUpdater } from './SkuConfigUpdater';
9
+
10
+ describe('updateSkuConfig', () => {
11
+ describe('SkuConfigUpdater', () => {
12
+ it('Should update a TypeScript sku config with a literal ESM default export', async () => {
13
+ const skuConfigFileName = 'sku.config.ts';
14
+
15
+ const fixture = await createFixture({
16
+ [skuConfigFileName]: dedent/* ts */ `
17
+ export default {
18
+ renderEntry: 'src/render.tsx',
19
+ clientEntry: 'src/client.tsx',
20
+ };`,
21
+ });
22
+
23
+ const modifier = await SkuConfigUpdater.fromFile(
24
+ fixture.getPath(skuConfigFileName),
25
+ );
26
+ modifier.upsertConfig({
27
+ property: 'renderEntry',
28
+ value: 'src/updated-render.tsx',
29
+ });
30
+ modifier.upsertConfig({
31
+ property: 'srcPaths',
32
+ value: ['./src', './cypress'],
33
+ });
34
+ await modifier.commitConfig();
35
+
36
+ const result = (await fixture.readFile(skuConfigFileName)).toString();
37
+ expect(result).toMatchInlineSnapshot(`
38
+ "export default {
39
+ renderEntry: 'src/updated-render.tsx',
40
+ clientEntry: 'src/client.tsx',
41
+ srcPaths: ['./src', './cypress'],
42
+ };
43
+ "
44
+ `);
45
+
46
+ await fixture.rm();
47
+ });
48
+
49
+ it('Should update a TypeScript sku config with a literal ESM default export and "satisfies"', async () => {
50
+ const skuConfigFileName = 'sku.config.ts';
51
+
52
+ const fixture = await createFixture({
53
+ [skuConfigFileName]: dedent/* ts */ `
54
+ import type { SkuConfig } from 'sku';
55
+
56
+ export default {
57
+ renderEntry: 'src/render.tsx',
58
+ clientEntry: 'src/client.tsx',
59
+ } satisfies SkuConfig;`,
60
+ });
61
+
62
+ const modifier = await SkuConfigUpdater.fromFile(
63
+ fixture.getPath(skuConfigFileName),
64
+ );
65
+ modifier.upsertConfig({
66
+ property: 'renderEntry',
67
+ value: 'src/updated-render.tsx',
68
+ });
69
+ modifier.upsertConfig({
70
+ property: 'srcPaths',
71
+ value: ['./src', './cypress'],
72
+ });
73
+ await modifier.commitConfig();
74
+
75
+ const result = (await fixture.readFile(skuConfigFileName)).toString();
76
+ expect(result).toMatchInlineSnapshot(`
77
+ "import type { SkuConfig } from 'sku';
78
+
79
+ export default {
80
+ renderEntry: 'src/updated-render.tsx',
81
+ clientEntry: 'src/client.tsx',
82
+ srcPaths: ['./src', './cypress'],
83
+ } satisfies SkuConfig;
84
+ "
85
+ `);
86
+
87
+ await fixture.rm();
88
+ });
89
+
90
+ it('Should update a TypeScript sku config with a non-literal ESM default export', async () => {
91
+ const skuConfigFileName = 'sku.config.ts';
92
+
93
+ const fixture = await createFixture({
94
+ [skuConfigFileName]: dedent/* ts */ `
95
+ import type { SkuConfig } from 'sku';
96
+
97
+ const skuConfig: SkuConfig = {
98
+ renderEntry: 'src/render.tsx',
99
+ clientEntry: 'src/client.tsx',
100
+ };
101
+
102
+ export default skuConfig;`,
103
+ });
104
+
105
+ const modifier = await SkuConfigUpdater.fromFile(
106
+ fixture.getPath(skuConfigFileName),
107
+ );
108
+ modifier.upsertConfig({
109
+ property: 'renderEntry',
110
+ value: 'src/updated-render.tsx',
111
+ });
112
+ modifier.upsertConfig({
113
+ property: 'srcPaths',
114
+ value: ['./src', './cypress'],
115
+ });
116
+ await modifier.commitConfig();
117
+
118
+ const result = (await fixture.readFile(skuConfigFileName)).toString();
119
+ expect(result).toMatchInlineSnapshot(`
120
+ "import type { SkuConfig } from 'sku';
121
+
122
+ const skuConfig: SkuConfig = {
123
+ renderEntry: 'src/updated-render.tsx',
124
+ clientEntry: 'src/client.tsx',
125
+ srcPaths: ['./src', './cypress'],
126
+ };
127
+
128
+ export default skuConfig;
129
+ "
130
+ `);
131
+
132
+ await fixture.rm();
133
+ });
134
+
135
+ it('Should update a JavaScript sku config with a literal CJS default export', async () => {
136
+ const skuConfigFileName = 'sku.config.ts';
137
+
138
+ const fixture = await createFixture({
139
+ [skuConfigFileName]: dedent/* ts */ `
140
+ module.exports = {
141
+ renderEntry: 'src/render.tsx',
142
+ clientEntry: 'src/client.tsx',
143
+ };`,
144
+ });
145
+
146
+ const modifier = await SkuConfigUpdater.fromFile(
147
+ fixture.getPath(skuConfigFileName),
148
+ );
149
+ modifier.upsertConfig({
150
+ property: 'renderEntry',
151
+ value: 'src/updated-render.tsx',
152
+ });
153
+ modifier.upsertConfig({
154
+ property: 'srcPaths',
155
+ value: ['./src', './cypress'],
156
+ });
157
+ await modifier.commitConfig();
158
+
159
+ const result = (await fixture.readFile(skuConfigFileName)).toString();
160
+ expect(result).toMatchInlineSnapshot(`
161
+ "module.exports = {
162
+ renderEntry: 'src/updated-render.tsx',
163
+ clientEntry: 'src/client.tsx',
164
+ srcPaths: ['./src', './cypress'],
165
+ };
166
+ "
167
+ `);
168
+
169
+ await fixture.rm();
170
+ });
171
+
172
+ it('Should update a JavaScript sku config with a non-literal CJS default export', async () => {
173
+ const skuConfigFileName = 'sku.config.ts';
174
+
175
+ const fixture = await createFixture({
176
+ [skuConfigFileName]: dedent/* ts */ `
177
+ const skuConfig = {
178
+ renderEntry: 'src/render.tsx',
179
+ clientEntry: 'src/client.tsx',
180
+ };
181
+
182
+ module.exports = skuConfig;`,
183
+ });
184
+
185
+ const modifier = await SkuConfigUpdater.fromFile(
186
+ fixture.getPath(skuConfigFileName),
187
+ );
188
+ modifier.upsertConfig({
189
+ property: 'renderEntry',
190
+ value: 'src/updated-render.tsx',
191
+ });
192
+ modifier.upsertConfig({
193
+ property: 'srcPaths',
194
+ value: ['./src', './cypress'],
195
+ });
196
+ await modifier.commitConfig();
197
+
198
+ const result = (await fixture.readFile(skuConfigFileName)).toString();
199
+ expect(result).toMatchInlineSnapshot(`
200
+ "const skuConfig = {
201
+ renderEntry: 'src/updated-render.tsx',
202
+ clientEntry: 'src/client.tsx',
203
+ srcPaths: ['./src', './cypress'],
204
+ };
205
+
206
+ module.exports = skuConfig;
207
+ "
208
+ `);
209
+
210
+ await fixture.rm();
211
+ });
212
+ });
213
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sku",
3
- "version": "13.3.0",
3
+ "version": "13.4.0",
4
4
  "description": "Front-end development toolkit, powered by Webpack, Babel, Vanilla Extract and Jest",
5
5
  "types": "./sku-types.d.ts",
6
6
  "bin": {
@@ -49,12 +49,12 @@
49
49
  "dependencies": {
50
50
  "@babel/core": "^7.21.8",
51
51
  "@babel/plugin-transform-react-constant-elements": "^7.21.3",
52
- "@babel/plugin-transform-react-inline-elements": "^7.21.0",
53
52
  "@babel/plugin-transform-runtime": "^7.21.4",
54
53
  "@babel/preset-env": "^7.21.5",
55
54
  "@babel/preset-react": "^7.18.6",
56
55
  "@babel/preset-typescript": "^7.21.5",
57
56
  "@babel/runtime": "^7.21.0",
57
+ "@babel/types": "^7.26.0",
58
58
  "@loadable/babel-plugin": "^5.13.2",
59
59
  "@loadable/component": "^5.14.1",
60
60
  "@loadable/server": "^5.14.0",
@@ -112,6 +112,7 @@
112
112
  "jest-environment-jsdom": "^29.0.0",
113
113
  "jest-watch-typeahead": "^2.2.0",
114
114
  "lint-staged": "^15.2.10",
115
+ "magicast": "^0.3.5",
115
116
  "mini-css-extract-plugin": "^2.6.1",
116
117
  "minimist": "^1.2.8",
117
118
  "nano-memoize": "^3.0.16",
@@ -148,13 +149,15 @@
148
149
  "@types/minimist": "^1.2.5",
149
150
  "@types/node": "^18.19.31",
150
151
  "@types/picomatch": "^2.3.3",
152
+ "@types/prettier": "^2.7.3",
151
153
  "@types/react": "^18.2.3",
152
154
  "@types/react-dom": "^18.2.3",
153
155
  "@types/webpack-bundle-analyzer": "^4.7.0",
154
156
  "@types/wrap-ansi": "^3.0.0",
155
157
  "@vanilla-extract/css": "^1.0.0",
156
158
  "@vocab/react": "^1.1.11",
157
- "braid-design-system": "^32.0.0",
159
+ "braid-design-system": "^33.1.0",
160
+ "fs-fixture": "^2.6.0",
158
161
  "react": "^18.2.0",
159
162
  "react-dom": "^18.2.0",
160
163
  "react-helmet": "^6.1.0",
@@ -1,7 +1,7 @@
1
1
  import 'braid-design-system/reset';
2
2
 
3
3
  import { BraidProvider } from 'braid-design-system';
4
- import apac from 'braid-design-system/themes/apac';
4
+ import seekJobs from 'braid-design-system/themes/seekJobs';
5
5
  import { StrictMode } from 'react';
6
6
 
7
7
  import { NextSteps } from './NextSteps';
@@ -12,7 +12,7 @@ interface AppProps {
12
12
 
13
13
  export default ({ environment }: AppProps) => (
14
14
  <StrictMode>
15
- <BraidProvider theme={apac}>
15
+ <BraidProvider theme={seekJobs}>
16
16
  <NextSteps environment={environment} />
17
17
  </BraidProvider>
18
18
  </StrictMode>
@@ -7,6 +7,7 @@ import {
7
7
  TextLink,
8
8
  Stack,
9
9
  List,
10
+ Divider,
10
11
  } from 'braid-design-system';
11
12
 
12
13
  import * as styles from './NextSteps.css';
@@ -48,12 +49,9 @@ export function NextSteps({ environment }: NextStepsProps) {
48
49
  boxShadow="large"
49
50
  padding={{ mobile: 'large', tablet: 'xlarge', desktop: 'xxlarge' }}
50
51
  >
51
- <Stack
52
- space={{ mobile: 'xlarge', desktop: 'xxlarge' }}
53
- dividers="strong"
54
- >
52
+ <Stack space={{ mobile: 'xlarge', desktop: 'xxlarge' }}>
55
53
  <Stack space="large">
56
- <Heading level="2">πŸƒπŸΎβ€β™€οΈ Next steps</Heading>
54
+ <Heading level="2">🧰 Features and Tooling</Heading>
57
55
  <Text>
58
56
  The project comes pre-configured with a lot of standardised
59
57
  tooling to make authoring web applications easier.
@@ -74,10 +72,7 @@ export function NextSteps({ environment }: NextStepsProps) {
74
72
  </TextLink>
75
73
  , <TextLink href="https://jestjs.io/">Jest</TextLink>,{' '}
76
74
  <TextLink href="https://eslint.org/">ESLint</TextLink>,{' '}
77
- <TextLink href="https://storybook.js.org/">
78
- Storybook
79
- </TextLink>{' '}
80
- , and more.
75
+ and more.
81
76
  </Text>
82
77
  </Stack>
83
78
 
@@ -107,6 +102,41 @@ export function NextSteps({ environment }: NextStepsProps) {
107
102
  </Stack>
108
103
  </Stack>
109
104
 
105
+ <Divider weight="strong" />
106
+
107
+ <Stack space="large">
108
+ <Heading level="2">πŸƒβ€β™€οΈ Next steps</Heading>
109
+ <Text>
110
+ Ensure you’re fully set up with these additional
111
+ recommendations.
112
+ </Text>
113
+ <Stack space="gutter">
114
+ <Heading level="3">Brand resources</Heading>
115
+ <Text>
116
+ To align your project to SEEK's brand standards it is
117
+ recommended to install{' '}
118
+ <TextLink href="https://github.com/SEEK-Jobs/shared-web-assets">
119
+ Shared web assets
120
+ </TextLink>
121
+ .
122
+ </Text>
123
+ </Stack>
124
+
125
+ <Stack space="gutter">
126
+ <Heading level="3">Telemetry</Heading>
127
+ <Text>
128
+ To help us improve sku, please install our private{' '}
129
+ <TextLink href="https://github.com/SEEK-Jobs/sku-telemetry">
130
+ sku telemetry
131
+ </TextLink>{' '}
132
+ package that gives us insights on usage, errors and
133
+ performance.
134
+ </Text>
135
+ </Stack>
136
+ </Stack>
137
+
138
+ <Divider weight="strong" />
139
+
110
140
  <Stack space="large">
111
141
  <Heading level="2">πŸ™‹β€β™‚οΈ Support</Heading>
112
142
 
@@ -150,6 +180,8 @@ export function NextSteps({ environment }: NextStepsProps) {
150
180
  </Stack>
151
181
  </Stack>
152
182
 
183
+ <Divider weight="strong" />
184
+
153
185
  <Stack space="large">
154
186
  <Heading level="2">πŸ“£ Announcements</Heading>
155
187