Initial release (#304)

This commit is contained in:
Alex Terentiev 2017-09-07 22:55:11 -07:00 committed by Vesa Juvonen
parent f82957cc56
commit 352710fa93
28 changed files with 15323 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

View File

@ -0,0 +1 @@
* text=auto

32
samples/js-solution-editions/.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,14 @@
# Folders
.vscode
coverage
node_modules
sharepoint
src
temp
# Files
*.csproj
.git*
.yo-rc.json
gulpfile.js
tsconfig.json

View File

@ -0,0 +1,8 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.1.3",
"libraryName": "js-solution-editions",
"libraryId": "d32a2713-6c4e-4b63-b1a3-b35698bf167b",
"environment": "spo"
}
}

View File

@ -0,0 +1,65 @@
# Handling Multiple Editions of SPFx Solution
## Summary
This sample shows possible approach of handling multiple editions (e.g. trial, lite, full) of SharePoint Framework solution.
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-ga-green.svg)
## Applies to
* [SharePoint Framework](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
## Solution
Solution|Author(s)
--------|---------
js-solution-editions | Alex Terentiev ([Sharepointalist Inc.](http://www.sharepointalist.com), [AJIXuMuK](https://github.com/AJIXuMuK))
## Version history
Version|Date|Comments
-------|----|--------
1.0|August 23, 2017|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
## Description
### Use Case
You are an ISV and developing some product that has multiple editions, let's say, trial, lite, full. You want to have separate package file (sppkg) for each edition and also reference different CDNs based on the edition.
### Problems to address
Thinking about the use case in details we can point several problems that should be addressed:
- When we're creating a new version we should create 3 separate sppkg files
- Each sppkg file should contain manifest that references different CDN endpoints
- It should be easy to upgrade customer from trial to lite and then to full; or directly from trial to full
- You should know current edition in the code to execute the logic based on the edition's restrictions
### Approach
This sample shows the approach that is based on custom Gulp task that should be run before bundling and packaging the solution.
The name of the task is `change-build-edition`. Parameter: `edition`.
The task updates SPFx solution configuration files to contain edition-specific information:
- deploy-azure-storage.json is updated to contain correct `container` value
- package-solution.json is updated to contain correct `solution.version` and `paths.zippedPackage` values. In this sample I'm using version's revision - 4th digit - to specify the edition: 0 for trial, 1 for lite, 2 for full. It allows to easily update customers. zippedPackage path is modified to create sppkg in subfolder based on edition configuration.
- write-manifests.json is updated to contain correct CDN endpoint URL.
Additionally, web part's source code folder contains `custom-config.json` file with `edition` property:
```
{
"edition": "full"
}
```
This file is modified by custom task as well to contain correct edition.
Later `custom-config.json` is referenced (`require('./custom-config.json')`) in web part code to provide custom logic based on current edition.
Use following commands to build specific edition version:
```
gulp change-build-edition --edition lite
gulp bundle --ship
gulp package-solution --ship
gulp deploy-azure-storage
```
## Resources
[Handling Multiple Editions of SPFx Solution](http://tricky-sharepoint.blogspot.com/2017/08/handling-multiple-editions-of-spfx.html)

View File

@ -0,0 +1,21 @@
{
"editions": [{
"edition": "trial",
"azureContainer": "js-solution-editions-trial",
"cdnPath": "<!-- PATH TO CDN TRIAL -->",
"revision": "0",
"pkgPath": "solution/trail/js-solution-editions.sppkg"
}, {
"edition": "lite",
"azureContainer": "js-solution-editions-lite",
"cdnPath": "<!-- PATH TO CDN LITE -->",
"revision": "1",
"pkgPath": "solution/lite/js-solution-editions.sppkg"
}, {
"edition": "full",
"azureContainer": "js-solution-editions-full",
"cdnPath": "<!-- PATH TO CDN TRIAL -->",
"revision": "2",
"pkgPath": "solution/full/js-solution-editions.sppkg"
}]
}

View File

@ -0,0 +1,13 @@
{
"entries": [
{
"entry": "./lib/webparts/helloWorld/HelloWorldWebPart.js",
"manifest": "./src/webparts/helloWorld/HelloWorldWebPart.manifest.json",
"outputPath": "./dist/hello-world.bundle.js"
}
],
"externals": {},
"localizedResources": {
"helloWorldStrings": "webparts/helloWorld/loc/{locale}.js"
}
}

View File

@ -0,0 +1,3 @@
{
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,6 @@
{
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "js-solution-editions",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,11 @@
{
"solution": {
"name": "js-solution-editions-client-side-solution",
"id": "d32a2713-6c4e-4b63-b1a3-b35698bf167b",
"version": "1.0.0.1",
"skipFeatureDeployment": true
},
"paths": {
"zippedPackage": "solution/js-solution-editions.sppkg"
}
}

View File

@ -0,0 +1,9 @@
{
"port": 4321,
"initialPage": "https://localhost:5432/workbench",
"https": true,
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,45 @@
{
// Display errors as warnings
"displayAsWarning": true,
// The TSLint task may have been configured with several custom lint rules
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
// project). If true, this flag will deactivate any of these rules.
"removeExistingRules": true,
// When true, the TSLint task is configured with some default TSLint "rules.":
"useDefaultConfigAsBase": false,
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
// which are active, other than the list of rules below.
"lintConfig": {
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-case": true,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-unused-imports": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"valid-typeof": true,
"variable-name": false,
"whitespace": false
}
}
}

View File

@ -0,0 +1,3 @@
{
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,170 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
const logging = require('@microsoft/gulp-core-build');
const fs = require('fs');
// path to editions config file
const buildConfigFilePath = './config/build-config.json';
// path to deploy-azure-storage.json
const azureConfigFilePath = './config/deploy-azure-storage.json';
// path to package-solution.json
const solutionConfigFilePath = './config/package-solution.json';
// path to write-manifests.json
const manifestFilePath = './config/write-manifests.json';
// path to custom-config.json that contains edition name to use in code
const customConfigFilePath = './src/webparts/helloWorld/custom-config.json';
// adding custom task. Can be executed with gulp change-build-edition --edition lite
build.task('change-build-edition', {
execute: (config) => {
return new Promise((resolve, reject) => {
try {
// getting edition parameter
const edition = config.args['edition'] || 'full';
// getting package-solution.json content
const solutionJSON = JSON.parse(fs.readFileSync(solutionConfigFilePath));
// getting deploy-azure-storage.json content
const azureJSON = JSON.parse(fs.readFileSync(azureConfigFilePath));
// getting write-manifests.json content
const manifestJSON = JSON.parse(fs.readFileSync(manifestFilePath));
// getting custom-config.json content
const customConfigJSON = JSON.parse(fs.readFileSync(customConfigFilePath));
// getting editions configurations
const buildJSON = JSON.parse(fs.readFileSync(buildConfigFilePath));
// getting edition settings by edition name
const editionInfo = getEditionInfo(buildJSON, edition);
if (!editionInfo) {
resolve();
return;
}
logging.log(`Configuring settings for edition: ${edition}`);
//
// updating custom-config.json file
//
customConfigJSON.edition = edition;
logging.log('Updating custom config for the web part...');
fs.writeFileSync(customConfigFilePath, JSON.stringify(customConfigJSON));
//
// updating package-solution.json
//
const revNumberStartIndex = solutionJSON.solution.version.lastIndexOf('.');
// new version
solutionJSON.solution.version = solutionJSON.solution.version.substring(0, revNumberStartIndex + 1) + editionInfo.revision;
logging.log(`Checking if sppkg directory '${editionInfo.pkgPath}' exists and creating if not...`);
// creating subfolder if doesn't exist
ensurePath(editionInfo.pkgPath);
// updating zippedPackage path
solutionJSON.paths.zippedPackage = editionInfo.pkgPath;
logging.log('Updating package-solution.json...');
fs.writeFileSync(solutionConfigFilePath, JSON.stringify(solutionJSON));
//
// updating deploy-azure-storage.json
//
azureJSON.container = editionInfo.azureContainer;
logging.log('Updating deploy-azure-storage.json...');
fs.writeFileSync(azureConfigFilePath, JSON.stringify(azureJSON));
//
// updating write-manifests.json
//
manifestJSON.cdnBasePath = editionInfo.cdnPath;
logging.log('Updating write-manifests.json...');
fs.writeFileSync(manifestFilePath, JSON.stringify(manifestJSON));
resolve();
}
catch (ex) {
logging.log(ex);
reject();
}
});
}
});
/**
* Gets edition settings by name
* @param {any} buildJSON editions settings
* @param {string} edition edition name
*/
function getEditionInfo(buildJSON, edition) {
edition = edition || 'full';
let result = null;
if (buildJSON && buildJSON.editions && buildJSON.editions.length) {
for (let i = 0, len = buildJSON.editions.length; i < len; i++) {
const ver = buildJSON.editions[i];
if (ver.edition === edition) {
result = ver;
break;
}
}
}
if (!result) {
result = {
'edition': 'full',
'azureContainer': 'js-solution-editions-full',
'cdnPath': '<!-- PATH TO CDN FULL -->',
'revision': '2',
'pkgPath': 'solution/full/js-solution-editions.sppkg'
};
}
return result;
}
/**
* Ensures that the subfolders from the path exist
* @param {string} path relative path to sppkg file (relative to ./sharepoint folder)
*/
function ensurePath(path) {
if (!path) {
return;
}
let pathArray = path.split('/');
if (!pathArray.length) {
return;
}
//
// removing filename from the path
//
if (pathArray[pathArray.length - 1].indexOf('.') !== -1) {
pathArray.pop();
}
//
// adding sharepoint as a root folder
//
if (pathArray[0] !== 'sharepoint') {
pathArray.unshift('sharepoint');
}
//
// creating all subfolders if needed
//
let currPath = '.';
for (let i = 0, length = pathArray.length; i < length; i++) {
const pathPart = pathArray[i];
currPath += `/${pathPart}`;
if (!fs.existsSync(currPath)) {
fs.mkdir(currPath);
}
}
}
build.initialize(gulp);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
{
"name": "js-solution-editions",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"dependencies": {
"@microsoft/sp-core-library": "~1.1.0",
"@microsoft/sp-webpart-base": "~1.1.1",
"@types/webpack-env": ">=1.12.1 <1.14.0"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.1.0",
"@microsoft/sp-module-interfaces": "~1.1.1",
"@microsoft/sp-webpart-workbench": "~1.1.0",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
}
}

View File

@ -0,0 +1,52 @@
.helloWorld {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
padding: 20px;
}
.listItem {
max-width: 715px;
margin: 5px auto 5px auto;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: #0078d7;
border-color: #0078d7;
color: #ffffff;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
font-weight: 400;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: 600;
font-size: 14px;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,27 @@
{
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
"id": "cfefc5ff-be4e-4cbf-bf58-ce98ee36c6fa",
"alias": "HelloWorldWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"preconfiguredEntries": [{
"groupId": "cfefc5ff-be4e-4cbf-bf58-ce98ee36c6fa",
"group": { "default": "Under Development" },
"title": { "default": "HelloWorld" },
"description": { "default": "HelloWorld description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "HelloWorld"
}
}]
}

View File

@ -0,0 +1,56 @@
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import { escape } from '@microsoft/sp-lodash-subset';
import styles from './HelloWorld.module.scss';
import * as strings from 'helloWorldStrings';
import { IHelloWorldWebPartProps } from './IHelloWorldWebPartProps';
var config:any = require('./custom-config.json');
export default class HelloWorldWebPart extends BaseClientSideWebPart<IHelloWorldWebPartProps> {
public render(): void {
this.domElement.innerHTML = `
<div class="${styles.helloWorld}">
<div class="${styles.container}">
<div class="ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}">
<div class="ms-Grid-col ms-u-lg10 ms-u-xl8 ms-u-xlPush2 ms-u-lgPush1">
<span class="ms-font-xl ms-fontColor-white">Current edition:</span>
<p class="ms-font-l ms-fontColor-white">${config.edition}</p>
</div>
</div>
</div>
</div>`;
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,3 @@
export interface IHelloWorldWebPartProps {
description: string;
}

View File

@ -0,0 +1,3 @@
{
"edition": "full"
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,10 @@
declare interface IHelloWorldStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'helloWorldStrings' {
const strings: IHelloWorldStrings;
export = strings;
}

View File

@ -0,0 +1,9 @@
/// <reference types="mocha" />
import { assert } from 'chai';
describe('HelloWorldWebPart', () => {
it('should do something', () => {
assert.ok(true);
});
});

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"types": [
"es6-promise",
"es6-collections",
"webpack-env"
]
}
}

View File

@ -0,0 +1,11 @@
// Type definitions for Microsoft ODSP projects
// Project: ODSP
/* Global definition for UNIT_TEST builds
Code that is wrapped inside an if(UNIT_TEST) {...}
block will not be included in the final bundle when the
--ship flag is specified */
declare const UNIT_TEST: boolean;
/* Global defintion for SPO builds */
declare const DATACENTER: boolean;

View File

@ -0,0 +1 @@
/// <reference path="@ms/odsp.d.ts" />