@flashdsm/genome 0.2.0 → 0.3.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/README.md +23 -7
- package/dist/{chunk-PHEWKKKF.js → chunk-YA26VZWF.js} +1 -0
- package/dist/index.d.ts +17 -1
- package/dist/index.js +1020 -163
- package/dist/smoke.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
GENOME_CONFIG_FILENAME,
|
|
4
4
|
GenomeConfigSchema,
|
|
5
|
+
GenomeOutputsSchema,
|
|
5
6
|
LEGACY_CONFIG_FILENAME,
|
|
6
7
|
TokenCategorySchema,
|
|
7
8
|
TokenSchema,
|
|
@@ -13,7 +14,7 @@ import {
|
|
|
13
14
|
parseCategoryInput,
|
|
14
15
|
parseFlashAtomicDocument,
|
|
15
16
|
resolveTokenFilePath
|
|
16
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-YA26VZWF.js";
|
|
17
18
|
|
|
18
19
|
// src/index.ts
|
|
19
20
|
import { resolve as resolve13 } from "path";
|
|
@@ -24,7 +25,7 @@ import {
|
|
|
24
25
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
25
26
|
import { z as z3 } from "zod";
|
|
26
27
|
import { readFileSync, realpathSync } from "fs";
|
|
27
|
-
import { readFile as
|
|
28
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
28
29
|
|
|
29
30
|
// ../core/dist/change-plan.js
|
|
30
31
|
import { z } from "zod";
|
|
@@ -509,7 +510,11 @@ function classify(name, value) {
|
|
|
509
510
|
if (/^#[0-9a-fA-F]{3,8}$/.test(v))
|
|
510
511
|
return { category: "colors", type: "color" };
|
|
511
512
|
if (/^-?\d*\.?\d+(px|rem|em|%)$/.test(v)) {
|
|
512
|
-
|
|
513
|
+
if (/radius/i.test(name))
|
|
514
|
+
return { category: "radius", type: "radius" };
|
|
515
|
+
if (/(?:^|-)(?:text|fs)-(?:\d?x+s|sm|md|base|lg|\d?x+l|\d{1,3}|display|hero|title|caption)$/i.test(name))
|
|
516
|
+
return { category: "typography", type: "font" };
|
|
517
|
+
return { category: "spacing", type: "size" };
|
|
513
518
|
}
|
|
514
519
|
return null;
|
|
515
520
|
}
|
|
@@ -628,9 +633,29 @@ function projectNameFromPackageJson(text) {
|
|
|
628
633
|
return null;
|
|
629
634
|
}
|
|
630
635
|
}
|
|
636
|
+
async function pulledFiles(source) {
|
|
637
|
+
const clean2 = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
638
|
+
const byPath = new Map(source.paths.map((p) => [clean2(p), p]));
|
|
639
|
+
const configPath = byPath.get(GENOME_CONFIG_FILENAME);
|
|
640
|
+
if (!configPath)
|
|
641
|
+
return () => false;
|
|
642
|
+
try {
|
|
643
|
+
const config = JSON.parse(await source.read(configPath) ?? "");
|
|
644
|
+
const { genomeDir } = GenomeOutputsSchema.parse(config?.outputs ?? {});
|
|
645
|
+
const lockPath = byPath.get(`${clean2(genomeDir).replace(/\/?$/, "/")}genome.lock`);
|
|
646
|
+
if (!lockPath)
|
|
647
|
+
return () => false;
|
|
648
|
+
const lock = JSON.parse(await source.read(lockPath) ?? "");
|
|
649
|
+
const files = new Set((Array.isArray(lock?.files) ? lock.files : []).filter((f) => typeof f === "string").map(clean2));
|
|
650
|
+
return (posix2) => files.has(clean2(posix2));
|
|
651
|
+
} catch {
|
|
652
|
+
return () => false;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
631
655
|
async function buildWorkspaceScan(source) {
|
|
632
656
|
const root = source.workspaceRoot;
|
|
633
|
-
const
|
|
657
|
+
const pulled = await pulledFiles(source);
|
|
658
|
+
const paths = source.paths.filter((p) => !pulled(p));
|
|
634
659
|
const tokens = {};
|
|
635
660
|
const files = [];
|
|
636
661
|
const fileSources = [];
|
|
@@ -2680,7 +2705,7 @@ function tokenHashes(tokens) {
|
|
|
2680
2705
|
}
|
|
2681
2706
|
|
|
2682
2707
|
// ../codegen/dist/from-atomic-document.js
|
|
2683
|
-
function atomicTokenToDesign(t, value = t.value) {
|
|
2708
|
+
function atomicTokenToDesign(t, value = t.value, ref = t.referencedTokenId) {
|
|
2684
2709
|
return {
|
|
2685
2710
|
name: t.name,
|
|
2686
2711
|
value,
|
|
@@ -2688,7 +2713,7 @@ function atomicTokenToDesign(t, value = t.value) {
|
|
|
2688
2713
|
category: t.category,
|
|
2689
2714
|
...t.description ? { description: t.description } : {},
|
|
2690
2715
|
...t.layer ? { layer: t.layer } : {},
|
|
2691
|
-
...
|
|
2716
|
+
...ref ? { referencedTokenId: ref } : {},
|
|
2692
2717
|
...t.colorGroup ? { colorGroup: t.colorGroup } : {},
|
|
2693
2718
|
...t.fallbackValue ? { fallbackValue: t.fallbackValue } : {},
|
|
2694
2719
|
...t.metadata ? { metadata: t.metadata } : {}
|
|
@@ -2702,6 +2727,10 @@ function atomicThemeValues(t) {
|
|
|
2702
2727
|
return [t.value, t.value];
|
|
2703
2728
|
return [dark ? String(dark) : null, light ? String(light) : null];
|
|
2704
2729
|
}
|
|
2730
|
+
function atomicThemeRef(t, theme) {
|
|
2731
|
+
const ref = t.metadata?.[`${theme}Ref`];
|
|
2732
|
+
return typeof ref === "string" && ref ? ref : t.referencedTokenId;
|
|
2733
|
+
}
|
|
2705
2734
|
function fromAtomicDocument(doc) {
|
|
2706
2735
|
const f = doc.foundations;
|
|
2707
2736
|
const colors = [];
|
|
@@ -2713,9 +2742,9 @@ function fromAtomicDocument(doc) {
|
|
|
2713
2742
|
for (const t of palette) {
|
|
2714
2743
|
const [dark, light] = atomicThemeValues(t);
|
|
2715
2744
|
if (dark !== null)
|
|
2716
|
-
colors.push(atomicTokenToDesign(t, dark));
|
|
2745
|
+
colors.push(atomicTokenToDesign(t, dark, atomicThemeRef(t, "dark")));
|
|
2717
2746
|
if (light !== null)
|
|
2718
|
-
lightColors.push(atomicTokenToDesign(t, light));
|
|
2747
|
+
lightColors.push(atomicTokenToDesign(t, light, atomicThemeRef(t, "light")));
|
|
2719
2748
|
}
|
|
2720
2749
|
return {
|
|
2721
2750
|
colors,
|
|
@@ -2881,6 +2910,52 @@ ${FLASH_BLOCK_END}
|
|
|
2881
2910
|
`;
|
|
2882
2911
|
}
|
|
2883
2912
|
|
|
2913
|
+
// ../codegen/dist/shared/codeNames.js
|
|
2914
|
+
function keepInTheme(tokens, codeNames) {
|
|
2915
|
+
let out = tokens;
|
|
2916
|
+
for (const [code, role] of Object.entries(codeNames)) {
|
|
2917
|
+
if (!code || !role || code === role)
|
|
2918
|
+
continue;
|
|
2919
|
+
const target = out.find((t) => t.name === role);
|
|
2920
|
+
if (!target)
|
|
2921
|
+
continue;
|
|
2922
|
+
const held = out.find((t) => t.name === code);
|
|
2923
|
+
const kept = {
|
|
2924
|
+
...held ?? { type: target.type, category: target.category },
|
|
2925
|
+
name: code,
|
|
2926
|
+
value: "",
|
|
2927
|
+
layer: "alias",
|
|
2928
|
+
referencedTokenId: role,
|
|
2929
|
+
description: held?.description ?? `From your code: ${code}`
|
|
2930
|
+
};
|
|
2931
|
+
out = out.map((t) => {
|
|
2932
|
+
if (t === held)
|
|
2933
|
+
return kept;
|
|
2934
|
+
if (t.referencedTokenId !== code)
|
|
2935
|
+
return t;
|
|
2936
|
+
return held?.referencedTokenId ? { ...t, referencedTokenId: held.referencedTokenId } : {
|
|
2937
|
+
...t,
|
|
2938
|
+
layer: "primitive",
|
|
2939
|
+
referencedTokenId: void 0,
|
|
2940
|
+
value: held?.value ?? t.value
|
|
2941
|
+
};
|
|
2942
|
+
});
|
|
2943
|
+
if (!held)
|
|
2944
|
+
out = [...out, kept];
|
|
2945
|
+
}
|
|
2946
|
+
return out;
|
|
2947
|
+
}
|
|
2948
|
+
function keepCodeNames(tokens, codeNames, cssPrefix) {
|
|
2949
|
+
if (!cssPrefix || !codeNames || Object.keys(codeNames).length === 0) {
|
|
2950
|
+
return tokens;
|
|
2951
|
+
}
|
|
2952
|
+
return {
|
|
2953
|
+
...tokens,
|
|
2954
|
+
colors: keepInTheme(tokens.colors, codeNames),
|
|
2955
|
+
lightColors: keepInTheme(tokens.lightColors, codeNames)
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2884
2959
|
// ../codegen/dist/shared/cssPrefix.js
|
|
2885
2960
|
function normalizeCssPrefix(raw) {
|
|
2886
2961
|
const cleaned = (raw ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -3744,6 +3819,9 @@ function pruneUnusedMotionTokens(tokens, components = []) {
|
|
|
3744
3819
|
}
|
|
3745
3820
|
|
|
3746
3821
|
// ../codegen/dist/repo-token-diff.js
|
|
3822
|
+
function repoEditValue(edit) {
|
|
3823
|
+
return edit.reference ? `var(--${tokensToCssVarName(edit.reference)})` : edit.after;
|
|
3824
|
+
}
|
|
3747
3825
|
var GROUP_SECTIONS = {
|
|
3748
3826
|
spacing: "spacing",
|
|
3749
3827
|
typography: "typography",
|
|
@@ -3822,22 +3900,116 @@ function diffRepoTokens(doc, repoDtcg, responsive) {
|
|
|
3822
3900
|
});
|
|
3823
3901
|
return { edits, findings };
|
|
3824
3902
|
}
|
|
3825
|
-
const
|
|
3826
|
-
|
|
3903
|
+
const palette = singleThemePalette(tokens);
|
|
3904
|
+
const defaultTheme = palette === tokens.colors ? "dark" : "light";
|
|
3905
|
+
const names = {
|
|
3906
|
+
dark: new Map(tokens.colors.map((t) => [tokensToCssVarName(t.name), t])),
|
|
3907
|
+
light: new Map(tokens.lightColors.map((t) => [tokensToCssVarName(t.name), t]))
|
|
3908
|
+
};
|
|
3909
|
+
const hasTheme = {
|
|
3910
|
+
dark: tokens.colors.length > 0,
|
|
3911
|
+
light: tokens.lightColors.length > 0
|
|
3912
|
+
};
|
|
3913
|
+
const addColour = (path, name, theme, value) => {
|
|
3914
|
+
if (!hasTheme[theme]) {
|
|
3915
|
+
findings.push({
|
|
3916
|
+
path,
|
|
3917
|
+
code: "missing-theme",
|
|
3918
|
+
after: value,
|
|
3919
|
+
reason: `The file gives a ${theme} value, and this design system has no ${theme} theme yet. Add the theme in Flash, then push again.`
|
|
3920
|
+
});
|
|
3827
3921
|
return;
|
|
3828
|
-
|
|
3922
|
+
}
|
|
3923
|
+
push({
|
|
3924
|
+
path,
|
|
3925
|
+
section: BUNDLE_SECTION[theme === "dark" ? "colors" : "lightColors"],
|
|
3926
|
+
name,
|
|
3927
|
+
theme,
|
|
3928
|
+
kind: "add",
|
|
3929
|
+
before: "",
|
|
3930
|
+
after: value
|
|
3931
|
+
});
|
|
3932
|
+
};
|
|
3933
|
+
const add = (group, key, entry) => {
|
|
3934
|
+
if (!entry || typeof entry !== "object")
|
|
3935
|
+
return false;
|
|
3936
|
+
if (group === "color") {
|
|
3937
|
+
const modes = modesOf(entry);
|
|
3938
|
+
const given = ["dark", "light"].filter((theme) => typeof modes?.[theme] === "string");
|
|
3939
|
+
if (given.length) {
|
|
3940
|
+
for (const theme of given) {
|
|
3941
|
+
addColour(`color.${key}`, key, theme, String(modes[theme]));
|
|
3942
|
+
}
|
|
3943
|
+
return true;
|
|
3944
|
+
}
|
|
3945
|
+
if (typeof entry.$value !== "string")
|
|
3946
|
+
return false;
|
|
3947
|
+
const themes = ["dark", "light"].filter((t) => hasTheme[t]);
|
|
3948
|
+
for (const theme of themes.length ? themes : [defaultTheme]) {
|
|
3949
|
+
addColour(`color.${key}`, key, theme, entry.$value);
|
|
3950
|
+
}
|
|
3951
|
+
return true;
|
|
3952
|
+
}
|
|
3953
|
+
const field = GROUP_SECTIONS[group];
|
|
3954
|
+
if (!field || typeof entry.$value !== "string" || ladderOfEntry(entry)) {
|
|
3955
|
+
return false;
|
|
3956
|
+
}
|
|
3957
|
+
push({
|
|
3958
|
+
path: `${group}.${key}`,
|
|
3959
|
+
section: BUNDLE_SECTION[field],
|
|
3960
|
+
name: key,
|
|
3961
|
+
kind: "add",
|
|
3962
|
+
before: "",
|
|
3963
|
+
after: entry.$value
|
|
3964
|
+
});
|
|
3965
|
+
return true;
|
|
3966
|
+
};
|
|
3967
|
+
const referenced = (value) => {
|
|
3968
|
+
const inner = value.trim().slice(1, -1);
|
|
3969
|
+
const dot = inner.indexOf(".");
|
|
3970
|
+
if (dot < 0)
|
|
3971
|
+
return void 0;
|
|
3972
|
+
const group = inner.slice(0, dot);
|
|
3973
|
+
const key = inner.slice(dot + 1);
|
|
3974
|
+
if (group === "color") {
|
|
3975
|
+
const held = names.dark.get(key) ?? names.light.get(key);
|
|
3976
|
+
if (held)
|
|
3977
|
+
return held.name;
|
|
3978
|
+
} else if (GROUP_SECTIONS[group]) {
|
|
3979
|
+
const held = tokens[GROUP_SECTIONS[group]].find((t) => groupKey(group, t.name) === key);
|
|
3980
|
+
if (held)
|
|
3981
|
+
return held.name;
|
|
3982
|
+
} else {
|
|
3983
|
+
return void 0;
|
|
3984
|
+
}
|
|
3985
|
+
return repo[group]?.[key] ? key : void 0;
|
|
3986
|
+
};
|
|
3987
|
+
const push = (edit) => {
|
|
3988
|
+
if (!isReference(edit.after)) {
|
|
3989
|
+
edits.push(edit);
|
|
3829
3990
|
return;
|
|
3830
|
-
|
|
3991
|
+
}
|
|
3992
|
+
const target = referenced(edit.after);
|
|
3993
|
+
if (!target) {
|
|
3994
|
+
if (findings.some((f) => f.path === edit.path && f.after === edit.after))
|
|
3995
|
+
return;
|
|
3831
3996
|
findings.push({
|
|
3832
|
-
path,
|
|
3997
|
+
path: edit.path,
|
|
3833
3998
|
code: "alias",
|
|
3834
|
-
before,
|
|
3835
|
-
after,
|
|
3836
|
-
reason: "This token is
|
|
3999
|
+
...edit.before ? { before: edit.before } : {},
|
|
4000
|
+
after: edit.after,
|
|
4001
|
+
reason: "This points at a token that is neither in the design system nor in the file. Add that token, or point at one that exists."
|
|
3837
4002
|
});
|
|
3838
4003
|
return;
|
|
3839
4004
|
}
|
|
3840
|
-
edits.push({
|
|
4005
|
+
edits.push({ ...edit, reference: target });
|
|
4006
|
+
};
|
|
4007
|
+
const compare = (path, section, name, before, after, theme) => {
|
|
4008
|
+
if (before === void 0 || after === void 0)
|
|
4009
|
+
return;
|
|
4010
|
+
if (same(before, after))
|
|
4011
|
+
return;
|
|
4012
|
+
push({
|
|
3841
4013
|
path,
|
|
3842
4014
|
section,
|
|
3843
4015
|
name,
|
|
@@ -3879,7 +4051,7 @@ function diffRepoTokens(doc, repoDtcg, responsive) {
|
|
|
3879
4051
|
code: "alias",
|
|
3880
4052
|
before: narrowest.value,
|
|
3881
4053
|
after: repoNarrowest.value,
|
|
3882
|
-
reason: "This token
|
|
4054
|
+
reason: "This token changes with the viewport, and its narrowest size became an alias. Point it in Flash; a push carries one literal size here."
|
|
3883
4055
|
});
|
|
3884
4056
|
} else {
|
|
3885
4057
|
edits.push({
|
|
@@ -3914,11 +4086,13 @@ function diffRepoTokens(doc, repoDtcg, responsive) {
|
|
|
3914
4086
|
for (const key of Object.keys(repoGroup)) {
|
|
3915
4087
|
if (key.startsWith("$") || key in webGroup)
|
|
3916
4088
|
continue;
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
4089
|
+
if (!add(group, key, repoGroup[key])) {
|
|
4090
|
+
findings.push({
|
|
4091
|
+
path: `${group}.${key}`,
|
|
4092
|
+
code: "added-in-repo",
|
|
4093
|
+
reason: "Not in the design system, and not a single value a push can add. Add it in Flash, then pull."
|
|
4094
|
+
});
|
|
4095
|
+
}
|
|
3922
4096
|
}
|
|
3923
4097
|
for (const key of Object.keys(webGroup)) {
|
|
3924
4098
|
if (key in repoGroup)
|
|
@@ -3932,20 +4106,20 @@ function diffRepoTokens(doc, repoDtcg, responsive) {
|
|
|
3932
4106
|
};
|
|
3933
4107
|
const webColors = web.color ?? {};
|
|
3934
4108
|
presence("color", webColors);
|
|
3935
|
-
const palette = singleThemePalette(tokens);
|
|
3936
|
-
const defaultTheme = palette === tokens.colors ? "dark" : "light";
|
|
3937
|
-
const names = {
|
|
3938
|
-
dark: new Map(tokens.colors.map((t) => [tokensToCssVarName(t.name), t])),
|
|
3939
|
-
light: new Map(tokens.lightColors.map((t) => [tokensToCssVarName(t.name), t]))
|
|
3940
|
-
};
|
|
3941
4109
|
for (const [key, webEntry] of Object.entries(webColors)) {
|
|
3942
4110
|
const repoEntry = repo.color?.[key];
|
|
3943
4111
|
if (!repoEntry)
|
|
3944
4112
|
continue;
|
|
3945
4113
|
for (const theme of ["dark", "light"]) {
|
|
3946
4114
|
const token = names[theme].get(key);
|
|
3947
|
-
if (!token)
|
|
4115
|
+
if (!token) {
|
|
4116
|
+
const own = modesOf(repoEntry)?.[theme];
|
|
4117
|
+
if (typeof own === "string" && own.trim()) {
|
|
4118
|
+
const other = names[theme === "dark" ? "light" : "dark"].get(key);
|
|
4119
|
+
addColour(`color.${key}`, other.name, theme, own);
|
|
4120
|
+
}
|
|
3948
4121
|
continue;
|
|
4122
|
+
}
|
|
3949
4123
|
compare(`color.${key}`, BUNDLE_SECTION[theme === "dark" ? "colors" : "lightColors"], token.name, themeValue(webEntry, theme, defaultTheme), themeValue(repoEntry, theme, defaultTheme, webEntry), theme);
|
|
3950
4124
|
}
|
|
3951
4125
|
}
|
|
@@ -4004,7 +4178,7 @@ function documentToRepoBundle(doc, options = {}) {
|
|
|
4004
4178
|
...options.paths
|
|
4005
4179
|
};
|
|
4006
4180
|
const components = atomicComponentsToExportSpecs(doc);
|
|
4007
|
-
const tokens = pruneUnusedMotionTokens(withResponsiveLayer(fromAtomicDocument(doc), options.responsive), components);
|
|
4181
|
+
const tokens = pruneUnusedMotionTokens(keepCodeNames(withResponsiveLayer(fromAtomicDocument(doc), options.responsive), options.codeNames, options.cssPrefix), components);
|
|
4008
4182
|
const projectName = options.projectName ?? doc.name;
|
|
4009
4183
|
const files = [];
|
|
4010
4184
|
if (paths.tokensDtcg) {
|
|
@@ -4197,10 +4371,20 @@ function responsiveOf(envelope) {
|
|
|
4197
4371
|
function exportSettingsOf(envelope) {
|
|
4198
4372
|
const raw = envelope.export_settings;
|
|
4199
4373
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
4200
|
-
const {
|
|
4374
|
+
const {
|
|
4375
|
+
css_prefix: cssPrefix,
|
|
4376
|
+
css_selector: cssSelector,
|
|
4377
|
+
code_names: rawCodeNames
|
|
4378
|
+
} = raw;
|
|
4379
|
+
const codeNames = rawCodeNames && typeof rawCodeNames === "object" && !Array.isArray(rawCodeNames) ? Object.fromEntries(
|
|
4380
|
+
Object.entries(rawCodeNames).filter(
|
|
4381
|
+
(e) => typeof e[1] === "string"
|
|
4382
|
+
)
|
|
4383
|
+
) : {};
|
|
4201
4384
|
return {
|
|
4202
4385
|
...typeof cssPrefix === "string" ? { cssPrefix } : {},
|
|
4203
|
-
...typeof cssSelector === "string" ? { cssSelector } : {}
|
|
4386
|
+
...typeof cssSelector === "string" ? { cssSelector } : {},
|
|
4387
|
+
...Object.keys(codeNames).length ? { codeNames } : {}
|
|
4204
4388
|
};
|
|
4205
4389
|
}
|
|
4206
4390
|
function editPath(edit) {
|
|
@@ -4662,19 +4846,43 @@ async function detectDrift(opts) {
|
|
|
4662
4846
|
};
|
|
4663
4847
|
}
|
|
4664
4848
|
|
|
4665
|
-
// src/tools/
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
}
|
|
4849
|
+
// src/tools/scan-workspace.ts
|
|
4850
|
+
import { readFile as readFile6, readdir, stat } from "fs/promises";
|
|
4851
|
+
import { join as join5, resolve as resolve7 } from "path";
|
|
4852
|
+
async function walk(root, dir, out) {
|
|
4853
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
4854
|
+
for (const e of entries) {
|
|
4855
|
+
const full = join5(dir, e.name);
|
|
4856
|
+
if (e.isDirectory()) {
|
|
4857
|
+
if (!isSkippedScanDir(e.name)) await walk(root, full, out);
|
|
4858
|
+
} else if (e.isFile()) out.push(full.slice(root.length + 1));
|
|
4859
|
+
}
|
|
4676
4860
|
}
|
|
4677
|
-
async function
|
|
4861
|
+
async function readSmallFile(path) {
|
|
4862
|
+
const info = await stat(path).catch(() => null);
|
|
4863
|
+
if (!info || !info.isFile() || info.size > MAX_SCAN_READ_BYTES) return null;
|
|
4864
|
+
return readFile6(path, "utf8").catch(() => null);
|
|
4865
|
+
}
|
|
4866
|
+
async function scanWorkspace(workspaceRoot, opts = {}) {
|
|
4867
|
+
const root = resolve7(workspaceRoot);
|
|
4868
|
+
const paths = [];
|
|
4869
|
+
await walk(root, root, paths);
|
|
4870
|
+
return buildWorkspaceScan({
|
|
4871
|
+
workspaceRoot: root,
|
|
4872
|
+
paths: opts.skip ? paths.filter((p) => !opts.skip(p)) : paths,
|
|
4873
|
+
read: (rel) => readSmallFile(join5(root, rel))
|
|
4874
|
+
});
|
|
4875
|
+
}
|
|
4876
|
+
|
|
4877
|
+
// src/tools/code-changes.ts
|
|
4878
|
+
function scanIsEmpty(scan) {
|
|
4879
|
+
const tokens = Object.values(scan.tokens ?? {}).reduce(
|
|
4880
|
+
(n, list2) => n + list2.length,
|
|
4881
|
+
0
|
|
4882
|
+
);
|
|
4883
|
+
return tokens === 0 && (scan.files ?? []).length === 0;
|
|
4884
|
+
}
|
|
4885
|
+
async function collectCodeChanges(opts) {
|
|
4678
4886
|
const state = await readRepoState(opts);
|
|
4679
4887
|
const { envelope, lock } = state;
|
|
4680
4888
|
if (!lock) {
|
|
@@ -4689,75 +4897,145 @@ async function pushToWeb(opts) {
|
|
|
4689
4897
|
`Flash has published a new version since this repo pulled (published ${lock.publishedVersionId ?? "unknown"}, now ${envelope.published_version_id ?? "none"}). A push now could undo those changes. Keep a copy of your edits, pull, re-apply them, then push.`
|
|
4690
4898
|
);
|
|
4691
4899
|
}
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4900
|
+
const { edits, findings } = state.tokensText === null ? {
|
|
4901
|
+
edits: [],
|
|
4902
|
+
findings: [
|
|
4903
|
+
{
|
|
4904
|
+
path: state.tokensPath,
|
|
4905
|
+
code: "unreadable",
|
|
4906
|
+
reason: `${state.tokensPath} is missing, so only the stylesheets were read. Pull again to restore it.`
|
|
4907
|
+
}
|
|
4908
|
+
]
|
|
4909
|
+
} : diffRepoTokens(state.doc, state.tokensText, state.responsive);
|
|
4910
|
+
const pulled = new Set(
|
|
4911
|
+
lock.files.map((f) => f.replace(/\\/g, "/").replace(/^\.\//, ""))
|
|
4912
|
+
);
|
|
4913
|
+
const scan = await scanWorkspace(opts.workspaceRoot, {
|
|
4914
|
+
skip: (rel) => pulled.has(rel.replace(/\\/g, "/"))
|
|
4915
|
+
});
|
|
4916
|
+
return {
|
|
4917
|
+
state,
|
|
4918
|
+
scan,
|
|
4919
|
+
edits,
|
|
4920
|
+
findings,
|
|
4921
|
+
request: {
|
|
4922
|
+
scan,
|
|
4923
|
+
changes: edits.map((edit) => ({
|
|
4924
|
+
section: edit.section,
|
|
4925
|
+
name: edit.name,
|
|
4926
|
+
value: repoEditValue(edit)
|
|
4927
|
+
})),
|
|
4928
|
+
base_revision: envelope.revision
|
|
4929
|
+
}
|
|
4930
|
+
};
|
|
4931
|
+
}
|
|
4932
|
+
|
|
4933
|
+
// src/tools/push-to-web.ts
|
|
4934
|
+
var NOT_TAKEN = /* @__PURE__ */ new Set(["unresolved", "unusable"]);
|
|
4935
|
+
var THEME_OF = {
|
|
4936
|
+
"design-tokens-dark-colors": "dark",
|
|
4937
|
+
"design-tokens-light-colors": "light"
|
|
4938
|
+
};
|
|
4939
|
+
function rowPath(section, name) {
|
|
4940
|
+
const theme = section ? THEME_OF[section] : void 0;
|
|
4941
|
+
return theme ? `${name} (${theme})` : name;
|
|
4942
|
+
}
|
|
4943
|
+
function findingEntry(finding) {
|
|
4944
|
+
return {
|
|
4945
|
+
action: "flag",
|
|
4946
|
+
targetKind: "token",
|
|
4947
|
+
path: finding.path,
|
|
4948
|
+
before: finding.before ?? null,
|
|
4949
|
+
after: finding.after ?? null,
|
|
4950
|
+
code: finding.code,
|
|
4951
|
+
reason: finding.reason
|
|
4952
|
+
};
|
|
4953
|
+
}
|
|
4954
|
+
function planFrom(response, edits) {
|
|
4955
|
+
const notes = response.notes ?? [];
|
|
4956
|
+
const noteFor = (section, name) => notes.find(
|
|
4957
|
+
(n) => !NOT_TAKEN.has(n.kind) && n.name === name && n.section === section
|
|
4702
4958
|
);
|
|
4703
|
-
const
|
|
4959
|
+
const rows = (response.diff ?? []).map((row) => {
|
|
4960
|
+
const note = noteFor(row.section, row.name);
|
|
4961
|
+
const edit = edits.find(
|
|
4962
|
+
(e) => e.section === row.section && e.name === row.name
|
|
4963
|
+
);
|
|
4964
|
+
return {
|
|
4965
|
+
...tokenChangeToPlanEntry(row),
|
|
4966
|
+
path: edit ? editPath(edit) : rowPath(row.section, row.name),
|
|
4967
|
+
...edit && edit.kind !== "add" ? { before: edit.before } : {},
|
|
4968
|
+
...edit && !edit.reference ? { after: edit.after } : {},
|
|
4969
|
+
...note ? { code: note.kind, reason: note.reason } : {}
|
|
4970
|
+
};
|
|
4971
|
+
});
|
|
4972
|
+
const refused = notes.filter((n) => NOT_TAKEN.has(n.kind)).map((n) => ({
|
|
4973
|
+
action: "flag",
|
|
4974
|
+
targetKind: "token",
|
|
4975
|
+
path: rowPath(n.section, n.name),
|
|
4976
|
+
code: n.kind,
|
|
4977
|
+
reason: n.reason
|
|
4978
|
+
}));
|
|
4979
|
+
return [...rows, ...refused];
|
|
4980
|
+
}
|
|
4981
|
+
async function pushToWeb(opts) {
|
|
4982
|
+
const collected = await collectCodeChanges(opts);
|
|
4983
|
+
const { envelope } = collected.state;
|
|
4984
|
+
const flagged = collected.findings.map(findingEntry);
|
|
4704
4985
|
const base = {
|
|
4705
4986
|
genomeId: envelope.id,
|
|
4706
4987
|
revision: envelope.revision,
|
|
4707
|
-
findings
|
|
4988
|
+
findings: collected.findings
|
|
4708
4989
|
};
|
|
4709
|
-
if (!
|
|
4990
|
+
if (!collected.request.changes.length && scanIsEmpty(collected.scan)) {
|
|
4710
4991
|
return {
|
|
4711
4992
|
...makeChangePlan(flagged, true),
|
|
4712
4993
|
...base,
|
|
4713
4994
|
status: "no_changes",
|
|
4714
4995
|
changeSetId: null,
|
|
4715
|
-
message: findings.length ? "
|
|
4996
|
+
message: collected.findings.length ? "Nothing in the code to send. The flagged entries are things a push does not carry." : "Nothing in the code to send: no stylesheet defines a token, and the tokens file matches the last pull."
|
|
4716
4997
|
};
|
|
4717
4998
|
}
|
|
4718
|
-
const
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4999
|
+
const dryRun = opts.confirm !== true;
|
|
5000
|
+
const codebase = opts.config.codebase?.name ?? opts.config.name;
|
|
5001
|
+
const response = await opts.client.callTool(
|
|
5002
|
+
"propose_code_scan",
|
|
5003
|
+
{
|
|
5004
|
+
...collected.request,
|
|
5005
|
+
scan: {
|
|
5006
|
+
...collected.request.scan,
|
|
5007
|
+
// The review card says where the code came from.
|
|
5008
|
+
workspaceRoot: codebase
|
|
5009
|
+
},
|
|
5010
|
+
...dryRun ? { dry_run: true } : {}
|
|
5011
|
+
}
|
|
5012
|
+
);
|
|
5013
|
+
const entries = [...planFrom(response, collected.edits), ...flagged];
|
|
5014
|
+
const changed = (response.diff ?? []).length;
|
|
5015
|
+
if (response.no_changes) {
|
|
5016
|
+
return {
|
|
5017
|
+
...makeChangePlan(entries, dryRun),
|
|
5018
|
+
...base,
|
|
5019
|
+
status: "no_changes",
|
|
5020
|
+
changeSetId: null,
|
|
5021
|
+
message: "Flash already has what the code holds, or the same proposal is waiting for review."
|
|
5022
|
+
};
|
|
5023
|
+
}
|
|
5024
|
+
if (dryRun) {
|
|
4726
5025
|
return {
|
|
4727
|
-
...makeChangePlan(
|
|
5026
|
+
...makeChangePlan(entries, true),
|
|
4728
5027
|
...base,
|
|
4729
5028
|
status: "dry_run",
|
|
4730
5029
|
changeSetId: null,
|
|
4731
|
-
message: `${
|
|
5030
|
+
message: `${changed} change${changed === 1 ? "" : "s"} to propose for review. Run again with confirm to open the change set in Flash.`
|
|
4732
5031
|
};
|
|
4733
5032
|
}
|
|
4734
|
-
const codebase = opts.config.codebase?.name ?? opts.config.name;
|
|
4735
|
-
const response = await opts.client.callTool(
|
|
4736
|
-
"propose_change_set",
|
|
4737
|
-
{
|
|
4738
|
-
base_revision: envelope.revision,
|
|
4739
|
-
source_label: `Genome \xB7 ${codebase}`,
|
|
4740
|
-
changes: edits.map((edit) => ({
|
|
4741
|
-
section: edit.section,
|
|
4742
|
-
name: edit.name,
|
|
4743
|
-
value: edit.after
|
|
4744
|
-
}))
|
|
4745
|
-
}
|
|
4746
|
-
);
|
|
4747
|
-
const skipped = (response.skipped ?? []).map((s) => ({
|
|
4748
|
-
action: "flag",
|
|
4749
|
-
targetKind: "token",
|
|
4750
|
-
path: `${s.section}/${s.name}`,
|
|
4751
|
-
code: "skipped-by-server",
|
|
4752
|
-
reason: s.reason
|
|
4753
|
-
}));
|
|
4754
|
-
const proposed = (response.diff ?? []).map(tokenChangeToPlanEntry);
|
|
4755
5033
|
return {
|
|
4756
|
-
...makeChangePlan(
|
|
5034
|
+
...makeChangePlan(entries, false),
|
|
4757
5035
|
...base,
|
|
4758
|
-
status:
|
|
5036
|
+
status: "proposed",
|
|
4759
5037
|
changeSetId: response.change_set_id ?? null,
|
|
4760
|
-
message:
|
|
5038
|
+
message: "Proposed for review. Open Sync in Flash to apply or discard it."
|
|
4761
5039
|
};
|
|
4762
5040
|
}
|
|
4763
5041
|
|
|
@@ -4861,8 +5139,8 @@ function ensureTrailingNewline(text) {
|
|
|
4861
5139
|
}
|
|
4862
5140
|
|
|
4863
5141
|
// src/workspace.ts
|
|
4864
|
-
import { readFile as
|
|
4865
|
-
import { resolve as
|
|
5142
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
5143
|
+
import { resolve as resolve8 } from "path";
|
|
4866
5144
|
|
|
4867
5145
|
// src/rpc.ts
|
|
4868
5146
|
var TOOL_ERROR = -32e3;
|
|
@@ -4967,7 +5245,7 @@ function genomeToken(explicit) {
|
|
|
4967
5245
|
async function readGenomeConfig(workspaceRoot, configPath) {
|
|
4968
5246
|
let legacy = false;
|
|
4969
5247
|
for (const path of configCandidates(configPath, workspaceRoot)) {
|
|
4970
|
-
const raw = await
|
|
5248
|
+
const raw = await readFile7(path, "utf8").catch(() => null);
|
|
4971
5249
|
if (!raw) continue;
|
|
4972
5250
|
if (path.endsWith(LEGACY_CONFIG_FILENAME)) legacy = true;
|
|
4973
5251
|
let parsed;
|
|
@@ -4990,7 +5268,7 @@ async function readGenomeConfig(workspaceRoot, configPath) {
|
|
|
4990
5268
|
);
|
|
4991
5269
|
}
|
|
4992
5270
|
async function workspaceContext(opts) {
|
|
4993
|
-
const workspaceRoot =
|
|
5271
|
+
const workspaceRoot = resolve8(process.cwd(), opts.workspace ?? ".");
|
|
4994
5272
|
const config = await readGenomeConfig(workspaceRoot, opts.configPath);
|
|
4995
5273
|
const client = createRpcClient({
|
|
4996
5274
|
// Already resolved by the caller in the documented order: the flag, then
|
|
@@ -5005,8 +5283,8 @@ async function workspaceContext(opts) {
|
|
|
5005
5283
|
|
|
5006
5284
|
// src/tools/link.ts
|
|
5007
5285
|
import { randomBytes } from "crypto";
|
|
5008
|
-
import { readFile as
|
|
5009
|
-
import { basename, join as
|
|
5286
|
+
import { readFile as readFile8, writeFile as writeFile2 } from "fs/promises";
|
|
5287
|
+
import { basename, join as join6, resolve as resolve9 } from "path";
|
|
5010
5288
|
var DEFAULT_INPUTS = {
|
|
5011
5289
|
tokens: ["design-tokens.json", "tokens.css"],
|
|
5012
5290
|
components: []
|
|
@@ -5025,9 +5303,9 @@ async function linkGenome(opts) {
|
|
|
5025
5303
|
"The server did not say which design system this token belongs to."
|
|
5026
5304
|
);
|
|
5027
5305
|
}
|
|
5028
|
-
const root =
|
|
5029
|
-
const path =
|
|
5030
|
-
const before = await
|
|
5306
|
+
const root = resolve9(opts.workspaceRoot);
|
|
5307
|
+
const path = join6(root, GENOME_CONFIG_FILENAME);
|
|
5308
|
+
const before = await readFile8(path, "utf8").catch(() => null);
|
|
5031
5309
|
let previous = null;
|
|
5032
5310
|
let unreadable = false;
|
|
5033
5311
|
if (before) {
|
|
@@ -5093,13 +5371,13 @@ function describeLink(result) {
|
|
|
5093
5371
|
}
|
|
5094
5372
|
|
|
5095
5373
|
// src/tools/apply-token-patch.ts
|
|
5096
|
-
import { readFile as
|
|
5374
|
+
import { readFile as readFile9, writeFile as writeFile3 } from "fs/promises";
|
|
5097
5375
|
function isRecord2(value) {
|
|
5098
5376
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5099
5377
|
}
|
|
5100
5378
|
async function applyTokenPatch(tokenFile, patch, options = {}) {
|
|
5101
5379
|
const confirm = options.confirm === true;
|
|
5102
|
-
const text = await
|
|
5380
|
+
const text = await readFile9(tokenFile, "utf8");
|
|
5103
5381
|
const raw = JSON.parse(text);
|
|
5104
5382
|
const { tokens, name } = normalizeTokenDocument(raw);
|
|
5105
5383
|
const beforeCount = tokens.length;
|
|
@@ -5161,30 +5439,30 @@ async function applyTokenPatch(tokenFile, patch, options = {}) {
|
|
|
5161
5439
|
}
|
|
5162
5440
|
|
|
5163
5441
|
// src/tools/brand-dna.ts
|
|
5164
|
-
import { readFile as
|
|
5165
|
-
import { dirname as dirname3, resolve as
|
|
5442
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
5443
|
+
import { dirname as dirname3, resolve as resolve10 } from "path";
|
|
5166
5444
|
function isRecord3(value) {
|
|
5167
5445
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5168
5446
|
}
|
|
5169
5447
|
async function getBrandDna(options) {
|
|
5170
5448
|
if (options.brandDnaFile) {
|
|
5171
|
-
const text2 = await
|
|
5449
|
+
const text2 = await readFile10(options.brandDnaFile, "utf8");
|
|
5172
5450
|
const raw2 = JSON.parse(text2);
|
|
5173
5451
|
if (!isRecord3(raw2)) {
|
|
5174
5452
|
throw new Error("brand-dna file must be a JSON object");
|
|
5175
5453
|
}
|
|
5176
5454
|
return { source: options.brandDnaFile, brandDna: raw2 };
|
|
5177
5455
|
}
|
|
5178
|
-
const sibling =
|
|
5456
|
+
const sibling = resolve10(dirname3(options.tokenFile), "brand-dna.json");
|
|
5179
5457
|
try {
|
|
5180
|
-
const text2 = await
|
|
5458
|
+
const text2 = await readFile10(sibling, "utf8");
|
|
5181
5459
|
const raw2 = JSON.parse(text2);
|
|
5182
5460
|
if (isRecord3(raw2)) {
|
|
5183
5461
|
return { source: sibling, brandDna: raw2 };
|
|
5184
5462
|
}
|
|
5185
5463
|
} catch {
|
|
5186
5464
|
}
|
|
5187
|
-
const text = await
|
|
5465
|
+
const text = await readFile10(options.tokenFile, "utf8");
|
|
5188
5466
|
const raw = JSON.parse(text);
|
|
5189
5467
|
if (!isRecord3(raw)) {
|
|
5190
5468
|
throw new Error(
|
|
@@ -5259,7 +5537,7 @@ async function generateComponentPrompt(componentsFile, tokenFile, name) {
|
|
|
5259
5537
|
}
|
|
5260
5538
|
|
|
5261
5539
|
// src/tools/list-violations.ts
|
|
5262
|
-
import { readFile as
|
|
5540
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
5263
5541
|
var HEX_RE = /#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g;
|
|
5264
5542
|
var ARBITRARY_TW_RE = /\b(?:bg|text|border|from|to|via|ring)-\[#[0-9a-fA-F]{3,8}\]/g;
|
|
5265
5543
|
var ARBITRARY_PX_RE = /\b(?:p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap|space-[xy])-\[(\d+)px\]/g;
|
|
@@ -5300,7 +5578,7 @@ function lineCol(source, index) {
|
|
|
5300
5578
|
}
|
|
5301
5579
|
async function listViolations(tokenFile, filePath) {
|
|
5302
5580
|
const loaded = await loadTokensFromFile(tokenFile);
|
|
5303
|
-
const source = await
|
|
5581
|
+
const source = await readFile11(filePath, "utf8");
|
|
5304
5582
|
const colors = colorTokenSet(loaded.tokens);
|
|
5305
5583
|
const spacing = spacingTokenSet(loaded.tokens);
|
|
5306
5584
|
const violations = [];
|
|
@@ -5369,11 +5647,11 @@ import { spawn } from "child_process";
|
|
|
5369
5647
|
import { createServer } from "http";
|
|
5370
5648
|
|
|
5371
5649
|
// src/oauth/credentials.ts
|
|
5372
|
-
import { chmod, mkdir as mkdir2, readFile as
|
|
5650
|
+
import { chmod, mkdir as mkdir2, readFile as readFile12, writeFile as writeFile4 } from "fs/promises";
|
|
5373
5651
|
import { homedir } from "os";
|
|
5374
|
-
import { dirname as dirname4, join as
|
|
5652
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
5375
5653
|
function credentialPath() {
|
|
5376
|
-
return process.env.FLASH_GENOME_CREDENTIALS ||
|
|
5654
|
+
return process.env.FLASH_GENOME_CREDENTIALS || join7(homedir(), ".config", "flash-dsm", "credentials.json");
|
|
5377
5655
|
}
|
|
5378
5656
|
async function writeCredential(cred) {
|
|
5379
5657
|
const path = credentialPath();
|
|
@@ -5385,7 +5663,7 @@ async function writeCredential(cred) {
|
|
|
5385
5663
|
async function readCredential() {
|
|
5386
5664
|
try {
|
|
5387
5665
|
const raw = JSON.parse(
|
|
5388
|
-
await
|
|
5666
|
+
await readFile12(credentialPath(), "utf8")
|
|
5389
5667
|
);
|
|
5390
5668
|
if (!raw.accessToken || !raw.apiUrl) return null;
|
|
5391
5669
|
if (raw.kind === "token") return raw;
|
|
@@ -5554,34 +5832,6 @@ async function loginWithOauth(opts) {
|
|
|
5554
5832
|
return { path };
|
|
5555
5833
|
}
|
|
5556
5834
|
|
|
5557
|
-
// src/tools/scan-workspace.ts
|
|
5558
|
-
import { readFile as readFile12, readdir, stat } from "fs/promises";
|
|
5559
|
-
import { join as join7, resolve as resolve10 } from "path";
|
|
5560
|
-
async function walk(root, dir, out) {
|
|
5561
|
-
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
5562
|
-
for (const e of entries) {
|
|
5563
|
-
const full = join7(dir, e.name);
|
|
5564
|
-
if (e.isDirectory()) {
|
|
5565
|
-
if (!isSkippedScanDir(e.name)) await walk(root, full, out);
|
|
5566
|
-
} else if (e.isFile()) out.push(full.slice(root.length + 1));
|
|
5567
|
-
}
|
|
5568
|
-
}
|
|
5569
|
-
async function readSmallFile(path) {
|
|
5570
|
-
const info = await stat(path).catch(() => null);
|
|
5571
|
-
if (!info || !info.isFile() || info.size > MAX_SCAN_READ_BYTES) return null;
|
|
5572
|
-
return readFile12(path, "utf8").catch(() => null);
|
|
5573
|
-
}
|
|
5574
|
-
async function scanWorkspace(workspaceRoot) {
|
|
5575
|
-
const root = resolve10(workspaceRoot);
|
|
5576
|
-
const paths = [];
|
|
5577
|
-
await walk(root, root, paths);
|
|
5578
|
-
return buildWorkspaceScan({
|
|
5579
|
-
workspaceRoot: root,
|
|
5580
|
-
paths,
|
|
5581
|
-
read: (rel) => readSmallFile(join7(root, rel))
|
|
5582
|
-
});
|
|
5583
|
-
}
|
|
5584
|
-
|
|
5585
5835
|
// src/tools/init.ts
|
|
5586
5836
|
import { readFile as readFile13 } from "fs/promises";
|
|
5587
5837
|
import { join as join8 } from "path";
|
|
@@ -5711,8 +5961,8 @@ function describeInit(result) {
|
|
|
5711
5961
|
|
|
5712
5962
|
// src/tools/connect.ts
|
|
5713
5963
|
import { spawnSync } from "child_process";
|
|
5714
|
-
import { readFile as
|
|
5715
|
-
import { join as
|
|
5964
|
+
import { readFile as readFile15 } from "fs/promises";
|
|
5965
|
+
import { join as join10 } from "path";
|
|
5716
5966
|
|
|
5717
5967
|
// src/cli-sync.ts
|
|
5718
5968
|
import { resolve as resolve11 } from "path";
|
|
@@ -5916,6 +6166,475 @@ async function runSyncCommand(opts, deps = {}) {
|
|
|
5916
6166
|
}
|
|
5917
6167
|
}
|
|
5918
6168
|
|
|
6169
|
+
// src/tools/live-preview.ts
|
|
6170
|
+
import { spawn as spawn2 } from "child_process";
|
|
6171
|
+
import { mkdir as mkdir3, readFile as readFile14, rm, writeFile as writeFile5 } from "fs/promises";
|
|
6172
|
+
import http from "http";
|
|
6173
|
+
import net from "net";
|
|
6174
|
+
import { homedir as homedir2 } from "os";
|
|
6175
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
6176
|
+
var LIVE_PREVIEW_PORTS = [4747, 4748, 4749, 4750];
|
|
6177
|
+
var LIVE_PATH = "/__flash/";
|
|
6178
|
+
var COMMON_DEV_PORTS = [
|
|
6179
|
+
5173,
|
|
6180
|
+
5174,
|
|
6181
|
+
3e3,
|
|
6182
|
+
3001,
|
|
6183
|
+
4321,
|
|
6184
|
+
4200,
|
|
6185
|
+
8080,
|
|
6186
|
+
8e3,
|
|
6187
|
+
5e3,
|
|
6188
|
+
1234
|
|
6189
|
+
];
|
|
6190
|
+
function livePreviewStatePath() {
|
|
6191
|
+
return process.env.FLASH_GENOME_PREVIEW_STATE || join9(homedir2(), ".config", "flash-dsm", "live-preview.json");
|
|
6192
|
+
}
|
|
6193
|
+
function httpOrigin(url) {
|
|
6194
|
+
if (!url) return null;
|
|
6195
|
+
try {
|
|
6196
|
+
const parsed = new URL(url);
|
|
6197
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.origin : null;
|
|
6198
|
+
} catch {
|
|
6199
|
+
return null;
|
|
6200
|
+
}
|
|
6201
|
+
}
|
|
6202
|
+
async function lastLivePreviewOrigin(apiUrl) {
|
|
6203
|
+
try {
|
|
6204
|
+
const state = JSON.parse(
|
|
6205
|
+
await readFile14(livePreviewStatePath(), "utf8")
|
|
6206
|
+
);
|
|
6207
|
+
if (typeof state.origin !== "string" || typeof state.apiOrigin !== "string")
|
|
6208
|
+
return null;
|
|
6209
|
+
if (state.apiOrigin !== httpOrigin(apiUrl)) return null;
|
|
6210
|
+
return httpOrigin(state.origin) === state.origin ? state.origin : null;
|
|
6211
|
+
} catch {
|
|
6212
|
+
return null;
|
|
6213
|
+
}
|
|
6214
|
+
}
|
|
6215
|
+
async function portsNamedByProject(root) {
|
|
6216
|
+
const found = [];
|
|
6217
|
+
const add = (raw) => {
|
|
6218
|
+
const port = Number(raw);
|
|
6219
|
+
if (Number.isInteger(port) && port > 0 && port < 65536 && !found.includes(port)) {
|
|
6220
|
+
found.push(port);
|
|
6221
|
+
}
|
|
6222
|
+
};
|
|
6223
|
+
try {
|
|
6224
|
+
const pkg = JSON.parse(
|
|
6225
|
+
await readFile14(join9(root, "package.json"), "utf8")
|
|
6226
|
+
);
|
|
6227
|
+
for (const script of Object.values(pkg.scripts ?? {})) {
|
|
6228
|
+
for (const m of script.matchAll(/(?:--port|-p)[=\s]+(\d{2,5})/g))
|
|
6229
|
+
add(m[1]);
|
|
6230
|
+
for (const m of script.matchAll(/\bPORT=(\d{2,5})/g)) add(m[1]);
|
|
6231
|
+
}
|
|
6232
|
+
} catch {
|
|
6233
|
+
}
|
|
6234
|
+
for (const name of [
|
|
6235
|
+
"vite.config.ts",
|
|
6236
|
+
"vite.config.js",
|
|
6237
|
+
"vite.config.mts",
|
|
6238
|
+
"vite.config.mjs"
|
|
6239
|
+
]) {
|
|
6240
|
+
try {
|
|
6241
|
+
const text = await readFile14(join9(root, name), "utf8");
|
|
6242
|
+
for (const m of text.matchAll(/\bport\s*:\s*(\d{2,5})/g)) add(m[1]);
|
|
6243
|
+
} catch {
|
|
6244
|
+
}
|
|
6245
|
+
}
|
|
6246
|
+
return found;
|
|
6247
|
+
}
|
|
6248
|
+
async function devServerCommand(root) {
|
|
6249
|
+
let scripts = {};
|
|
6250
|
+
try {
|
|
6251
|
+
scripts = JSON.parse(await readFile14(join9(root, "package.json"), "utf8")).scripts ?? {};
|
|
6252
|
+
} catch {
|
|
6253
|
+
return null;
|
|
6254
|
+
}
|
|
6255
|
+
const script = ["dev", "start", "serve"].find((name) => scripts[name]);
|
|
6256
|
+
if (!script) return null;
|
|
6257
|
+
const has = async (name) => readFile14(join9(root, name)).then(
|
|
6258
|
+
() => true,
|
|
6259
|
+
() => false
|
|
6260
|
+
);
|
|
6261
|
+
const pm = await has("pnpm-lock.yaml") ? "pnpm" : await has("yarn.lock") ? "yarn" : await has("bun.lock") || await has("bun.lockb") ? "bun" : "npm";
|
|
6262
|
+
if (pm === "pnpm" || pm === "yarn") return `${pm} ${script}`;
|
|
6263
|
+
if (pm === "npm" && script === "start") return "npm start";
|
|
6264
|
+
return `${pm} run ${script}`;
|
|
6265
|
+
}
|
|
6266
|
+
async function answersWithPage(url, fetchImpl) {
|
|
6267
|
+
const controller = new AbortController();
|
|
6268
|
+
const timer = setTimeout(() => controller.abort(), 900);
|
|
6269
|
+
try {
|
|
6270
|
+
const res = await fetchImpl(url, {
|
|
6271
|
+
signal: controller.signal,
|
|
6272
|
+
headers: { accept: "text/html" },
|
|
6273
|
+
redirect: "manual"
|
|
6274
|
+
});
|
|
6275
|
+
const type = res.headers.get("content-type") ?? "";
|
|
6276
|
+
return type.includes("text/html") || res.status >= 300 && res.status < 400;
|
|
6277
|
+
} catch {
|
|
6278
|
+
return false;
|
|
6279
|
+
} finally {
|
|
6280
|
+
clearTimeout(timer);
|
|
6281
|
+
}
|
|
6282
|
+
}
|
|
6283
|
+
async function findDevServer(root, fetchImpl = fetch) {
|
|
6284
|
+
const ports = [
|
|
6285
|
+
...await portsNamedByProject(root),
|
|
6286
|
+
...COMMON_DEV_PORTS
|
|
6287
|
+
].filter(
|
|
6288
|
+
(port, i, all) => all.indexOf(port) === i && !LIVE_PREVIEW_PORTS.includes(port)
|
|
6289
|
+
);
|
|
6290
|
+
for (const port of ports) {
|
|
6291
|
+
const url = `http://localhost:${port}`;
|
|
6292
|
+
if (await answersWithPage(`${url}/`, fetchImpl)) return url;
|
|
6293
|
+
}
|
|
6294
|
+
return null;
|
|
6295
|
+
}
|
|
6296
|
+
function liveCss(entries) {
|
|
6297
|
+
return entries.map(([name, value]) => {
|
|
6298
|
+
const inline = '[style*="' + name + ':"]';
|
|
6299
|
+
return ":root, :host, *:not(" + inline + "):not(" + inline + " *) { " + name + ": " + value + " !important; }";
|
|
6300
|
+
}).join("\n");
|
|
6301
|
+
}
|
|
6302
|
+
function liveScript(origin) {
|
|
6303
|
+
return `(() => {
|
|
6304
|
+
const ORIGIN = ${JSON.stringify(origin)};
|
|
6305
|
+
const NAME = /^--[A-Za-z0-9_-]{1,80}$/;
|
|
6306
|
+
const VALUE = /^[^;{}<>]{1,200}$/;
|
|
6307
|
+
const liveCss = ${liveCss.toString()};
|
|
6308
|
+
let css = "";
|
|
6309
|
+
const roots = new Set([document]);
|
|
6310
|
+
const sheetFor = (root) => {
|
|
6311
|
+
const host = root === document ? document.head || document.documentElement : root;
|
|
6312
|
+
let el = host.querySelector(":scope > style[data-flash-live]");
|
|
6313
|
+
if (!el) {
|
|
6314
|
+
el = document.createElement("style");
|
|
6315
|
+
el.setAttribute("data-flash-live", "");
|
|
6316
|
+
host.appendChild(el);
|
|
6317
|
+
}
|
|
6318
|
+
return el;
|
|
6319
|
+
};
|
|
6320
|
+
const collect = (node) => {
|
|
6321
|
+
const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);
|
|
6322
|
+
for (let el = walker.currentNode; el; el = walker.nextNode()) {
|
|
6323
|
+
if (el.shadowRoot && !roots.has(el.shadowRoot)) {
|
|
6324
|
+
roots.add(el.shadowRoot);
|
|
6325
|
+
observe(el.shadowRoot);
|
|
6326
|
+
collect(el.shadowRoot);
|
|
6327
|
+
}
|
|
6328
|
+
}
|
|
6329
|
+
};
|
|
6330
|
+
const paint = () => {
|
|
6331
|
+
for (const root of roots) {
|
|
6332
|
+
const el = sheetFor(root);
|
|
6333
|
+
if (el.textContent !== css) el.textContent = css;
|
|
6334
|
+
}
|
|
6335
|
+
};
|
|
6336
|
+
let queued = false;
|
|
6337
|
+
const observer = new MutationObserver(() => {
|
|
6338
|
+
if (queued) return;
|
|
6339
|
+
queued = true;
|
|
6340
|
+
requestAnimationFrame(() => {
|
|
6341
|
+
queued = false;
|
|
6342
|
+
collect(document.documentElement);
|
|
6343
|
+
paint();
|
|
6344
|
+
});
|
|
6345
|
+
});
|
|
6346
|
+
const observe = (root) =>
|
|
6347
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
6348
|
+
window.addEventListener("message", (event) => {
|
|
6349
|
+
if (event.origin !== ORIGIN || !event.data) return;
|
|
6350
|
+
const { type, vars } = event.data;
|
|
6351
|
+
if (type === "flash:reset") css = "";
|
|
6352
|
+
else if (type === "flash:vars" && vars && typeof vars === "object") {
|
|
6353
|
+
css = liveCss(
|
|
6354
|
+
Object.entries(vars).filter(
|
|
6355
|
+
([name, value]) =>
|
|
6356
|
+
NAME.test(name) && typeof value === "string" && VALUE.test(value),
|
|
6357
|
+
),
|
|
6358
|
+
);
|
|
6359
|
+
} else return;
|
|
6360
|
+
collect(document.documentElement);
|
|
6361
|
+
paint();
|
|
6362
|
+
});
|
|
6363
|
+
const start = () => {
|
|
6364
|
+
observe(document.documentElement);
|
|
6365
|
+
collect(document.documentElement);
|
|
6366
|
+
paint();
|
|
6367
|
+
if (window.parent !== window) {
|
|
6368
|
+
window.parent.postMessage({ type: "flash:ready" }, ORIGIN);
|
|
6369
|
+
}
|
|
6370
|
+
};
|
|
6371
|
+
if (document.readyState === "loading") {
|
|
6372
|
+
document.addEventListener("DOMContentLoaded", start, { once: true });
|
|
6373
|
+
} else start();
|
|
6374
|
+
})();
|
|
6375
|
+
`;
|
|
6376
|
+
}
|
|
6377
|
+
function cleanHeaders(headers, injected, origin) {
|
|
6378
|
+
const out = {};
|
|
6379
|
+
let csp = "";
|
|
6380
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
6381
|
+
const name = key.toLowerCase();
|
|
6382
|
+
if (name === "x-frame-options") continue;
|
|
6383
|
+
if (name === "content-security-policy") {
|
|
6384
|
+
csp = (Array.isArray(value) ? value.join(";") : String(value ?? "")).split(";").filter((part) => part.trim() && !/^\s*frame-ancestors\b/i.test(part)).join(";").trim();
|
|
6385
|
+
continue;
|
|
6386
|
+
}
|
|
6387
|
+
if (injected && (name === "content-length" || name === "etag")) continue;
|
|
6388
|
+
out[key] = value;
|
|
6389
|
+
}
|
|
6390
|
+
out["content-security-policy"] = [csp, `frame-ancestors ${origin}`].filter(Boolean).join("; ");
|
|
6391
|
+
return out;
|
|
6392
|
+
}
|
|
6393
|
+
function injectScript(html) {
|
|
6394
|
+
const tag = `<script src="${LIVE_PATH}live.js"></script>`;
|
|
6395
|
+
if (html.includes(tag)) return html;
|
|
6396
|
+
const head = html.match(/<head[^>]*>/i);
|
|
6397
|
+
if (head?.index !== void 0) {
|
|
6398
|
+
const at = head.index + head[0].length;
|
|
6399
|
+
return html.slice(0, at) + tag + html.slice(at);
|
|
6400
|
+
}
|
|
6401
|
+
return tag + html;
|
|
6402
|
+
}
|
|
6403
|
+
async function serveLivePreview(opts) {
|
|
6404
|
+
const target = new URL(opts.devUrl);
|
|
6405
|
+
const targetPort = Number(target.port || 80);
|
|
6406
|
+
const origin = opts.allowedOrigin.replace(/\/$/, "");
|
|
6407
|
+
const script = liveScript(origin);
|
|
6408
|
+
let info = null;
|
|
6409
|
+
const cors = (res) => {
|
|
6410
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
6411
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
6412
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
6413
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
6414
|
+
res.setHeader("Vary", "Origin");
|
|
6415
|
+
};
|
|
6416
|
+
const own = (req, res) => {
|
|
6417
|
+
const path = (req.url ?? "").split("?")[0];
|
|
6418
|
+
if (req.method === "OPTIONS") {
|
|
6419
|
+
cors(res);
|
|
6420
|
+
res.writeHead(204).end();
|
|
6421
|
+
return;
|
|
6422
|
+
}
|
|
6423
|
+
if (path === `${LIVE_PATH}hello`) {
|
|
6424
|
+
cors(res);
|
|
6425
|
+
res.writeHead(200, {
|
|
6426
|
+
"content-type": "application/json",
|
|
6427
|
+
"cache-control": "no-store"
|
|
6428
|
+
});
|
|
6429
|
+
res.end(JSON.stringify(info));
|
|
6430
|
+
return;
|
|
6431
|
+
}
|
|
6432
|
+
if (path === `${LIVE_PATH}live.js`) {
|
|
6433
|
+
res.writeHead(200, {
|
|
6434
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
6435
|
+
"cache-control": "no-store"
|
|
6436
|
+
});
|
|
6437
|
+
res.end(script);
|
|
6438
|
+
return;
|
|
6439
|
+
}
|
|
6440
|
+
res.writeHead(404).end();
|
|
6441
|
+
};
|
|
6442
|
+
const proxy = (req, res) => {
|
|
6443
|
+
const headers = { ...req.headers, host: target.host };
|
|
6444
|
+
headers["accept-encoding"] = "identity";
|
|
6445
|
+
const upstream = http.request(
|
|
6446
|
+
{
|
|
6447
|
+
hostname: target.hostname,
|
|
6448
|
+
port: targetPort,
|
|
6449
|
+
path: req.url,
|
|
6450
|
+
method: req.method,
|
|
6451
|
+
headers
|
|
6452
|
+
},
|
|
6453
|
+
(up) => {
|
|
6454
|
+
const type = String(up.headers["content-type"] ?? "");
|
|
6455
|
+
const encoded = up.headers["content-encoding"];
|
|
6456
|
+
if (type.includes("text/html") && (!encoded || encoded === "identity")) {
|
|
6457
|
+
const chunks = [];
|
|
6458
|
+
up.on("data", (c) => chunks.push(c));
|
|
6459
|
+
up.on("end", () => {
|
|
6460
|
+
const body = injectScript(Buffer.concat(chunks).toString("utf8"));
|
|
6461
|
+
res.writeHead(
|
|
6462
|
+
up.statusCode ?? 200,
|
|
6463
|
+
cleanHeaders(up.headers, true, origin)
|
|
6464
|
+
);
|
|
6465
|
+
res.end(body);
|
|
6466
|
+
});
|
|
6467
|
+
return;
|
|
6468
|
+
}
|
|
6469
|
+
res.writeHead(
|
|
6470
|
+
up.statusCode ?? 200,
|
|
6471
|
+
cleanHeaders(up.headers, false, origin)
|
|
6472
|
+
);
|
|
6473
|
+
up.pipe(res);
|
|
6474
|
+
}
|
|
6475
|
+
);
|
|
6476
|
+
upstream.on("error", () => {
|
|
6477
|
+
if (res.headersSent) {
|
|
6478
|
+
res.end();
|
|
6479
|
+
return;
|
|
6480
|
+
}
|
|
6481
|
+
res.writeHead(502, { "content-type": "text/html; charset=utf-8" });
|
|
6482
|
+
res.end(
|
|
6483
|
+
`<!doctype html><meta charset="utf-8"><body style="font:14px system-ui;padding:32px;color:#555"><p>Your app isn't answering on ${target.host}.</p><p>Start its dev server and this page shows it.</p></body>`
|
|
6484
|
+
);
|
|
6485
|
+
});
|
|
6486
|
+
req.pipe(upstream);
|
|
6487
|
+
};
|
|
6488
|
+
const fromThisMachine = (req) => {
|
|
6489
|
+
const address = server.address();
|
|
6490
|
+
const port = address && typeof address === "object" ? address.port : null;
|
|
6491
|
+
const host = /^(\[[^\]]+\]|[^:]+)(?::(\d+))?$/.exec(
|
|
6492
|
+
String(req.headers.host ?? "").toLowerCase()
|
|
6493
|
+
);
|
|
6494
|
+
return !!host && ["localhost", "127.0.0.1", "[::1]"].includes(host[1]) && (!host[2] || Number(host[2]) === port);
|
|
6495
|
+
};
|
|
6496
|
+
const server = http.createServer((req, res) => {
|
|
6497
|
+
if (!fromThisMachine(req)) {
|
|
6498
|
+
res.writeHead(403, { "content-type": "text/plain" }).end("Forbidden");
|
|
6499
|
+
return;
|
|
6500
|
+
}
|
|
6501
|
+
if ((req.url ?? "").startsWith(LIVE_PATH) || req.method === "OPTIONS") {
|
|
6502
|
+
own(req, res);
|
|
6503
|
+
} else {
|
|
6504
|
+
proxy(req, res);
|
|
6505
|
+
}
|
|
6506
|
+
});
|
|
6507
|
+
server.on("upgrade", (req, socket, head) => {
|
|
6508
|
+
if (!fromThisMachine(req)) {
|
|
6509
|
+
socket.destroy();
|
|
6510
|
+
return;
|
|
6511
|
+
}
|
|
6512
|
+
const upstream = net.connect(targetPort, target.hostname, () => {
|
|
6513
|
+
const lines = [`${req.method} ${req.url} HTTP/${req.httpVersion}`];
|
|
6514
|
+
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
|
6515
|
+
const name = req.rawHeaders[i];
|
|
6516
|
+
const value = name.toLowerCase() === "host" ? target.host : req.rawHeaders[i + 1];
|
|
6517
|
+
lines.push(`${name}: ${value}`);
|
|
6518
|
+
}
|
|
6519
|
+
upstream.write(lines.join("\r\n") + "\r\n\r\n");
|
|
6520
|
+
if (head.length) upstream.write(head);
|
|
6521
|
+
socket.pipe(upstream).pipe(socket);
|
|
6522
|
+
});
|
|
6523
|
+
upstream.on("error", () => socket.destroy());
|
|
6524
|
+
socket.on("error", () => upstream.destroy());
|
|
6525
|
+
});
|
|
6526
|
+
for (const port of opts.ports ?? LIVE_PREVIEW_PORTS) {
|
|
6527
|
+
const ok = await new Promise((resolve14) => {
|
|
6528
|
+
const onError = () => resolve14(false);
|
|
6529
|
+
server.once("error", onError);
|
|
6530
|
+
server.listen(port, "127.0.0.1", () => {
|
|
6531
|
+
server.off("error", onError);
|
|
6532
|
+
resolve14(true);
|
|
6533
|
+
});
|
|
6534
|
+
});
|
|
6535
|
+
if (ok) {
|
|
6536
|
+
info = {
|
|
6537
|
+
designSystemId: opts.designSystemId,
|
|
6538
|
+
devUrl: opts.devUrl,
|
|
6539
|
+
project: opts.project ?? null,
|
|
6540
|
+
port,
|
|
6541
|
+
version: opts.version ?? "0.0.0",
|
|
6542
|
+
pid: process.pid
|
|
6543
|
+
};
|
|
6544
|
+
return { server, info };
|
|
6545
|
+
}
|
|
6546
|
+
}
|
|
6547
|
+
throw new Error(
|
|
6548
|
+
`Ports ${(opts.ports ?? LIVE_PREVIEW_PORTS).join(", ")} are all taken; the live preview can't start.`
|
|
6549
|
+
);
|
|
6550
|
+
}
|
|
6551
|
+
async function askLivePreview(port, fetchImpl = fetch) {
|
|
6552
|
+
const controller = new AbortController();
|
|
6553
|
+
const timer = setTimeout(() => controller.abort(), 700);
|
|
6554
|
+
try {
|
|
6555
|
+
const res = await fetchImpl(`http://127.0.0.1:${port}${LIVE_PATH}hello`, {
|
|
6556
|
+
signal: controller.signal
|
|
6557
|
+
});
|
|
6558
|
+
if (!res.ok) return null;
|
|
6559
|
+
const body = await res.json();
|
|
6560
|
+
return body && typeof body.designSystemId === "number" ? body : null;
|
|
6561
|
+
} catch {
|
|
6562
|
+
return null;
|
|
6563
|
+
} finally {
|
|
6564
|
+
clearTimeout(timer);
|
|
6565
|
+
}
|
|
6566
|
+
}
|
|
6567
|
+
async function stopLivePreview(fetchImpl = fetch) {
|
|
6568
|
+
const path = livePreviewStatePath();
|
|
6569
|
+
let state;
|
|
6570
|
+
try {
|
|
6571
|
+
state = JSON.parse(await readFile14(path, "utf8"));
|
|
6572
|
+
} catch {
|
|
6573
|
+
return false;
|
|
6574
|
+
}
|
|
6575
|
+
const { pid, origin, apiOrigin } = state;
|
|
6576
|
+
if (origin && apiOrigin) {
|
|
6577
|
+
await writeFile5(path, JSON.stringify({ origin, apiOrigin }), {
|
|
6578
|
+
mode: 384
|
|
6579
|
+
});
|
|
6580
|
+
} else {
|
|
6581
|
+
await rm(path, { force: true });
|
|
6582
|
+
}
|
|
6583
|
+
if (typeof pid !== "number") return false;
|
|
6584
|
+
for (const port of LIVE_PREVIEW_PORTS) {
|
|
6585
|
+
const info = await askLivePreview(port, fetchImpl);
|
|
6586
|
+
if (info?.pid === pid) {
|
|
6587
|
+
try {
|
|
6588
|
+
process.kill(pid);
|
|
6589
|
+
} catch {
|
|
6590
|
+
}
|
|
6591
|
+
return true;
|
|
6592
|
+
}
|
|
6593
|
+
}
|
|
6594
|
+
return false;
|
|
6595
|
+
}
|
|
6596
|
+
async function startLivePreviewInBackground(opts) {
|
|
6597
|
+
await stopLivePreview();
|
|
6598
|
+
const child = spawn2(
|
|
6599
|
+
process.execPath,
|
|
6600
|
+
[
|
|
6601
|
+
opts.bin,
|
|
6602
|
+
"preview",
|
|
6603
|
+
"--serve",
|
|
6604
|
+
"--dev-url",
|
|
6605
|
+
opts.devUrl,
|
|
6606
|
+
"--system",
|
|
6607
|
+
String(opts.designSystemId),
|
|
6608
|
+
"--origin",
|
|
6609
|
+
opts.allowedOrigin,
|
|
6610
|
+
...opts.project ? ["--name", opts.project] : []
|
|
6611
|
+
],
|
|
6612
|
+
{ detached: true, stdio: "ignore" }
|
|
6613
|
+
);
|
|
6614
|
+
child.unref();
|
|
6615
|
+
const path = livePreviewStatePath();
|
|
6616
|
+
await mkdir3(dirname5(path), { recursive: true });
|
|
6617
|
+
await writeFile5(
|
|
6618
|
+
path,
|
|
6619
|
+
JSON.stringify({
|
|
6620
|
+
pid: child.pid,
|
|
6621
|
+
devUrl: opts.devUrl,
|
|
6622
|
+
designSystemId: opts.designSystemId,
|
|
6623
|
+
origin: opts.allowedOrigin.replace(/\/$/, ""),
|
|
6624
|
+
apiOrigin: httpOrigin(opts.apiUrl)
|
|
6625
|
+
}),
|
|
6626
|
+
{ mode: 384 }
|
|
6627
|
+
);
|
|
6628
|
+
for (let i = 0; i < 20; i++) {
|
|
6629
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
6630
|
+
for (const port of LIVE_PREVIEW_PORTS) {
|
|
6631
|
+
const info = await askLivePreview(port, opts.fetchImpl);
|
|
6632
|
+
if (info?.designSystemId === opts.designSystemId) return info;
|
|
6633
|
+
}
|
|
6634
|
+
}
|
|
6635
|
+
return null;
|
|
6636
|
+
}
|
|
6637
|
+
|
|
5919
6638
|
// src/tools/connect.ts
|
|
5920
6639
|
var GENOME_SERVER_KEY = "flash-genome";
|
|
5921
6640
|
var GENOME_LATEST = "@flashdsm/genome@latest";
|
|
@@ -5929,7 +6648,7 @@ var ConnectError = class extends Error {
|
|
|
5929
6648
|
async function packageName2(root) {
|
|
5930
6649
|
try {
|
|
5931
6650
|
return projectNameFromPackageJson(
|
|
5932
|
-
await
|
|
6651
|
+
await readFile15(join10(root, "package.json"), "utf8")
|
|
5933
6652
|
);
|
|
5934
6653
|
} catch {
|
|
5935
6654
|
return null;
|
|
@@ -6080,6 +6799,30 @@ async function connectFromBrowser(opts) {
|
|
|
6080
6799
|
token
|
|
6081
6800
|
})))(collected.token) : 1;
|
|
6082
6801
|
const mcp = (opts.addMcp ?? addMcpForClaudeCode)(scan.workspaceRoot);
|
|
6802
|
+
let livePreview = null;
|
|
6803
|
+
let noDevServer = false;
|
|
6804
|
+
let livePreviewFailed;
|
|
6805
|
+
let devCommand = null;
|
|
6806
|
+
if (opts.livePreview !== false) {
|
|
6807
|
+
const devUrl = await (opts.findDevServer ?? findDevServer)(scan.workspaceRoot);
|
|
6808
|
+
if (devUrl) {
|
|
6809
|
+
const start = opts.livePreview ?? ((args) => startLivePreviewInBackground({ ...args, bin: process.argv[1], apiUrl }));
|
|
6810
|
+
try {
|
|
6811
|
+
livePreview = await start({
|
|
6812
|
+
devUrl,
|
|
6813
|
+
designSystemId: collected.id,
|
|
6814
|
+
allowedOrigin: httpOrigin(collected.editor_url) ?? new URL(apiUrl).origin,
|
|
6815
|
+
project: collected.name
|
|
6816
|
+
});
|
|
6817
|
+
} catch {
|
|
6818
|
+
livePreview = null;
|
|
6819
|
+
}
|
|
6820
|
+
if (!livePreview) livePreviewFailed = devUrl;
|
|
6821
|
+
} else {
|
|
6822
|
+
noDevServer = true;
|
|
6823
|
+
devCommand = await devServerCommand(scan.workspaceRoot);
|
|
6824
|
+
}
|
|
6825
|
+
}
|
|
6083
6826
|
return {
|
|
6084
6827
|
system: {
|
|
6085
6828
|
id: collected.id,
|
|
@@ -6091,7 +6834,11 @@ async function connectFromBrowser(opts) {
|
|
|
6091
6834
|
link,
|
|
6092
6835
|
linkError,
|
|
6093
6836
|
pullExit,
|
|
6094
|
-
mcp
|
|
6837
|
+
mcp,
|
|
6838
|
+
livePreview,
|
|
6839
|
+
noDevServer,
|
|
6840
|
+
livePreviewFailed,
|
|
6841
|
+
devCommand
|
|
6095
6842
|
};
|
|
6096
6843
|
}
|
|
6097
6844
|
function describeConnect(result) {
|
|
@@ -6118,6 +6865,20 @@ function describeConnect(result) {
|
|
|
6118
6865
|
`So your editor reads the system while you work, add Genome as an MCP server. In Claude Code: ${result.mcp.command}`
|
|
6119
6866
|
);
|
|
6120
6867
|
}
|
|
6868
|
+
if (result.livePreview) {
|
|
6869
|
+
lines.push(
|
|
6870
|
+
`Your app (${result.livePreview.devUrl}) shows live in Flash's preview and recolours as you edit. It runs in the background; stop it with \`npx ${GENOME_LATEST} preview --stop\`.`
|
|
6871
|
+
);
|
|
6872
|
+
} else if (result.noDevServer) {
|
|
6873
|
+
const start = result.devCommand ? ` (\`${result.devCommand}\`)` : "";
|
|
6874
|
+
lines.push(
|
|
6875
|
+
`Your app's dev server isn't running, so Flash's preview shows a sample screen for now. To see your app there instead, start the dev server${start}, then run \`npx ${GENOME_LATEST} preview\` in this folder. The editor switches to your app by itself.`
|
|
6876
|
+
);
|
|
6877
|
+
} else if (result.livePreviewFailed) {
|
|
6878
|
+
lines.push(
|
|
6879
|
+
`Your app answers on ${result.livePreviewFailed}, but the live preview didn't start, so Flash's preview shows a sample screen. Run \`npx ${GENOME_LATEST} preview\` in this folder to try again.`
|
|
6880
|
+
);
|
|
6881
|
+
}
|
|
6121
6882
|
lines.push(
|
|
6122
6883
|
`Genome's sign-in is stored at ${result.credentialPath}. No token goes in any config.`
|
|
6123
6884
|
);
|
|
@@ -6295,8 +7056,8 @@ async function captureProjectShell(opts) {
|
|
|
6295
7056
|
}
|
|
6296
7057
|
|
|
6297
7058
|
// src/tools/scan-usage.ts
|
|
6298
|
-
import { readFile as
|
|
6299
|
-
import { basename as basename2, dirname as
|
|
7059
|
+
import { readFile as readFile16, readdir as readdir2 } from "fs/promises";
|
|
7060
|
+
import { basename as basename2, dirname as dirname6, join as join11, posix, resolve as resolve12 } from "path";
|
|
6300
7061
|
var PRODUCT_USAGE_VERSION = 1;
|
|
6301
7062
|
var USAGE_LIMITS = {
|
|
6302
7063
|
components: 400,
|
|
@@ -6320,11 +7081,11 @@ async function walk2(root, dir, out) {
|
|
|
6320
7081
|
for (const entry of entries) {
|
|
6321
7082
|
if (entry.isDirectory()) {
|
|
6322
7083
|
if (!SKIP_DIR.test(entry.name)) {
|
|
6323
|
-
await walk2(root,
|
|
7084
|
+
await walk2(root, join11(dir, entry.name), out);
|
|
6324
7085
|
}
|
|
6325
7086
|
} else {
|
|
6326
7087
|
out.push(
|
|
6327
|
-
|
|
7088
|
+
join11(dir, entry.name).slice(root.length + 1).replace(/\\/g, "/")
|
|
6328
7089
|
);
|
|
6329
7090
|
}
|
|
6330
7091
|
}
|
|
@@ -6388,7 +7149,7 @@ function tagUses(source, local) {
|
|
|
6388
7149
|
return (source.match(new RegExp(`<${escapeRe(local)}(?=[\\s/>.])`, "g")) ?? []).length;
|
|
6389
7150
|
}
|
|
6390
7151
|
async function repositoryName(root) {
|
|
6391
|
-
const gitConfig = await
|
|
7152
|
+
const gitConfig = await readFile16(join11(root, ".git", "config"), "utf8").catch(
|
|
6392
7153
|
() => ""
|
|
6393
7154
|
);
|
|
6394
7155
|
const remote = gitConfig.match(
|
|
@@ -6398,7 +7159,7 @@ async function repositoryName(root) {
|
|
|
6398
7159
|
if (slug) return slug;
|
|
6399
7160
|
try {
|
|
6400
7161
|
const pkg = JSON.parse(
|
|
6401
|
-
await
|
|
7162
|
+
await readFile16(join11(root, "package.json"), "utf8")
|
|
6402
7163
|
);
|
|
6403
7164
|
if (typeof pkg.name === "string" && pkg.name.trim()) return pkg.name.trim();
|
|
6404
7165
|
} catch {
|
|
@@ -6421,7 +7182,7 @@ async function scanProductUsage(workspaceRoot, opts = {}) {
|
|
|
6421
7182
|
for (const rel of paths) {
|
|
6422
7183
|
if (basename2(rel) !== "package.json") continue;
|
|
6423
7184
|
try {
|
|
6424
|
-
const pkg = JSON.parse(await
|
|
7185
|
+
const pkg = JSON.parse(await readFile16(join11(root, rel), "utf8"));
|
|
6425
7186
|
if (typeof pkg.name === "string") ownPackages.add(pkg.name);
|
|
6426
7187
|
} catch {
|
|
6427
7188
|
}
|
|
@@ -6431,13 +7192,13 @@ async function scanProductUsage(workspaceRoot, opts = {}) {
|
|
|
6431
7192
|
);
|
|
6432
7193
|
const found = /* @__PURE__ */ new Map();
|
|
6433
7194
|
for (const rel of sources) {
|
|
6434
|
-
const text = await
|
|
7195
|
+
const text = await readFile16(join11(root, rel), "utf8").catch(() => null);
|
|
6435
7196
|
if (!text || !text.includes("import")) continue;
|
|
6436
7197
|
for (const name of importedNames(text)) {
|
|
6437
7198
|
if (!definedAt.has(name.imported)) continue;
|
|
6438
7199
|
if (!isOwnSource(name.source, ownPackages)) continue;
|
|
6439
7200
|
const home = definedAt.get(name.imported);
|
|
6440
|
-
if (home && (home === rel ||
|
|
7201
|
+
if (home && (home === rel || dirname6(home) === dirname6(rel) && /(^|\/)index\.[a-z]+$/i.test(rel))) {
|
|
6441
7202
|
continue;
|
|
6442
7203
|
}
|
|
6443
7204
|
const uses = tagUses(text, name.local);
|
|
@@ -6657,6 +7418,11 @@ function parseCliArgs(argv) {
|
|
|
6657
7418
|
let confirm = false;
|
|
6658
7419
|
let json = false;
|
|
6659
7420
|
let noOpen = false;
|
|
7421
|
+
let serve = false;
|
|
7422
|
+
let stop = false;
|
|
7423
|
+
let devUrl;
|
|
7424
|
+
let system;
|
|
7425
|
+
let origin;
|
|
6660
7426
|
const VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
6661
7427
|
"--tokens",
|
|
6662
7428
|
"-t",
|
|
@@ -6668,7 +7434,10 @@ function parseCliArgs(argv) {
|
|
|
6668
7434
|
"--token",
|
|
6669
7435
|
"--cwd",
|
|
6670
7436
|
"--code",
|
|
6671
|
-
"--name"
|
|
7437
|
+
"--name",
|
|
7438
|
+
"--dev-url",
|
|
7439
|
+
"--system",
|
|
7440
|
+
"--origin"
|
|
6672
7441
|
]);
|
|
6673
7442
|
let command;
|
|
6674
7443
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -6732,6 +7501,27 @@ function parseCliArgs(argv) {
|
|
|
6732
7501
|
noOpen = true;
|
|
6733
7502
|
continue;
|
|
6734
7503
|
}
|
|
7504
|
+
if (arg === "--serve") {
|
|
7505
|
+
serve = true;
|
|
7506
|
+
continue;
|
|
7507
|
+
}
|
|
7508
|
+
if (arg === "--stop") {
|
|
7509
|
+
stop = true;
|
|
7510
|
+
continue;
|
|
7511
|
+
}
|
|
7512
|
+
if (arg === "--dev-url") {
|
|
7513
|
+
devUrl = argv[++i];
|
|
7514
|
+
continue;
|
|
7515
|
+
}
|
|
7516
|
+
if (arg === "--system") {
|
|
7517
|
+
const n = Number(argv[++i]);
|
|
7518
|
+
system = Number.isInteger(n) ? n : void 0;
|
|
7519
|
+
continue;
|
|
7520
|
+
}
|
|
7521
|
+
if (arg === "--origin") {
|
|
7522
|
+
origin = argv[++i];
|
|
7523
|
+
continue;
|
|
7524
|
+
}
|
|
6735
7525
|
if (arg === "--help" || arg === "-h") {
|
|
6736
7526
|
console.error(`Usage: flash-genome [command] [options]
|
|
6737
7527
|
|
|
@@ -6743,12 +7533,15 @@ Commands:
|
|
|
6743
7533
|
scans, opens Flash to confirm, then links the repo,
|
|
6744
7534
|
writes the system into it and (in Claude Code) adds
|
|
6745
7535
|
Genome as the project's MCP server
|
|
6746
|
-
|
|
7536
|
+
preview Show this app, running on its dev server, inside Flash
|
|
7537
|
+
and recolour it as you edit (connect starts it; this
|
|
7538
|
+
starts it again). --stop ends it
|
|
7539
|
+
init --code CODE Same as connect, with a setup code instead of the browser:
|
|
6747
7540
|
scans, creates it in Flash, stores the sign-in, links
|
|
6748
7541
|
the repo. CODE comes from Flash: Import > From your code
|
|
6749
7542
|
link Bind this repo to the design system you signed in to
|
|
6750
7543
|
pull Write the design system into this repo (dry run unless --confirm)
|
|
6751
|
-
push Propose this repo's
|
|
7544
|
+
push Propose what this repo's code changed to Flash (dry run unless --confirm)
|
|
6752
7545
|
drift Has the repo moved, has Flash, which tokens (read-only)
|
|
6753
7546
|
login Open Flash in the browser and store the sign-in locally
|
|
6754
7547
|
|
|
@@ -6795,7 +7588,12 @@ Exit codes (pull, push, drift):
|
|
|
6795
7588
|
name,
|
|
6796
7589
|
confirm,
|
|
6797
7590
|
json,
|
|
6798
|
-
noOpen
|
|
7591
|
+
noOpen,
|
|
7592
|
+
serve,
|
|
7593
|
+
stop,
|
|
7594
|
+
devUrl,
|
|
7595
|
+
system,
|
|
7596
|
+
origin
|
|
6799
7597
|
};
|
|
6800
7598
|
}
|
|
6801
7599
|
function textResult(data, isError = false) {
|
|
@@ -7154,7 +7952,7 @@ async function createServer2(opts) {
|
|
|
7154
7952
|
);
|
|
7155
7953
|
server.tool(
|
|
7156
7954
|
"push_to_web",
|
|
7157
|
-
"Propose
|
|
7955
|
+
"Propose what this repo's code changed to Flash, as one change set a person reviews on the Sync page: the app's own stylesheets (new tokens, light or dark values Flash lacks, aliases re-pointed with var(--step)) and edits to the pulled design-tokens.json. Files the pull wrote are not read as code. Nothing is applied. Returns the plan; proposes only with confirm. Refused while Flash is ahead of the last pull.",
|
|
7158
7956
|
workspaceArgs,
|
|
7159
7957
|
async (args) => {
|
|
7160
7958
|
try {
|
|
@@ -7253,7 +8051,7 @@ async function resolveApiUrl(cli, cwd) {
|
|
|
7253
8051
|
const fromEnv = process.env.FLASH_GENOME_API_URL || process.env.FLASH_DS_API_URL;
|
|
7254
8052
|
if (fromEnv) return fromEnv;
|
|
7255
8053
|
for (const path of configCandidates(cli.configPath, cwd)) {
|
|
7256
|
-
const raw = await
|
|
8054
|
+
const raw = await readFile17(path, "utf8").catch(() => null);
|
|
7257
8055
|
if (!raw) continue;
|
|
7258
8056
|
try {
|
|
7259
8057
|
const parsed = GenomeConfigSchema.safeParse(JSON.parse(raw));
|
|
@@ -7320,6 +8118,64 @@ async function runConnect(cli, apiUrl) {
|
|
|
7320
8118
|
process.exit(1);
|
|
7321
8119
|
}
|
|
7322
8120
|
}
|
|
8121
|
+
async function readLinkedSystem(root) {
|
|
8122
|
+
try {
|
|
8123
|
+
const config = await readGenomeConfig(root);
|
|
8124
|
+
return { id: config.genomeId, name: config.name ?? null };
|
|
8125
|
+
} catch {
|
|
8126
|
+
return null;
|
|
8127
|
+
}
|
|
8128
|
+
}
|
|
8129
|
+
async function runPreview(cli, apiUrl) {
|
|
8130
|
+
if (cli.stop) {
|
|
8131
|
+
const stopped = await stopLivePreview();
|
|
8132
|
+
console.log(stopped ? "Stopped the live preview." : "No live preview was running.");
|
|
8133
|
+
return;
|
|
8134
|
+
}
|
|
8135
|
+
const origin = (cli.origin ?? await lastLivePreviewOrigin(apiUrl) ?? new URL(apiUrl).origin).replace(/\/$/, "");
|
|
8136
|
+
if (cli.serve) {
|
|
8137
|
+
if (!cli.devUrl || cli.system === void 0) {
|
|
8138
|
+
console.error("preview --serve needs --dev-url and --system.");
|
|
8139
|
+
process.exit(2);
|
|
8140
|
+
}
|
|
8141
|
+
const { info: info2 } = await serveLivePreview({
|
|
8142
|
+
devUrl: cli.devUrl,
|
|
8143
|
+
designSystemId: cli.system,
|
|
8144
|
+
allowedOrigin: origin,
|
|
8145
|
+
project: cli.name ?? null,
|
|
8146
|
+
version: packageVersion()
|
|
8147
|
+
});
|
|
8148
|
+
console.error(`[flash-genome] live preview on http://localhost:${info2.port} -> ${info2.devUrl}`);
|
|
8149
|
+
return;
|
|
8150
|
+
}
|
|
8151
|
+
const root = resolve13(cli.cwd ?? process.cwd());
|
|
8152
|
+
const linked = await readLinkedSystem(root);
|
|
8153
|
+
if (!linked) {
|
|
8154
|
+
console.error(
|
|
8155
|
+
"This folder isn't linked to a Flash system yet. Run `npx @flashdsm/genome@latest connect` first."
|
|
8156
|
+
);
|
|
8157
|
+
process.exit(2);
|
|
8158
|
+
}
|
|
8159
|
+
const devUrl = cli.devUrl ?? await findDevServer(root);
|
|
8160
|
+
if (!devUrl) {
|
|
8161
|
+
const start = await devServerCommand(root) ?? "npm run dev";
|
|
8162
|
+
console.error(
|
|
8163
|
+
`Couldn't find your app's dev server on localhost, so Flash still shows its sample screen. Start it (${start}), then run this again, or pass --dev-url http://localhost:PORT.`
|
|
8164
|
+
);
|
|
8165
|
+
process.exit(1);
|
|
8166
|
+
}
|
|
8167
|
+
const info = await startLivePreviewInBackground({
|
|
8168
|
+
bin: process.argv[1],
|
|
8169
|
+
apiUrl,
|
|
8170
|
+
devUrl,
|
|
8171
|
+
designSystemId: linked.id,
|
|
8172
|
+
allowedOrigin: origin,
|
|
8173
|
+
project: linked.name
|
|
8174
|
+
});
|
|
8175
|
+
console.log(
|
|
8176
|
+
info ? `Flash shows ${devUrl} live on its preview (through localhost:${info.port}). Stop it with \`npx @flashdsm/genome preview --stop\`.` : "The live preview didn't start. Run `npx @flashdsm/genome preview --serve --dev-url " + devUrl + " --system " + linked.id + "` to see why."
|
|
8177
|
+
);
|
|
8178
|
+
}
|
|
7323
8179
|
async function main() {
|
|
7324
8180
|
const cli = parseCliArgs(process.argv.slice(2));
|
|
7325
8181
|
const apiUrl = await resolveApiUrl(cli, resolve13(cli.cwd ?? process.cwd()));
|
|
@@ -7348,6 +8204,7 @@ async function main() {
|
|
|
7348
8204
|
if (cli.command === "link") return runLink(cli);
|
|
7349
8205
|
if (cli.command === "init") return runInit(cli, apiUrl);
|
|
7350
8206
|
if (cli.command === "connect") return runConnect(cli, apiUrl);
|
|
8207
|
+
if (cli.command === "preview") return runPreview(cli, apiUrl);
|
|
7351
8208
|
if (cli.command) {
|
|
7352
8209
|
console.error(
|
|
7353
8210
|
`Unknown command "${cli.command}". Run flash-genome --help for the list.`
|