@pantoken/scaffold-base 0.1.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/LICENSE +21 -0
- package/README.md +31 -0
- package/dist/index.d.mts +50 -0
- package/dist/index.mjs +196 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Danny Wahl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @pantoken/scaffold-base
|
|
2
|
+
|
|
3
|
+
The shared [`bingo-stratum`](https://www.create.bingo/engines/stratum) `Base` every pantoken scaffold
|
|
4
|
+
platform Preset is built from. Provides:
|
|
5
|
+
|
|
6
|
+
- **Common options schema**: `projectName` string with `"pantoken-app"` default.
|
|
7
|
+
- **Shared Blocks**: `blockCssdoc` (writes `cssdoc.json`), `blockWrapperContext` (provides markup derived
|
|
8
|
+
from [`@pantoken/plugin-layouts`](https://www.npmjs.com/package/@pantoken/plugin-layouts)'s `wrapper`
|
|
9
|
+
app-shell layout for platform-specific rendering).
|
|
10
|
+
- **Factory function**: `createPreset()` for type-safe Preset construction with `--isolatedDeclarations`
|
|
11
|
+
compatibility.
|
|
12
|
+
|
|
13
|
+
Used by platform packages (`@pantoken/components`, `@pantoken/react`, `@pantoken/vue`,
|
|
14
|
+
`@pantoken/web-components`) to define their own presets, which are then auto-discovered and
|
|
15
|
+
registered by [`@pantoken/scaffold`](https://www.npmjs.com/package/@pantoken/scaffold).
|
|
16
|
+
|
|
17
|
+
Not meant to be used standalone — see `@pantoken/scaffold` for the full scaffolding workflow.
|
|
18
|
+
|
|
19
|
+
## API
|
|
20
|
+
|
|
21
|
+
- **`base`** — the shared `bingo-stratum` `Base`, exported for re-use in platform presets.
|
|
22
|
+
- **`createPreset(input)`** — factory function constructing a type-safe `Preset` with platform-specific
|
|
23
|
+
options. Workaround for `--isolatedDeclarations` mode, which requires explicit types on all exports.
|
|
24
|
+
- **`blockCssdoc`** — a `Base`-scoped Block that writes the shared `cssdoc.json` scaffold file.
|
|
25
|
+
- **`getWrapperContext()`** — returns `{ wrapperRootClass, wrapperContainerHtml(depth?), wrapperContainerJsx(depth?) }`,
|
|
26
|
+
derived from the `wrapper` app-shell layout, for use as Handlebars context in a platform's own
|
|
27
|
+
templates.
|
|
28
|
+
|
|
29
|
+
## License
|
|
30
|
+
|
|
31
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Base, BlockWithoutAddons, Preset, Preset as Preset$1 } from "bingo-stratum";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/base.d.ts
|
|
4
|
+
declare const options: {
|
|
5
|
+
name: z.ZodDefault<z.ZodString>;
|
|
6
|
+
};
|
|
7
|
+
/** Shared options every pantoken scaffold platform Preset takes in. */
|
|
8
|
+
declare const base: Base<typeof options>;
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/preset.d.ts
|
|
11
|
+
interface PresetInput {
|
|
12
|
+
name: string;
|
|
13
|
+
base: Base<any>;
|
|
14
|
+
blocks?: any[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create a scaffold Preset with explicit return type for TypeScript `--isolatedDeclarations`.
|
|
18
|
+
*
|
|
19
|
+
* @param input - Preset configuration with name, base, and optional blocks
|
|
20
|
+
* @returns Properly typed Bingo Preset
|
|
21
|
+
*/
|
|
22
|
+
declare function createPreset(input: PresetInput): Preset$1;
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/blocks/cssdoc.d.ts
|
|
25
|
+
/** Writes the shared `cssdoc.json` schema file, unmodified by platform or options. */
|
|
26
|
+
declare const blockCssdoc: BlockWithoutAddons<{
|
|
27
|
+
name: string;
|
|
28
|
+
}>;
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/wrapper-context.d.ts
|
|
31
|
+
/**
|
|
32
|
+
* Render the wrapper layout's `.container` part tree as markup, starting at the given indent depth.
|
|
33
|
+
* `format: "html"` covers plain HTML/Angular templates (`class="..."`); `"jsx"` covers React/Next
|
|
34
|
+
* (`className="..."`).
|
|
35
|
+
*/
|
|
36
|
+
declare function renderWrapperContainer(format: "html" | "jsx", depth?: number): string;
|
|
37
|
+
/** The wrapper layout's root class name (applied to the real document `<body>` or component root). */
|
|
38
|
+
declare function wrapperRootClassName(): string;
|
|
39
|
+
/**
|
|
40
|
+
* The wrapper layout markup/tokens every platform Preset feeds into its own Handlebars context —
|
|
41
|
+
* `{{projectName}}` inside the rendered markup is left as-is, for the platform's own template to
|
|
42
|
+
* resolve.
|
|
43
|
+
*/
|
|
44
|
+
declare function getWrapperContext(): {
|
|
45
|
+
wrapperRootClass: string;
|
|
46
|
+
wrapperContainerHtml: (depth?: number) => string;
|
|
47
|
+
wrapperContainerJsx: (depth?: number) => string;
|
|
48
|
+
};
|
|
49
|
+
//#endregion
|
|
50
|
+
export { type Preset, base, blockCssdoc, createPreset, getWrapperContext, renderWrapperContainer, wrapperRootClassName };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { createBase } from "bingo-stratum";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { wrapperRules } from "@pantoken/plugin-layouts";
|
|
4
|
+
//#region src/base.ts
|
|
5
|
+
const options = { name: z.string().default("pantoken-app").describe("Name of the scaffolded project") };
|
|
6
|
+
/** Shared options every pantoken scaffold platform Preset takes in. */
|
|
7
|
+
const base = createBase({ options });
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/preset.ts
|
|
10
|
+
/**
|
|
11
|
+
* Create a scaffold Preset with explicit return type for TypeScript `--isolatedDeclarations`.
|
|
12
|
+
*
|
|
13
|
+
* @param input - Preset configuration with name, base, and optional blocks
|
|
14
|
+
* @returns Properly typed Bingo Preset
|
|
15
|
+
*/
|
|
16
|
+
function createPreset(input) {
|
|
17
|
+
return {
|
|
18
|
+
about: { name: input.name },
|
|
19
|
+
base: input.base,
|
|
20
|
+
blocks: input.blocks ?? []
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region generated/cssdoc.ts
|
|
25
|
+
const CSSDOC_JSON = "{\n \"$schema\": \"https://cssdoc.dev/cssdoc.schema.json\",\n \"extends\": [\"./node_modules/@pantoken/pantoken/cssdoc-base.json\"],\n \"modifierConvention\": \"rscss\",\n \"inlineComments\": \"ignore\",\n // This provider resolves pantoken's documented components/utilities for lint/hover/completions. It\n // assumes the default \"instui-\" class prefix pantoken ships with — if you build your OWN pantoken\n // CSS with a different prefix spelling (e.g. componentsCss({ prefix: \"acme\" })), rewrite it here.\n // `to` is used verbatim — no \"-\" is assumed or added, so match whatever you actually built with:\n // \"prefix\": { \"from\": \"instui-\", \"to\": \"acme-\" }\n // Built unprefixed (componentsCss({ prefix: null })) or with no separator (e.g. \"ialert\" instead of\n // \"instui-alert\")? Set `to` to match exactly — omit it (or use \"\") to strip to a bare class:\n // \"prefix\": { \"from\": \"instui-\", \"to\": \"i\" }\n \"providers\": [{ \"path\": \"./node_modules/@pantoken/pantoken/model.json\" }]\n}\n";
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/blocks/cssdoc.ts
|
|
28
|
+
/** Writes the shared `cssdoc.json` schema file, unmodified by platform or options. */
|
|
29
|
+
const blockCssdoc = base.createBlock({
|
|
30
|
+
about: { name: "cssdoc" },
|
|
31
|
+
produce: () => ({ files: { "cssdoc.json": CSSDOC_JSON } })
|
|
32
|
+
});
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/wrapper-context.ts
|
|
35
|
+
/**
|
|
36
|
+
* Derives each scaffold platform's entry markup from `@pantoken/plugin-layouts`'s `wrapper` layout,
|
|
37
|
+
* so every preset's template stays structurally in sync with that layout automatically. Called at
|
|
38
|
+
* scaffold-produce-time (not build-time) — every platform preset feeds these values into its own
|
|
39
|
+
* Handlebars context instead of hand-authoring the wrapper markup.
|
|
40
|
+
*
|
|
41
|
+
* @module
|
|
42
|
+
* @alpha
|
|
43
|
+
*/
|
|
44
|
+
function extractSelector(raw) {
|
|
45
|
+
const lastSemi = raw.lastIndexOf(";");
|
|
46
|
+
return (lastSemi === -1 ? raw : raw.slice(lastSemi + 1)).trim();
|
|
47
|
+
}
|
|
48
|
+
/** Split CSS text into top-level (brace-depth-aware) rules, ignoring plain declarations. */
|
|
49
|
+
function splitRules(css) {
|
|
50
|
+
const rules = [];
|
|
51
|
+
let depth = 0;
|
|
52
|
+
let selectorStart = 0;
|
|
53
|
+
let bodyStart = -1;
|
|
54
|
+
for (let i = 0; i < css.length; i++) {
|
|
55
|
+
const ch = css[i];
|
|
56
|
+
if (ch === "{") {
|
|
57
|
+
if (depth === 0) bodyStart = i + 1;
|
|
58
|
+
depth++;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (ch !== "}") continue;
|
|
62
|
+
depth--;
|
|
63
|
+
if (depth !== 0 || bodyStart === -1) continue;
|
|
64
|
+
const selector = extractSelector(css.slice(selectorStart, bodyStart - 1));
|
|
65
|
+
if (selector) rules.push({
|
|
66
|
+
selector,
|
|
67
|
+
body: css.slice(bodyStart, i)
|
|
68
|
+
});
|
|
69
|
+
selectorStart = i + 1;
|
|
70
|
+
bodyStart = -1;
|
|
71
|
+
}
|
|
72
|
+
return rules;
|
|
73
|
+
}
|
|
74
|
+
/** Parse a single selector into a node shape (tag/class/attrs), or `null` for a `&`-scoped state rule. */
|
|
75
|
+
function parseSelector(selector) {
|
|
76
|
+
if (selector.startsWith("@")) return {
|
|
77
|
+
tag: "",
|
|
78
|
+
comment: selector.replace(/^@component\s+/, "")
|
|
79
|
+
};
|
|
80
|
+
if (selector.startsWith("&")) return null;
|
|
81
|
+
const optional = selector.includes(":optional");
|
|
82
|
+
const attrMatch = /^([a-z]*)\[([\w-]+)(?:~|)=(?:"([^"]*)"|'([^']*)')\]/i.exec(selector);
|
|
83
|
+
if (attrMatch) {
|
|
84
|
+
const [, tag, attrName, dq, sq] = attrMatch;
|
|
85
|
+
const value = dq ?? sq ?? "";
|
|
86
|
+
if (attrName === "class") return {
|
|
87
|
+
tag: tag || "div",
|
|
88
|
+
className: value,
|
|
89
|
+
optional
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
tag: tag || "div",
|
|
93
|
+
attrs: { [attrName]: value },
|
|
94
|
+
optional
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const tagMatch = /^[a-z][a-z0-9]*/i.exec(selector);
|
|
98
|
+
const classMatch = /\.([\w-]+)/.exec(selector);
|
|
99
|
+
return {
|
|
100
|
+
tag: tagMatch?.[0] ?? "div",
|
|
101
|
+
className: classMatch?.[1],
|
|
102
|
+
optional
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function parseNodes(css) {
|
|
106
|
+
const nodes = [];
|
|
107
|
+
for (const { selector, body } of splitRules(css)) for (const single of selector.split(",")) {
|
|
108
|
+
const parsed = parseSelector(single.trim());
|
|
109
|
+
if (!parsed) continue;
|
|
110
|
+
nodes.push({
|
|
111
|
+
...parsed,
|
|
112
|
+
children: parseNodes(body)
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return nodes;
|
|
116
|
+
}
|
|
117
|
+
/** Strip the leading cssdoc block comment so it isn't parsed as a rule body. */
|
|
118
|
+
function stripLeadingDocComment(css) {
|
|
119
|
+
const end = css.indexOf("*/");
|
|
120
|
+
return end === -1 ? css : css.slice(end + 2);
|
|
121
|
+
}
|
|
122
|
+
/** The wrapper layout's root class name and its `.container` node (everything the root wraps). */
|
|
123
|
+
function parseWrapperLayout() {
|
|
124
|
+
const [root] = parseNodes(stripLeadingDocComment(wrapperRules()));
|
|
125
|
+
if (!root) throw new Error("wrapper layout: no root rule found");
|
|
126
|
+
const container = root.children.find((n) => n.className === "container");
|
|
127
|
+
if (!container) throw new Error("wrapper layout: no .container part found");
|
|
128
|
+
return {
|
|
129
|
+
rootClassName: root.className ?? "",
|
|
130
|
+
container
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** Placeholder text injected at specific parts, keyed by class name. */
|
|
134
|
+
const PART_TEXT = {
|
|
135
|
+
title: "{{projectName}}",
|
|
136
|
+
description: "Styled with <code>@pantoken/components</code> and laid out with the <code>wrapper</code> layout from <code>@pantoken/plugin-layouts</code>."
|
|
137
|
+
};
|
|
138
|
+
/** The `slot[name="content"]` part gets the platform's actual getting-started content, per format. */
|
|
139
|
+
const MAIN_CONTENT = {
|
|
140
|
+
html: "<p>Get started building with pantoken.</p>\n<button class=\"instui-button -color-primary\" type=\"button\">Get started</button>",
|
|
141
|
+
jsx: "<p>Get started building with pantoken.</p>\n<button className=\"instui-button -color-primary\" type=\"button\">Get started</button>"
|
|
142
|
+
};
|
|
143
|
+
function attrName(format) {
|
|
144
|
+
return format === "jsx" ? "className" : "class";
|
|
145
|
+
}
|
|
146
|
+
function comment(format, text) {
|
|
147
|
+
return format === "jsx" ? `{/* ${text} */}` : `<!-- ${text} -->`;
|
|
148
|
+
}
|
|
149
|
+
function renderNode(node, format, depth) {
|
|
150
|
+
const pad = " ".repeat(depth);
|
|
151
|
+
if (node.comment) return `${pad}${comment(format, node.comment)}`;
|
|
152
|
+
const attrs = [];
|
|
153
|
+
if (node.className) attrs.push(`${attrName(format)}="${node.className}"`);
|
|
154
|
+
for (const [name, value] of Object.entries(node.attrs ?? {})) attrs.push(`${name}="${value}"`);
|
|
155
|
+
const openTag = `<${node.tag}${attrs.length ? ` ${attrs.join(" ")}` : ""}>`;
|
|
156
|
+
const closeTag = `</${node.tag}>`;
|
|
157
|
+
const optionalNote = node.optional ? ` ${comment(format, "optional")}` : "";
|
|
158
|
+
const isMainSlot = node.tag === "slot" && node.attrs?.name === "content";
|
|
159
|
+
const text = PART_TEXT[node.className ?? ""] ?? (isMainSlot ? MAIN_CONTENT[format] : void 0);
|
|
160
|
+
const childLines = node.children.map((child) => renderNode(child, format, depth + 1));
|
|
161
|
+
const bodyPad = " ".repeat(depth + 1);
|
|
162
|
+
const bodyLines = text ? text.split("\n").map((line) => `${bodyPad}${line}`) : childLines;
|
|
163
|
+
if (bodyLines.length === 0) return `${pad}${openTag}${closeTag}${optionalNote}`;
|
|
164
|
+
return [
|
|
165
|
+
`${pad}${openTag}${optionalNote}`,
|
|
166
|
+
...bodyLines,
|
|
167
|
+
`${pad}${closeTag}`
|
|
168
|
+
].join("\n");
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Render the wrapper layout's `.container` part tree as markup, starting at the given indent depth.
|
|
172
|
+
* `format: "html"` covers plain HTML/Angular templates (`class="..."`); `"jsx"` covers React/Next
|
|
173
|
+
* (`className="..."`).
|
|
174
|
+
*/
|
|
175
|
+
function renderWrapperContainer(format, depth = 0) {
|
|
176
|
+
const { container } = parseWrapperLayout();
|
|
177
|
+
return renderNode(container, format, depth);
|
|
178
|
+
}
|
|
179
|
+
/** The wrapper layout's root class name (applied to the real document `<body>` or component root). */
|
|
180
|
+
function wrapperRootClassName() {
|
|
181
|
+
return parseWrapperLayout().rootClassName;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The wrapper layout markup/tokens every platform Preset feeds into its own Handlebars context —
|
|
185
|
+
* `{{projectName}}` inside the rendered markup is left as-is, for the platform's own template to
|
|
186
|
+
* resolve.
|
|
187
|
+
*/
|
|
188
|
+
function getWrapperContext() {
|
|
189
|
+
return {
|
|
190
|
+
wrapperRootClass: wrapperRootClassName(),
|
|
191
|
+
wrapperContainerHtml: (depth = 0) => renderWrapperContainer("html", depth),
|
|
192
|
+
wrapperContainerJsx: (depth = 0) => renderWrapperContainer("jsx", depth)
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
//#endregion
|
|
196
|
+
export { base, blockCssdoc, createPreset, getWrapperContext, renderWrapperContainer, wrapperRootClassName };
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pantoken/scaffold-base",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared bingo-stratum Base for pantoken's scaffold presets: common options, the cssdoc.json block, and wrapper-layout markup context.",
|
|
5
|
+
"homepage": "https://pantoken.iywahl.com",
|
|
6
|
+
"bugs": "https://github.com/thedannywahl/pantoken/issues",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/thedannywahl/pantoken.git",
|
|
11
|
+
"directory": "packages/scaffold-base"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./dist/index.mjs",
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@pantoken/plugin-layouts": "0.1.6",
|
|
27
|
+
"bingo-stratum": "^0.5.13",
|
|
28
|
+
"zod": "^3.24.2"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^26.3.0",
|
|
32
|
+
"typescript": "^7.0.2",
|
|
33
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.3.0",
|
|
34
|
+
"vite-plus": "0.3.0"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"generate": "node scripts/generate.ts",
|
|
38
|
+
"dev": "vp pack --watch",
|
|
39
|
+
"test": "vp run generate && vp test",
|
|
40
|
+
"check": "vp check"
|
|
41
|
+
}
|
|
42
|
+
}
|