@wix/origami-transforms 0.1.26 → 0.1.28

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.
@@ -1,4 +1,7 @@
1
- export function loadSvgs(ids: any): Promise<undefined> | undefined;
1
+ export function loadSvgs(ids: any): Promise<{
2
+ fetched: number;
3
+ failed: number;
4
+ }> | undefined;
2
5
  export function loadedSvg(id: any): any;
3
6
  export function svgInfo(svg: any): {
4
7
  type: any;
@@ -0,0 +1,4 @@
1
+ /** Aggregate completed dev invocations; paths are only in this local set, never in rows. */
2
+ export function countPlugin(context: any, duration: any): void;
3
+ /** @param {'malformed_custom_enum' | 'unmappable_preset' | 'svg_not_loaded' | 'media_not_wix'} key */
4
+ export function warn(key: "malformed_custom_enum" | "unmappable_preset" | "svg_not_loaded" | "media_not_wix"): void;
@@ -1,3 +1,4 @@
1
+ export function pageIdFor(context: any): any;
1
2
  export function createContextFactory({ layout, command }: {
2
3
  layout: any;
3
4
  command: any;
@@ -1,4 +1,7 @@
1
- export function pluginError(plugin: any, context: any, cause: any): Error;
1
+ export function pluginError(plugin: any, context: any, cause: any): Error & {
2
+ error_name: import("@wix/origami-bi").ErrorName;
3
+ error_kind: import("@wix/origami-bi").ErrorKind;
4
+ };
2
5
  export function runCssPlugins({ plugins, root, context }: {
3
6
  plugins: any;
4
7
  root: any;
@@ -9,6 +9,7 @@ export class LayoutError extends Error {
9
9
  line?: number;
10
10
  column?: number;
11
11
  });
12
+ error_name: string;
12
13
  stack: string;
13
14
  violations: readonly string[];
14
15
  location: {
@@ -15,15 +15,10 @@ export function createTransformPipeline({ layout, jsxPlugins, cssPlugins }: {
15
15
  * @param {{ ssr?: boolean | undefined }} [contextOptions] */
16
16
  transformJsx: (code: string, id: string, { ssr }?: {
17
17
  ssr?: boolean | undefined;
18
- }) => Promise<{
19
- code: any;
20
- map: any;
21
- }>;
18
+ }) => Promise<any>;
22
19
  postcssPlugin: {
20
+ Once: (...args: any[]) => any;
23
21
  postcssPlugin: string;
24
- Once(root: any, { result }: {
25
- result: any;
26
- }): void | Promise<void>;
27
22
  };
28
23
  babelOptionsFor: (id: any, { ssr }?: {
29
24
  ssr?: boolean | undefined;
@@ -43,5 +38,8 @@ export function createTransformPipeline({ layout, jsxPlugins, cssPlugins }: {
43
38
  };
44
39
  /** Whether a stylesheet whose last CSS run failed declared `file`, so an edit to it may fix the stylesheet. */
45
40
  failedStylesheetDependsOn: (file: any) => boolean;
41
+ close: () => any;
42
+ /** @param {string} file @param {() => void} reload */
43
+ handleHotUpdate: (file: string, reload: () => void) => any;
46
44
  setCommand: (value: any) => void;
47
45
  };
@@ -69,7 +69,10 @@ export type JsxTransformPlugin = TransformPluginBase & {
69
69
  * that needs nothing loaded stays synchronous for PostCSS.
70
70
  */
71
71
  export type CssTransformPlugin = TransformPluginBase & {
72
- prepare?: (root: import("postcss").Root, context: CssTransformContext) => Promise<void> | void;
72
+ prepare?: (root: import("postcss").Root, context: CssTransformContext) => Promise<void | {
73
+ fetched: number;
74
+ failed: number;
75
+ }> | void;
73
76
  validate?: (root: import("postcss").Root, context: CssTransformContext) => void;
74
77
  transform: (root: import("postcss").Root, context: CssTransformContext) => void;
75
78
  };
@@ -80,5 +83,5 @@ export type CssTransformPlugin = TransformPluginBase & {
80
83
  */
81
84
  export type CssTransformContext = TransformContext & {
82
85
  addDependency?: (file: string) => void;
83
- warn?: (node: import("postcss").Node, text: string) => void;
86
+ warn?: (node: import("postcss").Node, text: string, key?: "malformed_custom_enum" | "unmappable_preset" | "svg_not_loaded") => void;
84
87
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/origami-transforms",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Babel and PostCSS checks and transforms for Wix origami page files.",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -12,16 +12,18 @@
12
12
  "lint": "oxlint .",
13
13
  "test": "node --test test/*.spec.js",
14
14
  "test:browser": "node test/spx.browser.mjs && node test/semanticContainer.browser.mjs",
15
- "clean": "rm -rf dist tsconfig.tsbuildinfo"
15
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
16
+ "typecheck": "tsc --noEmit"
16
17
  },
17
18
  "dependencies": {
18
19
  "@babel/core": "^7.26.0",
19
20
  "@babel/parser": "^7.25.0",
20
21
  "@babel/types": "^7.25.2",
21
- "@wix/origami-fonts": "1.0.3",
22
+ "@wix/origami-fonts": "1.0.4",
22
23
  "@wix/origami-system-components": "1.10.6",
23
24
  "postcss": "^8.5.3",
24
- "postcss-custom-media": "^12.0.1"
25
+ "postcss-custom-media": "^12.0.1",
26
+ "@wix/origami-bi": "0.10.0"
25
27
  },
26
28
  "devDependencies": {
27
29
  "@types/node": "^22.0.0",
@@ -69,5 +71,5 @@
69
71
  },
70
72
  "type": "module",
71
73
  "types": "./dist/index.d.ts",
72
- "falconPackageHash": "9227a9507341d80c3e8c3d3ec21398c04bb4a02e81c616a7cfb70ceb"
74
+ "falconPackageHash": "1fbeabb662063771eaf3dbbb2b84d3ea8ba71598556ee82a91d545fa"
73
75
  }
@@ -331,7 +331,8 @@ export const customEnumsPlugin = {
331
331
  `type, such as wixEditorElements.Button, in an element rule under the editor element's. That ` +
332
332
  `element's enums are left as authored.`
333
333
  : `custom-enums: @custom-enum ${block.params}${where(block)}: can't be read; it needs a --key, a ` +
334
- `default: that is one of its @option blocks, and an element rule around it. Its key is left as authored.`
334
+ `default: that is one of its @option blocks, and an element rule around it. Its key is left as authored.`,
335
+ 'malformed_custom_enum'
335
336
  )
336
337
  }
337
338
 
@@ -53,7 +53,8 @@ export const presetsPlugin = {
53
53
  declaration,
54
54
  `presets: ${context.relativePath ?? context.filePath}: ` +
55
55
  `${declaration.prop} on "${selectors.join(', ')}": set --preset on #id or .id alone, ` +
56
- `and an inner element's as --preset-{key} on the rule of the element it sits in. Left as authored.`
56
+ `and an inner element's as --preset-{key} on the rule of the element it sits in. Left as authored.`,
57
+ 'unmappable_preset'
57
58
  )
58
59
  continue
59
60
  }
@@ -125,7 +125,7 @@ export const shapeDividersPlugin = {
125
125
  const allVars = { ...baseVars.get(id), ...vars }
126
126
  const declarations = layerDeclarations(id, allVars)
127
127
  if (declarations === undefined) {
128
- if (allVars['--svgId']) context.warn?.(rule, `Couldn't draw the shape divider's SVG ${allVars['--svgId']}: it didn't load from Wix's CDN, or has no view box`)
128
+ if (allVars['--svgId']) context.warn?.(rule, `Couldn't draw the shape divider's SVG ${allVars['--svgId']}: it didn't load from Wix's CDN, or has no view box`, 'svg_not_loaded')
129
129
  continue
130
130
  }
131
131
  const layerRule = postcss.rule({ selector: `${rule.selector} ${LAYER}`, source: rule.source })
@@ -17,14 +17,15 @@ import { literalIdOf, parseStructure, structureFileOf, walk } from './structureF
17
17
  const CONTENT_WRAPPER = 'internal-container-content-wrapper'
18
18
  const BASE = ''
19
19
 
20
- // What a stack child may write, per direction. `0` is allowed wherever a length is.
20
+ // What a stack child may write, per direction. `0` is allowed wherever a length is. Exports write `width: max-content`
21
+ // on a stack child, and DM's import accepts it.
21
22
  const ALLOWED = {
22
- vertical: { width: ['spx', 'px', 'vw', 'vh', '%', 'calc'], margin: ['spx', 'px', 'vw', 'vh', '%'] },
23
- horizontal: { width: ['spx', 'px', 'vw', 'vh', 'calc'], margin: ['spx', 'px', 'vw', 'vh'] },
23
+ vertical: { width: ['spx', 'px', 'vw', 'vh', '%', 'calc', 'max-content'], margin: ['spx', 'px', 'vw', 'vh', '%'] },
24
+ horizontal: { width: ['spx', 'px', 'vw', 'vh', 'calc', 'max-content'], margin: ['spx', 'px', 'vw', 'vh'] },
24
25
  }
25
26
 
26
27
  const UNIT = /^-?(?:\d+\.?\d*|\.\d+)(px|vw|vh|%)$/i
27
- const LABELS = { spx: '--spx()', px: 'px', vw: 'vw', vh: 'vh', '%': '%', calc: 'calc()' }
28
+ const LABELS = { spx: '--spx()', px: 'px', vw: 'vw', vh: 'vh', '%': '%', calc: 'calc()', 'max-content': 'max-content' }
28
29
 
29
30
  /** `width` → 'width', `margin`/`margin-*` → 'margin', anything else → undefined. */
30
31
  const kindOf = prop =>
@@ -47,9 +48,10 @@ const tokens = value => {
47
48
  return result
48
49
  }
49
50
 
50
- /** The kind of length a token is: 'spx', 'px', 'vw', 'vh', '%', 'calc', 'zero', or undefined. */
51
+ /** The kind of length a token is: 'spx', 'px', 'vw', 'vh', '%', 'calc', 'max-content', 'zero', or undefined. */
51
52
  const lengthOf = token => {
52
53
  if (/^--spx\(.*\)$/.test(token)) return 'spx'
54
+ if (/^max-content$/i.test(token)) return 'max-content'
53
55
  if (/^calc\(.*\)$/i.test(token)) return 'calc'
54
56
  if (/^-?0*\.?0+$/.test(token)) return 'zero'
55
57
  return UNIT.exec(token)?.[1].toLowerCase()
@@ -16,13 +16,16 @@ const loading = new Map()
16
16
  const svgFileName = id => (id.endsWith('.svg') ? id : `${id}.svg`)
17
17
 
18
18
  const load = async id => {
19
+ let ok = false
19
20
  try {
20
21
  const response = await fetch(SHAPES_URL + svgFileName(id), { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) })
21
22
  const svg = response.ok ? await response.text() : ''
22
- loaded.set(id, SVG_TAG.test(svg) ? svg : null)
23
+ ok = SVG_TAG.test(svg)
24
+ loaded.set(id, ok ? svg : null)
23
25
  } catch {
24
26
  // Not loaded: the plugin warns and keeps the value as written.
25
27
  }
28
+ return ok
26
29
  }
27
30
 
28
31
  /** Loads the SVGs of `ids` that aren't loaded yet. */
@@ -33,7 +36,7 @@ export const loadSvgs = ids => {
33
36
  if (!loading.has(id)) loading.set(id, load(id).finally(() => loading.delete(id)))
34
37
  return loading.get(id)
35
38
  })
36
- return pending.length ? Promise.all(pending).then(() => undefined) : undefined
39
+ return pending.length ? Promise.all(pending).then(results => ({ fetched: results.length, failed: results.filter(ok => !ok).length })) : undefined
37
40
  }
38
41
 
39
42
  /** The SVG's markup, once `loadSvgs` loaded it. */
@@ -29,7 +29,7 @@ export const wixVectorUrlsPlugin = {
29
29
  declaration.value = declaration.value.replace(WIX_VECTOR_URL, (url, _quote, id) => {
30
30
  const svg = loadedSvg(id)
31
31
  if (svg === undefined) {
32
- context.warn?.(declaration, `Couldn't load the SVG ${id} from Wix's CDN, so ${url} stays as written`)
32
+ context.warn?.(declaration, `Couldn't load the SVG ${id} from Wix's CDN, so ${url} stays as written`, 'svg_not_loaded')
33
33
  return url
34
34
  }
35
35
  return svgDataUrl(scaleSvg(svg, svgInfo(svg), 'none'))
@@ -1,3 +1,4 @@
1
+ import { warn } from '../../transform/bi.js'
1
2
  // Fails the build on an Image whose `uri` is not a Wix media ID, such as a web URL or a local file.
2
3
  // The Wix import accepts such a `uri` but the image is broken on the site; `wix origami upload-media`
3
4
  // uploads it and points the Image at the uploaded file. `yarn dev` only warns, so the page still
@@ -41,7 +42,10 @@ export const mediaValidationPlugin = {
41
42
  })
42
43
  if (violations.length === 0) return
43
44
  if (context.command === 'serve') {
44
- for (const violation of violations) console.warn(`[origami] ${violation}`)
45
+ for (const violation of violations) {
46
+ warn('media_not_wix')
47
+ console.warn(`[origami] ${violation}`)
48
+ }
45
49
  return
46
50
  }
47
51
  throw new LayoutError(violations, first)
@@ -0,0 +1,34 @@
1
+ import { currentRow } from '@wix/origami-bi'
2
+ const summaries = new WeakMap()
3
+
4
+ /** Aggregate completed dev invocations; paths are only in this local set, never in rows. */
5
+ export function countPlugin(context, duration) {
6
+ const parent = currentRow()
7
+ if (!parent || context.command !== 'serve') return
8
+ const { run } = parent
9
+ let summary = summaries.get(run)
10
+ if (!summary) {
11
+ summary = { files: new Set(), plugins: 0, duration_ms: 0 }
12
+ summaries.set(run, summary)
13
+ const timer = setInterval(() => {
14
+ if (!run.commandRow.isOpen) { clearInterval(timer); summaries.delete(run); return }
15
+ if (!summary.plugins) return
16
+ run.commandRow.runInContext(() => run.openRow('step', 'transform_summary', {
17
+ step_details: { files: summary.files.size, plugins: summary.plugins, duration_ms: Math.round(summary.duration_ms) },
18
+ }).end())
19
+ summary.files.clear(); summary.plugins = 0; summary.duration_ms = 0
20
+ }, 60_000)
21
+ timer.unref()
22
+ }
23
+ summary.files.add(context.filePath)
24
+ summary.plugins++
25
+ summary.duration_ms += duration
26
+ }
27
+
28
+ /** @param {'malformed_custom_enum' | 'unmappable_preset' | 'svg_not_loaded' | 'media_not_wix'} key */
29
+ export function warn(key) {
30
+ const current = currentRow()
31
+ // A passing dev plugin row is discarded; warnings must retain a live parent.
32
+ const parent = current?.step_type === 'build_plugin' ? current.parent : current
33
+ parent?.runInContext(() => parent.run.openRow('warning', key, { error_name: key }).end())
34
+ }
@@ -1,5 +1,44 @@
1
+ import { currentRow } from '@wix/origami-bi'
2
+ import { existsSync, readFileSync, statSync } from 'node:fs'
3
+ import { dirname, join, relative } from 'node:path'
4
+ import { parse } from '@babel/parser'
1
5
  import { relativeToSite } from './relativeToSite.js'
2
6
 
7
+ const identities = new Map()
8
+ export function pageIdFor(context) {
9
+ if (!currentRow()?.run.enabled || !context.layout?.root) return undefined
10
+ const { root } = context.layout
11
+ const file = context.filePath
12
+ let folder = dirname(file)
13
+ const pages = join(root, 'src/pages')
14
+ while (relative(pages, folder) && !relative(pages, folder).startsWith('..')) {
15
+ const structure = ['structure.jsx', 'structure.tsx'].map(name => join(folder, name)).find(existsSync)
16
+ if (structure) {
17
+ try {
18
+ const modified = statSync(structure).mtimeMs
19
+ if (identities.get(structure)?.modified === modified) return identities.get(structure).id
20
+ const ast = parse(readFileSync(structure, 'utf8'), { sourceType: 'module', plugins: ['jsx', 'typescript'] })
21
+ let id
22
+ const visit = node => {
23
+ if (!node || typeof node !== 'object') return
24
+ if (node.type === 'JSXOpeningElement' && node.name?.name === 'Page') {
25
+ const attribute = node.attributes.find(value => value.type === 'JSXAttribute' && value.name.name === 'id')
26
+ if (attribute?.value?.type === 'StringLiteral') id ??= attribute.value.value
27
+ }
28
+ for (const value of Object.values(node)) {
29
+ if (Array.isArray(value)) value.forEach(visit)
30
+ else if (value && typeof value === 'object') visit(value)
31
+ }
32
+ }
33
+ visit(ast)
34
+ identities.set(structure, { modified, id })
35
+ return id
36
+ } catch { return undefined }
37
+ }
38
+ folder = dirname(folder)
39
+ }
40
+ }
41
+
3
42
  export const createContextFactory =
4
43
  ({ layout, command }) =>
5
44
  (id, { ssr = false } = {}) =>
@@ -8,6 +8,8 @@
8
8
  // stylesheet the plugins see is the whole page: a rule declared in one carrier and a rule that
9
9
  // depends on it in another arrive in the same document.
10
10
 
11
+ import { currentRow } from '@wix/origami-bi'
12
+ import { warn as biWarn } from '../bi.js'
11
13
  import { removeCustomEnums } from '../../cssPlugins/customEnums.js'
12
14
  import { pluginError, runCssPlugins } from './runPlugins.js'
13
15
 
@@ -54,7 +56,10 @@ export const createCssPostcssPlugin = ({ selectPlugins, contextFor, failures })
54
56
  }
55
57
  // A plugin reports a problem that need not stop the build as a PostCSS warning, which Vite prints with the
56
58
  // node's code frame.
57
- const warn = (node, text) => node.warn(result, text)
59
+ const warn = (node, text, key) => {
60
+ if (key) biWarn(key)
61
+ return node.warn(result, text)
62
+ }
58
63
  const context = { ...fileContext, addDependency, warn }
59
64
  const fail = error => {
60
65
  failures?.set(from, declared)
@@ -70,10 +75,20 @@ export const createCssPostcssPlugin = ({ selectPlugins, contextFor, failures })
70
75
  }
71
76
  // PostCSS awaits the hook only when it returns a promise; a stylesheet nothing loads for stays synchronous.
72
77
  const loading = plugins.flatMap(plugin => {
78
+ if (!plugin.prepare) return []
79
+ const row = currentRow()?.run.openRow('op', 'css_prepare', { step_details: { plugin: plugin.name, fetched: 0, failed: 0 } })
73
80
  try {
74
- const prepared = plugin.prepare?.(root, context)
75
- return prepared ? [prepared.catch(cause => fail(pluginError(plugin, context, cause)))] : []
81
+ const prepared = row ? row.runInContext(() => plugin.prepare(root, context)) : plugin.prepare(root, context)
82
+ if (!prepared) { row?.end(); return [] }
83
+ return [prepared.then(counts => {
84
+ if (counts) row?.update({ step_details: counts })
85
+ row?.end()
86
+ }, cause => {
87
+ row?.end(false, { error_name: 'plugin_error' })
88
+ return fail(pluginError(plugin, context, cause))
89
+ })]
76
90
  } catch (cause) {
91
+ row?.end(false, { error_name: 'plugin_error' })
77
92
  return fail(pluginError(plugin, context, cause))
78
93
  }
79
94
  })
@@ -5,6 +5,9 @@
5
5
  // A LayoutError comes from a plugin that only checks, so the Root is as it was and the later plugins still run:
6
6
  // the author gets every plugin's violations in one error, at the first one's position, not one plugin's per build.
7
7
 
8
+ import { currentRow, namedError } from '@wix/origami-bi'
9
+ import { countPlugin } from '../bi.js'
10
+ import { pageIdFor } from '../context.js'
8
11
  import { LayoutError } from '../layoutError.js'
9
12
 
10
13
  const reason = cause => (cause instanceof Error ? cause.message : String(cause))
@@ -13,7 +16,7 @@ const reason = cause => (cause instanceof Error ? cause.message : String(cause))
13
16
  export const pluginError = (plugin, context, cause) =>
14
17
  // Tooling such as Vite's error overlay prints only the message, so the reason goes in it as well. A
15
18
  // plugin may throw any value, so it is read without assuming an Error.
16
- new Error(`CSS transform "${plugin.name}" failed for ${context.relativePath ?? context.filePath}: ${reason(cause)}`, {
19
+ namedError('plugin_error', `CSS transform "${plugin.name}" failed for ${context.relativePath ?? context.filePath}: ${reason(cause)}`, {
17
20
  cause,
18
21
  })
19
22
 
@@ -22,17 +25,36 @@ export const runCssPlugins = ({ plugins, root, context }) => {
22
25
  const layoutErrors = []
23
26
  for (const phase of ['validate', 'transform']) {
24
27
  for (const plugin of plugins) {
28
+ if (!plugin[phase]) continue
29
+ const pageId = pageIdFor(context)
30
+ const row = currentRow()?.run.openRow('build_plugin', plugin.name, {
31
+ ...(pageId ? { item_type: 'page', item_id: pageId } : {}),
32
+ step_details: { phase, outcome: 'ok', violations: 0 },
33
+ })
34
+ const started = performance.now()
25
35
  try {
26
- plugin[phase]?.(root, context)
36
+ if (row) row.runInContext(() => plugin[phase](root, context))
37
+ else plugin[phase](root, context)
38
+ if (context.command === 'serve') row?.discard()
39
+ else row?.end()
27
40
  } catch (cause) {
41
+ const layout = cause instanceof LayoutError
42
+ row?.end(false, { error_name: layout ? 'layout_error' : 'plugin_error' }, {
43
+ outcome: layout ? 'layout_error' : 'plugin_error', violations: layout ? cause.violations.length : 0,
44
+ })
28
45
  if (cause instanceof LayoutError) {
29
46
  layoutErrors.push(cause)
30
47
  continue
31
48
  }
32
49
  throw pluginError(plugin, context, cause)
50
+ } finally {
51
+ countPlugin(context, performance.now() - started)
33
52
  }
34
53
  }
35
54
  }
55
+ if (layoutErrors.length) currentRow()?.run.openRow('op', 'css_layout_errors', {
56
+ step_details: { violations: layoutErrors.reduce((count, error) => count + error.violations.length, 0) },
57
+ }).end()
36
58
  if (layoutErrors.length === 1) throw layoutErrors[0]
37
59
  if (layoutErrors.length > 1) {
38
60
  throw new LayoutError(
@@ -2,22 +2,40 @@
2
2
  // to Babel, which drives this stage — the only thing added here is attributing a failure to the
3
3
  // plugin and the file that caused it. A LayoutError is the author's, so it passes through.
4
4
 
5
+ import { currentRow, namedError } from '@wix/origami-bi'
6
+ import { countPlugin } from '../bi.js'
7
+ import { pageIdFor } from '../context.js'
5
8
  import { LayoutError } from '../layoutError.js'
6
9
 
7
10
  const reason = cause => (cause instanceof Error ? cause.message : String(cause))
8
11
 
9
12
  export const runJsxPlugins = ({ plugins, program, context }) => {
10
13
  for (const plugin of plugins) {
14
+ const pageId = pageIdFor(context)
15
+ const row = currentRow()?.run.openRow('build_plugin', plugin.name, {
16
+ ...(pageId ? { item_type: 'page', item_id: pageId } : {}),
17
+ step_details: { outcome: 'ok', violations: 0 },
18
+ })
19
+ const started = performance.now()
11
20
  try {
12
- plugin.transform(program, context)
21
+ if (row) row.runInContext(() => plugin.transform(program, context))
22
+ else plugin.transform(program, context)
23
+ if (context.command === 'serve') row?.discard()
24
+ else row?.end()
13
25
  } catch (cause) {
26
+ const layout = cause instanceof LayoutError
27
+ row?.end(false, { error_name: layout ? 'layout_error' : 'plugin_error' }, {
28
+ outcome: layout ? 'layout_error' : 'plugin_error', violations: layout ? cause.violations.length : 0,
29
+ })
14
30
  if (cause instanceof LayoutError) throw cause
15
31
  // Tooling such as Vite's error overlay prints only the message, so the reason goes in it as well. A
16
32
  // plugin may throw any value, so it is read without assuming an Error.
17
- throw new Error(
33
+ throw namedError('plugin_error',
18
34
  `JSX transform "${plugin.name}" failed for ${context.relativePath ?? context.filePath}: ${reason(cause)}`,
19
35
  { cause }
20
36
  )
37
+ } finally {
38
+ countPlugin(context, performance.now() - started)
21
39
  }
22
40
  }
23
41
  }
@@ -11,6 +11,7 @@ export class LayoutError extends Error {
11
11
  constructor(violations, location = {}) {
12
12
  super(violations.length === 1 ? violations[0] : `\n ${violations.join('\n ')}`)
13
13
  this.name = 'LayoutError'
14
+ this.error_name = 'layout_error'
14
15
  this.stack = ''
15
16
  this.violations = violations
16
17
  this.location = location
@@ -1,4 +1,6 @@
1
1
  // Select and cache transform plugins by stage, file, and SSR context.
2
+ import { AsyncLocalStorage } from 'node:async_hooks'
3
+ import { currentRow, trackStepSync } from '@wix/origami-bi'
2
4
  import { transformAsync } from '@babel/core'
3
5
  import { cssPlugins as defaultCssPlugins } from '../cssPlugins/index.js'
4
6
  import { jsxPlugins as defaultJsxPlugins } from '../jsxPlugins/index.js'
@@ -33,6 +35,10 @@ const createState = ({ layout, registries, command }) => {
33
35
  * }} options
34
36
  */
35
37
  export const createTransformPipeline = ({ layout, jsxPlugins = defaultJsxPlugins, cssPlugins = defaultCssPlugins }) => {
38
+ // Vite/Rolldown can dispatch hooks outside creation's async context.
39
+ const snapshot = AsyncLocalStorage.snapshot()
40
+ let devStep
41
+ const inContext = fn => snapshot(() => devStep ? devStep.runInContext(fn) : fn())
36
42
  const registries = {
37
43
  jsx: createTransformRegistry(jsxPlugins),
38
44
  css: createTransformRegistry(cssPlugins),
@@ -47,6 +53,7 @@ export const createTransformPipeline = ({ layout, jsxPlugins = defaultJsxPlugins
47
53
  const cssFailures = new Map()
48
54
 
49
55
  const babelOptionsFor = createBabelOptionsSelector({ contextFor, selectPlugins: selectFor('jsx') })
56
+ const postcssPlugin = createCssPostcssPlugin({ contextFor, selectPlugins: selectFor('css'), failures: cssFailures })
50
57
 
51
58
  return {
52
59
  /** Whether any JSX transform selects this module (structure.jsx/.tsx by default).
@@ -58,7 +65,7 @@ export const createTransformPipeline = ({ layout, jsxPlugins = defaultJsxPlugins
58
65
  * @param {string} code
59
66
  * @param {string} id
60
67
  * @param {{ ssr?: boolean | undefined }} [contextOptions] */
61
- transformJsx: async (code, id, { ssr = false } = {}) => {
68
+ transformJsx: async (code, id, { ssr = false } = {}) => inContext(async () => {
62
69
  const filename = id.split('?', 1)[0]
63
70
  const options = babelOptionsFor(id, { ssr })
64
71
  const result = await transformAsync(code, {
@@ -77,15 +84,28 @@ export const createTransformPipeline = ({ layout, jsxPlugins = defaultJsxPlugins
77
84
  },
78
85
  })
79
86
  return { code: result.code, map: result.map }
80
- },
87
+ }),
81
88
 
82
- postcssPlugin: createCssPostcssPlugin({ contextFor, selectPlugins: selectFor('css'), failures: cssFailures }),
89
+ postcssPlugin: { ...postcssPlugin, Once: (...args) => inContext(() => postcssPlugin.Once(...args)) },
83
90
  babelOptionsFor,
84
91
 
85
92
  /** Whether a stylesheet whose last CSS run failed declared `file`, so an edit to it may fix the stylesheet. */
86
93
  failedStylesheetDependsOn: file => [...cssFailures.values()].some(files => files.has(file)),
87
94
 
95
+ close: () => devStep?.end(),
96
+
97
+ /** @param {string} file @param {() => void} reload */
98
+ handleHotUpdate: (file, reload) => inContext(() => {
99
+ if (command === 'serve' && [...cssFailures.values()].some(files => files.has(file)))
100
+ trackStepSync('op', 'hmr_full_reload', {}, reload)
101
+ }),
102
+
88
103
  setCommand: value => {
104
+ if (value === 'serve' && !devStep) snapshot(() => {
105
+ const run = currentRow()?.run
106
+ devStep = run?.commandRow.runInContext(() => run.openRow('step', 'build_plugins', {}))
107
+ })
108
+ inContext(() => currentRow()?.run.openRow('op', 'pipeline_create', { step_details: { plugins: jsxPlugins.length + cssPlugins.length, command: value } }).end())
89
109
  command = value
90
110
  state = undefined
91
111
  },
@@ -36,7 +36,7 @@
36
36
  * and applied before any of them run.
37
37
  *
38
38
  * @typedef {TransformPluginBase & {
39
- * prepare?: (root: import('postcss').Root, context: CssTransformContext) => Promise<void> | void,
39
+ * prepare?: (root: import('postcss').Root, context: CssTransformContext) => Promise<void | { fetched: number, failed: number }> | void,
40
40
  * validate?: (root: import('postcss').Root, context: CssTransformContext) => void,
41
41
  * transform: (root: import('postcss').Root, context: CssTransformContext) => void
42
42
  * }} CssTransformPlugin
@@ -46,7 +46,7 @@
46
46
  *
47
47
  * @typedef {TransformContext & {
48
48
  * addDependency?: (file: string) => void,
49
- * warn?: (node: import('postcss').Node, text: string) => void
49
+ * warn?: (node: import('postcss').Node, text: string, key?: 'malformed_custom_enum' | 'unmappable_preset' | 'svg_not_loaded') => void
50
50
  * }} CssTransformContext
51
51
  * `addDependency` declares a file the plugin read besides the stylesheet, so the bundler re-runs the
52
52
  * stylesheet when it changes. `warn` reports a problem at `node` without failing the run, as a PostCSS