fontdue-js 3.0.0-alpha13 → 3.0.0-alpha15

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.d.ts +1 -1
  4. package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.js +9 -3
  5. package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.d.ts +1 -1
  6. package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.js +9 -3
  7. package/dist/__generated__/CartOrderUpdateMutation.graphql.d.ts +1 -1
  8. package/dist/__generated__/CartOrderUpdateMutation.graphql.js +9 -3
  9. package/dist/__generated__/CartQuery.graphql.d.ts +1 -1
  10. package/dist/__generated__/CartQuery.graphql.js +9 -3
  11. package/dist/__generated__/CartStateUpdateMutation.graphql.d.ts +1 -1
  12. package/dist/__generated__/CartStateUpdateMutation.graphql.js +9 -3
  13. package/dist/__generated__/CharacterViewerIDQuery.graphql.d.ts +1 -1
  14. package/dist/__generated__/CharacterViewerIDQuery.graphql.js +9 -3
  15. package/dist/__generated__/CharacterViewerSlugQuery.graphql.d.ts +1 -1
  16. package/dist/__generated__/CharacterViewerSlugQuery.graphql.js +9 -3
  17. package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.d.ts +1 -1
  18. package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.js +9 -3
  19. package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.d.ts +1 -1
  20. package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.js +9 -3
  21. package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.d.ts +1 -1
  22. package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.js +9 -3
  23. package/dist/__generated__/CollectionAa_Query.graphql.d.ts +1 -1
  24. package/dist/__generated__/CollectionAa_Query.graphql.js +9 -3
  25. package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +1 -1
  26. package/dist/__generated__/FontFamiliesQuery.graphql.js +9 -3
  27. package/dist/__generated__/PrecartAddToCartMutation.graphql.d.ts +1 -1
  28. package/dist/__generated__/PrecartAddToCartMutation.graphql.js +9 -3
  29. package/dist/__generated__/StoreModalCartQuery.graphql.d.ts +1 -1
  30. package/dist/__generated__/StoreModalCartQuery.graphql.js +9 -3
  31. package/dist/__generated__/StoreModalContainerQuery.graphql.d.ts +1 -1
  32. package/dist/__generated__/StoreModalContainerQuery.graphql.js +9 -3
  33. package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +1 -1
  34. package/dist/__generated__/StoreModalIndexQuery.graphql.js +9 -3
  35. package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +1 -1
  36. package/dist/__generated__/StoreModalProductQuery.graphql.js +9 -3
  37. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
  38. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +9 -3
  39. package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.d.ts +1 -1
  40. package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.js +9 -3
  41. package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.d.ts +1 -1
  42. package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.js +9 -3
  43. package/dist/__generated__/TypeTesterStandaloneQuery.graphql.d.ts +1 -1
  44. package/dist/__generated__/TypeTesterStandaloneQuery.graphql.js +9 -3
  45. package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.d.ts +1 -1
  46. package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.js +9 -3
  47. package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
  48. package/dist/__generated__/TypeTestersIDQuery.graphql.js +9 -3
  49. package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
  50. package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +9 -3
  51. package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
  52. package/dist/__generated__/TypeTestersSlugQuery.graphql.js +9 -3
  53. package/dist/__generated__/useFontStyle_fontStyle.graphql.d.ts +2 -1
  54. package/dist/__generated__/useFontStyle_fontStyle.graphql.js +8 -2
  55. package/dist/__tests__/metricFallback.test.js +74 -0
  56. package/dist/__tests__/nextAdapter.test.js +4 -98
  57. package/dist/components/CharacterViewer/index.js +17 -8
  58. package/dist/components/ConfigContext.d.ts +3 -0
  59. package/dist/components/ConfigContext.js +3 -1
  60. package/dist/components/useFontStyle.d.ts +1 -0
  61. package/dist/components/useFontStyle.js +12 -3
  62. package/dist/data/unicodeNamesUrl.d.ts +2 -0
  63. package/dist/data/unicodeNamesUrl.js +18 -0
  64. package/dist/data/unicodeNamesVersion.d.ts +1 -0
  65. package/dist/data/unicodeNamesVersion.js +4 -0
  66. package/dist/fallbackFontData.d.ts +2 -0
  67. package/dist/fallbackFontData.js +10 -0
  68. package/dist/fontdue.css +4 -4
  69. package/dist/metricFallback.d.ts +48 -0
  70. package/dist/metricFallback.js +98 -0
  71. package/dist/next/index.d.ts +1 -1
  72. package/dist/next/index.js +12 -14
  73. package/dist/next/registerSingleTenantResolver.js +2 -3
  74. package/dist/next/tenant.d.ts +0 -2
  75. package/dist/next/tenant.js +9 -51
  76. package/dist/relay/environment.js +1 -1
  77. package/dist/relay/serverConfig.d.ts +0 -7
  78. package/dist/relay/serverConfig.js +0 -12
  79. package/dist/scripts/publishUnicodeData.js +68 -0
  80. package/dist/scripts/updateUnicodeData.js +41 -6
  81. package/dist/server/index.js +4 -3
  82. package/package.json +2 -1
@@ -20,13 +20,14 @@
20
20
  //
21
21
  // The fontdue-js components embedded in pages fetch the same way: their
22
22
  // server-side preloads read the per-render config set by
23
- // __prepareFontdueRender (or configureFontduePreview in single-tenant apps),
24
- // and in the browser they fetch the relative /graphql on the page's own
25
- // origin so multi-tenant mode needs no NEXT_PUBLIC_FONTDUE_URL at all.
23
+ // __prepareFontdueRender (multi-tenant) or, in single-tenant apps, the ambient
24
+ // resolver that <FontdueProvider> registers no per-render setup call and in
25
+ // the browser they fetch the relative /graphql on the page's own origin, so
26
+ // multi-tenant mode needs no NEXT_PUBLIC_FONTDUE_URL at all.
26
27
 
27
28
  import { notFound, unstable_rethrow } from 'next/navigation';
28
29
  import { cookies, draftMode } from 'next/headers';
29
- import { setFontdueServerConfig, getFontdueSlotConfig } from '../relay/serverConfig.js';
30
+ import { setFontdueServerConfig } from '../relay/serverConfig.js';
30
31
  import { PREVIEW_TOKEN_COOKIE, previewAuthHeaders } from '../preview/index.js';
31
32
  export const isMultiTenant = process.env.FONTDUE_MULTI_TENANT === '1';
32
33
  export const singleTenantUrl = process.env.NEXT_PUBLIC_FONTDUE_URL;
@@ -90,8 +91,7 @@ export function fontdueServerConfig(domain) {
90
91
  return {
91
92
  url: origin,
92
93
  headers,
93
- cacheTags: [`graphql:${domain}`],
94
- domain
94
+ cacheTags: [`graphql:${domain}`]
95
95
  };
96
96
  }
97
97
 
@@ -134,32 +134,6 @@ async function applyRenderConfig(domain) {
134
134
  setFontdueServerConfig(await buildRenderConfig(domain));
135
135
  }
136
136
 
137
- // Single-tenant render setup — the one line at the top of every page, layout
138
- // and generateMetadata that fetches Fontdue data or renders Fontdue components:
139
- //
140
- // await configureFontduePreview();
141
- //
142
- // It points this render's server-side fetches — your own (createFontdueFetch)
143
- // AND the embedded components' preloads (type testers, store, character viewer)
144
- // — at your Fontdue site (NEXT_PUBLIC_FONTDUE_URL), and when a logged-in admin
145
- // has entered preview it forwards their token and serves a live (uncached)
146
- // render so hidden (unpublished) fonts show up. Public renders stay cached and
147
- // tagged for /api/revalidate.
148
- //
149
- // Call it per entry point, not just the layout: a soft navigation re-renders
150
- // only the page segment with a fresh render store. Reading draft mode is
151
- // static-safe — it only forces dynamic rendering when an admin is actually
152
- // previewing, so static generation keeps working otherwise.
153
- //
154
- // Returns the endpoint, handy for e.g. a metadataBase fallback. Route handlers
155
- // (robots/sitemap) aren't React renders, so call fontdueEndpoint()
156
- // there instead.
157
- export async function configureFontduePreview() {
158
- const domain = new URL(requireSingleTenantUrl()).host;
159
- await applyRenderConfig(domain);
160
- return endpointForDomain(domain);
161
- }
162
-
163
137
  // Internal multi-tenant API (double-underscored, not part of the public
164
138
  // foundry surface): the one line at the top of every page, layout and
165
139
  // generateMetadata body in the multi-tenant [domain] route tree (the
@@ -169,8 +143,9 @@ export async function configureFontduePreview() {
169
143
  //
170
144
  // Reads the request's site from the route param, 404s anything that isn't a
171
145
  // plain hostname, and configures the render (including preview) for that site.
172
- // Like configureFontduePreview, it runs per entry point. Route handlers are not
173
- // React renders — use the returned endpoint explicitly there.
146
+ // It runs per entry point: a soft navigation re-renders only the page segment
147
+ // with a fresh render store. Route handlers are not React renders — use the
148
+ // returned endpoint explicitly there.
174
149
  export async function __prepareFontdueRender(props) {
175
150
  const {
176
151
  domain
@@ -217,23 +192,6 @@ async function readPreviewHeaders() {
217
192
  }
218
193
  }
219
194
 
220
- // Endpoint for the app's own GraphQL fetches in this render pass: whatever
221
- // __prepareFontdueRender configured, or — in a single-tenant app without the
222
- // [domain] tree, where __prepareFontdueRender is never called — the
223
- // NEXT_PUBLIC_FONTDUE_URL site. Throwing rather than guessing in
224
- // multi-tenant mode turns a forgotten __prepareFontdueRender into an
225
- // unmissable error instead of a silent wrong-site fetch on soft
226
- // navigations.
227
- export function fontdueEndpoint() {
228
- var _getFontdueSlotConfig;
229
- const domain = (_getFontdueSlotConfig = getFontdueSlotConfig()) === null || _getFontdueSlotConfig === void 0 ? void 0 : _getFontdueSlotConfig.domain;
230
- if (domain) return endpointForDomain(domain);
231
- if (isMultiTenant) {
232
- throw new Error('fontdue-js/next: no render config set — call __prepareFontdueRender(props) at the top of every page, layout and generateMetadata that fetches.');
233
- }
234
- return endpointForDomain(new URL(requireSingleTenantUrl()).host);
235
- }
236
-
237
195
  // The single-tenant ambient resolver that lets a foundry skip per-render setup
238
196
  // is registered from ./registerSingleTenantResolver.ts (imported as a side
239
197
  // effect by the FontdueProvider react-server entrypoint), not here — so this
@@ -7,7 +7,7 @@ import { PREVIEW_HEADER, hasPreviewMarkerCookie } from '../preview/constants.js'
7
7
  // (defineVersionPlugin in .babelrc.cjs) with the literal package.json#version.
8
8
  // Exported so UI (the admin toolbar) can surface it without re-reading the
9
9
  // build-time global in a 'use client' module.
10
- export const version = "3.0.0-alpha13";
10
+ export const version = "3.0.0-alpha15";
11
11
  const IS_SERVER = typeof window === typeof undefined;
12
12
 
13
13
  // Read env from either process.env (Node/Next.js) or import.meta.env (Vite/Astro).
@@ -5,17 +5,10 @@ export interface FontdueServerConfig {
5
5
  headers?: Record<string, string>;
6
6
  /** Extra Next.js fetch cache tags applied to every server-side GraphQL fetch. */
7
7
  cacheTags?: string[];
8
- /**
9
- * The site domain this render is for, when known. Set by
10
- * fontdue-js/next's __prepareFontdueRender so render-scoped helpers
11
- * (fontdueEndpoint) can recover it.
12
- */
13
- domain?: string;
14
8
  }
15
9
  type MaybePromise<T> = T | Promise<T>;
16
10
  type AmbientConfigResolver = () => MaybePromise<FontdueServerConfig | undefined>;
17
11
  export declare function setFontdueServerConfig(config: FontdueServerConfig): void;
18
12
  export declare function registerAmbientConfigResolver(resolver: AmbientConfigResolver): void;
19
- export declare function getFontdueSlotConfig(): FontdueServerConfig | undefined;
20
13
  export declare function resolveFontdueServerConfig(): Promise<FontdueServerConfig | undefined>;
21
14
  export {};
@@ -99,18 +99,6 @@ function mergeServerConfig(ambient, slot) {
99
99
  };
100
100
  }
101
101
 
102
- // Slot-only read of the render-scoped config. It deliberately does NOT run the
103
- // ambient resolver: the single-tenant Next resolver is async (it awaits
104
- // draftMode()/cookies()), so a synchronous read could never reflect it without
105
- // silently dropping the preview token. Anything that needs the resolved
106
- // url/headers — every server-side fetch — must use resolveFontdueServerConfig()
107
- // instead. This exists only to recover the multi-tenant `domain` the slot
108
- // carries (set by __prepareFontdueRender), which fontdueEndpoint() reads from a
109
- // synchronous path.
110
- export function getFontdueSlotConfig() {
111
- return store().getSlot().current;
112
- }
113
-
114
102
  // Asynchronous read used by the server-side fetch paths (the Relay network
115
103
  // layer and createFontdueFetch). Awaits the ambient resolver so a resolver that
116
104
  // reads request-scoped async APIs (Next's draftMode()/cookies()) can feed
@@ -0,0 +1,68 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const DATA_DIR = path.join(__dirname, '../data');
7
+
8
+ // CDN buckets that serve the unicode-names asset. Both the npm package and the
9
+ // script-tag bundle default to cdn.fontdue.com (prod), so prod is the only
10
+ // strictly-required one — but the asset is reference data, not code, and we
11
+ // push to all three so dev/staging (and any `cdnUrl` override used for testing)
12
+ // resolve too. See docs/GCS.md.
13
+ const BUCKETS = ['fontdue-dev', 'fontdue-staging', 'fontdue-prod'];
14
+ const CACHE_CONTROL = 'public, max-age=31536000, immutable';
15
+
16
+ // Read the content hash written into unicodeNamesVersion.ts. It's the single
17
+ // source of truth for both the CDN filename and the runtime fetch URL, so the
18
+ // uploaded object always matches what CharacterViewer requests.
19
+ export function readUnicodeNamesVersion() {
20
+ const src = readFileSync(path.join(DATA_DIR, 'unicodeNamesVersion.ts'), 'utf-8');
21
+ const match = src.match(/UNICODE_NAMES_VERSION\s*=\s*['"]([^'"]+)['"]/);
22
+ if (!match) {
23
+ throw new Error('Could not read UNICODE_NAMES_VERSION from src/data/unicodeNamesVersion.ts');
24
+ }
25
+ return match[1];
26
+ }
27
+
28
+ // Upload data/unicodeNames.json to each CDN bucket at the immutable,
29
+ // content-hashed path the runtime fetches. `-n` (no-clobber) makes this
30
+ // idempotent: a re-run is a no-op unless the data — and therefore the hash —
31
+ // changed. Requires gsutil on PATH and credentials with write access to the
32
+ // buckets (`gcloud auth login`). Throws on failure so the caller can decide
33
+ // whether that's fatal.
34
+ export function publishUnicodeData() {
35
+ let {
36
+ buckets = BUCKETS
37
+ } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
38
+ const version = readUnicodeNamesVersion();
39
+ const source = path.join(DATA_DIR, 'unicodeNames.json');
40
+ const fileName = `unicode-names.${version}.json`;
41
+ for (const bucket of buckets) {
42
+ const dest = `gs://${bucket}/js/dist/${fileName}`;
43
+ console.log(`uploading ${fileName} → ${dest}`);
44
+ execFileSync('gsutil', ['-h', `Cache-Control:${CACHE_CONTROL}`, '-h', 'Content-Type:application/json', 'cp', '-n', '-a', 'public-read', source, dest], {
45
+ stdio: 'inherit'
46
+ });
47
+ }
48
+ console.log(`unicode names ${version} published to: ${buckets.join(', ')}`);
49
+ }
50
+
51
+ // A friendly hint for the two failure modes worth distinguishing: gsutil not
52
+ // installed vs. installed-but-can't-write (usually missing auth).
53
+ export function describePublishError(err) {
54
+ if (err && err.code === 'ENOENT') {
55
+ return 'gsutil not found on PATH. Install the Google Cloud SDK and authenticate:\n' + ' brew install --cask gcloud-cli # then restart your shell\n' + ' gcloud auth login\n' + 'See docs/GCS.md.';
56
+ }
57
+ return 'Failed to upload to the CDN. Make sure you are authenticated with write\n' + 'access to the buckets:\n' + ' gcloud auth login\n' + 'See docs/GCS.md.';
58
+ }
59
+
60
+ // CLI entry point: `npm run publish-unicode-data`
61
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
62
+ try {
63
+ publishUnicodeData();
64
+ } catch (err) {
65
+ console.error(describePublishError(err));
66
+ process.exit(1);
67
+ }
68
+ }
@@ -1,6 +1,10 @@
1
- const https = require('https');
2
- const fs = require('fs');
3
- const path = require('path');
1
+ import https from 'node:https';
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { publishUnicodeData, describePublishError } from './publishUnicodeData.js';
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
4
8
 
5
9
  // URL from which to fetch the Unicode data
6
10
  const UNICODE_DATA_URL = 'https://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt';
@@ -31,15 +35,46 @@ function parseAndConvertData(data) {
31
35
  return result;
32
36
  }
33
37
 
34
- // Main function to fetch, parse, and write the Unicode data to a JSON file
38
+ // Main function to fetch, parse, write, and publish the Unicode data.
39
+ //
40
+ // The map is NOT bundled into the package — it's ~1.2 MB of reference data used
41
+ // only to label glyphs in CharacterViewer. Instead it ships as a static JSON
42
+ // asset served from the Fontdue CDN, which CharacterViewer fetches at runtime.
43
+ // The asset is reference data on its own (roughly yearly) release schedule, so
44
+ // it isn't coupled to the npm publish or the app deploy: regenerating the data
45
+ // here also publishes it to the CDN, and any code that later references the new
46
+ // hash just works.
47
+ //
48
+ // We write two files:
49
+ // - data/unicodeNames.json the raw map (uploaded to the CDN; excluded
50
+ // from the npm tarball via .npmignore `src/*`)
51
+ // - data/unicodeNamesVersion.ts a content hash of that JSON
52
+ //
53
+ // The hash is the single source of truth for the immutable CDN filename AND the
54
+ // runtime fetch URL, so the two always agree. Refresh = re-run this script.
35
55
  async function main() {
36
56
  try {
37
57
  const data = await fetchUnicodeData();
38
58
  const jsonData = parseAndConvertData(data);
39
- fs.writeFileSync(path.join(__dirname, '../data/unicodeData.ts'), `const data: { [code: string]: string} = ${JSON.stringify(jsonData)}; export default data;`);
40
- console.log('unicode data has been written to unicodeData.ts');
59
+ const json = JSON.stringify(jsonData);
60
+ const hash = crypto.createHash('sha256').update(json).digest('hex').slice(0, 16);
61
+ const dataDir = path.join(__dirname, '../data');
62
+ fs.writeFileSync(path.join(dataDir, 'unicodeNames.json'), json);
63
+ fs.writeFileSync(path.join(dataDir, 'unicodeNamesVersion.ts'), `// Generated by \`npm run update-unicode-data\`. Content hash of unicodeNames.json.\n` + `// Both the CDN asset filename and the runtime fetch URL derive from this, so\n` + `// they always agree. Do not edit by hand.\n` + `export const UNICODE_NAMES_VERSION = ${JSON.stringify(hash)};\n`);
64
+ console.log(`unicode data written: ${Object.keys(jsonData).length} entries, hash ${hash}`);
65
+
66
+ // Publish the freshly-written map to the CDN. If this fails (no gsutil /
67
+ // credentials), don't fail the regen — the data is valid and the hash is
68
+ // committed; just point the way to publishing it before the change ships.
69
+ try {
70
+ publishUnicodeData();
71
+ } catch (err) {
72
+ console.warn('\n⚠ Data regenerated, but the CDN upload was skipped or failed. The new\n' + ' hash is in unicodeNamesVersion.ts, but the asset must be on the CDN\n' + ' before any code referencing it ships. Once you have gsutil + creds:\n' + ' npm run publish-unicode-data\n');
73
+ console.warn(describePublishError(err));
74
+ }
41
75
  } catch (error) {
42
76
  console.error('Error:', error);
77
+ process.exit(1);
43
78
  }
44
79
  }
45
80
 
@@ -15,9 +15,10 @@
15
15
  // 1. Ambient context (preferred): whatever set the per-render config.
16
16
  // - Astro / React Router etc.: runWithPreview (fontdue-js/preview/server)
17
17
  // rides the token through AsyncLocalStorage.
18
- // - Next: configureFontduePreview / __prepareFontdueRender
19
- // (fontdue-js/next) write the site URL, cache tags, and preview token
20
- // into the render-scoped slot.
18
+ // - Next: the single-tenant ambient resolver (registered by mounting
19
+ // <FontdueProvider>, no per-render call) or __prepareFontdueRender
20
+ // (fontdue-js/next, multi-tenant) supply the site URL, cache tags, and
21
+ // preview token for the render.
21
22
  // Either way:
22
23
  //
23
24
  // const fetchGraphql = createFontdueFetch(); // once, at module scope
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fontdue-js",
3
- "version": "3.0.0-alpha13",
3
+ "version": "3.0.0-alpha15",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "npm run relay && run-p build-js build-css build-ts",
@@ -9,6 +9,7 @@
9
9
  "build-css": "sass --load-path=node_modules --no-source-map src/index.scss dist/fontdue.css",
10
10
  "relay": "relay-compiler",
11
11
  "update-unicode-data": "node src/scripts/updateUnicodeData.js",
12
+ "publish-unicode-data": "node src/scripts/publishUnicodeData.js",
12
13
  "test": "vitest run",
13
14
  "test:watch": "vitest",
14
15
  "prepublishOnly": "npm run build"